Repository: MesserLab/SLiM Branch: master Commit: 4f77dd527b81 Files: 1314 Total size: 22.8 MB Directory structure: gitextract_mptrfmw2/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── issue.md │ └── workflows/ │ └── tests.yml ├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── CONTENTS.txt ├── CONTRIBUTING.md ├── EidosSLiMTests/ │ ├── EidosTests.mm │ ├── Info.plist │ └── SLiMTests.mm ├── EidosScribe/ │ ├── Base.lproj/ │ │ ├── EidosAboutWindow.xib │ │ ├── EidosConsoleWindow.xib │ │ ├── EidosHelpWindow.xib │ │ └── MainMenu.xib │ ├── EidosAppDelegate.h │ ├── EidosAppDelegate.mm │ ├── EidosCocoaExtra.h │ ├── EidosCocoaExtra.mm │ ├── EidosConsoleTextView.h │ ├── EidosConsoleTextView.mm │ ├── EidosConsoleTextViewDelegate.h │ ├── EidosConsoleWindowController.h │ ├── EidosConsoleWindowController.mm │ ├── EidosConsoleWindowControllerDelegate.h │ ├── EidosHelpClasses.rtf │ ├── EidosHelpController.h │ ├── EidosHelpController.mm │ ├── EidosHelpFunctions.rtf │ ├── EidosHelpOperators.rtf │ ├── EidosHelpStatements.rtf │ ├── EidosHelpTypes.rtf │ ├── EidosPrettyprinter.h │ ├── EidosPrettyprinter.mm │ ├── EidosScribe-Info.plist │ ├── EidosScribe.entitlements │ ├── EidosScribe_multi-Info.plist │ ├── EidosScribe_multi.entitlements │ ├── EidosTextView.h │ ├── EidosTextView.mm │ ├── EidosTextViewDelegate.h │ ├── EidosValueWrapper.h │ ├── EidosValueWrapper.mm │ ├── EidosVariableBrowserController.h │ ├── EidosVariableBrowserController.mm │ ├── EidosVariableBrowserControllerDelegate.h │ ├── Images.xcassets/ │ │ └── AppIcon.appiconset/ │ │ └── Contents.json │ └── main.m ├── LICENSE ├── PARALLEL ├── QtSLiM/ │ ├── QtSLiM.pro │ ├── QtSLiMAbout.cpp │ ├── QtSLiMAbout.h │ ├── QtSLiMAbout.ui │ ├── QtSLiMAppDelegate.cpp │ ├── QtSLiMAppDelegate.h │ ├── QtSLiMChromosomeWidget.cpp │ ├── QtSLiMChromosomeWidget.h │ ├── QtSLiMChromosomeWidget_GL.cpp │ ├── QtSLiMChromosomeWidget_QT.cpp │ ├── QtSLiMConsoleTextEdit.cpp │ ├── QtSLiMConsoleTextEdit.h │ ├── QtSLiMDebugOutputWindow.cpp │ ├── QtSLiMDebugOutputWindow.h │ ├── QtSLiMDebugOutputWindow.ui │ ├── QtSLiMEidosConsole.cpp │ ├── QtSLiMEidosConsole.h │ ├── QtSLiMEidosConsole.ui │ ├── QtSLiMEidosConsole_glue.cpp │ ├── QtSLiMEidosPrettyprinter.cpp │ ├── QtSLiMEidosPrettyprinter.h │ ├── QtSLiMExtras.cpp │ ├── QtSLiMExtras.h │ ├── QtSLiMFindPanel.cpp │ ├── QtSLiMFindPanel.h │ ├── QtSLiMFindPanel.ui │ ├── QtSLiMFindRecipe.cpp │ ├── QtSLiMFindRecipe.h │ ├── QtSLiMFindRecipe.ui │ ├── QtSLiMGraphView.cpp │ ├── QtSLiMGraphView.h │ ├── QtSLiMGraphView_1DPopulationSFS.cpp │ ├── QtSLiMGraphView_1DPopulationSFS.h │ ├── QtSLiMGraphView_1DSampleSFS.cpp │ ├── QtSLiMGraphView_1DSampleSFS.h │ ├── QtSLiMGraphView_2DPopulationSFS.cpp │ ├── QtSLiMGraphView_2DPopulationSFS.h │ ├── QtSLiMGraphView_2DSampleSFS.cpp │ ├── QtSLiMGraphView_2DSampleSFS.h │ ├── QtSLiMGraphView_AgeDistribution.cpp │ ├── QtSLiMGraphView_AgeDistribution.h │ ├── QtSLiMGraphView_CustomPlot.cpp │ ├── QtSLiMGraphView_CustomPlot.h │ ├── QtSLiMGraphView_FitnessOverTime.cpp │ ├── QtSLiMGraphView_FitnessOverTime.h │ ├── QtSLiMGraphView_FixationTimeHistogram.cpp │ ├── QtSLiMGraphView_FixationTimeHistogram.h │ ├── QtSLiMGraphView_FrequencyTrajectory.cpp │ ├── QtSLiMGraphView_FrequencyTrajectory.h │ ├── QtSLiMGraphView_LifetimeReproduction.cpp │ ├── QtSLiMGraphView_LifetimeReproduction.h │ ├── QtSLiMGraphView_LossTimeHistogram.cpp │ ├── QtSLiMGraphView_LossTimeHistogram.h │ ├── QtSLiMGraphView_MultispeciesPopSizeOverTime.cpp │ ├── QtSLiMGraphView_MultispeciesPopSizeOverTime.h │ ├── QtSLiMGraphView_PopFitnessDist.cpp │ ├── QtSLiMGraphView_PopFitnessDist.h │ ├── QtSLiMGraphView_PopSizeOverTime.cpp │ ├── QtSLiMGraphView_PopSizeOverTime.h │ ├── QtSLiMGraphView_PopulationVisualization.cpp │ ├── QtSLiMGraphView_PopulationVisualization.h │ ├── QtSLiMGraphView_SubpopFitnessDists.cpp │ ├── QtSLiMGraphView_SubpopFitnessDists.h │ ├── QtSLiMHaplotypeManager.cpp │ ├── QtSLiMHaplotypeManager.h │ ├── QtSLiMHaplotypeManager_GL.cpp │ ├── QtSLiMHaplotypeManager_QT.cpp │ ├── QtSLiMHaplotypeOptions.cpp │ ├── QtSLiMHaplotypeOptions.h │ ├── QtSLiMHaplotypeOptions.ui │ ├── QtSLiMHaplotypeProgress.cpp │ ├── QtSLiMHaplotypeProgress.h │ ├── QtSLiMHaplotypeProgress.ui │ ├── QtSLiMHelpWindow.cpp │ ├── QtSLiMHelpWindow.h │ ├── QtSLiMHelpWindow.ui │ ├── QtSLiMIndividualsWidget.cpp │ ├── QtSLiMIndividualsWidget.h │ ├── QtSLiMIndividualsWidget_GL.cpp │ ├── QtSLiMIndividualsWidget_QT.cpp │ ├── QtSLiMOpenGL.cpp │ ├── QtSLiMOpenGL.h │ ├── QtSLiMOpenGL_Emulation.h │ ├── QtSLiMPopulationTable.cpp │ ├── QtSLiMPopulationTable.h │ ├── QtSLiMPreferences.cpp │ ├── QtSLiMPreferences.h │ ├── QtSLiMPreferences.ui │ ├── QtSLiMScriptTextEdit.cpp │ ├── QtSLiMScriptTextEdit.h │ ├── QtSLiMSyntaxHighlighting.cpp │ ├── QtSLiMSyntaxHighlighting.h │ ├── QtSLiMTablesDrawer.cpp │ ├── QtSLiMTablesDrawer.h │ ├── QtSLiMTablesDrawer.ui │ ├── QtSLiMVariableBrowser.cpp │ ├── QtSLiMVariableBrowser.h │ ├── QtSLiMVariableBrowser.ui │ ├── QtSLiMWindow.cpp │ ├── QtSLiMWindow.h │ ├── QtSLiMWindow.ui │ ├── QtSLiMWindow_glue.cpp │ ├── QtSLiM_AppIcon.icns │ ├── QtSLiM_DocIcon.icns │ ├── QtSLiM_Info.plist │ ├── QtSLiM_Plot.cpp │ ├── QtSLiM_Plot.h │ ├── QtSLiM_SLiMgui.cpp │ ├── QtSLiM_SLiMgui.h │ ├── buttons.qrc │ ├── buttons_DARK.qrc │ ├── help/ │ │ ├── EidosHelpClasses.html │ │ ├── EidosHelpFunctions.html │ │ ├── EidosHelpOperators.html │ │ ├── EidosHelpStatements.html │ │ ├── EidosHelpTypes.html │ │ ├── SLiMHelpCallbacks.html │ │ ├── SLiMHelpClasses.html │ │ └── SLiMHelpFunctions.html │ ├── help.qrc │ ├── icons.qrc │ ├── main.cpp │ ├── recipes/ │ │ ├── Recipe 10.1 - Temporally varying selection.txt │ │ ├── Recipe 10.2 - Spatially varying selection.txt │ │ ├── Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis I.txt │ │ ├── Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis II.txt │ │ ├── Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection I.txt │ │ ├── Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection II.txt │ │ ├── Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection III.txt │ │ ├── Recipe 10.4.2 - Fitness as a function of population composition, Cultural effects on fitness.txt │ │ ├── Recipe 10.4.3 - Fitness as a function of population composition, Kin selection and the green-beard effect.txt │ │ ├── Recipe 10.5 - Changing selection coefficients with setSelectionCoeff().txt │ │ ├── Recipe 10.6 - Varying the dominance coefficient among mutations I.txt │ │ ├── Recipe 10.6 - Varying the dominance coefficient among mutations II.txt │ │ ├── Recipe 11.1 - Assortative mating.txt │ │ ├── Recipe 11.2 - Sequential mate search I.txt │ │ ├── Recipe 11.2 - Sequential mate search II.txt │ │ ├── Recipe 11.3 - Gametophytic self-incompatibility.txt │ │ ├── Recipe 12.1 - Social learning of cultural traits.txt │ │ ├── Recipe 12.2 - Lethal epistasis I.txt │ │ ├── Recipe 12.2 - Lethal epistasis II.txt │ │ ├── Recipe 12.3 - Simulating gene drive.txt │ │ ├── Recipe 12.4 - Suppressing hermaphroditic selfing.txt │ │ ├── Recipe 12.5 - Tracking separate sexes in script.txt │ │ ├── Recipe 13.1 - Polygenic selection.txt │ │ ├── Recipe 13.2 - A simple model of variable QTL effect sizes.txt │ │ ├── Recipe 13.3 - A model of discrete QTL effects across multiple chromosomes.txt │ │ ├── Recipe 13.4 - A quantitative genetics model with heritability.txt │ │ ├── Recipe 13.5 - A QTL-based model with two quantitative phenotypic traits and pleiotropy.txt │ │ ├── Recipe 13.6 - A variety of fitness functions I (stabilizing selection).txt │ │ ├── Recipe 13.6 - A variety of fitness functions II (directional selection).txt │ │ ├── Recipe 13.6 - A variety of fitness functions III (disruptive selection).txt │ │ ├── Recipe 13.6 - A variety of fitness functions IV (truncation selection).txt │ │ ├── Recipe 13.7 - Negative frequency-dependence on a quantitative trait.txt │ │ ├── Recipe 14.1 - Relatedness, inbreeding, and heterozygosity.txt │ │ ├── Recipe 14.10 - Modeling transposable elements.txt │ │ ├── Recipe 14.11 - Modeling opposite ends of a chromosome I.txt │ │ ├── Recipe 14.11 - Modeling opposite ends of a chromosome II.txt │ │ ├── Recipe 14.12 - Visualizing ancestry and admixture with mutation() callbacks.txt │ │ ├── Recipe 14.13 - Modeling biallelic loci with a mutation() callback I.txt │ │ ├── Recipe 14.13 - Modeling biallelic loci with a mutation() callback II.txt │ │ ├── Recipe 14.14 - Modeling biallelic loci in script.txt │ │ ├── Recipe 14.15 - Using runs of homozygosity (ROH) to track inbreeding.txt │ │ ├── Recipe 14.16 - Visualizing linkage disequilibrium.txt │ │ ├── Recipe 14.2 - Mortality-based fitness I.txt │ │ ├── Recipe 14.2 - Mortality-based fitness II.txt │ │ ├── Recipe 14.2 - Mortality-based fitness III.txt │ │ ├── Recipe 14.3 - Reading initial simulation state from an MS output file I.txt │ │ ├── Recipe 14.3 - Reading initial simulation state from an MS output file II.txt │ │ ├── Recipe 14.4 - Modeling chromosomal inversions with a recombination() callback.txt │ │ ├── Recipe 14.5 - Estimating model parameters with ABC.txt │ │ ├── Recipe 14.6 - Tracking local ancestry along the chromosome.txt │ │ ├── Recipe 14.7 - Live plotting with R using system().txt │ │ ├── Recipe 14.8 - Using mutation rate variation to model varying functional density.txt │ │ ├── Recipe 14.9 - Modeling microsatellites.txt │ │ ├── Recipe 15.1 - A minimal nonWF model.txt │ │ ├── Recipe 15.10 - Recording a pedigree.txt │ │ ├── Recipe 15.11 - Dynamic population structure in nonWF models.txt │ │ ├── Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model I.txt │ │ ├── Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model II.txt │ │ ├── Recipe 15.13 - Range expansion in a stepping-stone model I.txt │ │ ├── Recipe 15.13 - Range expansion in a stepping-stone model II.txt │ │ ├── Recipe 15.14 - Logistic population growth with the Beverton-Holt model.txt │ │ ├── Recipe 15.2 - Age structure (a life table model).txt │ │ ├── Recipe 15.3 - Handling all reproduction at once with big bang reproduction.txt │ │ ├── Recipe 15.4 - Monogamous mating and variation in litter size.txt │ │ ├── Recipe 15.5 - Beneficial mutations and absolute fitness.txt │ │ ├── Recipe 15.6 - A metapopulation extinction-colonization model.txt │ │ ├── Recipe 15.7 - Habitat choice.txt │ │ ├── Recipe 15.8 - Evolutionary rescue after environmental change.txt │ │ ├── Recipe 15.9 - Litter size and parental investment.txt │ │ ├── Recipe 16.1 - Pollen flow.txt │ │ ├── Recipe 16.10 - Modeling pseudo-autosomal regions (PARs) with addMultiRecombinant().txt │ │ ├── Recipe 16.11 - Life-long monogamous mating.txt │ │ ├── Recipe 16.2 - Following a pedigree.txt │ │ ├── Recipe 16.3 - Modeling clonal haploid bacteria with horizontal gene transfer.txt │ │ ├── Recipe 16.4 - Alternation of generations.txt │ │ ├── Recipe 16.5 - Meiotic drive.txt │ │ ├── Recipe 16.6 - Sperm storage with a survival() callback.txt │ │ ├── Recipe 16.7 - Tracking separate sexes in script, nonWF style.txt │ │ ├── Recipe 16.8 - Modeling haplodiploidy with addRecombinant().txt │ │ ├── Recipe 16.9 - Complex multi-chromosome inheritance with addMultiRecombinant().txt │ │ ├── Recipe 17.1 - A simple 2D continuous-space model.txt │ │ ├── Recipe 17.10 - A simple biogeographic landscape model.txt │ │ ├── Recipe 17.11 - Local adaptation on a heterogeneous landscape map.txt │ │ ├── Recipe 17.12 - Periodic spatial boundaries.txt │ │ ├── Recipe 17.13 - Density-dependent fecundity with summarizeIndividuals().txt │ │ ├── Recipe 17.14 - Directed dispersal with the SpatialMap class.txt │ │ ├── Recipe 17.15 - Spatial competition and spatial mate choice in a nonWF model.txt │ │ ├── Recipe 17.16 - A spatial model with carrying-capacity density.txt │ │ ├── Recipe 17.17 - A spatial epidemiological S-I-R model.txt │ │ ├── Recipe 17.18 - A sexual, age-structured spatial model.txt │ │ ├── Recipe 17.19 - Modeling indirect competition mediated by resource availability.txt │ │ ├── Recipe 17.2 - Spatial competition.txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions I (stopping boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions II (reflecting boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions III (absorbing boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions IV (reprising boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions V (dispersal kernels).txt │ │ ├── Recipe 17.4 - Mate choice with a spatial kernel.txt │ │ ├── Recipe 17.5 - Mate choice with a nearest-neighbor search.txt │ │ ├── Recipe 17.6 - Divergence due to phenotypic competition with an interaction() callback.txt │ │ ├── Recipe 17.7 - Modeling phenotype as a spatial dimension.txt │ │ ├── Recipe 17.8 - Sympatric speciation facilitated by assortative mating.txt │ │ ├── Recipe 17.9 - Speciation due to spatial variation in selection.txt │ │ ├── Recipe 18.1 - A minimal tree-seq model.txt │ │ ├── Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation I.txt │ │ ├── Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation II.py │ │ ├── Recipe 18.11 - Optimizing tree-sequence simplification.txt │ │ ├── Recipe 18.2 - Overlaying neutral mutations.py │ │ ├── Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt │ │ ├── Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt │ │ ├── Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt │ │ ├── Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py │ │ ├── Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) I.txt │ │ ├── Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) II.py │ │ ├── Recipe 18.6 - Measuring the coalescence time of a model I.txt │ │ ├── Recipe 18.6 - Measuring the coalescence time of a model II.txt │ │ ├── Recipe 18.7 - Analyzing selection coefficients in Python with tskit I.txt │ │ ├── Recipe 18.7 - Analyzing selection coefficients in Python with tskit II.py │ │ ├── Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history I.py │ │ ├── Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history II.txt │ │ ├── Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history III.py │ │ ├── Recipe 18.9 - Starting a sexual nonWF model with a coalescent history I.py │ │ ├── Recipe 18.9 - Starting a sexual nonWF model with a coalescent history II.txt │ │ ├── Recipe 19.1 - A simple neutral nucleotide-based model.txt │ │ ├── Recipe 19.10 - Varying the mutation rate along the chromosome in a nucleotide-based model.txt │ │ ├── Recipe 19.11 - Modeling GC-biased gene conversion (gBGC).txt │ │ ├── Recipe 19.12 - Reading VCF files to create nucleotide-based SNPs.txt │ │ ├── Recipe 19.13 - Tree-sequence recording and nucleotide-based models I.txt │ │ ├── Recipe 19.13 - Tree-sequence recording and nucleotide-based models II.py │ │ ├── Recipe 19.13 - Tree-sequence recording and nucleotide-based models III.py │ │ ├── Recipe 19.14 - Modeling identity by state (IBS) (uniquing mutations with a mutation() callback).txt │ │ ├── Recipe 19.15 - Modeling identity by state (IBS) (uniquing back-mutations to the ancestral state).txt │ │ ├── Recipe 19.2 - Reading an ancestral nucleotide sequence from a FASTA file.txt │ │ ├── Recipe 19.3 - Sequence output from nucleotide-based models.txt │ │ ├── Recipe 19.4 - Back-mutations, independent mutational lineages, and VCF output.txt │ │ ├── Recipe 19.5 - Modeling elevated CpG mutation rates and equilibrium nucleotide frequencies.txt │ │ ├── Recipe 19.6 - A nucleotide-based model with introduced non-nucleotide-based mutations.txt │ │ ├── Recipe 19.7 - Using standard SLiM fitness effects with nucleotides (modeling synonymous sites).txt │ │ ├── Recipe 19.8 - Defining sequence-based fitness effects at the nucleotide level.txt │ │ ├── Recipe 19.9 - Defining sequence-based fitness effects at the amino acid level.txt │ │ ├── Recipe 20.1 - A simple multispecies model.txt │ │ ├── Recipe 20.2 - A two-species model.txt │ │ ├── Recipe 20.3 - A deterministic host-parasitoid model.txt │ │ ├── Recipe 20.4 - An individual-based host-parasitoid model I.txt │ │ ├── Recipe 20.4 - An individual-based host-parasitoid model II.txt │ │ ├── Recipe 20.5 - A continuous-space host-parasitoid model.txt │ │ ├── Recipe 20.6 - A coevolutionary host-parasitoid trait-matching model.txt │ │ ├── Recipe 20.7 - A coevolutionary host-parasite matching-allele model.txt │ │ ├── Recipe 20.8 - Within-host reproduction in a host-pathogen model.txt │ │ ├── Recipe 4.1 - A basic neutral simulation.txt │ │ ├── Recipe 4.1.10 - Using symbolic constants for model parameters.txt │ │ ├── Recipe 4.2.1 - Basic output, Entire population.txt │ │ ├── Recipe 4.2.2 - Basic output, Random population sample.txt │ │ ├── Recipe 4.2.3 - Basic output, Sampling individuals for output.txt │ │ ├── Recipe 4.2.4 - Basic output, Substitutions.txt │ │ ├── Recipe 4.2.5 - Basic output, Automatic logging with LogFile.txt │ │ ├── Recipe 4.2.6 - Basic output, Custom output with Eidos.txt │ │ ├── Recipe 5.1.1 - Subpopulation size, Instantaneous changes.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth I.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth II.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth III.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth IV.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth V.txt │ │ ├── Recipe 5.1.4 - Subpopulation size, Cyclical changes.txt │ │ ├── Recipe 5.1.5 - Subpopulation size, Context-dependent changes (Muller's Ratchet).txt │ │ ├── Recipe 5.2.1 - Population structure, Adding subpopulations.txt │ │ ├── Recipe 5.2.2 - Population structure, Removing subpopulations.txt │ │ ├── Recipe 5.2.3 - Population structure, Splitting subpopulations.txt │ │ ├── Recipe 5.2.4 - Population structure, Joining subpopulations.txt │ │ ├── Recipe 5.3.1 - Migration and admixture, A linear stepping-stone model.txt │ │ ├── Recipe 5.3.2 - Migration and admixture, A non-spatial metapopulation.txt │ │ ├── Recipe 5.3.3 - Migration and admixture, A two-dimensional subpopulation matrix.txt │ │ ├── Recipe 5.3.4 - Migration and admixture, A random, sparse spatial metapopulation.txt │ │ ├── Recipe 5.3.5 - Migration and admixture, Reading a migration matrix from a file.txt │ │ ├── Recipe 5.4 - The Gravel et al. (2011) model of human evolution I.txt │ │ ├── Recipe 5.4 - The Gravel et al. (2011) model of human evolution II.txt │ │ ├── Recipe 5.5 - Rescaling population sizes to improve simulation performance I.txt │ │ ├── Recipe 5.5 - Rescaling population sizes to improve simulation performance II.txt │ │ ├── Recipe 6.1 - Genomic structure, Part I (Mutation types and fitness effects).txt │ │ ├── Recipe 6.2 - Genomic structure, Part II (Genomic element types).txt │ │ ├── Recipe 6.3 - Genomic structure, Part III (Chromosome organization).txt │ │ ├── Recipe 6.4 - Genomic structure, Part IV (Custom display colors in SLiMgui).txt │ │ ├── Recipe 8.1.1 - Reproduction, Enabling separate sexes.txt │ │ ├── Recipe 8.1.2 - Reproduction, Sex ratios I.txt │ │ ├── Recipe 8.1.2 - Reproduction, Sex ratios II.txt │ │ ├── Recipe 8.1.3 - Reproduction, Selfing in hermaphroditic populations.txt │ │ ├── Recipe 8.1.4 - Reproduction, Cloning I.txt │ │ ├── Recipe 8.1.4 - Reproduction, Cloning II.txt │ │ ├── Recipe 8.2.1 - Recombination, Making a random recombination map.txt │ │ ├── Recipe 8.2.2 - Recombination, Reading a recombination map from a file.txt │ │ ├── Recipe 8.2.3 - Recombination, Unlinked loci.txt │ │ ├── Recipe 8.2.4 - Recombination, Gene conversion.txt │ │ ├── Recipe 8.3.1 - Multiple diploid autosomes.txt │ │ ├── Recipe 8.3.2 - Clonal haploids and chromosome types.txt │ │ ├── Recipe 8.3.3 - Haploids with recombination.txt │ │ ├── Recipe 8.3.4 - Sex-chromosome evolution and null haplosomes.txt │ │ ├── Recipe 8.3.5 - Modeling the full human genome.txt │ │ ├── Recipe 8.3.6 - A model of bryophytes with UV sex determination.txt │ │ ├── Recipe 8.3.7 - Output from multiple-chromosome models.txt │ │ ├── Recipe 9.1 - Introducing adaptive mutations.txt │ │ ├── Recipe 9.10 - Tracking the fate of background mutations.txt │ │ ├── Recipe 9.11 - Effective population size versus census population size.txt │ │ ├── Recipe 9.12 - Observing the site frequency spectrum (SFS) during selective sweeps.txt │ │ ├── Recipe 9.2 - Making sweeps conditional on fixation.txt │ │ ├── Recipe 9.3 - Making sweeps conditional on establishment.txt │ │ ├── Recipe 9.4 - Partial sweeps.txt │ │ ├── Recipe 9.5.1 - A soft sweep from recurrent de novo mutations in a large population.txt │ │ ├── Recipe 9.5.2 - A soft sweep with a fixed de novo mutation schedule.txt │ │ ├── Recipe 9.5.3 - A soft sweep with a random de novo mutation schedule.txt │ │ ├── Recipe 9.6.1 - A sweep from standing variation at a random locus.txt │ │ ├── Recipe 9.6.2 - A sweep from standing variation at a predetermined locus.txt │ │ ├── Recipe 9.7 - Adaptive introgression.txt │ │ ├── Recipe 9.8 - Fixation probabilities under Hill-Robertson interference.txt │ │ ├── Recipe 9.9 - Keeping a reference to a sweep mutation.txt │ │ └── _README.txt │ └── recipes.qrc ├── README.md ├── SLiM.pro ├── SLiM.spec ├── SLiM.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata/ │ └── xcschemes/ │ ├── EidosScribe.xcscheme │ ├── EidosScribe_multi.xcscheme │ ├── SLiM.xcscheme │ ├── SLiM_multi.xcscheme │ ├── SLiMguiLegacy.xcscheme │ ├── SLiMguiLegacy_multi.xcscheme │ ├── eidos.xcscheme │ └── eidos_multi.xcscheme ├── SLiMgui/ │ ├── AppDelegate.h │ ├── AppDelegate.mm │ ├── Base.lproj/ │ │ ├── AboutWindow.xib │ │ ├── FindRecipePanel.xib │ │ ├── GraphAxisRescaleSheet.xib │ │ ├── GraphBarRescaleSheet.xib │ │ ├── GraphWindow.xib │ │ ├── HelpWindow.xib │ │ ├── MainMenu.xib │ │ ├── ProfileReport.xib │ │ ├── SLiMWindow.xib │ │ ├── ScriptModSheet.xib │ │ ├── ScriptMod_AddGenomicElement.xib │ │ ├── ScriptMod_AddGenomicElementType.xib │ │ ├── ScriptMod_AddMutationType.xib │ │ ├── ScriptMod_AddRecombinationRate.xib │ │ ├── ScriptMod_AddSexConfiguration.xib │ │ ├── ScriptMod_AddSubpop.xib │ │ ├── ScriptMod_ChangeCloning.xib │ │ ├── ScriptMod_ChangeMigration.xib │ │ ├── ScriptMod_ChangeSelfing.xib │ │ ├── ScriptMod_ChangeSexRatio.xib │ │ ├── ScriptMod_ChangeSubpopSize.xib │ │ ├── ScriptMod_OutputFixedMutations.xib │ │ ├── ScriptMod_OutputFullPopulation.xib │ │ ├── ScriptMod_OutputSubpopSample.xib │ │ ├── ScriptMod_RemoveSubpop.xib │ │ ├── ScriptMod_SplitSubpop.xib │ │ └── TipsWindow.xib │ ├── ChromosomeView.h │ ├── ChromosomeView.mm │ ├── CocoaExtra.h │ ├── CocoaExtra.mm │ ├── FindRecipeController.h │ ├── FindRecipeController.mm │ ├── GraphView.h │ ├── GraphView.mm │ ├── GraphView_FitnessOverTime.h │ ├── GraphView_FitnessOverTime.mm │ ├── GraphView_MutationFixationTimeHistogram.h │ ├── GraphView_MutationFixationTimeHistogram.mm │ ├── GraphView_MutationFrequencySpectra.h │ ├── GraphView_MutationFrequencySpectra.mm │ ├── GraphView_MutationFrequencyTrajectory.h │ ├── GraphView_MutationFrequencyTrajectory.mm │ ├── GraphView_MutationLossTimeHistogram.h │ ├── GraphView_MutationLossTimeHistogram.mm │ ├── GraphView_PopulationVisualization.h │ ├── GraphView_PopulationVisualization.mm │ ├── Images.xcassets/ │ │ └── AppIcon.appiconset/ │ │ └── Contents.json │ ├── PopulationView.h │ ├── PopulationView.mm │ ├── Recipes/ │ │ ├── Recipe 10.1 - Temporally varying selection.txt │ │ ├── Recipe 10.2 - Spatially varying selection.txt │ │ ├── Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis I.txt │ │ ├── Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis II.txt │ │ ├── Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection I.txt │ │ ├── Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection II.txt │ │ ├── Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection III.txt │ │ ├── Recipe 10.4.2 - Fitness as a function of population composition, Cultural effects on fitness.txt │ │ ├── Recipe 10.4.3 - Fitness as a function of population composition, Kin selection and the green-beard effect.txt │ │ ├── Recipe 10.5 - Changing selection coefficients with setSelectionCoeff().txt │ │ ├── Recipe 10.6 - Varying the dominance coefficient among mutations I.txt │ │ ├── Recipe 10.6 - Varying the dominance coefficient among mutations II.txt │ │ ├── Recipe 11.1 - Assortative mating.txt │ │ ├── Recipe 11.2 - Sequential mate search I.txt │ │ ├── Recipe 11.2 - Sequential mate search II.txt │ │ ├── Recipe 11.3 - Gametophytic self-incompatibility.txt │ │ ├── Recipe 12.1 - Social learning of cultural traits.txt │ │ ├── Recipe 12.2 - Lethal epistasis I.txt │ │ ├── Recipe 12.2 - Lethal epistasis II.txt │ │ ├── Recipe 12.3 - Simulating gene drive.txt │ │ ├── Recipe 12.4 - Suppressing hermaphroditic selfing.txt │ │ ├── Recipe 12.5 - Tracking separate sexes in script.txt │ │ ├── Recipe 13.1 - Polygenic selection.txt │ │ ├── Recipe 13.2 - A simple model of variable QTL effect sizes.txt │ │ ├── Recipe 13.3 - A model of discrete QTL effects across multiple chromosomes.txt │ │ ├── Recipe 13.4 - A quantitative genetics model with heritability.txt │ │ ├── Recipe 13.5 - A QTL-based model with two quantitative phenotypic traits and pleiotropy.txt │ │ ├── Recipe 13.6 - A variety of fitness functions I (stabilizing selection).txt │ │ ├── Recipe 13.6 - A variety of fitness functions II (directional selection).txt │ │ ├── Recipe 13.6 - A variety of fitness functions III (disruptive selection).txt │ │ ├── Recipe 13.6 - A variety of fitness functions IV (truncation selection).txt │ │ ├── Recipe 13.7 - Negative frequency-dependence on a quantitative trait.txt │ │ ├── Recipe 14.1 - Relatedness, inbreeding, and heterozygosity.txt │ │ ├── Recipe 14.10 - Modeling transposable elements.txt │ │ ├── Recipe 14.11 - Modeling opposite ends of a chromosome I.txt │ │ ├── Recipe 14.11 - Modeling opposite ends of a chromosome II.txt │ │ ├── Recipe 14.12 - Visualizing ancestry and admixture with mutation() callbacks.txt │ │ ├── Recipe 14.13 - Modeling biallelic loci with a mutation() callback I.txt │ │ ├── Recipe 14.13 - Modeling biallelic loci with a mutation() callback II.txt │ │ ├── Recipe 14.14 - Modeling biallelic loci in script.txt │ │ ├── Recipe 14.15 - Using runs of homozygosity (ROH) to track inbreeding.txt │ │ ├── Recipe 14.16 - Visualizing linkage disequilibrium.txt │ │ ├── Recipe 14.2 - Mortality-based fitness I.txt │ │ ├── Recipe 14.2 - Mortality-based fitness II.txt │ │ ├── Recipe 14.2 - Mortality-based fitness III.txt │ │ ├── Recipe 14.3 - Reading initial simulation state from an MS output file I.txt │ │ ├── Recipe 14.3 - Reading initial simulation state from an MS output file II.txt │ │ ├── Recipe 14.4 - Modeling chromosomal inversions with a recombination() callback.txt │ │ ├── Recipe 14.5 - Estimating model parameters with ABC.txt │ │ ├── Recipe 14.6 - Tracking local ancestry along the chromosome.txt │ │ ├── Recipe 14.7 - Live plotting with R using system().txt │ │ ├── Recipe 14.8 - Using mutation rate variation to model varying functional density.txt │ │ ├── Recipe 14.9 - Modeling microsatellites.txt │ │ ├── Recipe 15.1 - A minimal nonWF model.txt │ │ ├── Recipe 15.10 - Recording a pedigree.txt │ │ ├── Recipe 15.11 - Dynamic population structure in nonWF models.txt │ │ ├── Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model I.txt │ │ ├── Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model II.txt │ │ ├── Recipe 15.13 - Range expansion in a stepping-stone model I.txt │ │ ├── Recipe 15.13 - Range expansion in a stepping-stone model II.txt │ │ ├── Recipe 15.14 - Logistic population growth with the Beverton-Holt model.txt │ │ ├── Recipe 15.2 - Age structure (a life table model).txt │ │ ├── Recipe 15.3 - Handling all reproduction at once with big bang reproduction.txt │ │ ├── Recipe 15.4 - Monogamous mating and variation in litter size.txt │ │ ├── Recipe 15.5 - Beneficial mutations and absolute fitness.txt │ │ ├── Recipe 15.6 - A metapopulation extinction-colonization model.txt │ │ ├── Recipe 15.7 - Habitat choice.txt │ │ ├── Recipe 15.8 - Evolutionary rescue after environmental change.txt │ │ ├── Recipe 15.9 - Litter size and parental investment.txt │ │ ├── Recipe 16.1 - Pollen flow.txt │ │ ├── Recipe 16.10 - Modeling pseudo-autosomal regions (PARs) with addMultiRecombinant().txt │ │ ├── Recipe 16.11 - Life-long monogamous mating.txt │ │ ├── Recipe 16.2 - Following a pedigree.txt │ │ ├── Recipe 16.3 - Modeling clonal haploid bacteria with horizontal gene transfer.txt │ │ ├── Recipe 16.4 - Alternation of generations.txt │ │ ├── Recipe 16.5 - Meiotic drive.txt │ │ ├── Recipe 16.6 - Sperm storage with a survival() callback.txt │ │ ├── Recipe 16.7 - Tracking separate sexes in script, nonWF style.txt │ │ ├── Recipe 16.8 - Modeling haplodiploidy with addRecombinant().txt │ │ ├── Recipe 16.9 - Complex multi-chromosome inheritance with addMultiRecombinant().txt │ │ ├── Recipe 17.1 - A simple 2D continuous-space model.txt │ │ ├── Recipe 17.10 - A simple biogeographic landscape model.txt │ │ ├── Recipe 17.11 - Local adaptation on a heterogeneous landscape map.txt │ │ ├── Recipe 17.12 - Periodic spatial boundaries.txt │ │ ├── Recipe 17.13 - Density-dependent fecundity with summarizeIndividuals().txt │ │ ├── Recipe 17.14 - Directed dispersal with the SpatialMap class.txt │ │ ├── Recipe 17.15 - Spatial competition and spatial mate choice in a nonWF model.txt │ │ ├── Recipe 17.16 - A spatial model with carrying-capacity density.txt │ │ ├── Recipe 17.17 - A spatial epidemiological S-I-R model.txt │ │ ├── Recipe 17.18 - A sexual, age-structured spatial model.txt │ │ ├── Recipe 17.19 - Modeling indirect competition mediated by resource availability.txt │ │ ├── Recipe 17.2 - Spatial competition.txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions I (stopping boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions II (reflecting boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions III (absorbing boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions IV (reprising boundaries).txt │ │ ├── Recipe 17.3 - Boundaries and boundary conditions V (dispersal kernels).txt │ │ ├── Recipe 17.4 - Mate choice with a spatial kernel.txt │ │ ├── Recipe 17.5 - Mate choice with a nearest-neighbor search.txt │ │ ├── Recipe 17.6 - Divergence due to phenotypic competition with an interaction() callback.txt │ │ ├── Recipe 17.7 - Modeling phenotype as a spatial dimension.txt │ │ ├── Recipe 17.8 - Sympatric speciation facilitated by assortative mating.txt │ │ ├── Recipe 17.9 - Speciation due to spatial variation in selection.txt │ │ ├── Recipe 18.1 - A minimal tree-seq model.txt │ │ ├── Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation I.txt │ │ ├── Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation II.py │ │ ├── Recipe 18.11 - Optimizing tree-sequence simplification.txt │ │ ├── Recipe 18.2 - Overlaying neutral mutations.py │ │ ├── Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt │ │ ├── Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt │ │ ├── Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt │ │ ├── Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py │ │ ├── Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) I.txt │ │ ├── Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) II.py │ │ ├── Recipe 18.6 - Measuring the coalescence time of a model I.txt │ │ ├── Recipe 18.6 - Measuring the coalescence time of a model II.txt │ │ ├── Recipe 18.7 - Analyzing selection coefficients in Python with tskit I.txt │ │ ├── Recipe 18.7 - Analyzing selection coefficients in Python with tskit II.py │ │ ├── Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history I.py │ │ ├── Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history II.txt │ │ ├── Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history III.py │ │ ├── Recipe 18.9 - Starting a sexual nonWF model with a coalescent history I.py │ │ ├── Recipe 18.9 - Starting a sexual nonWF model with a coalescent history II.txt │ │ ├── Recipe 19.1 - A simple neutral nucleotide-based model.txt │ │ ├── Recipe 19.10 - Varying the mutation rate along the chromosome in a nucleotide-based model.txt │ │ ├── Recipe 19.11 - Modeling GC-biased gene conversion (gBGC).txt │ │ ├── Recipe 19.12 - Reading VCF files to create nucleotide-based SNPs.txt │ │ ├── Recipe 19.13 - Tree-sequence recording and nucleotide-based models I.txt │ │ ├── Recipe 19.13 - Tree-sequence recording and nucleotide-based models II.py │ │ ├── Recipe 19.13 - Tree-sequence recording and nucleotide-based models III.py │ │ ├── Recipe 19.14 - Modeling identity by state (IBS) (uniquing mutations with a mutation() callback).txt │ │ ├── Recipe 19.15 - Modeling identity by state (IBS) (uniquing back-mutations to the ancestral state).txt │ │ ├── Recipe 19.2 - Reading an ancestral nucleotide sequence from a FASTA file.txt │ │ ├── Recipe 19.3 - Sequence output from nucleotide-based models.txt │ │ ├── Recipe 19.4 - Back-mutations, independent mutational lineages, and VCF output.txt │ │ ├── Recipe 19.5 - Modeling elevated CpG mutation rates and equilibrium nucleotide frequencies.txt │ │ ├── Recipe 19.6 - A nucleotide-based model with introduced non-nucleotide-based mutations.txt │ │ ├── Recipe 19.7 - Using standard SLiM fitness effects with nucleotides (modeling synonymous sites).txt │ │ ├── Recipe 19.8 - Defining sequence-based fitness effects at the nucleotide level.txt │ │ ├── Recipe 19.9 - Defining sequence-based fitness effects at the amino acid level.txt │ │ ├── Recipe 20.1 - A simple multispecies model.txt │ │ ├── Recipe 20.2 - A two-species model.txt │ │ ├── Recipe 20.3 - A deterministic host-parasitoid model.txt │ │ ├── Recipe 20.4 - An individual-based host-parasitoid model I.txt │ │ ├── Recipe 20.4 - An individual-based host-parasitoid model II.txt │ │ ├── Recipe 20.5 - A continuous-space host-parasitoid model.txt │ │ ├── Recipe 20.6 - A coevolutionary host-parasitoid trait-matching model.txt │ │ ├── Recipe 20.7 - A coevolutionary host-parasite matching-allele model.txt │ │ ├── Recipe 20.8 - Within-host reproduction in a host-pathogen model.txt │ │ ├── Recipe 4.1 - A basic neutral simulation.txt │ │ ├── Recipe 4.1.10 - Using symbolic constants for model parameters.txt │ │ ├── Recipe 4.2.1 - Basic output, Entire population.txt │ │ ├── Recipe 4.2.2 - Basic output, Random population sample.txt │ │ ├── Recipe 4.2.3 - Basic output, Sampling individuals for output.txt │ │ ├── Recipe 4.2.4 - Basic output, Substitutions.txt │ │ ├── Recipe 4.2.5 - Basic output, Automatic logging with LogFile.txt │ │ ├── Recipe 4.2.6 - Basic output, Custom output with Eidos.txt │ │ ├── Recipe 5.1.1 - Subpopulation size, Instantaneous changes.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth I.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth II.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth III.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth IV.txt │ │ ├── Recipe 5.1.2 - Subpopulation size, Exponential growth V.txt │ │ ├── Recipe 5.1.4 - Subpopulation size, Cyclical changes.txt │ │ ├── Recipe 5.1.5 - Subpopulation size, Context-dependent changes (Muller's Ratchet).txt │ │ ├── Recipe 5.2.1 - Population structure, Adding subpopulations.txt │ │ ├── Recipe 5.2.2 - Population structure, Removing subpopulations.txt │ │ ├── Recipe 5.2.3 - Population structure, Splitting subpopulations.txt │ │ ├── Recipe 5.2.4 - Population structure, Joining subpopulations.txt │ │ ├── Recipe 5.3.1 - Migration and admixture, A linear stepping-stone model.txt │ │ ├── Recipe 5.3.2 - Migration and admixture, A non-spatial metapopulation.txt │ │ ├── Recipe 5.3.3 - Migration and admixture, A two-dimensional subpopulation matrix.txt │ │ ├── Recipe 5.3.4 - Migration and admixture, A random, sparse spatial metapopulation.txt │ │ ├── Recipe 5.3.5 - Migration and admixture, Reading a migration matrix from a file.txt │ │ ├── Recipe 5.4 - The Gravel et al. (2011) model of human evolution I.txt │ │ ├── Recipe 5.4 - The Gravel et al. (2011) model of human evolution II.txt │ │ ├── Recipe 5.5 - Rescaling population sizes to improve simulation performance I.txt │ │ ├── Recipe 5.5 - Rescaling population sizes to improve simulation performance II.txt │ │ ├── Recipe 6.1 - Genomic structure, Part I (Mutation types and fitness effects).txt │ │ ├── Recipe 6.2 - Genomic structure, Part II (Genomic element types).txt │ │ ├── Recipe 6.3 - Genomic structure, Part III (Chromosome organization).txt │ │ ├── Recipe 6.4 - Genomic structure, Part IV (Custom display colors in SLiMgui).txt │ │ ├── Recipe 8.1.1 - Reproduction, Enabling separate sexes.txt │ │ ├── Recipe 8.1.2 - Reproduction, Sex ratios I.txt │ │ ├── Recipe 8.1.2 - Reproduction, Sex ratios II.txt │ │ ├── Recipe 8.1.3 - Reproduction, Selfing in hermaphroditic populations.txt │ │ ├── Recipe 8.1.4 - Reproduction, Cloning I.txt │ │ ├── Recipe 8.1.4 - Reproduction, Cloning II.txt │ │ ├── Recipe 8.2.1 - Recombination, Making a random recombination map.txt │ │ ├── Recipe 8.2.2 - Recombination, Reading a recombination map from a file.txt │ │ ├── Recipe 8.2.3 - Recombination, Unlinked loci.txt │ │ ├── Recipe 8.2.4 - Recombination, Gene conversion.txt │ │ ├── Recipe 8.3.1 - Multiple diploid autosomes.txt │ │ ├── Recipe 8.3.2 - Clonal haploids and chromosome types.txt │ │ ├── Recipe 8.3.3 - Haploids with recombination.txt │ │ ├── Recipe 8.3.4 - Sex-chromosome evolution and null haplosomes.txt │ │ ├── Recipe 8.3.5 - Modeling the full human genome.txt │ │ ├── Recipe 8.3.6 - A model of bryophytes with UV sex determination.txt │ │ ├── Recipe 8.3.7 - Output from multiple-chromosome models.txt │ │ ├── Recipe 9.1 - Introducing adaptive mutations.txt │ │ ├── Recipe 9.10 - Tracking the fate of background mutations.txt │ │ ├── Recipe 9.11 - Effective population size versus census population size.txt │ │ ├── Recipe 9.12 - Observing the site frequency spectrum (SFS) during selective sweeps.txt │ │ ├── Recipe 9.2 - Making sweeps conditional on fixation.txt │ │ ├── Recipe 9.3 - Making sweeps conditional on establishment.txt │ │ ├── Recipe 9.4 - Partial sweeps.txt │ │ ├── Recipe 9.5.1 - A soft sweep from recurrent de novo mutations in a large population.txt │ │ ├── Recipe 9.5.2 - A soft sweep with a fixed de novo mutation schedule.txt │ │ ├── Recipe 9.5.3 - A soft sweep with a random de novo mutation schedule.txt │ │ ├── Recipe 9.6.1 - A sweep from standing variation at a random locus.txt │ │ ├── Recipe 9.6.2 - A sweep from standing variation at a predetermined locus.txt │ │ ├── Recipe 9.7 - Adaptive introgression.txt │ │ ├── Recipe 9.8 - Fixation probabilities under Hill-Robertson interference.txt │ │ ├── Recipe 9.9 - Keeping a reference to a sweep mutation.txt │ │ └── _README.txt │ ├── SLiMDocument.h │ ├── SLiMDocument.mm │ ├── SLiMDocumentController.h │ ├── SLiMDocumentController.mm │ ├── SLiMHelpCallbacks.rtf │ ├── SLiMHelpClasses.rtf │ ├── SLiMHelpFunctions.rtf │ ├── SLiMPDFDocument.h │ ├── SLiMPDFDocument.mm │ ├── SLiMPDFView.h │ ├── SLiMPDFView.mm │ ├── SLiMPDFWindow.xib │ ├── SLiMPDFWindowController.h │ ├── SLiMPDFWindowController.mm │ ├── SLiMWindowController.h │ ├── SLiMWindowController.mm │ ├── SLiMgui.entitlements │ ├── SLiMguiLegacy-Info.plist │ ├── SLiMguiLegacy_multi-Info.plist │ ├── SLiMguiLegacy_multi.entitlements │ ├── Tip_END.rtfd/ │ │ ├── Pasted Graphic.tiff │ │ └── TXT.rtf │ ├── Tips/ │ │ ├── Tip 1 optclick for help.rtfd/ │ │ │ ├── Pasted Graphic 5.tiff │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 10 generation textfield.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ ├── Pasted Graphic 2.tiff │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 11 chromosome subrange.rtfd/ │ │ │ ├── Pasted Graphic 2.tiff │ │ │ ├── Pasted Graphic 3.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 12 see script object text.rtfd/ │ │ │ ├── Pasted Graphic 2.tiff │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 13 speed slider.rtfd/ │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 14 console window.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 15 variable browser.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ ├── Pasted Graphic 2.tiff │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 16 chromosome muttypes.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ ├── Pasted Graphic 2.tiff │ │ │ ├── Pasted Graphic 4.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 17 population display options.rtfd/ │ │ │ ├── Pasted Graphic 6.tiff │ │ │ ├── Pasted Graphic 7.tiff │ │ │ ├── Pasted Graphic 8.tiff │ │ │ ├── Pasted Graphic 9.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 18 profiling simulations.rtfd/ │ │ │ └── TXT.rtf │ │ ├── Tip 19 chromosome haplotypes.rtfd/ │ │ │ └── TXT.rtf │ │ ├── Tip 2 autogen script.rtfd/ │ │ │ ├── Pasted Graphic 2.tiff │ │ │ ├── Pasted Graphic 3.tiff │ │ │ ├── Pasted Graphic 4.tiff │ │ │ ├── Pasted Graphic 5.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 20 script prettyprinting.rtfd/ │ │ │ └── TXT.rtf │ │ ├── Tip 21 MutationType DFE visualization.rtfd/ │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 3 see prototypes.rtfd/ │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 4 code completion.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ ├── Pasted Graphic 4.tiff │ │ │ ├── Pasted Graphic 5.tiff │ │ │ ├── Pasted Graphic 6.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 5 config chromosome view.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 6 fast recipe access.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 7 comment in out.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ ├── Tip 8 shift left right.rtfd/ │ │ │ ├── Pasted Graphic 1.tiff │ │ │ ├── Pasted Graphic.tiff │ │ │ └── TXT.rtf │ │ └── Tip 9 help by title or content.rtfd/ │ │ ├── Pasted Graphic 1.tiff │ │ └── TXT.rtf │ ├── TipsWindowController.h │ ├── TipsWindowController.m │ ├── main.m │ ├── plot.h │ ├── plot.mm │ ├── slim_gui.h │ └── slim_gui.mm ├── TO_DO ├── VERSIONS ├── cmake/ │ ├── AboutTheseModules.cmake │ ├── GetGitRevisionDescription.cmake │ ├── GetGitRevisionDescription.cmake.in │ ├── GitSHA1.cpp.in │ ├── GitSHA1.h │ ├── GitSHA1_Xcode.cpp │ ├── GitSHA1_qmake.cpp │ ├── LICENSE_1_0.txt │ ├── README.markdown │ ├── _README.txt │ └── toolchain-mingw64.cmake ├── codecov.yml ├── core/ │ ├── chromosome.cpp │ ├── chromosome.h │ ├── community.cpp │ ├── community.h │ ├── community_eidos.cpp │ ├── core.pro │ ├── genomic_element.cpp │ ├── genomic_element.h │ ├── genomic_element_type.cpp │ ├── genomic_element_type.h │ ├── haplosome.cpp │ ├── haplosome.h │ ├── individual.cpp │ ├── individual.h │ ├── interaction_type.cpp │ ├── interaction_type.h │ ├── log_file.cpp │ ├── log_file.h │ ├── main.cpp │ ├── mutation.cpp │ ├── mutation.h │ ├── mutation_run.cpp │ ├── mutation_run.h │ ├── mutation_type.cpp │ ├── mutation_type.h │ ├── polymorphism.cpp │ ├── polymorphism.h │ ├── population.cpp │ ├── population.h │ ├── slim_eidos_block.cpp │ ├── slim_eidos_block.h │ ├── slim_functions.cpp │ ├── slim_functions.h │ ├── slim_globals.cpp │ ├── slim_globals.h │ ├── slim_test.cpp │ ├── slim_test.h │ ├── slim_test_core.cpp │ ├── slim_test_genetics.cpp │ ├── slim_test_other.cpp │ ├── slim_test_parallel.h │ ├── sparse_vector.cpp │ ├── sparse_vector.h │ ├── spatial_kernel.cpp │ ├── spatial_kernel.h │ ├── spatial_map.cpp │ ├── spatial_map.h │ ├── species.cpp │ ├── species.h │ ├── species_eidos.cpp │ ├── subpopulation.cpp │ ├── subpopulation.h │ ├── substitution.cpp │ └── substitution.h ├── data/ │ ├── applications/ │ │ └── org.messerlab.slimgui.desktop │ ├── metainfo/ │ │ ├── org.messerlab.slimgui.appdata.xml │ │ └── org.messerlab.slimgui.metainfo.xml │ └── mime/ │ └── packages/ │ └── org.messerlab.slimgui-mime.xml ├── debian/ │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── rules │ ├── slimsim.install │ └── source/ │ └── format ├── eidos/ │ ├── eidos.pro │ ├── eidos_ast_node.cpp │ ├── eidos_ast_node.h │ ├── eidos_beep.cpp │ ├── eidos_beep.h │ ├── eidos_call_signature.cpp │ ├── eidos_call_signature.h │ ├── eidos_class_DataFrame.cpp │ ├── eidos_class_DataFrame.h │ ├── eidos_class_Dictionary.cpp │ ├── eidos_class_Dictionary.h │ ├── eidos_class_Image.cpp │ ├── eidos_class_Image.h │ ├── eidos_class_Object.cpp │ ├── eidos_class_Object.h │ ├── eidos_class_TestElement.cpp │ ├── eidos_class_TestElement.h │ ├── eidos_functions.cpp │ ├── eidos_functions.h │ ├── eidos_functions_colors.cpp │ ├── eidos_functions_distributions.cpp │ ├── eidos_functions_files.cpp │ ├── eidos_functions_math.cpp │ ├── eidos_functions_matrices.cpp │ ├── eidos_functions_other.cpp │ ├── eidos_functions_stats.cpp │ ├── eidos_functions_strings.cpp │ ├── eidos_functions_values.cpp │ ├── eidos_globals.cpp │ ├── eidos_globals.h │ ├── eidos_interpreter.cpp │ ├── eidos_interpreter.h │ ├── eidos_intrusive_ptr.h │ ├── eidos_object_pool.h │ ├── eidos_openmp.h │ ├── eidos_property_signature.cpp │ ├── eidos_property_signature.h │ ├── eidos_rng.cpp │ ├── eidos_rng.h │ ├── eidos_script.cpp │ ├── eidos_script.h │ ├── eidos_simd.h │ ├── eidos_sorting.cpp │ ├── eidos_sorting.h │ ├── eidos_sorting.inc │ ├── eidos_symbol_table.cpp │ ├── eidos_symbol_table.h │ ├── eidos_test.cpp │ ├── eidos_test.h │ ├── eidos_test_builtins.h │ ├── eidos_test_functions_math.cpp │ ├── eidos_test_functions_other.cpp │ ├── eidos_test_functions_statistics.cpp │ ├── eidos_test_functions_vector.cpp │ ├── eidos_test_operators_arithmetic.cpp │ ├── eidos_test_operators_comparison.cpp │ ├── eidos_test_operators_other.cpp │ ├── eidos_test_parallel.h │ ├── eidos_tinycolormap.h │ ├── eidos_token.cpp │ ├── eidos_token.h │ ├── eidos_type_interpreter.cpp │ ├── eidos_type_interpreter.h │ ├── eidos_type_table.cpp │ ├── eidos_type_table.h │ ├── eidos_value.cpp │ ├── eidos_value.h │ ├── json.hpp │ ├── json_fwd.hpp │ ├── lodepng.cpp │ ├── lodepng.h │ ├── pcg_extras.hpp │ ├── pcg_random.hpp │ ├── robin_hood.h │ └── sleef/ │ ├── LICENSE │ ├── SLEEF_HEADER_GENERATION.md │ ├── generate_arm_sleef.sh │ ├── generate_avx2_sleef.sh │ ├── sleef_config.h │ ├── sleefinline_advsimd.h │ └── sleefinline_avx2.h ├── eidos_multi.entitlements ├── eidos_zlib/ │ ├── ChangeLog │ ├── README │ ├── adler32.c │ ├── compress.c │ ├── crc32.c │ ├── crc32.h │ ├── deflate.c │ ├── deflate.h │ ├── eidos_zlib.pro │ ├── gzguts.h │ ├── gzlib.c │ ├── gzwrite.c │ ├── trees.c │ ├── trees.h │ ├── zconf.h │ ├── zlib.h │ ├── zutil.c │ └── zutil.h ├── eidostool/ │ └── main.cpp ├── gsl/ │ ├── AUTHORS │ ├── COPYING │ ├── README │ ├── THANKS │ ├── _README │ ├── blas/ │ │ ├── blas.c │ │ ├── gsl_blas.h │ │ └── gsl_blas_types.h │ ├── block/ │ │ ├── gsl_block.h │ │ ├── gsl_block_double.h │ │ ├── gsl_check_range.h │ │ ├── init.c │ │ └── init_source.inc │ ├── build.h │ ├── cblas/ │ │ ├── cblas.h │ │ ├── daxpy.c │ │ ├── ddot.c │ │ ├── dgemv.c │ │ ├── dtrmv.c │ │ ├── dtrsv.c │ │ ├── error_cblas.h │ │ ├── error_cblas_l2.h │ │ ├── gsl_cblas.h │ │ ├── source_axpy_r.h │ │ ├── source_dot_r.h │ │ ├── source_gemv_r.h │ │ ├── source_trmv_r.h │ │ ├── source_trsv_r.h │ │ └── xerbla.c │ ├── cdf/ │ │ ├── beta_inc.inc │ │ ├── gauss.c │ │ ├── gaussinv.c │ │ ├── gsl_cdf.h │ │ ├── rat_eval.h │ │ └── tdist.c │ ├── complex/ │ │ ├── gsl_complex.h │ │ ├── gsl_complex_math.h │ │ ├── inline.c │ │ └── math.c │ ├── config.h │ ├── err/ │ │ ├── error.c │ │ ├── gsl_message.h │ │ ├── message.c │ │ └── stream.c │ ├── gsl.pro │ ├── gsl_errno.h │ ├── gsl_inline.h │ ├── gsl_machine.h │ ├── gsl_math.h │ ├── gsl_minmax.h │ ├── gsl_nan.h │ ├── gsl_pow_int.h │ ├── gsl_precision.h │ ├── gsl_types.h │ ├── gsl_version.h │ ├── interpolation/ │ │ ├── accel.c │ │ ├── akima.c │ │ ├── bicubic.c │ │ ├── bilinear.c │ │ ├── cspline.c │ │ ├── gsl_interp.h │ │ ├── gsl_interp2d.h │ │ ├── gsl_spline.h │ │ ├── gsl_spline2d.h │ │ ├── inline.c │ │ ├── integ_eval.h │ │ ├── interp.c │ │ ├── interp2d.c │ │ ├── linear.c │ │ ├── spline.c │ │ └── spline2d.c │ ├── linalg/ │ │ ├── cholesky.c │ │ ├── gsl_linalg.h │ │ ├── lu.c │ │ ├── tridiag.c │ │ └── tridiag.h │ ├── matrix/ │ │ ├── copy.c │ │ ├── copy_source.inc │ │ ├── gsl_matrix.h │ │ ├── gsl_matrix_double.h │ │ ├── init.c │ │ ├── init_source.inc │ │ ├── matrix.c │ │ ├── rowcol.c │ │ ├── rowcol_source.inc │ │ ├── submatrix.c │ │ ├── submatrix_source.inc │ │ ├── swap.c │ │ ├── swap_source.inc │ │ └── view.h │ ├── permutation/ │ │ ├── gsl_permutation.h │ │ ├── gsl_permute.h │ │ ├── gsl_permute_double.h │ │ ├── gsl_permute_matrix.h │ │ ├── gsl_permute_matrix_double.h │ │ ├── gsl_permute_vector.h │ │ ├── gsl_permute_vector_double.h │ │ ├── init.c │ │ ├── permutation.c │ │ ├── permute.c │ │ └── permute_source.inc │ ├── randist/ │ │ ├── beta.c │ │ ├── binomial_tpe.c │ │ ├── cauchy.c │ │ ├── chisq.c │ │ ├── dirichlet.c │ │ ├── discrete.c │ │ ├── exponential.c │ │ ├── fdist.c │ │ ├── gamma.c │ │ ├── gauss.c │ │ ├── gausszig.c │ │ ├── geometric.c │ │ ├── gsl_randist.h │ │ ├── laplace.c │ │ ├── lognormal.c │ │ ├── multinomial.c │ │ ├── mvgauss.c │ │ ├── nbinomial.c │ │ ├── poisson.c │ │ ├── shuffle.c │ │ ├── tdist.c │ │ └── weibull.c │ ├── rng/ │ │ ├── gsl_rng.h │ │ ├── inline.c │ │ ├── mt.c │ │ ├── rng.c │ │ └── taus.c │ ├── specfunc/ │ │ ├── beta.c │ │ ├── cheb_eval.inc │ │ ├── chebyshev.h │ │ ├── check.h │ │ ├── elementary.c │ │ ├── erfc.c │ │ ├── error.h │ │ ├── eval.h │ │ ├── exp.c │ │ ├── expint.c │ │ ├── gamma.c │ │ ├── gamma_inc.c │ │ ├── gsl_sf_elementary.h │ │ ├── gsl_sf_erf.h │ │ ├── gsl_sf_exp.h │ │ ├── gsl_sf_expint.h │ │ ├── gsl_sf_gamma.h │ │ ├── gsl_sf_log.h │ │ ├── gsl_sf_pow_int.h │ │ ├── gsl_sf_psi.h │ │ ├── gsl_sf_result.h │ │ ├── gsl_sf_trig.h │ │ ├── gsl_sf_zeta.h │ │ ├── log.c │ │ ├── pow_int.c │ │ ├── psi.c │ │ ├── trig.c │ │ └── zeta.c │ ├── sys/ │ │ ├── coerce.c │ │ ├── fdiv.c │ │ ├── gsl_sys.h │ │ ├── infnan.c │ │ ├── minmax.c │ │ └── pow_int.c │ ├── templates_off.h │ ├── templates_on.h │ └── vector/ │ ├── copy.c │ ├── copy_source.inc │ ├── gsl_vector.h │ ├── gsl_vector_double.h │ ├── init.c │ ├── init_source.inc │ ├── oper.c │ ├── oper_source.inc │ ├── vector.c │ ├── view.c │ ├── view.h │ └── view_source.inc ├── simd_benchmarks/ │ ├── README.md │ ├── SIMD_BUILD_FLAGS.md │ ├── benchmark_all_kernels.slim │ ├── dnorm_benchmark.eidos │ ├── run_benchmarks.sh │ ├── simd_benchmark.eidos │ ├── sleef_benchmark.slim │ └── slim_benchmark.slim ├── slim_multi.entitlements ├── sonar-project.properties ├── treerec/ │ ├── _README │ ├── implementation.md │ ├── tests/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── conda-requirements.txt │ │ ├── conftest.py │ │ ├── environment.yml │ │ ├── init.slim │ │ ├── init_marked_mutations.slim │ │ ├── pip-requirements.txt │ │ ├── recipe_specs.py │ │ ├── test_consistency.py │ │ ├── test_metadata_schemas.py │ │ ├── test_recipes/ │ │ │ ├── dont_test_recapitation.py │ │ │ ├── dont_test_recapitation.slim │ │ │ ├── test_000_ancestral_marks.slim │ │ │ ├── test_000_ongoing_muts.slim │ │ │ ├── test_000_sexual_WF.slim │ │ │ ├── test_000_sexual_nonwf.slim │ │ │ ├── test_000_simple_nonwf.slim │ │ │ ├── test_000_withdraw_indivs.slim │ │ │ ├── test_002_quick_neutral.slim │ │ │ ├── test_004 simple sexual A.slim │ │ │ ├── test_005 simple sexual X.slim │ │ │ ├── test_006 simple sexual Y.slim │ │ │ ├── test_007 cloning and selfing.slim │ │ │ ├── test_008 cyclical subpop.slim │ │ │ ├── test_009 linear island.slim │ │ │ ├── test_013 gene conversion.slim │ │ │ ├── test_024 gene drive.slim │ │ │ ├── test_038 pure cloning.slim │ │ │ ├── test_039 pure selfing.slim │ │ │ ├── test_040 pure cloning sexual.slim │ │ │ ├── test_042 haploid clonals.slim │ │ │ ├── test_097 modeling nucleotides.slim │ │ │ ├── test_1610_modeling pseudo-autosomal regions.slim │ │ │ ├── test_169_complex multi-chromosome inheritance.slim │ │ │ ├── test_831_multiple diploid autosomes.slim │ │ │ ├── test_832_clonal haploids and chromosome types.slim │ │ │ ├── test_833_haploids with recombination.slim │ │ │ ├── test_834_sex-chromosome evolution and null haplosomes.slim │ │ │ ├── test_836_output from multiple-chromosome models.slim │ │ │ ├── test_____H-_chromosome.slim │ │ │ ├── test_____all_the_chromosome_types.slim │ │ │ ├── test_____all_the_chromosomes.slim │ │ │ ├── test_____ancestral_marks.slim │ │ │ ├── test_____multipops.slim │ │ │ ├── test_____no_generations.slim │ │ │ ├── test_____pop_names_nondefault.slim │ │ │ ├── test_____pop_names_pX.slim │ │ │ ├── test_____remember_individuals.slim │ │ │ ├── test_____retain_and_remember_individuals.slim │ │ │ ├── test_____retain_individuals_nonWF.slim │ │ │ ├── test_____retain_individuals_nonWF_unary.slim │ │ │ ├── test_____retain_individuals_unary.slim │ │ │ ├── test_____sexual_WF.slim │ │ │ ├── test_____sexual_nonwf.slim │ │ │ ├── test_____simple_none.slim │ │ │ ├── test_____simple_nonwf.slim │ │ │ ├── test_____simple_not_perm.slim │ │ │ ├── test_____simple_perm.slim │ │ │ └── test_____withdraw_indivs.slim │ │ └── test_specific_recipes.py │ └── tskit/ │ ├── convert.c │ ├── convert.h │ ├── core.c │ ├── core.h │ ├── genotypes.c │ ├── genotypes.h │ ├── haplotype_matching.c │ ├── haplotype_matching.h │ ├── kastore/ │ │ ├── kastore.c │ │ └── kastore.h │ ├── stats.c │ ├── stats.h │ ├── tables.c │ ├── tables.h │ ├── text_input.c │ ├── text_input.h │ ├── trees.c │ ├── trees.h │ └── tskit.pro └── windows_compat/ ├── gnulib/ │ ├── Makefile.am │ ├── Makefile.in │ ├── aclocal.m4 │ ├── build-aux/ │ │ ├── compile │ │ ├── config.guess │ │ ├── config.sub │ │ ├── depcomp │ │ ├── install-sh │ │ └── missing │ ├── config.h.in │ ├── configure │ ├── configure.ac │ ├── gllib/ │ │ ├── Makefile.am │ │ ├── Makefile.in │ │ ├── _Noreturn.h │ │ ├── arg-nonnull.h │ │ ├── c++defs.h │ │ ├── cdefs.h │ │ ├── errno.in.h │ │ ├── execinfo.c │ │ ├── execinfo.in.h │ │ ├── fd-hook.c │ │ ├── fd-hook.h │ │ ├── getdelim.c │ │ ├── gethostname.c │ │ ├── getline.c │ │ ├── gettimeofday.c │ │ ├── libc-config.h │ │ ├── limits.in.h │ │ ├── msvc-inval.c │ │ ├── msvc-inval.h │ │ ├── msvc-nothrow.c │ │ ├── msvc-nothrow.h │ │ ├── random.c │ │ ├── random_r.c │ │ ├── sockets.c │ │ ├── sockets.h │ │ ├── stdalign.in.h │ │ ├── stddef.in.h │ │ ├── stdint.in.h │ │ ├── stdio.in.h │ │ ├── stdlib.in.h │ │ ├── sys_resource.in.h │ │ ├── sys_socket.c │ │ ├── sys_socket.in.h │ │ ├── sys_stat.in.h │ │ ├── sys_time.in.h │ │ ├── sys_types.in.h │ │ ├── sys_uio.in.h │ │ ├── sys_utsname.in.h │ │ ├── time.in.h │ │ ├── time_r.c │ │ ├── uname.c │ │ ├── unistd.c │ │ ├── unistd.in.h │ │ ├── w32sock.h │ │ └── warn-on-use.h │ └── glm4/ │ ├── 00gnulib.m4 │ ├── Makefile.am │ ├── Makefile.in │ ├── __inline.m4 │ ├── absolute-header.m4 │ ├── errno_h.m4 │ ├── execinfo.m4 │ ├── extensions.m4 │ ├── extern-inline.m4 │ ├── getdelim.m4 │ ├── gethostname.m4 │ ├── getline.m4 │ ├── gettimeofday.m4 │ ├── gnulib-common.m4 │ ├── include_next.m4 │ ├── limits-h.m4 │ ├── msvc-inval.m4 │ ├── msvc-nothrow.m4 │ ├── multiarch.m4 │ ├── off_t.m4 │ ├── pid_t.m4 │ ├── random.m4 │ ├── random_r.m4 │ ├── socketlib.m4 │ ├── sockets.m4 │ ├── socklen.m4 │ ├── sockpfaf.m4 │ ├── ssize_t.m4 │ ├── std-gnu11.m4 │ ├── stdalign.m4 │ ├── stddef_h.m4 │ ├── stdint.m4 │ ├── stdio_h.m4 │ ├── stdlib_h.m4 │ ├── sys_resource_h.m4 │ ├── sys_socket_h.m4 │ ├── sys_stat_h.m4 │ ├── sys_time_h.m4 │ ├── sys_types_h.m4 │ ├── sys_uio_h.m4 │ ├── sys_utsname_h.m4 │ ├── time_h.m4 │ ├── time_r.m4 │ ├── uname.m4 │ ├── unistd_h.m4 │ ├── warn-on-use.m4 │ ├── wchar_t.m4 │ ├── wint_t.m4 │ └── zzgnulib.m4 └── notes.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # see http://robots.thoughtbot.com/xcode-and-git-bridging-the-gap *.pbxproj binary merge=union ================================================ FILE: .github/ISSUE_TEMPLATE/issue.md ================================================ --- name: Issue about: Create a report to help us improve title: '' labels: '' assignees: '' --- Hi! Please don't ask questions about how to use SLiM on GitHub; instead, please go to the slim-discuss mailing list at https://groups.google.com/g/slim-discuss. If you have a bug report or feature request, on the other hand, you have found the right place. Please provide any information that will allow us to reproduce the bug, including a complete SLiM script (if appropriate) that is self-contained, with all defined constants set up in script rather than needing to be passed on the command line. If the script requires any external files to run, please attach those too, or provide a link to where they can be downloaded. For more guidance on how to file a good bug report, you might read "Ten simple rules for reporting a bug" (https://doi.org/10.1371/journal.pcbi.1010540). Incidentally, if you are a new SLiM user and have not yet done the SLiM Workshop, it is highly recommended to get through the initial learning curve. It is available for free online at http://benhaller.com/workshops/workshops.html. ================================================ FILE: .github/workflows/tests.yml ================================================ name: tests on: # Run tests on every push to the main branch, # on every pull request, and once a week (every monday at 05:00) push: pull_request: schedule: - cron: "0 5 * * 1" jobs: canceller: runs-on: ubuntu-latest steps: - name: Cancel previous runs uses: styfle/cancel-workflow-action@0.12.1 coverage: if: github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository == 'messerlab/slim') runs-on: ubuntu-24.04 steps: - name: Check out repository code uses: actions/checkout@v4 - name: Set up GCC run: | sudo apt-get install gcc-12 g++-12 && \ sudo update-alternatives \ --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 \ --slave /usr/bin/g++ g++ /usr/bin/g++-12 - name: Build with coverage run: | mkdir Coverage && cd Coverage && \ cmake -DCMAKE_BUILD_TYPE=Debug -DCOVERAGE=ON .. && \ make -j 2 && \ env CTEST_OUTPUT_ON_FAILURE=1 make test - name: Generate coverage report run: | sudo apt-get update && \ sudo apt-get install -y lcov && \ lcov --gcov-tool gcov-12 --ignore-errors gcov,mismatch --rc geninfo_unexecuted_blocks=1 --capture --directory Coverage --output-file coverage.info && \ lcov --gcov-tool gcov-12 --ignore-errors gcov,mismatch,unused --remove coverage.info '/usr/*' '*/gsl/*' '*/eidos_zlib/*' --output-file coverage.info - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.info fail_ci_if_error: true tests-Unix-CLI: if: github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository == 'messerlab/slim') strategy: fail-fast: false matrix: include: # Ubuntu with the oldest supported GCC # BCH 2/12/2025: Ubuntu 22.04 is being discontinued, shifting forward - { os: ubuntu-22.04, gcc: 9, python: 3.9 } - { os: ubuntu-24.04, gcc: 12, python: 3.12 } # macOS, oldest supported version # (macos-10.15, macos-11, macos-12, and macos-13 were removed by GitHub) - { os: macos-14, python: 3.9 } # macOS, newest supported version - { os: macos-26, python: 3.13 } runs-on: ${{ matrix.os }} steps: - name: Check out repository code uses: actions/checkout@v4 - name: Setup Miniforge uses: conda-incubator/setup-miniconda@v3 with: miniforge-version: latest activate-environment: anaconda-client-env python-version: ${{ matrix.python }} - name: Get Date id: get-date run: echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_OUTPUT shell: bash - name: Cache Conda env uses: actions/cache@v3 with: path: ${{ env.CONDA }}/envs key: conda-${{ runner.os }}--python-${{ matrix.python }}--${{ runner.arch }}--${{ steps.get-date.outputs.today }}-${{ hashFiles('treerec/tests/environment.yml') }}-${{ env.CACHE_NUMBER}} env: # Increase this value to reset cache if # treerec/tests/environment.yml has not changed CACHE_NUMBER: 0 id: cache - name: Update environment run: conda env update -n anaconda-client-env -f treerec/tests/environment.yml if: steps.cache.outputs.cache-hit != 'true' - name: Workaround for gcc-11 if: startsWith(matrix.os, 'ubuntu') && matrix.gcc == 11 run: | sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y && \ sudo apt-get update -y - name: Set up default GCC (in Ubuntu) if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get install gcc-${{ matrix.gcc }} g++-${{ matrix.gcc }} && \ sudo update-alternatives \ --install /usr/bin/gcc gcc /usr/bin/gcc-${{ matrix.gcc }} 100 \ --slave /usr/bin/g++ g++ /usr/bin/g++-${{ matrix.gcc }} - name: Build and test (Debug) run: | mkdir Debug && \ cd Debug && \ cmake -DCMAKE_BUILD_TYPE=Debug .. && \ make -j 2 && \ # Show any output from the test program whenever the test fails env CTEST_OUTPUT_ON_FAILURE=1 make test - name: Build and test (Release) run: | mkdir Release && \ cd Release && \ cmake -DCMAKE_BUILD_TYPE=Release .. && \ make -j 2 && \ # Show any output from the test program whenever the test fails env CTEST_OUTPUT_ON_FAILURE=1 make test - name: Treesequence tests shell: bash -el {0} run: | conda activate anaconda-client-env && \ export PATH=$PATH:$PWD/Release && \ echo $PATH && \ cd treerec/tests && python -m pytest -xv tests-Windows-CLI: if: github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository == 'messerlab/slim') runs-on: windows-latest strategy: fail-fast: false matrix: include: - { sys: mingw64, env: x86_64, python: 3.9 } - { sys: ucrt64, env: ucrt-x86_64, python: 3.9 } defaults: run: shell: msys2 {0} steps: - name: Check out repository code uses: actions/checkout@v4 - name: Setup MSYS2 ${{matrix.sys}} uses: msys2/setup-msys2@v2 with: msystem: ${{matrix.sys}} update: true install: >- git base-devel msys2-devel mingw-w64-${{matrix.env}}-toolchain mingw-w64-${{matrix.env}}-cmake - name: Setup Miniforge uses: conda-incubator/setup-miniconda@v3 with: miniforge-version: latest activate-environment: anaconda-client-env auto-update-conda: true python-version: ${{ matrix.python }} - name: Get Date id: get-date run: echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_OUTPUT shell: bash - name: Cache Conda env uses: actions/cache@v3 env: # Increase this value to reset cache if treerec/tests/environment.yml has not changed CACHE_NUMBER: 0 with: # Use faster GNU tar enableCrossOsArchive: true path: D:\conda_pkgs_dir key: conda-${{ runner.os }}--python-${{ matrix.python }}--${{ runner.arch }}--${{ steps.get-date.outputs.today }}-${{ hashFiles('treerec/tests/environment.yml') }}-${{ env.CACHE_NUMBER}} id: cache - uses: conda-incubator/setup-miniconda@v3 with: activate-environment: anaconda-client-env environment-file: treerec/tests/environment.yml pkgs-dirs: D:\conda_pkgs_dir - name: Update environment shell: bash -el {0} run: conda env update -n anaconda-client-env -f treerec/tests/environment.yml if: steps.cache.outputs.cache-hit != 'true' - name: Build and test (Debug) run: | cd windows_compat/gnulib && \ touch --date="`date`" aclocal.m4 Makefile.am configure configure.ac config.h.in Makefile.in && \ cd ../.. && \ mkdir Debug && \ cd Debug && \ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug .. && \ make -j 2 && \ # Show any output from the test program whenever the test fails env CTEST_OUTPUT_ON_FAILURE=1 make test - name: Build and test (Release) run: | cd windows_compat/gnulib && \ touch --date="`date`" aclocal.m4 Makefile.am configure configure.ac config.h.in Makefile.in && \ cd ../.. && \ mkdir Release && \ cd Release && \ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Release .. && \ make -j 2 && \ # Show any output from the test program whenever the test fails env CTEST_OUTPUT_ON_FAILURE=1 make test - name: Treesequence tests shell: bash -el {0} run: | conda activate anaconda-client-env && \ export PATH=$PATH:$PWD/Release && \ echo $PATH && \ cd treerec/tests && python -m pytest -xv tests-Unix-GUI: if: github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository == 'messerlab/slim') strategy: fail-fast: false matrix: include: # BCH 2/12/2025: Ubuntu 22.04 is being discontinued, shifting forward # Ubuntu 22.04 with the oldest supported Qt5 and GCC. - { os: ubuntu-22.04, qt: 5.9.5, gcc: 9 } # Ubuntu 22.04 with last official LTS Qt5 and GCC. - { os: ubuntu-22.04, qt: 5.15.2, gcc: 12 } # Ubuntu 24.04 with the most recent Qt6 and GCC. - { os: ubuntu-22.04, qt: 6.8.2, gcc: 12 } # macos-12 and macos-13 were removed by GitHub, # so we can't CI with Qt 5.X on macOS any more, oh well # old macOS with last official LTS Qt5 #- { os: macos-13, qt: 5.15.2 } # old macOS ARM64 with older Qt6 - { os: macos-14, qt: 6.7.3 } # macOS Intel with older Qt6 - { os: macos-15-intel, qt: 6.7.3 } # new macOS with a very recent Qt6 - { os: macos-26, qt: 6.9.2 } runs-on: ${{ matrix.os }} env: CXXFLAGS: -D NO_QT_VERSION_ERROR defaults: run: shell: bash steps: - name: Check out repository code uses: actions/checkout@v4 - name: Workaround for gcc-11 if: startsWith(matrix.os, 'ubuntu') && matrix.gcc == 11 run: | sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y && \ sudo apt-get update -y - name: Set up default GCC (in Ubuntu) if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get install gcc-${{ matrix.gcc }} g++-${{ matrix.gcc }} && \ sudo update-alternatives \ --install /usr/bin/gcc gcc /usr/bin/gcc-${{ matrix.gcc }} 100 \ --slave /usr/bin/g++ g++ /usr/bin/g++-${{ matrix.gcc }} - name: Install Qt uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt }} - name: Release build with SLiMGUI run: | mkdir Release && \ cd Release && \ cmake -D BUILD_SLIMGUI=ON -DCMAKE_BUILD_TYPE=Release .. && \ make -j 2 && \ # Show any output from the test program whenever the test fails env CTEST_OUTPUT_ON_FAILURE=1 make test tests-Windows-GUI: if: github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository == 'messerlab/slim') strategy: fail-fast: false matrix: include: - { sys: mingw64, env: x86_64 } - { sys: ucrt64, env: ucrt-x86_64 } runs-on: windows-latest defaults: run: shell: msys2 {0} steps: - name: Check out repository code uses: actions/checkout@v4 - name: Setup MSYS2 ${{matrix.sys}} uses: msys2/setup-msys2@v2 with: msystem: ${{matrix.sys}} update: true install: >- git base-devel msys2-devel mingw-w64-${{matrix.env}}-toolchain mingw-w64-${{matrix.env}}-cmake mingw-w64-${{matrix.env}}-qt5-base - name: Release build run: | cd . && \ cd windows_compat/gnulib && \ touch --date="`date`" aclocal.m4 Makefile.am configure configure.ac config.h.in Makefile.in && \ cd ../.. && \ mkdir Release && \ cd Release && \ cmake -G"MSYS Makefiles" -DBUILD_SLIMGUI=ON -DCMAKE_BUILD_TYPE=Release .. && \ make -j 2 && \ # Show any output from the test program whenever the test fails env CTEST_OUTPUT_ON_FAILURE=1 make test tests-windows-latest-pacman: if: github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository == 'messerlab/slim') runs-on: windows-latest strategy: fail-fast: false matrix: include: - { sys: mingw64, env: x86_64 } - { sys: ucrt64, env: ucrt-x86_64 } defaults: run: shell: msys2 {0} steps: - name: Check out repository code uses: actions/checkout@v4 - name: Setup MSYS2 ${{matrix.sys}} uses: msys2/setup-msys2@v2 with: msystem: ${{matrix.sys}} update: true install: >- mingw-w64-${{matrix.env}}-slim-simulator - name: Test run: | eidos -testEidos slim -testEidos slim -testSLiM ================================================ FILE: .gitignore ================================================ ######################### # .gitignore file for Xcode4 and Xcode5 Source projects # # Apple bugs, waiting for Apple to fix/respond: # # 15564624 - what does the xccheckout file in Xcode5 do? Where's the documentation? # # Version 2.3 # For latest version, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects # # 2014 updates: # - appended non-standard items DISABLED by default (uncomment if you use those tools) # - removed the edit that an SO.com moderator made without bothering to ask me # - researched CocoaPods .lock more carefully, thanks to Gokhan Celiker # 2013 updates: # - fixed the broken "save personal Schemes" # - added line-by-line explanations for EVERYTHING (some were missing) # # NB: if you are storing "built" products, this WILL NOT WORK, # and you should use a different .gitignore (or none at all) # This file is for SOURCE projects, where there are many extra # files that we want to exclude # ######################### ##### # OS X temporary files that should never be committed # # c.f. http://www.westwind.com/reference/os-x/invisibles.html .DS_Store # c.f. http://www.westwind.com/reference/os-x/invisibles.html .Trashes # c.f. http://www.westwind.com/reference/os-x/invisibles.html *.swp # # *.lock - this is used and abused by many editors for many different things. # For the main ones I use (e.g. Eclipse), it should be excluded # from source-control, but YMMV. # (lock files are usually local-only file-synchronization on the local FS that should NOT go in git) # c.f. the "OPTIONAL" section at bottom though, for tool-specific variations! *.lock # # profile - REMOVED temporarily (on double-checking, I can't find it in OS X docs?) #profile #### # Xcode temporary files that should never be committed # # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... *~.nib #### # Xcode build files - # # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" DerivedData/ # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" build/ ##### # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) # # This is complicated: # # SOMETIMES you need to put this file in version control. # Apple designed it poorly - if you use "custom executables", they are # saved in this file. # 99% of projects do NOT use those, so they do NOT want to version control this file. # ..but if you're in the 1%, comment out the line "*.pbxuser" # .pbxuser: http://lists.apple.com/archives/xcode-users/2004/Jan/msg00193.html *.pbxuser # .mode1v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html *.mode1v3 # .mode2v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html *.mode2v3 # .perspectivev3: http://stackoverflow.com/questions/5223297/xcode-projects-what-is-a-perspectivev3-file *.perspectivev3 # NB: also, whitelist the default ones, some projects need to use these !default.pbxuser !default.mode1v3 !default.mode2v3 !default.perspectivev3 #### # Xcode 4 - semi-personal settings # # # OPTION 1: --------------------------------- # throw away ALL personal settings (including custom schemes! # - unless they are "shared") # # NB: this is exclusive with OPTION 2 below xcuserdata # OPTION 2: --------------------------------- # get rid of ALL personal settings, but KEEP SOME OF THEM # - NB: you must manually uncomment the bits you want to keep # # NB: this *requires* git v1.8.2 or above; you may need to upgrade to latest OS X, # or manually install git over the top of the OS X version # NB: this is exclusive with OPTION 1 above # #xcuserdata/**/* # (requires option 2 above): Personal Schemes # #!xcuserdata/**/xcschemes/* #### # XCode 4 workspaces - more detailed # # Workspaces are important! They are a core feature of Xcode - don't exclude them :) # # Workspace layout is quite spammy. For reference: # # /(root)/ # /(project-name).xcodeproj/ # project.pbxproj # /project.xcworkspace/ # contents.xcworkspacedata # /xcuserdata/ # /(your name)/xcuserdatad/ # UserInterfaceState.xcuserstate # /xcsshareddata/ # /xcschemes/ # (shared scheme name).xcscheme # /xcuserdata/ # /(your name)/xcuserdatad/ # (private scheme).xcscheme # xcschememanagement.plist # # #### # Xcode 4 - Deprecated classes # # Allegedly, if you manually "deprecate" your classes, they get moved here. # # We're using source-control, so this is a "feature" that we do not want! *.moved-aside #### # OPTIONAL: Some well-known tools that people use side-by-side with Xcode / iOS development # # NB: I'd rather not include these here, but gitignore's design is weak and doesn't allow # modular gitignore: you have to put EVERYTHING in one file. # # COCOAPODS: # # c.f. http://guides.cocoapods.org/using/using-cocoapods.html#what-is-a-podfilelock # c.f. http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # #!Podfile.lock # # RUBY: # # c.f. http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/ # #!Gemfile.lock # # IDEA: # #.idea # # TEXTMATE: # # -- UNVERIFIED: c.f. http://stackoverflow.com/a/50283/153422 # #tm_build_errors #### # UNKNOWN: recommended by others, but I can't discover what these files are # # Community suggestions (unverified, no evidence available - DISABLED by default) # # 1. Xcode 5 - VCS file # # "The data in this file not represent state of your project. # If you'll leave this file in git - you will have merge conflicts during # pull your cahnges to other's repo" # *.xccheckout #### # My Own # # This keeps a built executable of slim, or a bin directory, from getting added slim bin #### # SonarCloud cruft bw-output .scannerwork #### # Qt cruft from here on down # C++ objects and libs *.slo *.lo *.o *.a *.la *.lai *.so *.dll *.dylib # Qt-es object_script.*.Release object_script.*.Debug *_plugin_import.cpp /.qmake.cache /.qmake.stash *.pro.user *.pro.user.* *.qbs.user *.qbs.user.* *.moc moc_*.cpp moc_*.h qrc_*.cpp ui_*.h *.qmlc *.jsc Makefile* *build-* !windows_compat/gnulib/build-aux !windows_compat/gnulib/Makefile* !windows_compat/gnulib/gllib/Makefile* !windows_compat/gnulib/glm4/Makefile* ## gnulib windows_compat/gnulib/autom4te.cache windows_compat/gnulib/configure~ # Qt unit tests target_wrapper.* # QtCreator *.autosave # QtCreator Qml *.qmlproject.user *.qmlproject.user.* # QtCreator CMake CMakeLists.txt.user* # QtCreator 4.8< compilation database compile_commands.json # QtCreator local machine specific files for imported projects *creator.user* # VSCode .vscode ================================================ FILE: .travis.yml ================================================ # the Qt stuff is adapted from https://github.com/VioletGiraffe/file-commander/blob/master/.travis.yml # there seems to be no simple solution to building Qt5 apps on travis; you have to install all this stuff language: cpp matrix: include: - os: osx osx_image: xcode9.4 compiler: clang - os: linux dist: xenial compiler: gcc before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi # C++17 - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository --yes ppa:ubuntu-toolchain-r/test; fi install: # C++17 - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository -y ppa:beineri/opt-qt-5.12.3-xenial; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qy update; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -qq g++-8 gcc-8; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-8 90; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 90; fi # Qt5 - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install qt5; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew link --force qt5; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export QMAKE=/usr/local/bin/qmake; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PATH=/usr/local/opt/qt5/bin:$PATH; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -qq qt512-meta-minimal; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -qq libx11-xcb-dev libglu1-mesa-dev; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export QMAKE=/opt/qt512/bin/qmake; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export PATH=/opt/qt512/bin/:$PATH; fi # set up build directories for Release and Debug builds and run cmake in each; "make" should then work # note that we start out in /home/travis/build/MesserLab/SLiM here, and end in /home/travis/build/MesserLab before_script: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then gcc --version; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then g++ --version; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then clang --version; fi - cd .. - mkdir Release - cd Release - cmake -D BUILD_SLIMGUI=ON -D CMAKE_BUILD_TYPE=Release ../SLiM - cd .. - mkdir Debug - cd Debug - cmake -D BUILD_SLIMGUI=ON -D CMAKE_BUILD_TYPE=Debug ../SLiM - cd .. # build using make, then run Eidos and SLiM tests; do for each of Release and Debug # builds can take more than 10 minutes so we use travis_wait script: - cd Release - travis_wait make VERBOSE=1 -j 5 eidos - travis_wait make VERBOSE=1 -j 5 slim - travis_wait make VERBOSE=1 -j 5 SLiMgui - ./eidos -testEidos - ./slim -testEidos - ./slim -testSLiM - cd ../Debug - travis_wait make VERBOSE=1 -j 5 eidos - travis_wait make VERBOSE=1 -j 5 slim - travis_wait make VERBOSE=1 -j 5 SLiMgui - ./eidos -testEidos - ./slim -testEidos - ./slim -testSLiM # test only the master branch for now; add other branches as needed branches: only: - master # notify me by email after all builds notifications: email: on_success: always on_failure: always ================================================ FILE: CMakeLists.txt ================================================ # To use CMake to build SLiM, create a new subdirectory alongside your source directory (assumed here # to be named SLiM), e.g., "build", then run the following commands: # # cd build # cmake ../SLiM # make -j10 # # This will make a Release build, with optimization and without debugging symbols, by default. # The built executables will be placed in the build directory upon successful completion. The optional # -j argument specifies the number of threads to use for the build, which should be a value related # to the number of cores available on your machine (typically less than or equal to the number of cores). # # You can also explicitly make a Release build; this is typically done in a directory named "Release" # instead of "build": # # mkdir Release # cd Release # cmake -D CMAKE_BUILD_TYPE=Release ../SLiM # make # # Or you can make a Debug build (without optimization, with debugging symbols): # # mkdir Debug # cd Debug # cmake -D CMAKE_BUILD_TYPE=Debug ../SLiM # make # # To build the Qt5-based GUI, make sure you have the Qt5 Widgets, Core, and Gui libraries installed, # then configure with: # cd build # cmake -D BUILD_SLIMGUI=ON ../SLiM # make -j10 # # In all cases the concept is the same: make a build directory of some name, cd into it, run cmake # to set up the build (with a CMAKE_BUILD_TYPE flag if desired, otherwise Release will be used by # default), then run make to actually do the build. This setup (1) keeps all build products out of # your source tree, which is generally a good idea, and (2) allows you to have both Release and # Debug builds going simultaneously. # # You can do "make VERBOSE=1" instead of just "make" to see the full command lines used. There are # also various targets defined by cmake for make, such as "slim", "eidos", "clean", "all", etc. To # rebuild all of cmake's internal caches etc. (which is generally a good idea after a "git pull", # for example, or after the addition or removal of source files), the simplest thing is generally # to touch the CMakeLists.txt file in the source tree top-level directory: # # touch ../SLiM/CMakeLists.txt # # Then you can just do "make"; cmake will automatically be re-run by make since the CMakeLists.txt # file has changed. # This syntax sets the minimum version to 2.8.12, for compatibility with very old installs, but # sets the "policy max" to 4.0. It's hard to tell from the CMake doc whether the "policy" changes # that actually signs us up for matter (wow, CMake is complicated!); but we're not doing anything # fancy, so it's hard to imagine it matters. Be careful to add the 4.0 policy max everywhere that # cmake_minimum_required is set. BCH 4/12/2025 cmake_minimum_required (VERSION 2.8.12...4.0 FATAL_ERROR) # clang-tidy support (for internal development); must come before the project() line # note that the hard-coded paths below will need to be fixed for other platforms option(TIDY "Run clang-tidy on SLiM (for development)" OFF) if(TIDY) # cmake_minimum_required(VERSION 3.6...4.0 FATAL_ERROR) if(CMAKE_VERSION VERSION_LESS "3.6") message(FATAL_ERROR "To use the clang-tidy wrapper TIDY in this project you will need a version of CMake at least as new as 3.6.") endif() message(STATUS "TIDY is ${TIDY}; building with clang-tidy (for development)") set(CMAKE_C_COMPILER "/opt/local/libexec/llvm-17/bin/clang") set(CMAKE_CXX_COMPILER "/opt/local/libexec/llvm-17/bin/clang++") find_program(CLANG_TIDY_EXE NAMES "clang-tidy" PATHS "/opt/local/libexec/llvm-17/bin/" NO_DEFAULT_PATH REQUIRED) set(CLANG_TIDY_COMMAND "${CLANG_TIDY_EXE}" "-checks=-*,modernize-*,-modernize-redundant-void-arg,-modernize-use-trailing-return-type,-modernize-use-auto,-modernize-avoid-c-arrays,-modernize-use-equals-default,-modernize-deprecated-headers,-modernize-use-nullptr,-modernize-return-braced-init-list,-modernize-use-using,bugprone-*,-bugprone-narrowing-conversions,-bugprone-easily-swappable-parameters,-bugprone-reserved-identifier,-bugprone-suspicious-include,performance-*,-performance-avoid-endl,-performance-inefficient-string-concatenation") message(STATUS "+++ clang-tidy is at ${CLANG_TIDY_EXE}") message(STATUS "+++ CLANG_TIDY_COMMAND is ${CLANG_TIDY_COMMAND}") endif() # cppcheck support (for internal development) # for now this processes one file at a time; there is an alternative approach that # outputs a project file with CMAKE_EXPORT_COMPILE_COMMANDS and then runs cppcheck # on that, but I'm not sure how to do it within cmake so for now I'm punting on it option(CPPCHECK "Run cppcheck on SLiM (for development)" OFF) if(CPPCHECK) if(CMAKE_VERSION VERSION_LESS "3.10") message(FATAL_ERROR "To use the cppcheck wrapper CPPCHECK in this project you will need a version of CMake at least as new as 3.10.") endif() find_program(CPPCHECK_EXECUTABLE cppcheck) if (CPPCHECK_EXECUTABLE) message(STATUS "CPPCHECK is ${CPPCHECK}; building with cppcheck (for development)") set(CPPCHECK_COMMAND "${CPPCHECK_EXECUTABLE}" "--enable=all;--suppress=missingIncludeSystem;--suppress=syntaxError;--suppress=unmatchedSuppression;--inline-suppr;--std=c++11;--quiet;--suppress=*:robin_hood.h;--suppress=*:lodepng.cpp;--suppress=*:pcg_extras.hpp;--suppress=*:pcg_random.hpp;--suppress=checkersReport;--suppress=*:eidos_openmp.h;--suppress=unusedFunction") message(STATUS "+++ cppcheck is at ${CPPCHECK_EXECUTABLE}") message(STATUS "+++ CPPCHECK_COMMAND is ${CPPCHECK_COMMAND}") else() message(FATAL_ERROR "The CPPCHECK option could be enabled because the cppcheck command could not be found.") endif() endif() # Support a COVERAGE flag for Codecov; see the GitHub Actions YAML file for the use of this option(COVERAGE "Enable code coverage" OFF) if(COVERAGE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") endif() project(SLiM) if(WIN32) include(ExternalProject) endif() # # BUILD OPTIONS # # Make a Release build by default if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif(NOT CMAKE_BUILD_TYPE) # Add "-D BUILD_SLIMGUI=ON" to the CMake command to build SLiMgui # This requires that Qt, a widget framework, is installed option(BUILD_SLIMGUI "Build the Qt-based GUI for SLiM" OFF) # Add "-D ASAN=ON" to the CMake command to build with ASan runtime checking option(ASAN "Build with ASan runtime checking" OFF) # Add "-D UBSAN=ON" to the CMake command to build with ASan runtime checking option(UBSAN "Build with UBSan runtime checking" OFF) # Add "-D PARALLEL=ON" to the CMake command to make a parallel (multi-threaded) build # This is supported only for the command-line tools, not for SLiMgui; do not set BUILD_SLIMGUI option(PARALLEL "Build parallel versions of eidos and slim" OFF) if(PARALLEL) # BCH 12/4/2023: Parallel SLiM is presently unreleased and unsupported. Caveat lector. message(FATAL_ERROR "Multithreaded SLiM is not released, not thoroughly tested, and generally not yet ready for prime time. It is not recommended for end-user use, especially not for 'production' runs, and the documentation for it is not yet public. Please do not ask for any kind of support for this feature if you choose to experiment with it." ) endif(PARALLEL) # Add "-D PROFILE=ON" to the CMake command to make a build of command-line slim with runtime profiling # This makes SLiM slower, so it is not for production use. It may be set ON only for Release builds. option(PROFILE "Build slim for runtime profiling" OFF) # Add "-D BUILD_NATIVE=ON" to build natively for the build machine's architecture # this can result in better optimization, but the executable will only run on the build machine, # so it should only be enabled when you are building on the same machine you will do your runs on option(BUILD_NATIVE "Build native for the build machine" OFF) # Add "-D BUILD_LTO=ON" to enable link-time optimization; this may improve performance slightly, # but fails (see issue #33) on some machines with incompatible toolchains (so then don't enable it) option(BUILD_LTO "Build with link-time optimization" OFF) # obtain the Git commit SHA-1; see ./cmake/_README.txt and https://stackoverflow.com/a/4318642/2752221 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/") include(GetGitRevisionDescription) get_git_head_revision(GIT_REFSPEC GIT_SHA1) #message(STATUS "GIT_SHA1 is ${GIT_SHA1}") # Use the flags below for [all / Debug / Release] builds; these flags are built in to cmake # Note that -fno-math-errno is deliberately set for C++ (for eidos and slim) but not for C (for gsl, eidos_zlib, kastore, tables) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11 -Wno-attributes -Wunused-label -Wimplicit -Wunused-variable -Wunused-value -Wno-pragmas -Wempty-body -Wshadow -Wparentheses -Wmissing-prototypes -Wswitch -Wpointer-sign -Wsign-compare -Wstrict-prototypes -Wno-sign-conversion -Wuninitialized") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wno-attributes -Wunused-label -Wunused-variable -Wunused-value -Wno-pragmas -Wempty-body -Wshadow -Wparentheses -Wswitch -Wsign-compare -Wno-sign-conversion -Wuninitialized -fno-math-errno") # Add -march=native if requested if(BUILD_NATIVE) message(STATUS "BUILD_NATIVE is ${BUILD_NATIVE}; building native (for this machine only)") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=native") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native") endif() # Windows specific flags and variables if(WIN32) set(CMAKE_CXX_STANDARD_LIBRARIES "-static-libgcc -static-libstdc++ -lwsock32 -lws2_32 ${CMAKE_CXX_STANDARD_LIBRARIES}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive") set(GNULIB_NAMESPACE_SOURCES "${PROJECT_SOURCE_DIR}/core/chromosome.cpp" "${PROJECT_SOURCE_DIR}/core/haplosome.cpp" "${PROJECT_SOURCE_DIR}/core/individual.cpp" "${PROJECT_SOURCE_DIR}/core/population.cpp" "${PROJECT_SOURCE_DIR}/core/slim_globals.cpp" "${PROJECT_SOURCE_DIR}/core/slim_sim_eidos.cpp" "${PROJECT_SOURCE_DIR}/core/slim_sim.cpp" "${PROJECT_SOURCE_DIR}/core/species.cpp" "${PROJECT_SOURCE_DIR}/core/species_eidos.cpp" "${PROJECT_SOURCE_DIR}/core/subpopulation.cpp" "${PROJECT_SOURCE_DIR}/eidos/eidos_functions_files.cpp" "${PROJECT_SOURCE_DIR}/eidos/eidos_functions_other.cpp" "${PROJECT_SOURCE_DIR}/eidos/eidos_globals.cpp" "${PROJECT_SOURCE_DIR}/eidos/eidos_class_DataFrame.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMAbout.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMAppDelegate.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMFindRecipe.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMGraphView.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMHaplotypeOptions.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMHelpWindow.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMScriptTextEdit.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMVariableBrowser.cpp" "${PROJECT_SOURCE_DIR}/QtSLiM/QtSLiMWindow.cpp") endif() set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -Og -DDEBUG=1") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -Og -DDEBUG=1") set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") # Build with ASan enabled if requested if(ASAN) message(STATUS "ASAN is ${ASAN}") add_compile_options(-fsanitize=address -fno-omit-frame-pointer) # -fno-omit-frame-pointer for better stack traces add_link_options(-fsanitize=address) else() message(STATUS "ASAN is ${ASAN}; use -DASAN=ON to enable ASan run-time checking") endif(ASAN) # Build with UBSan enabled if requested if(UBSAN) message(STATUS "UBSAN is ${UBSAN}") add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer) # -fno-omit-frame-pointer for better stack traces add_link_options(-fsanitize=undefined) else() message(STATUS "UBSAN is ${UBSAN}; use -DUBSAN=ON to enable UBSan run-time checking") endif(UBSAN) # Report whether the build is parallel or not if(PARALLEL) # -Xclang is also needed on macOS; it is not set here since macOS builds are done in Xcode set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fopenmp") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fopenmp") link_directories(/usr/local/lib/) message(STATUS "PARALLEL is ${PARALLEL}") else() # At least for now, I don't want to advertise the existence of this flag #message(STATUS "PARALLEL is ${PARALLEL}; use -DPARALLEL=ON to enable OpenMP parallelization") endif(PARALLEL) # Report whether the build has runtime profiling support or not if(PROFILE) # compile flags for this get set at the end of this script message(STATUS "PROFILE is ${PROFILE}") else() # At least for now, I don't want to advertise the existence of this flag #message(STATUS "PROFILE is ${PROFILE}; use -PROFILE=ON to enable runtime profiling") endif(PROFILE) # Report the build type and SLiMgui build status message(STATUS "CMAKE_BUILD_TYPE is ${CMAKE_BUILD_TYPE}") if(BUILD_SLIMGUI) message(STATUS "BUILD_SLIMGUI is ${BUILD_SLIMGUI}") else() message(STATUS "BUILD_SLIMGUI is ${BUILD_SLIMGUI}; use -DBUILD_SLIMGUI=ON to enable building SLiMgui") endif() # Windows compat if(WIN32) set(GNU_DIR ${CMAKE_CURRENT_BINARY_DIR}/libgnu-prefix/src/libgnu-build) set(GNU_STATIC_LIB ${GNU_DIR}/gllib/libgnu.a) set(GNU_INCLUDES ${GNU_DIR}/gllib) file(MAKE_DIRECTORY ${GNU_INCLUDES}) ExternalProject_Add(libgnu SOURCE_DIR ${PROJECT_SOURCE_DIR}/windows_compat/gnulib CONFIGURE_COMMAND ${PROJECT_SOURCE_DIR}/windows_compat/gnulib/configure BUILD_COMMAND ${MAKE} BUILD_BYPRODUCTS ${GNU_STATIC_LIB}) add_library(gnu STATIC IMPORTED GLOBAL) add_dependencies(gnu libgnu) set_target_properties(gnu PROPERTIES IMPORTED_LOCATION ${GNU_STATIC_LIB}) set_target_properties(gnu PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${GNU_INCLUDES}) set_target_properties(gnu PROPERTIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES ${GNU_INCLUDES}) endif() # Do link-time optimization with -flto if requested and supported if(BUILD_LTO) include(CheckCXXCompilerFlag) include(CheckCCompilerFlag) CHECK_CXX_COMPILER_FLAG(-flto CXX_SUPPORTS_FLTO) CHECK_C_COMPILER_FLAG(-flto C_SUPPORTS_FLTO) if(CXX_SUPPORTS_FLTO AND C_SUPPORTS_FLTO) message(STATUS "BUILD_LTO is ${BUILD_LTO}; building with link-time optimization") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto") else() message(AUTHOR_WARNING "BUILD_LTO is ${BUILD_LTO} but -flto is not supported by the compiler") endif() endif() # # SIMD SUPPORT (independent of OpenMP) # # Option to disable SIMD entirely option(USE_SIMD "Enable SIMD optimizations (SSE4.2/AVX2 on x86_64, NEON on ARM64)" ON) # Check architecture # CMAKE_SYSTEM_PROCESSOR is "x86_64" on Intel Macs and Linux x86_64, "arm64"/"aarch64" on ARM set(IS_X86_64 FALSE) set(IS_ARM64 FALSE) if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64|i686|i386") set(IS_X86_64 TRUE) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64") set(IS_ARM64 TRUE) endif() if(USE_SIMD AND NOT WIN32 AND IS_X86_64) include(CheckCXXCompilerFlag) # Check for AVX2 support check_cxx_compiler_flag("-mavx2" COMPILER_SUPPORTS_AVX2) check_cxx_compiler_flag("-msse4.2" COMPILER_SUPPORTS_SSE42) check_cxx_compiler_flag("-mfma" COMPILER_SUPPORTS_FMA) if(COMPILER_SUPPORTS_AVX2) message(STATUS "SIMD: AVX2 support detected") add_compile_definitions(EIDOS_HAS_AVX2=1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx2") if(COMPILER_SUPPORTS_FMA) message(STATUS "SIMD: FMA support detected") add_compile_definitions(EIDOS_HAS_FMA=1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfma") endif() elseif(COMPILER_SUPPORTS_SSE42) message(STATUS "SIMD: SSE4.2 support detected (no AVX2)") add_compile_definitions(EIDOS_HAS_SSE42=1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -msse4.2") else() message(STATUS "SIMD: No x86 SIMD support detected, using scalar fallback") endif() elseif(USE_SIMD AND NOT WIN32 AND IS_ARM64) # ARM64 NEON is always available on ARM64, no compiler flag needed message(STATUS "SIMD: ARM64 NEON support enabled") add_compile_definitions(EIDOS_HAS_NEON=1) elseif(USE_SIMD AND NOT WIN32) message(STATUS "SIMD: Unknown architecture (${CMAKE_SYSTEM_PROCESSOR}), using scalar fallback") elseif(USE_SIMD AND WIN32) # Windows SIMD detection - MinGW uses GCC, so we can use the same flags as Linux if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") include(CheckCXXCompilerFlag) check_cxx_compiler_flag("-mavx2" COMPILER_SUPPORTS_AVX2_WIN) check_cxx_compiler_flag("-mfma" COMPILER_SUPPORTS_FMA_WIN) if(COMPILER_SUPPORTS_AVX2_WIN) message(STATUS "SIMD: AVX2 support detected (Windows/MinGW)") add_compile_definitions(EIDOS_HAS_AVX2=1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx2") if(COMPILER_SUPPORTS_FMA_WIN) message(STATUS "SIMD: FMA support detected (Windows/MinGW)") add_compile_definitions(EIDOS_HAS_FMA=1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfma") endif() else() message(STATUS "SIMD: No AVX2 support on Windows/MinGW, using scalar fallback") endif() else() # MSVC path - not currently used but could be added in the future message(STATUS "SIMD: MSVC SIMD detection not yet implemented, using scalar fallback") endif() else() message(STATUS "SIMD: Disabled by user") endif() # GSL - adding /usr/local/include so all targets that use GSL_INCLUDES get omp.h set(TARGET_NAME_GSL gsl) file(GLOB_RECURSE GSL_SOURCES ${PROJECT_SOURCE_DIR}/gsl/*.c ${PROJECT_SOURCE_DIR}/gsl/*/*.c) set(GSL_INCLUDES ${PROJECT_SOURCE_DIR}/gsl ${PROJECT_SOURCE_DIR}/gsl/specfunc ${PROJECT_SOURCE_DIR}/gsl/blas ${PROJECT_SOURCE_DIR}/gsl/rng ${PROJECT_SOURCE_DIR}/gsl/cdf ${PROJECT_SOURCE_DIR}/gsl/vector ${PROJECT_SOURCE_DIR}/gsl/err ${PROJECT_SOURCE_DIR}/gsl/sys ${PROJECT_SOURCE_DIR}/gsl/randist ${PROJECT_SOURCE_DIR}/gsl/matrix ${PROJECT_SOURCE_DIR}/gsl/cblas ${PROJECT_SOURCE_DIR}/gsl/complex ${PROJECT_SOURCE_DIR}/gsl/block ${PROJECT_SOURCE_DIR}/gsl/interpolation ${PROJECT_SOURCE_DIR}/gsl/linalg ${PROJECT_SOURCE_DIR}/gsl/permutation /usr/local/include) add_library(${TARGET_NAME_GSL} STATIC ${GSL_SOURCES}) target_include_directories(${TARGET_NAME_GSL} PUBLIC ${GSL_INCLUDES}) # ZLIB set(TARGET_NAME_ZLIB eidos_zlib) file(GLOB_RECURSE ZLIB_SOURCES ${PROJECT_SOURCE_DIR}/eidos_zlib/*.c) set(ZLIB_INCLUDES ${PROJECT_SOURCE_DIR}/eidos_zlib) add_library(${TARGET_NAME_ZLIB} STATIC ${ZLIB_SOURCES}) target_include_directories(${TARGET_NAME_ZLIB} PUBLIC) # KASTORE set(TARGET_NAME_KASTORE kastore) file(GLOB_RECURSE KASTORE_SOURCES ${PROJECT_SOURCE_DIR}/treerec/tskit/kastore/*.c) set(KASTORE_INCLUDES ${PROJECT_SOURCE_DIR}/treerec/tskit/kastore) add_library(${TARGET_NAME_KASTORE} STATIC ${KASTORE_SOURCES}) target_include_directories(${TARGET_NAME_KASTORE} PUBLIC) # TSKIT set(TARGET_NAME_TSKIT tables) file(GLOB_RECURSE TABLE_SOURCES ${PROJECT_SOURCE_DIR}/treerec/tskit/*.c) set(TSKIT_INCLUDES ${PROJECT_SOURCE_DIR}/treerec) add_library(${TARGET_NAME_TSKIT} STATIC ${TABLE_SOURCES}) target_include_directories(${TARGET_NAME_TSKIT} PRIVATE ${GSL_INCLUDES} ${KASTORE_INCLUDES}) target_include_directories(${TARGET_NAME_TSKIT} PUBLIC ${KASTORE_INCLUDES} ${TSKIT_INCLUDES}) if(WIN32) target_link_libraries(${TARGET_NAME_TSKIT} PUBLIC gnu) endif() # SLIM if(PARALLEL) set(TARGET_NAME_SLIM slim_multi) else() set(TARGET_NAME_SLIM slim) endif(PARALLEL) file(GLOB_RECURSE SLIM_SOURCES ${PROJECT_SOURCE_DIR}/core/*.cpp ${PROJECT_SOURCE_DIR}/eidos/*.cpp) # use the Git commit SHA-1 obtained above configure_file("${PROJECT_SOURCE_DIR}/cmake/GitSHA1.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" @ONLY) list(APPEND SLIM_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/GitSHA1.cpp" ${PROJECT_SOURCE_DIR}/cmake/GitSHA1.h) add_executable(${TARGET_NAME_SLIM} ${SLIM_SOURCES}) target_include_directories(${TARGET_NAME_SLIM} PRIVATE ${GSL_INCLUDES} "${PROJECT_SOURCE_DIR}/core" "${PROJECT_SOURCE_DIR}/eidos") target_link_libraries(${TARGET_NAME_SLIM} PUBLIC gsl eidos_zlib tables) if(PARALLEL) # linking in the OpenMP library is maybe automatic with gcc? #target_link_libraries(${TARGET_NAME_SLIM} PUBLIC omp) endif() if(WIN32) set_source_files_properties(${SLIM_SOURCES} PROPERTIES COMPILE_FLAGS "-include config.h") set_source_files_properties(${GNULIB_NAMESPACE_SOURCES} TARGET_DIRECTORY slim PROPERTIES COMPILE_FLAGS "-include config.h -DGNULIB_NAMESPACE=gnulib") target_include_directories(${TARGET_NAME_SLIM} BEFORE PUBLIC ${GNU_DIR}) target_link_libraries(${TARGET_NAME_SLIM} PUBLIC gnu PRIVATE bcrypt) endif() # EIDOS if(PARALLEL) set(TARGET_NAME_EIDOS eidos_multi) else() set(TARGET_NAME_EIDOS eidos) endif(PARALLEL) file(GLOB_RECURSE EIDOS_SOURCES ${PROJECT_SOURCE_DIR}/eidos/*.cpp ${PROJECT_SOURCE_DIR}/eidostool/*.cpp) add_executable(${TARGET_NAME_EIDOS} ${EIDOS_SOURCES}) target_include_directories(${TARGET_NAME_EIDOS} PRIVATE ${GSL_INCLUDES} "${PROJECT_SOURCE_DIR}/eidos") target_link_libraries(${TARGET_NAME_EIDOS} PUBLIC gsl eidos_zlib tables) if(PARALLEL) # linking in the OpenMP library is maybe automatic with gcc? #target_link_libraries(${TARGET_NAME_EIDOS} PUBLIC omp) endif() if(WIN32) set_source_files_properties(${EIDOS_SOURCES} PROPERTIES COMPILE_FLAGS "-include config.h") set_source_files_properties(${GNULIB_NAMESPACE_SOURCES} TARGET_DIRECTORY slim eidos PROPERTIES COMPILE_FLAGS "-include config.h -DGNULIB_NAMESPACE=gnulib") target_include_directories(${TARGET_NAME_EIDOS} BEFORE PUBLIC ${GNU_DIR}) target_link_libraries(${TARGET_NAME_EIDOS} PUBLIC gnu PRIVATE bcrypt) endif() if(PARALLEL) install(TARGETS slim_multi eidos_multi DESTINATION bin) else() install(TARGETS slim eidos DESTINATION bin) endif(PARALLEL) # SLiMgui -- this can be enabled with the -DBUILD_SLIMGUI=ON option to cmake if(BUILD_SLIMGUI) cmake_minimum_required (VERSION 3.1.0...4.0 FATAL_ERROR) set(TARGET_NAME_SLIMGUI SLiMgui) find_package(OpenGL REQUIRED) # Default to Qt6 if available, fall back to Qt5; this defines QT_VERSION_MAJOR to be either 5 or 6 # This is complicated slightly by the modules needed differing between Qt5 and Qt6 # see https://doc.qt.io/qt-6/cmake-qt5-and-qt6-compatibility.html # I found that find_package(QT NAMES Qt6 Qt5 ...) was behaving oddly, so I shifted to the below find_package(Qt6 COMPONENTS Core Gui Widgets OpenGLWidgets) if(Qt6_FOUND) set(QT_VERSION_MAJOR 6) message(STATUS "Found Qt6 (${Qt6_VERSION}) at ${QT6_INSTALL_PREFIX}") else() find_package(Qt5 COMPONENTS Core Gui Widgets) if(Qt5_FOUND) set(QT_VERSION_MAJOR 5) message(STATUS "Could not find Qt6; if this is unexpected, you may wish to set Qt6_DIR and/or CMAKE_PREFIX_PATH") message(STATUS "Found Qt5 (${Qt5_VERSION}) at ${Qt5_DIR}") # note that on macOS, Qt5 has only the x86_64 architecture, so if you are on macOS-arm64 your build will fail # you can supply -D CMAKE_OSX_ARCHITECTURES="x86_64" at the command line and it should build for x86_64 else() message(FATAL_ERROR "Could not find Qt5 or Qt6; you may wish to set Qt6_DIR, Qt5_DIR, and/or CMAKE_PREFIX_PATH") endif() endif() # a useful bit of debugging code that prints all defined variables #get_cmake_property(_variableNames VARIABLES) #list (SORT _variableNames) #foreach (_variableName ${_variableNames}) # message(STATUS "${_variableName}=${${_variableName}}") #endforeach() if(WIN32) set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME_SLIMGUI}_autogen/mocs_compilation.cpp" PROPERTIES COMPILE_FLAGS "-include config.h -DGNULIB_NAMESPACE=gnulib") endif() set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) list(REMOVE_ITEM SLIM_SOURCES ${PROJECT_SOURCE_DIR}/core/main.cpp) file(GLOB_RECURSE QTSLIM_SOURCES ${PROJECT_SOURCE_DIR}/QtSLiM/*.cpp ${PROJECT_SOURCE_DIR}/QtSLiM/*.qrc ${PROJECT_SOURCE_DIR}/eidos/*.cpp) add_executable(${TARGET_NAME_SLIMGUI} "${QTSLIM_SOURCES}" "${SLIM_SOURCES}") set_target_properties( ${TARGET_NAME_SLIMGUI} PROPERTIES LINKER_LANGUAGE CXX) target_compile_definitions( ${TARGET_NAME_SLIMGUI} PRIVATE EIDOSGUI=1 SLIMGUI=1) target_include_directories(${TARGET_NAME_SLIMGUI} PUBLIC ${GSL_INCLUDES} "${PROJECT_SOURCE_DIR}/QtSLiM" "${PROJECT_SOURCE_DIR}/eidos" "${PROJECT_SOURCE_DIR}/core" "${PROJECT_SOURCE_DIR}/treerec" "${PROJECT_SOURCE_DIR}/treerec/tskit/kastore") # Qt dependencies, which depend on the Qt version used. For Qt6, we also need C++17; the last -std flag supplied ought to take priority. if(${QT_VERSION_MAJOR} EQUAL 5) target_link_libraries( ${TARGET_NAME_SLIMGUI} PUBLIC Qt5::Widgets Qt5::Core Qt5::Gui ) else() target_link_libraries( ${TARGET_NAME_SLIMGUI} PUBLIC Qt6::Widgets Qt6::Core Qt6::Gui Qt6::OpenGLWidgets ) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c17") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") endif() # Operating System-specific install stuff. if(APPLE) target_link_libraries( ${TARGET_NAME_SLIMGUI} PUBLIC OpenGL::GL gsl tables eidos_zlib ) else() if(WIN32) set_source_files_properties(${QTSLIM_SOURCES} PROPERTIES COMPILE_FLAGS "-include config.h") set_source_files_properties(${GNULIB_NAMESPACE_SOURCES} TARGET_DIRECTORY slim eidos SLiMgui PROPERTIES COMPILE_FLAGS "-include config.h -DGNULIB_NAMESPACE=gnulib") target_include_directories(${TARGET_NAME_SLIMGUI} BEFORE PUBLIC ${GNU_DIR}) target_link_libraries(${TARGET_NAME_SLIMGUI} PUBLIC OpenGL::GL gsl tables eidos_zlib gnu PRIVATE bcrypt) else() target_link_libraries( ${TARGET_NAME_SLIMGUI} PUBLIC OpenGL::GL gsl tables eidos_zlib ) # Install icons and desktop files to the data root directory (usually /usr/local/share, or /usr/share). if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.14") install(DIRECTORY data/ TYPE DATA) else() message(WARNING "The CMake version is less than 3.14, so installation of icons, desktop files, mime types, etc. must occur manually.") endif() endif() endif() install(TARGETS ${TARGET_NAME_SLIMGUI} DESTINATION bin) endif(BUILD_SLIMGUI) # Deal with the PROFILE and PARALLEL flags, which interact and are handled in a complex way. # # For SLiMgui, profiling is always on for Release builds, always off for Debug builds; PROFILE does not affect it # For slim, profiling follows the PROFILE setting for Release builds, but in Debug builds it is an error for PROFILE to be on # For eidos, profiling is always off; the eidos command-line tool does not support profiling on its own, only in SLiM # # Note that SLiMgui cannot be built parallel; if you want to build parallel, do not set BUILD_SLIMGUI if(PROFILE) if(CMAKE_BUILD_TYPE STREQUAL Debug) message(FATAL_ERROR "PROFILE is not allowed for Debug builds") endif() if(PARALLEL) target_compile_definitions( eidos_multi PRIVATE SLIMPROFILING=0) target_compile_definitions( slim_multi PRIVATE SLIMPROFILING=1) else() target_compile_definitions( eidos PRIVATE SLIMPROFILING=0) target_compile_definitions( slim PRIVATE SLIMPROFILING=1) endif(PARALLEL) else() if(PARALLEL) target_compile_definitions( eidos_multi PRIVATE SLIMPROFILING=0) target_compile_definitions( slim_multi PRIVATE SLIMPROFILING=0) else() target_compile_definitions( eidos PRIVATE SLIMPROFILING=0) target_compile_definitions( slim PRIVATE SLIMPROFILING=0) endif(PARALLEL) endif(PROFILE) if(BUILD_SLIMGUI) if(CMAKE_BUILD_TYPE STREQUAL Release) target_compile_definitions( SLiMgui PRIVATE SLIMPROFILING=1) else() target_compile_definitions( SLiMgui PRIVATE SLIMPROFILING=0) endif(CMAKE_BUILD_TYPE STREQUAL Release) if(PARALLEL) message(FATAL_ERROR "PARALLEL is not allowed for SLiMgui; running SLiMgui multi-threaded is not supported. If you wish to build SLiM parallel, do not set BUILD_SLIMGUI for that build.") endif() endif(BUILD_SLIMGUI) # implement clang-tidy for all end-user targets (not for gsl, zlib, kastore, tskit) if(TIDY) if(PARALLEL) set_target_properties(eidos_multi PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}") set_target_properties(slim_multi PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}") else() set_target_properties(eidos PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}") set_target_properties(slim PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}") endif(PARALLEL) if(BUILD_SLIMGUI) set_target_properties(SLiMgui PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}") endif(BUILD_SLIMGUI) endif() # implement cppcheck for all end-user targets (not for gsl, zlib, kastore, tskit) if(CPPCHECK) if(PARALLEL) set_target_properties(eidos_multi PROPERTIES CXX_CPPCHECK "${CPPCHECK_COMMAND}") set_target_properties(slim_multi PROPERTIES CXX_CPPCHECK "${CPPCHECK_COMMAND}") else() set_target_properties(eidos PROPERTIES CXX_CPPCHECK "${CPPCHECK_COMMAND}") set_target_properties(slim PROPERTIES CXX_CPPCHECK "${CPPCHECK_COMMAND}") endif(PARALLEL) if(BUILD_SLIMGUI) set_target_properties(SLiMgui PROPERTIES CXX_CPPCHECK "${CPPCHECK_COMMAND}") endif(BUILD_SLIMGUI) endif() # add testing so it can be called using make test enable_testing() # test SLiM add_test( NAME testSLiM COMMAND ${TARGET_NAME_SLIM} -testSLiM ) # test Eidos from SLiM add_test( NAME testEidosSLiM COMMAND ${TARGET_NAME_SLIM} -testEidos ) # test Eidos from Eidos add_test( NAME testEidosEidos COMMAND ${TARGET_NAME_EIDOS} -testEidos ) ================================================ FILE: CONTENTS.txt ================================================ This file briefly describes the top-level contents of this repository, for reference. If new files/directories are added to the top level, please add a description of them here. EIDOS SOURCE FILES ------------------ eidos/ : the Eidos language interpreter eidostool/ : the `eidos` command-line tool eidos_zlib/ : a modified copy of the open-source `zlib` library, used by Eidos gsl/ : a modified copy of the open-source Gnu Scientific Library, used by Eidos windows_compat/ : gnulib sources providing APIs (POSIX etc.) needed to build on Windows EidosScribe/ : a macOS-only app based on Cocoa, for interactive Eidos programming SLiM SOURCE FILES ----------------- core/ : the SLiM core; main.cpp is for the `slim` command-line tool treerec/ : a copy of the open-source tskit and kastore libraries, used by SLiM, with tests QtSLiM/ : the modern cross-platform SLiMgui app based on Qt SLiMgui/ : the old macOS-only SLiMgui app based on Cocoa, now called SLiMguiLegacy PROJECT FILES ------------- SLiM.xcodeproj/ : Xcode project files, used for development on macOS CMakeLists.txt : CMake build configuration, for building at the command line SLiM.pro : qmake / Qt Creator build configuration, for making QtSLiM; see other .pro files also GIT AND GITHUB FILES -------------------- .github/ : GitHub issue templaces, GitHub Actions workflows .gitattributes : git configuration file .gitignore : git configuration file README.md : the README information visible on SLiM GitHub home page DISTRIBUTION FILES ------------------ org.messerlab.* : files used by Linux packages for desktop configuration; see freedesktop.org for more information about these files. debian/ : files used by Debian and Debian-derived Linux distributions for creating deb source and binary packages. See the "Guide for Debian Maintainers" and "Debian Developer's Reference" for more information: https://www.debian.org/doc/devel-manuals. SLiM-3.7.1.spec : "a recipe that the rpmbuild utility uses to build an RPM," as described by the Red Hat documentation in the "PACKAGING AND DISTRIBUTING SOFTWARE" section of the RHEL 8 manual. MISCELLANEOUS FILES ------------------- .travis.yml : configuration for Travis-CI; not currently used, kept for posterity sonar-project.properties : configuration for a code linting tool called Sonar EidosSLiMTests/ : a test harness used inside Xcode; not used much, not maintained LICENSE : the legal licence document for Eidos and SLiM (GNU General Public License 3.0) TO_DO : misnamed; actually a list of command-line stuff that I have found useful when developing VERSIONS : a log of significant changes made in each version of SLiM, back to 2.0 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to SLiM Welcome! This document outlines some guidelines and procedures for participating in and contributing to the SLiM community. You can read about SLiM at the [SLiM home page](https://messerlab.org/slim/). ## Code of conduct Be polite, kind, and considerate. Do not spam, advertise, or proselytize. **AI-generated contributions violate the code of conduct.** An exception is made for the use of AI to translate text from another language. ## Getting started with SLiM If you're a **new user wondering how to get started**, the [SLiM home page](https://messerlab.org/slim/) is a good place to start. It provides a quick overview of what SLiM is, with links to a couple of podcast episodes in which SLiM was discussed. Downloadable manuals for Eidos and SLiM are available there. It also has a section about the **SLiM Workshop**, a tutorial process we have developed for new users. The [SLiM Workshop](https://messerlab.org/slim/#Workshops) is probably the best way to get into SLiM; you can register to take it in person when it is offered, or download the free online materials for the workshop and do it on your own time. The in-person workshops generally fill up quickly, so you would want to register for them soon after they are posted. You can see announcements of new workshops on the [slim-discuss list](https://groups.google.com/g/slim-discuss), or the [slim-announce list](https://groups.google.com/g/slim-announce) if you only want to see announcements (not questions). **If you wish to join slim-discuss, you must use an institutional email address (.edu, .gov, etc.), state your institution, and supply a SLiM-related "reason for joining" in your request.** ## Bug reports and feature requests If you think you're seeing **a bug in SLiM or Eidos**, or you have a **feature request for SLiM or Eidos**, please open a new issue on [SLiM's GitHub repository](https://github.com/MesserLab/SLiM/issues). For **bugs or feature requests pyslim**, please open an issue in the [pyslim repository](https://github.com/tskit-dev/pyslim/issues). Please first search for similar issues, to save yourself and others time. For bugs in other parts of the software ecosystem, such as msprime or tskit, please use a search engine to find the correct process; please do *not* ask us for support for that software. Writing up **a good bug report** is an art form, and it is greatly appreciated by us, the developers, on the other end – and it makes it much more likely that your bug will actually get fixed. For further guidance on writing a good bug report, please consult the paper [Ten simple rules on reporting a bug](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1010540). Particularly important is constructing a **minimal reproducible example**; without that, it is often difficult for us to do much with your bug report. If you're not sure what a minimal reproducible example is, please read that paper. ## Asking a question If you want to ask a question about SLiM, Eidos, or pyslim, please do not file an issue on GitHub. Instead, join the [slim-discuss list](https://groups.google.com/g/slim-discuss) and ask it there. Please search first to see whether others have already asked the same question. Note that slim-discuss is frequently targeted by spammers, and is therefore heavily moderated. **If you wish to join slim-discuss, you must use an institutional email address (.edu, .gov, etc.), state your institution, and supply a SLiM-related "reason for joining" in your request.** If your question is about a specific model that you're working on, then the guidelines for asking a good question are similar to filing a good bug report, so again, please read the paper [Ten simple rules on reporting a bug](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1010540). For example: search slim-discuss for previous answers to your question; try to answer your question yourself using the manual first; whittle down your model to a minimal reproducible example; post the complete model code so that we can see what you're actually doing; clearly state what you're trying to do and why; if you're getting an error message, copy/paste that error message rather than just saying "I got an error"; and so forth. Be specific and give us the information we will need to answer your question; don't make us guess. A particularly common type of question is along the lines of: #### "Why doesn't my model's behavior match theory?" These questions can be tricky to answer. The reason they are tricky is because "theory" can mean a lot of different things, and for that matter, so can "match". So, this sort of question is much more likely to get a useful and interesting answer if it includes: * an explicit description of what the theoretical result is * a plot of the observed and expected values as they change with some parameter * citation of somewhere that explains/derives the theoretical result * ideally, an overview of what assumptions and approximations underly the result (e.g., "under the WF model for large N with infinite sites mutations") * a high-level description of the SLiM model and how the relevant statistics are calculated the SLiM recipe and other code, ideally set up to run without command-line arguments or input files on someone else's computer, made as simple as possible These sorts of questions are pretty common, and natural, and it's not obvious to many people how a question like this can be hard to answer without additional information of this sort. Often the results from simulation don't match theory because they're not *expected* to match theory; the simulation violates assumptions used by the theory in important ways, or the theory is known to be limited or biased or approximate, or the simulations – being stochastic – don't exactly match theory simply due to stochasticity. Try to think about these possibilities and rule them out before asking us. We don't want to discourage relevant questions, but keep in mind that we're here to answer questions about our software, not to provide general consulting services for any and all questions about population genetics. :-> ## Opening a discussion If you want to start a conversation with the SLiM community about an idea, perhaps the best place for it is the new [slim-community organization](https://github.com/slim-community) on GitHub. Just click the "Request to Join" button to join, and once you receive your invite from a community moderator, you're good to go! To give your discussion visibility, please feel free to post, *just once*, on slim-discuss to tell people that your discussion is there. The slim-discuss mailing list might also be a good place to post, but please do be aware that hundreds of people subscribe to that mailing list; think about whether you really want to put something in all those people's inboxes, or whether you might want to just bounce the idea off of the developers – us – in a GitHub issue, or make a slim-community discussion that people can join in on. But if you think your idea is really appropriate for the mailing list, then go for it! ## SLiM-related job postings If you are looking to hire someone with SLiM skills, or you have SLiM skills and you're looking to get hired, please post in the [slim-community organization](https://github.com/slim-community), specifically in the [Jobs discussion area](https://github.com/orgs/slim-community/discussions/categories/jobs). The [EvolDir mailing list](https://evol.mcmaster.ca/evoldir.html) is also a good place to watch for job listings (or post your own), and does sometimes have listings that specifically mention SLiM. Good luck! ## Contributing code **First of all, all contributions to SLiM must be human-authored in accordance with the code of conduct.** It is acceptable to use AI as a reference tool, to essentially provide documentation that you consult while working. It is *not* acceptable to use AI to write code, documentation, or any other part of a PR that you submit to the SLiM repository. PRs that incorporate AI-generated content will be rejected, with reference to this policy. Repeated attempts will result in blocking. Thank you for understanding and complying with this policy. If you wish to make a contribution to SLiM, it would be a good idea to open an issue and get feedback first. This repository is under intensive development, with specific goals in mind; contributions that don't fit with the overall vision of SLiM, or that conflict heavily with other changes underway or planned, may be rejected. Discussing your planned changes before you make them would help you to craft a contribution that would be accepted. That said, PRs that make a useful contribution, such as adding a new function into Eidos, are quite welcome! The standard workflow for contributions is to fork and open a pull request (PR) based upon a branch in your fork. ## Contributing documentation and recipes. **First of all, all contributions to SLiM must be human-authored in accordance with the code of conduct.** That said, if you wish to make contributions to SLiM's documentation, such as edits or new recipes, that's great. SLiM's documentation is kept in a macOS app called Pages that stores its data in binary form, so the doc is not available online in its original form, and pull requests against it are not possible. In general the best approach is thus to open a new issue and provide a description of the edits you would suggest, with a rationale for them. For new recipes/models, unless you believe that they are of sufficient general interest to belong in the SLiM manual itself, the best place to contribute is probably the [SLiM-Extras repository](https://github.com/MesserLab/SLiM-Extras). New pull requests for SLiM-Extras are generally welcome. Please edit the appropriate README files to add a mention of your contribution, and please provide a credit to yourself, including your name, institution, and an email address. ## Contributing whole repositories If you have a contribution that is large enough to make sense as its own standalone repository, such as a new software tool that could join the SLiM ecosystem, the best home is the [slim-community organization](https://github.com/slim-community) on GitHub. You can request to join that organization by clicking the big "Request to Join" button, and you can see some existing contributed repositories there. ## Closing remarks Thanks for reading this and thinking about the best way to engage; we appreciate it. And thanks for being a part of the SLiM community! Happy SLiMulating! *— Ben Haller, 30 December 2025* ================================================ FILE: EidosSLiMTests/EidosTests.mm ================================================ // // EidosTests.m // SLiM // // Created by Ben Haller on 1/27/17. // Copyright (c) 2017-2022 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #import "eidos_test.h" @interface EidosTests : XCTestCase @end @implementation EidosTests - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. #ifdef _OPENMP Eidos_WarmUpOpenMP(&SLIM_ERRSTREAM, changed_max_thread_count, (int)max_thread_count, true, /* max per-task thread counts */ "maxThreads"); #endif Eidos_WarmUp(); } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; } - (void)testEidos { // Since we roll our own testing code, for testability by the user, we don't fit very well into // the Xcode testing framework. We make no attempt here to split the individual tests into their // own methods, or to call Xcode's assert functions to assert failures, or anything like that. // The point of having these tests at all is to be able to assess test code coverage in Xcode. RunEidosTests(); } @end ================================================ FILE: EidosSLiMTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: EidosSLiMTests/SLiMTests.mm ================================================ // // SLiMTests.m // SLiM // // Created by Ben Haller on 1/27/17. // Copyright (c) 2017-2022 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #import "slim_test.h" #import "individual.h" #import "mutation.h" @interface SLiMTests : XCTestCase @end @implementation SLiMTests - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. #ifdef _OPENMP Eidos_WarmUpOpenMP(&SLIM_ERRSTREAM, changed_max_thread_count, (int)max_thread_count, true, /* max per-task thread counts */ "maxThreads"); #endif SLiM_ConfigureContext(); Eidos_WarmUp(); SLiM_WarmUp(); // our self-tests run in SLiMgui, but eidosConsoleWindowControllerDidExecuteScript: puts nasty values into // these variables to help find bugs, and we run our tests outside of any SLiMgui window so the nasty // values bite us... gSLiM_next_pedigree_id = 0; gSLiM_next_mutation_id = 0; } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; } - (void)testSLiM { // Since we roll our own testing code, for testability by the user, we don't fit very well into // the Xcode testing framework. We make no attempt here to split the individual tests into their // own methods, or to call Xcode's assert functions to assert failures, or anything like that. // The point of having these tests at all is to be able to assess test code coverage in Xcode. RunSLiMTests(); } @end ================================================ FILE: EidosScribe/Base.lproj/EidosAboutWindow.xib ================================================ Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Eidos. If not, see http://www.gnu.org/licenses/. Please see the Eidos manual for credits and license information for code that has been incorporated into Eidos from other authors. ================================================ FILE: EidosScribe/Base.lproj/EidosConsoleWindow.xib ================================================ ================================================ FILE: EidosScribe/Base.lproj/EidosHelpWindow.xib ================================================ ================================================ FILE: EidosScribe/Base.lproj/MainMenu.xib ================================================ DQ DQ ================================================ FILE: EidosScribe/EidosAppDelegate.h ================================================ // // EidosAppDelegate.h // EidosScribe // // Created by Ben Haller on 4/7/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import @class EidosConsoleWindowController; /* EidosAppDelegate is an NSApplication delegate for EidosScribe that provides some behavior for menu items and such. If you make your own Context app, you are unlikely to want to reuse this class, although it might illustrate how to set up an EidosConsoleWindowController. */ @interface EidosAppDelegate : NSObject { } // An outlet for the console controller, associated with EidosConsoleWindow.xib @property (nonatomic, retain) IBOutlet EidosConsoleWindowController *consoleController; // Actions for menu commands in MainMenu.xib - (IBAction)showAboutWindow:(id)sender; - (IBAction)sendFeedback:(id)sender; - (IBAction)showMesserLab:(id)sender; - (IBAction)showBenHaller:(id)sender; - (IBAction)showStickSoftware:(id)sender; @end ================================================ FILE: EidosScribe/EidosAppDelegate.mm ================================================ // // EidosAppDelegate.m // EidosScribe // // Created by Ben Haller on 4/7/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosAppDelegate.h" #import "EidosTextView.h" #import "EidosValueWrapper.h" #import "EidosConsoleWindowController.h" #import "EidosConsoleWindowControllerDelegate.h" #import "EidosHelpController.h" #import "EidosCocoaExtra.h" #import "eidos_beep.h" #include "eidos_globals.h" #include "eidos_test.h" #include @interface EidosAppDelegate () { // About window outlets associated with EidosAboutWindow.xib IBOutlet NSWindow *aboutWindow; IBOutlet NSTextField *aboutVersionTextField; IBOutlet NSTextField *messerLabLineTextField; IBOutlet NSTextField *benHallerLineTextField; IBOutlet NSTextField *licenseTextField; } @end @implementation EidosAppDelegate - (void)dealloc { // Ask the console controller to forget us as its delegate, to avoid a stale pointer [_consoleController setDelegate:nil]; [self setConsoleController:nil]; [super dealloc]; } - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { // Require light appearance, at least for now; supporting dark mode would require custom art etc. if ([NSApp respondsToSelector:@selector(setAppearance:)]) [NSApp setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameAqua]]; // Install our custom beep handler Eidos_Beep = &Eidos_Beep_MACOS; // Warm up our back end before anything else happens #ifdef _OPENMP // Multithreading in EidosScribe is not for end user use; this is for testing/debugging only. // We always use 4 threads; we don't want to hog the whole machine, just run with a couple threads. // We pass false for active_threads to let the worker threads sleep, otherwise the CPU is pegged // the whole time EidosScribe is running, even when sitting idle. Eidos_WarmUpOpenMP(&std::cout, true, 4, false, /* default per-task thread counts */ ""); #endif Eidos_WarmUp(); } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Load our console window nib; we are set up as the delegate in the nib [[NSBundle mainBundle] loadNibNamed:@"EidosConsoleWindow" owner:self topLevelObjects:NULL]; // Make the console window visible [_consoleController showWindow]; // Show a status message [_consoleController displayStartupMessage]; } // // Actions // #pragma mark - #pragma mark Actions - (IBAction)showAboutWindow:(id)sender { [[NSBundle mainBundle] loadNibNamed:@"EidosAboutWindow" owner:self topLevelObjects:NULL]; // The window is the top-level object in this nib. It will release itself when closed, so we will retain it on its behalf here. // Note that the aboutWindow and aboutWebView outlets do not get zeroed out when the about window closes; but we only use them here. // This is not very clean programming practice – just a quick and dirty hack – so don't emulate this code. :-> [aboutWindow retain]; // Set our version number string NSString *bundleVersionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; NSString *versionString = [NSString stringWithFormat:@"version %@", bundleVersionString]; [aboutVersionTextField setStringValue:versionString]; // Fix up hyperlinks [messerLabLineTextField eidosSetHyperlink:[NSURL URLWithString:@"http://messerlab.org/slim/"] onText:@"http://messerlab.org/slim/"]; [benHallerLineTextField eidosSetHyperlink:[NSURL URLWithString:@"http://benhaller.com/"] onText:@"http://benhaller.com/"]; [licenseTextField eidosSetHyperlink:[NSURL URLWithString:@"http://www.gnu.org/licenses/"] onText:@"http://www.gnu.org/licenses/"]; // Now that everything is set up, show the window [aboutWindow makeKeyAndOrderFront:nil]; } - (IBAction)sendFeedback:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"mailto:philipp.messer@gmail.com?subject=Eidos%20Feedback"]]; } - (IBAction)showEidosHomePage:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://benhaller.com/eidos.html"]]; } - (IBAction)showMesserLab:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://messerlab.org/"]]; } - (IBAction)showBenHaller:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.benhaller.com/"]]; } - (IBAction)showStickSoftware:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sticksoftware.com/"]]; } - (IBAction)showHelp:(id)sender { [[EidosHelpController sharedController] showWindow]; } // Dummy actions; see validateMenuItem: - (IBAction)toggleConsoleVisibility:(id)sender {} - (IBAction)toggleBrowserVisibility:(id)sender {} - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; // Handle validation for menu items that really belong to the console window. This provides a default validation // for these menu items when no console window is receiving. if (sel == @selector(toggleConsoleVisibility:)) { [menuItem setTitle:@"Show Eidos Console"]; return NO; } if (sel == @selector(toggleBrowserVisibility:)) { [menuItem setTitle:@"Show Variable Browser"]; return NO; } return YES; } // // EidosConsoleWindowControllerDelegate methods // #pragma mark - #pragma mark EidosConsoleWindowControllerDelegate - (void)eidosConsoleWindowControllerAppendWelcomeMessageAddendum:(EidosConsoleWindowController *)eidosConsoleController { // EidosScribe runs the standard Eidos test suite on launch if the option key is down. // You would probably not want to do this in your own Context. if ([NSEvent modifierFlags] & NSEventModifierFlagOption) RunEidosTests(); } - (void)eidosConsoleWindowControllerConsoleWindowWillClose:(EidosConsoleWindowController *)eidosConsoleController { // EidosScribe quits when its console window is closed, but that // behavior is not in any way required or expected. NSApplication *app = [NSApplication sharedApplication]; [app terminate:nil]; } @end ================================================ FILE: EidosScribe/EidosCocoaExtra.h ================================================ // // EidosCocoaExtra.h // SLiM // // Created by Ben Haller on 9/11/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #include class EidosCallSignature; class EidosPropertySignature; /* Some Cocoa utility categories for working with Eidos. Note that this header uses Objective-C++, so it can only be imported by Objective-C++ compilations (.mm files instead of .m files). The hope is that the use of Objective-C++ in EidosScribe headers can be limited mostly to this file and a few others, so that reuse of main EidosScribe UI classes in a new Context does not force the entire project to be Objective-C++. */ @interface NSAttributedString (EidosAdditions) // This provides a nicely syntax-colored call signature string for use in the console window status bar and such places. + (NSAttributedString *)eidosAttributedStringForCallSignature:(const EidosCallSignature *)signature size:(double)fontSize; + (NSAttributedString *)eidosAttributedStringForPropertySignature:(const EidosPropertySignature *)signature size:(double)fontSize; @end @interface NSMutableAttributedString (EidosAdditions) // A shorthand to avoid having to construct autoreleased temporary attributed strings - (void)eidosAppendString:(NSString *)str attributes:(NSDictionary *)attrs; @end @interface NSDictionary (EidosAdditions) // The standard font (Menlo 11 with 3-space tabs) with a given color, used to assemble attributed strings + (NSDictionary *)eidosTextAttributesWithColor:(NSColor *)textColor size:(double)fontSize; // Some standard attribute dictionaries for Eidos syntax coloring + (NSDictionary *)eidosPromptAttrsWithSize:(double)fontSize; + (NSDictionary *)eidosInputAttrsWithSize:(double)fontSize; + (NSDictionary *)eidosOutputAttrsWithSize:(double)fontSize; + (NSDictionary *)eidosErrorAttrsWithSize:(double)fontSize; + (NSDictionary *)eidosTokensAttrsWithSize:(double)fontSize; + (NSDictionary *)eidosParseAttrsWithSize:(double)fontSize; + (NSDictionary *)eidosExecutionAttrsWithSize:(double)fontSize; // Add a hyperlink to an existing attribute dictionary + (NSDictionary *)eidosBaseAttributes:(NSDictionary *)baseAttrs withHyperlink:(NSString *)link; @end @interface NSSplitView (EidosAdditions) // A bug fix to make NSSplitView correctly restore its position/layout. // Borrowed from http://stackoverflow.com/questions/16587058/nssplitview-auto-saving-divider-positions-doesnt-work-with-auto-layout-enable // Ah, NSSplitView, how I love thee? Let me count the ways. OK, I'm done counting. // BCH 25 April 2017: added the autosaveName parameter so that we can restore from our autosave before setting // the autosave name on the splitview, because Apple has broken NSSplitView even more severely than before... - (void)eidosRestoreAutosavedPositionsWithName:(NSString *)autosaveName; @end @interface NSTextField (EidosAdditions) - (void)eidosSetHyperlink:(NSURL *)url onText:(NSString *)text; @end @interface NSString (EidosAdditions) - (int64_t)eidosScoreAsCompletionOfString:(NSString *)completionBase; @end extern std::string Eidos_Beep_MACOS(const std::string &p_sound_name); ================================================ FILE: EidosScribe/EidosCocoaExtra.mm ================================================ // // EidosCocoaExtra.m // SLiM // // Created by Ben Haller on 9/11/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosCocoaExtra.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "eidos_value.h" #include "eidos_beep.h" @implementation NSAttributedString (EidosAdditions) + (NSAttributedString *)eidosAttributedStringForCallSignature:(const EidosCallSignature *)signature size:(double)fontSize { if (signature) { // // Note this logic is paralleled in the function operator<<(ostream &, const EidosCallSignature &). // These two should be kept in synch so the user-visible format of signatures is consistent. // // Build an attributed string showing the call signature with syntax coloring for its parts NSMutableAttributedString *attrStr = [[[NSMutableAttributedString alloc] init] autorelease]; std::string &&prefix_string = signature->CallPrefix(); NSString *prefixString = [NSString stringWithUTF8String:prefix_string.c_str()]; // "", "–\u00A0", or "+\u00A0" std::string &&return_type_string = StringForEidosValueMask(signature->return_mask_, signature->return_class_, "", nullptr); NSString *returnTypeString = [NSString stringWithUTF8String:return_type_string.c_str()]; NSString *functionNameString = [NSString stringWithUTF8String:signature->call_name_.c_str()]; NSDictionary *plainAttrs = [NSDictionary eidosOutputAttrsWithSize:fontSize]; NSDictionary *typeAttrs = [NSDictionary eidosInputAttrsWithSize:fontSize]; NSDictionary *functionAttrs = [NSDictionary eidosParseAttrsWithSize:fontSize]; NSDictionary *paramAttrs = [NSDictionary eidosPromptAttrsWithSize:fontSize]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:prefixString attributes:plainAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"(" attributes:plainAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:returnTypeString attributes:typeAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@")" attributes:plainAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:functionNameString attributes:functionAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"(" attributes:plainAttrs] autorelease]]; int arg_mask_count = (int)signature->arg_masks_.size(); if (arg_mask_count == 0) { [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"void" attributes:typeAttrs] autorelease]]; } else { for (int arg_index = 0; arg_index < arg_mask_count; ++arg_index) { EidosValueMask type_mask = signature->arg_masks_[arg_index]; const std::string &arg_name = signature->arg_names_[arg_index]; const EidosClass *arg_obj_class = signature->arg_classes_[arg_index]; EidosValue_SP arg_default = signature->arg_defaults_[arg_index]; // skip private arguments if ((arg_name.length() >= 1) && (arg_name[0] == '_')) continue; if (arg_index > 0) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@", " attributes:plainAttrs] autorelease]]; // // Note this logic is paralleled in the function StringForEidosValueMask(). // These two should be kept in synch so the user-visible format of signatures is consistent. // if (arg_name == gEidosStr_ELLIPSIS) { [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"..." attributes:plainAttrs] autorelease]]; continue; } bool is_optional = !!(type_mask & kEidosValueMaskOptional); bool requires_singleton = !!(type_mask & kEidosValueMaskSingleton); EidosValueMask stripped_mask = type_mask & kEidosValueMaskFlagStrip; if (is_optional) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"[" attributes:plainAttrs] autorelease]]; if (stripped_mask == kEidosValueMaskNone) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"?" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskAny) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"*" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskAnyBase) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"+" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskVOID) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"void" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskNULL) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"NULL" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskLogical) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"logical" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskString) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"string" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskInt) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"integer" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskFloat) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"float" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskObject) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"object" attributes:typeAttrs] autorelease]]; else if (stripped_mask == kEidosValueMaskNumeric) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"numeric" attributes:typeAttrs] autorelease]]; else { if (stripped_mask & kEidosValueMaskVOID) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"v" attributes:typeAttrs] autorelease]]; if (stripped_mask & kEidosValueMaskNULL) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"N" attributes:typeAttrs] autorelease]]; if (stripped_mask & kEidosValueMaskLogical) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"l" attributes:typeAttrs] autorelease]]; if (stripped_mask & kEidosValueMaskInt) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"i" attributes:typeAttrs] autorelease]]; if (stripped_mask & kEidosValueMaskFloat) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"f" attributes:typeAttrs] autorelease]]; if (stripped_mask & kEidosValueMaskString) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"s" attributes:typeAttrs] autorelease]]; if (stripped_mask & kEidosValueMaskObject) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"o" attributes:typeAttrs] autorelease]]; } if (arg_obj_class && (stripped_mask & kEidosValueMaskObject)) { const std::string &obj_type_name = arg_obj_class->ClassNameForDisplay(); NSString *objTypeName = [NSString stringWithUTF8String:obj_type_name.c_str()]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"<" attributes:typeAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:objTypeName attributes:typeAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@">" attributes:typeAttrs] autorelease]]; } if (requires_singleton) [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"$" attributes:typeAttrs] autorelease]]; if (arg_name.length() > 0) { NSString *argName = [NSString stringWithUTF8String:arg_name.c_str()]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"\u00A0" attributes:plainAttrs] autorelease]]; // non-breaking space [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:argName attributes:paramAttrs] autorelease]]; } if (is_optional) { if (arg_default && (arg_default != gStaticEidosValueNULLInvisible.get())) { [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"\u00A0=\u00A0" attributes:plainAttrs] autorelease]]; std::ostringstream default_string_stream; arg_default->Print(default_string_stream); std::string &&default_string = default_string_stream.str(); NSString *defaultString = [NSString stringWithUTF8String:default_string.c_str()]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:defaultString attributes:plainAttrs] autorelease]]; } [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"]" attributes:plainAttrs] autorelease]]; } } } [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@")" attributes:plainAttrs] autorelease]]; // if the function is provided by a delegate, show the delegate's name //p_outstream << p_signature.CallDelegate(); [attrStr addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [attrStr length])]; return attrStr; } return [[[NSAttributedString alloc] init] autorelease]; } + (NSAttributedString *)eidosAttributedStringForPropertySignature:(const EidosPropertySignature *)signature size:(double)fontSize { if (signature) { // // Note this logic is paralleled in the function operator<<(ostream &, const EidosPropertySignature &). // These two should be kept in synch so the user-visible format of signatures is consistent. // // Build an attributed string showing the call signature with syntax coloring for its parts NSMutableAttributedString *attrStr = [[[NSMutableAttributedString alloc] init] autorelease]; std::string &&connector_string = signature->PropertySymbol(); NSString *connectorString = [NSString stringWithUTF8String:connector_string.c_str()]; // "<–>" or "=>" std::string &&value_type_string = StringForEidosValueMask(signature->value_mask_, signature->value_class_, "", nullptr); NSString *valueTypeString = [NSString stringWithUTF8String:value_type_string.c_str()]; NSString *propertyNameString = [NSString stringWithUTF8String:signature->property_name_.c_str()]; NSDictionary *plainAttrs = [NSDictionary eidosOutputAttrsWithSize:fontSize]; NSDictionary *typeAttrs = [NSDictionary eidosInputAttrsWithSize:fontSize]; NSDictionary *functionAttrs = [NSDictionary eidosParseAttrsWithSize:fontSize]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:propertyNameString attributes:functionAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@" " attributes:plainAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:connectorString attributes:plainAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@" (" attributes:plainAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:valueTypeString attributes:typeAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@")" attributes:plainAttrs] autorelease]]; [attrStr addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [attrStr length])]; return attrStr; } return [[[NSAttributedString alloc] init] autorelease]; } @end @implementation NSMutableAttributedString (EidosAdditions) // A shorthand to avoid having to construct autoreleased temporary attributed strings - (void)eidosAppendString:(NSString *)str attributes:(NSDictionary *)attrs { NSAttributedString *tempString = [[NSAttributedString alloc] initWithString:str attributes:attrs]; [self appendAttributedString:tempString]; [tempString release]; } @end @implementation NSDictionary (EidosAdditions) + (NSDictionary *)eidosTextAttributesWithColor:(NSColor *)textColor size:(double)fontSize { static double menloFontSize = 0.0; static NSFont *menloFont = nil; static NSMutableParagraphStyle *paragraphStyle = nil; bool recachedFont = false; if (!menloFont || (menloFontSize != fontSize)) { if (menloFont) [menloFont release]; menloFont = [[NSFont fontWithName:@"Menlo" size:fontSize] retain]; recachedFont = true; } if (!paragraphStyle || recachedFont) { if (paragraphStyle) [paragraphStyle release]; paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; CGFloat tabInterval = [menloFont maximumAdvancement].width * 3; NSMutableArray *tabs = [NSMutableArray array]; [paragraphStyle setDefaultTabInterval:tabInterval]; for (int tabStop = 1; tabStop <= 20; ++tabStop) [tabs addObject:[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:tabInterval * tabStop options:@{}]]; // @{} suppresses a non-null warning, but should not be necessary; Apple header bug [paragraphStyle setTabStops:tabs]; } menloFontSize = fontSize; if (textColor) return @{NSForegroundColorAttributeName : textColor, NSFontAttributeName : menloFont, NSParagraphStyleAttributeName : paragraphStyle}; else return @{NSFontAttributeName : menloFont, NSParagraphStyleAttributeName : paragraphStyle}; } + (NSDictionary *)eidosPromptAttrsWithSize:(double)fontSize { static double cachedFontSize = 0.0; static NSDictionary *promptAttrs = nil; if (!promptAttrs || (fontSize != cachedFontSize)) { if (promptAttrs) [promptAttrs release]; promptAttrs = [[NSDictionary eidosTextAttributesWithColor:[NSColor colorWithCalibratedRed:170/255.0 green:13/255.0 blue:145/255.0 alpha:1.0] size:fontSize] retain]; } return promptAttrs; } + (NSDictionary *)eidosInputAttrsWithSize:(double)fontSize { static double cachedFontSize = 0.0; static NSDictionary *inputAttrs = nil; if (!inputAttrs || (fontSize != cachedFontSize)) { if (inputAttrs) [inputAttrs release]; inputAttrs = [[NSDictionary eidosTextAttributesWithColor:[NSColor colorWithCalibratedRed:28/255.0 green:0/255.0 blue:207/255.0 alpha:1.0] size:fontSize] retain]; } return inputAttrs; } + (NSDictionary *)eidosOutputAttrsWithSize:(double)fontSize { static double cachedFontSize = 0.0; static NSDictionary *outputAttrs = nil; if (!outputAttrs || (fontSize != cachedFontSize)) { if (outputAttrs) [outputAttrs release]; outputAttrs = [[NSDictionary eidosTextAttributesWithColor:[NSColor colorWithCalibratedRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:1.0] size:fontSize] retain]; } return outputAttrs; } + (NSDictionary *)eidosErrorAttrsWithSize:(double)fontSize { static double cachedFontSize = 0.0; static NSDictionary *errorAttrs = nil; if (!errorAttrs || (fontSize != cachedFontSize)) { if (errorAttrs) [errorAttrs release]; errorAttrs = [[NSDictionary eidosTextAttributesWithColor:[NSColor colorWithCalibratedRed:196/255.0 green:26/255.0 blue:22/255.0 alpha:1.0] size:fontSize] retain]; } return errorAttrs; } + (NSDictionary *)eidosTokensAttrsWithSize:(double)fontSize { static double cachedFontSize = 0.0; static NSDictionary *tokensAttrs = nil; if (!tokensAttrs || (fontSize != cachedFontSize)) { if (tokensAttrs) [tokensAttrs release]; tokensAttrs = [[NSDictionary eidosTextAttributesWithColor:[NSColor colorWithCalibratedRed:100/255.0 green:56/255.0 blue:32/255.0 alpha:1.0] size:fontSize] retain]; } return tokensAttrs; } + (NSDictionary *)eidosParseAttrsWithSize:(double)fontSize { static double cachedFontSize = 0.0; static NSDictionary *parseAttrs = nil; if (!parseAttrs || (fontSize != cachedFontSize)) { if (parseAttrs) [parseAttrs release]; parseAttrs = [[NSDictionary eidosTextAttributesWithColor:[NSColor colorWithCalibratedRed:0/255.0 green:116/255.0 blue:0/255.0 alpha:1.0] size:fontSize] retain]; } return parseAttrs; } + (NSDictionary *)eidosExecutionAttrsWithSize:(double)fontSize { static double cachedFontSize = 0.0; static NSDictionary *executionAttrs = nil; if (!executionAttrs || (fontSize != cachedFontSize)) { if (executionAttrs) [executionAttrs release]; executionAttrs = [[NSDictionary eidosTextAttributesWithColor:[NSColor colorWithCalibratedRed:63/255.0 green:110/255.0 blue:116/255.0 alpha:1.0] size:fontSize] retain]; } return executionAttrs; } + (NSDictionary *)eidosBaseAttributes:(NSDictionary *)baseAttrs withHyperlink:(NSString *)link { NSMutableDictionary *attrs = [NSMutableDictionary dictionaryWithDictionary:baseAttrs]; [attrs setObject:link forKey:NSLinkAttributeName]; return attrs; } @end @implementation NSSplitView (EidosAdditions) - (void)eidosRestoreAutosavedPositionsWithName:(NSString *)autosaveName { NSString *key = [NSString stringWithFormat:@"NSSplitView Subview Frames %@", autosaveName]; NSArray *subviewFrames = [[NSUserDefaults standardUserDefaults] valueForKey:key]; // the last frame is skipped because I have one less divider than I have frames for (NSUInteger i = 0; i < subviewFrames.count; i++) { if (i < self.subviews.count) // safety-check (in case number of views have been removed while dev) { // this is the saved frame data - it's an NSString NSString *frameString = subviewFrames[i]; NSArray *components = [frameString componentsSeparatedByString:@", "]; // Manage the 'hidden state' per view BOOL hidden = [components[4] boolValue]; NSView *subView = [self subviews][i]; [subView setHidden:hidden]; // Set height (horizontal) or width (vertical) if (![self isVertical]) // BCH 4/7/2016: vertical property not available in 10.9 { CGFloat height = [components[3] floatValue]; [subView setFrameSize:NSMakeSize(subView.frame.size.width, height)]; } else { CGFloat width = [components[2] floatValue]; [subView setFrameSize:NSMakeSize(width, subView.frame.size.height)]; } } } } @end @implementation NSTextField (EidosAdditions) - (void)eidosSetHyperlink:(NSURL *)url onText:(NSString *)text { NSMutableAttributedString *attrString = [[self attributedStringValue] mutableCopy]; NSString *string = [attrString string]; NSRange range = [string rangeOfString:text]; if (range.location != NSNotFound) { // Apple sez: both are needed, otherwise hyperlink won't accept mousedown [self setAllowsEditingTextAttributes: YES]; [self setSelectable: YES]; // Add the link attribute [attrString beginEditing]; [attrString addAttribute:NSLinkAttributeName value:[url absoluteString] range:range]; // link [attrString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:0.7 alpha:1.0] range:range]; // dark blue [attrString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSUnderlineStyleSingle] range:range]; // underlined [attrString endEditing]; [self setAttributedStringValue:attrString]; } [attrString release]; } @end // Eidos_Beep() is declared in eidos_beep.h, with a default implementation; in the GUI case (EidosScribe and SLiMgui) // it is redefined here, because we want to be able to use Objective-C and Cocoa. std::string Eidos_Beep_MACOS(const std::string &p_sound_name) { NSString *soundName = [NSString stringWithUTF8String:p_sound_name.c_str()]; if (!soundName || ![soundName length]) soundName = @"Pop"; static NSSound *cachedSound = nil; static NSTimeInterval cachedSoundDuration = 0; static NSString *cachedSoundName = nil; static NSDate *playFinishDate = nil; std::string return_string; // If playFinishDate is non-nil, we started playing a sound before, and we need to wait synchronously here for it to finish if (playFinishDate) { NSTimeInterval remainingTime = [playFinishDate timeIntervalSinceNow]; if (remainingTime > 0) usleep((useconds_t)(remainingTime * 1000000.0)); [playFinishDate release]; playFinishDate = nil; } // If cachedSound is non-nil, we played it in the past, and we should now make sure that it is stopped, otherwise it ignores -play if (cachedSound) [cachedSound stop]; // Now we switch cachedSound over to the requested sound if (!cachedSound || ![soundName isEqualToString:cachedSoundName]) { [cachedSound release]; [cachedSoundName release]; cachedSound = [[NSSound soundNamed:soundName] retain]; // If the requested sound did not exist, we fall back on the default, which ought to always exist if (!cachedSound) { return_string = "#WARNING (Eidos_Beep): function beep() could not find the requested sound."; soundName = @"Pop"; cachedSound = [[NSSound soundNamed:soundName] retain]; } cachedSoundName = [soundName retain]; cachedSoundDuration = [cachedSound duration]; } // Start playing our sound. We return immediately, but we make a note of when we expect the sound to stop playing. If we get called // again to play a sound before the current one has finished, we will synchronously wait the remaining time, above. [cachedSound play]; playFinishDate = [[NSDate dateWithTimeIntervalSinceNow:cachedSoundDuration] retain]; return return_string; } @implementation NSString (EidosAdditions) - (int64_t)eidosScoreAsCompletionOfString:(NSString *)base { // Evaluate the quality of the target as a completion for completionBase and return a score. // We look for each character of completionBase in self, in order, case-insensitive; all // characters must be present in order for the target to be a completion at all. Beyond that, // a higher score is garnered if the matches in self are (1) either uppercase or the 0th character, // and (2) if they are relatively near the beginning, and (3) if they occur contiguously. int64_t score = 0; NSUInteger selfLength = [self length], baseLength = [base length]; // Do the comparison scan; find a match for each composed character sequence in base. We work // with composed character sequences and use rangeOfString: to do searches, to avoid issues with // diacritical marks, alternative composition sequences, casing, etc. NSUInteger firstUnusedIndex = 0, firstUnmatchedIndex = 0; do { NSRange baseRangeToMatch = [base rangeOfComposedCharacterSequenceAtIndex:firstUnmatchedIndex]; NSString *stringToMatch = [base substringWithRange:baseRangeToMatch]; NSString *uppercaseStringToMatch = [stringToMatch uppercaseString]; NSRange selfMatchRange; if ([stringToMatch isEqualToString:uppercaseStringToMatch] && (firstUnmatchedIndex != 0)) { // If the character in base is uppercase, we only want to match an uppercase character in self. // The exception is the first character of base; WTF should match writeTempFile() well. selfMatchRange = [self rangeOfString:stringToMatch options:(NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch) range:NSMakeRange(firstUnusedIndex, selfLength - firstUnusedIndex)]; score += 1000; // uppercase match } else { // If the character in base is not uppercase, we will match any case in self, but we prefer a // lowercase character if it matches the very next part of self, otherwise we prefer uppercase. selfMatchRange = [self rangeOfString:stringToMatch options:(NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch) range:NSMakeRange(firstUnusedIndex, selfLength - firstUnusedIndex)]; if (selfMatchRange.location == firstUnusedIndex) { score += 2000; // next-character match is even better than upper-case; continuity trumps camelcase } else { NSRange uppercaseMatchRange = [self rangeOfString:uppercaseStringToMatch options:(NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch) range:NSMakeRange(firstUnusedIndex, selfLength - firstUnusedIndex)]; if (uppercaseMatchRange.location != NSNotFound) { selfMatchRange = uppercaseMatchRange; score += 1000; // uppercase match } else if (firstUnusedIndex > 0) { // This match is crap; we're jumping forward to a lowercase letter, so it's unlikely to be what // the user wants. So we bail. This can be commented out to return lower-quality matches. return INT64_MIN; } } } // no match in self for the composed character sequence in base; self is not a good completion of base if (selfMatchRange.location == NSNotFound) return INT64_MIN; // matching the very beginning of self is very good; we really want to match the start of a candidate // otherwise, earlier matches are better; a match at position 0 gets the largest score increment if (selfMatchRange.location == 0) score += 100000; else score -= selfMatchRange.location; // move firstUnusedIndex to follow the matched range in self firstUnusedIndex = selfMatchRange.location + selfMatchRange.length; // move to the next composed character sequence in base firstUnmatchedIndex = baseRangeToMatch.location + baseRangeToMatch.length; if (firstUnmatchedIndex >= baseLength) break; } while (YES); // We want argument-name matches to be at the top, always, when they are available, so bump their score if ([self hasSuffix:@"="]) score += 1000000; return score; } @end ================================================ FILE: EidosScribe/EidosConsoleTextView.h ================================================ // // EidosConsoleTextView.h // EidosScribe // // Created by Ben Haller on 4/8/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #import "EidosTextView.h" // BCH 4/7/2016: So we can build against the OS X 10.9 SDK #ifndef NS_DESIGNATED_INITIALIZER #define NS_DESIGNATED_INITIALIZER #endif @protocol EidosConsoleTextViewDelegate; /* EidosConsoleTextView provides a standard Eidos console textview. It is integrated into EidosConsoleWindowController, so if you use that class you get a console textview for free, and do not have to use this class directly. If you build your own Eidos UI, you might wish to use this class to provide a console. */ @interface EidosConsoleTextView : EidosTextView { @public NSRange lastPromptRange; NSMutableArray *history; NSUInteger historyIndex; BOOL lastHistoryItemIsProvisional; // got added to the history by a moveUp: event but is not an executed command } // A delegate for Eidos functionality; this is the same object as the NSText/NSTextView delegate, and is not declared explicitly // here because overriding properties with a different type doesn't really work. So when you call setDelegate: on EidosTextView, // you will not get the proper type-checking, and you will get a runtime error if your delegate object does not in fact conform // to the EidosTextViewDelegate protocol. But if you declare your conformance to the protocol, you should be fine. This is a // little weird, but the only good alternative is to have a separate delegate object for Eidos, which would just be annoying and // confusing. // //@property (nonatomic, assign) id delegate; // Initializers are inherited from NSTextView //- (instancetype)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)container NS_DESIGNATED_INITIALIZER; //- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; // The character position following the current Eidos prompt; user input starts at this character position - (NSUInteger)promptRangeEnd; - (void)setPromptRangeEnd:(NSUInteger)promptEnd; // Can be called to show the standard Eidos welcome message in the console - (void)showWelcomeMessage; // Adds a new Eidos prompt to the console, on the assumption that output is finished and the console is ready for input - (void)showPrompt; - (void)showPrompt:(unichar)promptChar; // Adds a thin spacer line to the console's output text; this provides nicer formatting for output - (void)appendSpacer; // Clears output in the console - (void)clearOutputToPosition:(NSUInteger)clearPosition; // This adds a command to the console window's history; it should be called to register all commands executed // in the console, including commands sent to the delegate to execute by -eidosConsoleTextViewExecuteInput:, // since the console does not assume that that call results in successful execution. - (void)registerNewHistoryItem:(NSString *)newItem; @end ================================================ FILE: EidosScribe/EidosConsoleTextView.mm ================================================ // // EidosConsoleTextView.m // EidosScribe // // Created by Ben Haller on 4/8/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosConsoleTextView.h" #import "EidosConsoleTextViewDelegate.h" #import "EidosCocoaExtra.h" #include "eidos_globals.h" @implementation EidosConsoleTextView - (void)dealloc { [history release]; history = nil; [super dealloc]; } // The method signature is inherited from NSTextView, but we want to check that the delegate follows our delegate protocol - (void)setDelegate:(id)delegate { if (delegate && ![delegate conformsToProtocol:@protocol(EidosConsoleTextViewDelegate)]) NSLog(@"Delegate %@ assigned to EidosConsoleTextView %p does not conform to the EidosConsoleTextViewDelegate protocol!", delegate, self); [super setDelegate:delegate]; } - (void)insertNewline:(id)sender { id delegate = [self delegate]; if ([delegate respondsToSelector:@selector(eidosConsoleTextViewExecuteInput:)]) [delegate eidosConsoleTextViewExecuteInput:self]; } // This is option-return or option-enter; we deliberately let it go, to let people put newlines into their input //- (void)insertNewlineIgnoringFieldEditor:(id)sender //{ //} - (NSString *)currentCommandAtPrompt { NSString *outputString = [self string]; NSString *commandString = [outputString substringFromIndex:[self promptRangeEnd]]; return commandString; } - (void)setCommandAtPrompt:(NSString *)newCommand { NSTextStorage *ts = [self textStorage]; NSUInteger promptEnd = [self promptRangeEnd]; NSDictionary *inputAttrs = [NSDictionary eidosInputAttrsWithSize:[self displayFontSize]]; NSAttributedString *newAttrCommand = [[NSAttributedString alloc] initWithString:newCommand attributes:inputAttrs]; [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange(promptEnd, [ts length] - promptEnd) withAttributedString:newAttrCommand]; [ts endEditing]; [newAttrCommand release]; [self setSelectedRange:NSMakeRange([[self string] length], 0)]; [self setTypingAttributes:inputAttrs]; [self scrollRangeToVisible:NSMakeRange([[self string] length], 0)]; } - (void)moveUp:(id)sender { if (history && (historyIndex > 0)) { // If the user has typed at the current prompt and it is unsaved, save it in the history before going up if (historyIndex == [history count]) { NSString *commandString = [self currentCommandAtPrompt]; if ([commandString length] > 0) { // If there is a provisional item on the top of the stack, get rid of it and replace it if (lastHistoryItemIsProvisional) { [history removeLastObject]; lastHistoryItemIsProvisional = NO; --historyIndex; } [history addObject:commandString]; lastHistoryItemIsProvisional = YES; } } // if the only item on the stack was provisional, and we just replaced it, we may have nowhere up to go if (historyIndex > 0) { --historyIndex; [self setCommandAtPrompt:[history objectAtIndex:historyIndex]]; } //NSLog(@"moveUp: end:\nhistory = %@\nhistoryIndex = %d\nlastHistoryItemIsProvisional = %@", history, historyIndex, lastHistoryItemIsProvisional ? @"YES" : @"NO"); } } - (void)moveDown:(id)sender { if (historyIndex <= [history count]) { // If the user has typed at the current prompt and it is unsaved, save it in the history before going down if (historyIndex == [history count]) { NSString *commandString = [self currentCommandAtPrompt]; if ([commandString length] > 0) { // If there is a provisional item on the top of the stack, get rid of it and replace it if (lastHistoryItemIsProvisional) { [history removeLastObject]; lastHistoryItemIsProvisional = NO; --historyIndex; } if (!history) history = [[NSMutableArray alloc] init]; [history addObject:commandString]; lastHistoryItemIsProvisional = YES; } else return; } ++historyIndex; if (historyIndex == [history count]) [self setCommandAtPrompt:@""]; else [self setCommandAtPrompt:[history objectAtIndex:historyIndex]]; //NSLog(@"moveDown: end:\nhistory = %@\nhistoryIndex = %d\nlastHistoryItemIsProvisional = %@", history, historyIndex, lastHistoryItemIsProvisional ? @"YES" : @"NO"); } } - (void)registerNewHistoryItem:(NSString *)newItem { if (!history) history = [[NSMutableArray alloc] init]; // If there is a provisional item on the top of the stack, get rid of it and replace it if (lastHistoryItemIsProvisional) { [history removeLastObject]; lastHistoryItemIsProvisional = NO; } [history addObject:newItem]; historyIndex = (int)[history count]; // a new prompt, one beyond the last history item //NSLog(@"registerNewHistoryItem: end:\nhistory = %@\nhistoryIndex = %d\nlastHistoryItemIsProvisional = %@", history, historyIndex, lastHistoryItemIsProvisional ? @"YES" : @"NO"); } - (NSUInteger)promptRangeEnd { return lastPromptRange.location + lastPromptRange.length; } - (void)setPromptRangeEnd:(NSUInteger)promptEnd { lastPromptRange.location = promptEnd - lastPromptRange.length; } - (NSUInteger)rangeOffsetForCompletionRange { return [self promptRangeEnd]; } - (void)showWelcomeMessage { NSTextStorage *ts = [self textStorage]; NSDictionary *outputAttrs = [NSDictionary eidosOutputAttrsWithSize:[self displayFontSize]]; NSString *versionString = [NSString stringWithFormat:@"Eidos version %s\n\nBy Benjamin C. Haller (", EIDOS_VERSION_STRING]; NSAttributedString *welcomeString1 = [[NSAttributedString alloc] initWithString:versionString attributes:outputAttrs]; // EIDOS VERSION NSAttributedString *welcomeString2 = [[NSAttributedString alloc] initWithString:@"http://benhaller.com/" attributes:[NSDictionary eidosBaseAttributes:outputAttrs withHyperlink:@"http://benhaller.com/"]]; NSAttributedString *welcomeString3 = [[NSAttributedString alloc] initWithString:@").\nCopyright (c) 2016–2026 Benjamin C. Haller.\nAll rights reserved.\n\nEidos is free software with ABSOLUTELY NO WARRANTY.\nType license() for license and distribution details.\n\nGo to " attributes:outputAttrs]; NSAttributedString *welcomeString4 = [[NSAttributedString alloc] initWithString:@"https://github.com/MesserLab/SLiM" attributes:[NSDictionary eidosBaseAttributes:outputAttrs withHyperlink:@"https://github.com/MesserLab/SLiM"]]; NSAttributedString *welcomeString5 = [[NSAttributedString alloc] initWithString:@" for source\ncode, documentation, examples, and other information.\n\nWelcome to Eidos!\n\n-----------------------------------------------------\n\n" attributes:outputAttrs]; [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:welcomeString1]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:welcomeString2]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:welcomeString3]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:welcomeString4]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:welcomeString5]; [ts endEditing]; [welcomeString1 release]; [welcomeString2 release]; [welcomeString3 release]; [welcomeString4 release]; [welcomeString5 release]; } - (void)showPrompt:(unichar)promptChar { NSTextStorage *ts = [self textStorage]; NSDictionary *promptAttrs = [NSDictionary eidosPromptAttrsWithSize:[self displayFontSize]]; NSDictionary *inputAttrs = [NSDictionary eidosInputAttrsWithSize:[self displayFontSize]]; // The prompt uses the inputAttrs for the second (space) character, to make sure typing attributes are always correct NSString *prompt = [NSString stringWithCharacters:&promptChar length:1]; NSAttributedString *promptString1 = [[NSAttributedString alloc] initWithString:prompt attributes:promptAttrs]; NSAttributedString *promptString2 = [[NSAttributedString alloc] initWithString:@" " attributes:inputAttrs]; // We remember the prompt range for various purposes such as uneditability of old content lastPromptRange = NSMakeRange([[self string] length], 2); [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange(lastPromptRange.location, 0) withAttributedString:promptString1]; [ts replaceCharactersInRange:NSMakeRange(lastPromptRange.location + 1, 0) withAttributedString:promptString2]; [ts endEditing]; [promptString1 release]; [promptString2 release]; [self setSelectedRange:NSMakeRange(lastPromptRange.location + 2, 0)]; [self setTypingAttributes:inputAttrs]; [self scrollRangeToVisible:NSMakeRange([[self string] length], 0)]; } - (void)showPrompt { [self showPrompt:'>']; } - (void)appendSpacer { static NSAttributedString *spacerString = nil; if (!spacerString) spacerString = [[NSAttributedString alloc] initWithString:@"\n" attributes:@{NSForegroundColorAttributeName : [NSColor blackColor], NSFontAttributeName : [NSFont fontWithName:@"Menlo" size:3.0]}]; NSTextStorage *ts = [self textStorage]; // we only add a spacer newline if the current contents already end in a newline; we don't introduce new breaks if ([[ts string] characterAtIndex:[ts length] - 1] == '\n') { [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:spacerString]; [ts endEditing]; } } - (void)clearOutputToPosition:(NSUInteger)clearPosition { NSTextStorage *ts = [self textStorage]; NSRange selectedRange = [self selectedRange]; NSRange rangeToClear = NSMakeRange(0, clearPosition); [ts beginEditing]; [ts replaceCharactersInRange:rangeToClear withString:@""]; [ts endEditing]; lastPromptRange.location -= clearPosition; if (selectedRange.location + selectedRange.length <= rangeToClear.location + rangeToClear.length) { // the selection was entirely in the deleted range, so reset it to the end of the textview selectedRange = NSMakeRange([ts length], 0); } else if (selectedRange.location <= rangeToClear.location + rangeToClear.length) { // the selected range started in the deleted range but extended into the undeleted range, so preserve the remaining portion unsigned long deletedLength = rangeToClear.location + rangeToClear.length - selectedRange.location; selectedRange.location = 0; selectedRange.length -= deletedLength; } else { // the selected range was entirely beyond the deleted range, so preserve the whole thing selectedRange.location -= rangeToClear.length; } [self setSelectedRange:selectedRange]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; if ((sel == @selector(shiftSelectionLeft:)) || (sel == @selector(shiftSelectionRight:)) || (sel == @selector(commentUncommentSelection:))) return NO; return [super validateMenuItem:menuItem]; } @end ================================================ FILE: EidosScribe/EidosConsoleTextViewDelegate.h ================================================ // // EidosConsoleTextViewDelegate.h // SLiM // // Created by Ben Haller on 9/11/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import @class EidosConsoleTextView; // A protocol that the delegate should respond to, to be notified when the user presses return/enter; this should // trigger execution of the current line of input (i.e., all text after -promptRangeEnd). @protocol EidosConsoleTextViewDelegate @required - (void)eidosConsoleTextViewExecuteInput:(EidosConsoleTextView *)textView; @end ================================================ FILE: EidosScribe/EidosConsoleWindowController.h ================================================ // // EidosConsoleWindowController.h // EidosScribe // // Created by Ben Haller on 6/13/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #import "EidosConsoleTextView.h" #import "EidosVariableBrowserController.h" // BCH 4/7/2016: So we can build against the OS X 10.9 SDK #ifndef NS_DESIGNATED_INITIALIZER #define NS_DESIGNATED_INITIALIZER #endif @protocol EidosConsoleWindowControllerDelegate; /* EidosConsoleWindowController provides a prefab Eidos console window containing a script view, a console view, a status bar, and various toolbar buttons. It can be reused in Context code if you just want a standard Eidos console, and can be customized by supplying a delegate to it. */ // Defaults keys used to control various aspects of the user experience extern NSString *EidosDefaultsShowTokensKey; extern NSString *EidosDefaultsShowParseKey; extern NSString *EidosDefaultsShowExecutionKey; extern NSString *EidosDefaultsSuppressScriptCheckSuccessPanelKey; @interface EidosConsoleWindowController : NSObject { // ivars for handling input continuation BOOL isContinuationPrompt; NSUInteger originalPromptEnd; } // A delegate may be provided to customize various aspects of this class; see EidosConsoleWindowControllerDelegate.h @property (nonatomic, assign) IBOutlet NSObject *delegate; // This property controls the enable state of UI that depends on the state of Eidos or its Context. Some of // the console window's UI does not; you can show/hide script help at any time, even if Eidos or its Context // is in an invalid state, for example. Other UI does; you can't execute if things are in an invalid state. @property (nonatomic) BOOL interfaceEnabled; // Outlets from EidosConsoleWindow.xib; it is unlikely that client code will need to access these @property (nonatomic, retain) IBOutlet EidosVariableBrowserController *browserController; @property (nonatomic, retain) IBOutlet NSWindow *scriptWindow; @property (nonatomic, assign) IBOutlet NSSplitView *bottomSplitView; @property (nonatomic, assign) IBOutlet EidosTextView *scriptTextView; @property (nonatomic, assign) IBOutlet EidosConsoleTextView *outputTextView; @property (nonatomic, assign) IBOutlet NSTextField *statusTextField; @property (nonatomic, assign) IBOutlet NSButton *browserToggleButton; // Normally EidosConsoleWindowController is instantiated in the EidosConsoleWindow.xib nib - (instancetype)init NS_DESIGNATED_INITIALIZER; // Show the console window and make the console output first responder - (void)showWindow; - (void)hideWindow; - (void)displayStartupMessage; // Tell the controller that the console window should be disposed of, not just closed; breaks retain loops - (void)cleanup; // Get the console textview; this can be used to append new output in the console, for example - (EidosConsoleTextView *)textView; // Throw away the current symbol table - (void)invalidateSymbolTableAndFunctionMap; // Make a new symbol table from our delegate's current state; this actually executes a minimal script, ";", // to produce the symbol table as a side effect of setting up for the script's execution - (void)validateSymbolTableAndFunctionMap; // Execute the given script string, with the terminating semicolon being optional if requested - (void)executeScriptString:(NSString *)scriptString withOptionalSemicolon:(BOOL)semicolonOptional; // Actions used by EidosConsoleWindow.xib; may be called directly // Check the syntax of the current script; will call eidosConsoleWindowController:checkScriptDidSucceed: // if implemented by the delegate - (IBAction)checkScript:(id)sender; // Prettyprint the current script (after checking its syntax) - (IBAction)prettyprintScript:(id)sender; // Shows the shared script help window - (IBAction)showScriptHelp:(id)sender; // Clears all output in the console textview - (IBAction)clearOutput:(id)sender; // Executes all script currently in the script textview - (IBAction)executeAll:(id)sender; // Executes the line(s) containing the selection in the script textview - (IBAction)executeSelection:(id)sender; // Toggles the visibility of the console window - (IBAction)toggleConsoleVisibility:(id)sender; // Toggles the visibility of the variables browser - (IBAction)toggleBrowserVisibility:(id)sender; @end ================================================ FILE: EidosScribe/EidosConsoleWindowController.mm ================================================ // // EidosConsoleWindowController.m // EidosScribe // // Created by Ben Haller on 6/13/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosConsoleWindowController.h" #import "EidosConsoleWindowControllerDelegate.h" #import "EidosTextView.h" #import "EidosCocoaExtra.h" #import "EidosVariableBrowserControllerDelegate.h" #import "EidosTextViewDelegate.h" #import "EidosConsoleTextViewDelegate.h" #import "EidosHelpController.h" #import "EidosPrettyprinter.h" #include "eidos_script.h" #include "eidos_globals.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include #include #include #include // User defaults keys NSString *EidosDefaultsShowTokensKey = @"EidosShowTokens"; NSString *EidosDefaultsShowParseKey = @"EidosShowParse"; NSString *EidosDefaultsShowExecutionKey = @"EidosShowExecution"; NSString *EidosDefaultsSuppressScriptCheckSuccessPanelKey = @"EidosSuppressScriptCheckSuccessPanel"; @interface EidosConsoleWindowController () { // The symbol table for the console interpreter; needs to be wiped whenever the symbol table changes EidosSymbolTable *global_symbols; // The function map for the console interpreter; carries over from invocation to invocation EidosFunctionMap *global_function_map; BOOL global_function_map_owned; // if our delegate gave us a map, it owns it; if we made one, we own it } @end @implementation EidosConsoleWindowController @synthesize delegate, browserController, scriptWindow, bottomSplitView, scriptTextView, outputTextView, statusTextField; + (void)initialize { [[NSUserDefaults standardUserDefaults] registerDefaults:@{ EidosDefaultsShowTokensKey : @NO, EidosDefaultsShowParseKey : @NO, EidosDefaultsShowExecutionKey : @NO, EidosDefaultsSuppressScriptCheckSuccessPanelKey : @NO }]; } - (instancetype)init { if (self = [super init]) { [self setInterfaceEnabled:YES]; // Observe notifications to keep our variable browser toggle button up to date [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(browserWillShow:) name:EidosVariableBrowserWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(browserWillHide:) name:EidosVariableBrowserWillHideNotification object:nil]; } return self; } - (void)awakeFromNib { [self setInterfaceEnabled:YES]; // Tell Cocoa that we can go full-screen [scriptWindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; // Fix our splitview's position restore, which NSSplitView sometimes screws up [bottomSplitView eidosRestoreAutosavedPositionsWithName:@"Eidos Console Splitter"]; // THEN set the autosave names on the splitviews; this prevents NSSplitView from getting confused [bottomSplitView setAutosaveName:@"Eidos Console Splitter"]; // Show a welcome message [outputTextView showWelcomeMessage]; if ([delegate respondsToSelector:@selector(eidosConsoleWindowControllerAppendWelcomeMessageAddendum:)]) [delegate eidosConsoleWindowControllerAppendWelcomeMessageAddendum:self]; // And show our prompt [outputTextView showPrompt]; // Set up syntax coloring for the script view; note the console view's coloring is handled internally by us [scriptTextView setSyntaxColoring:kEidosSyntaxColoringEidos]; // Execute a null statement to get our symbols set up, for code completion etc. // Note this has the side effect of creating a random number generator gEidos_RNG for our use. [self validateSymbolTableAndFunctionMap]; } - (void)dealloc { //NSLog(@"EidosConsoleWindowController dealloc"); [[NSNotificationCenter defaultCenter] removeObserver:self]; [self cleanup]; [self setBottomSplitView:nil]; [self setScriptTextView:nil]; [self setOutputTextView:nil]; [self setStatusTextField:nil]; [self setBrowserToggleButton:nil]; [super dealloc]; } - (void)setDelegate:(NSObject *)newDelegate { if (newDelegate && ![newDelegate conformsToProtocol:@protocol(EidosConsoleWindowControllerDelegate)]) NSLog(@"Delegate %@ assigned to EidosConsoleWindowController %p does not conform to the EidosConsoleWindowControllerDelegate protocol!", newDelegate, self); delegate = newDelegate; // nonatomic, assign } - (NSObject *)delegate { return delegate; } - (void)browserWillShow:(NSNotification *)note { if ([note object] == browserController) [_browserToggleButton setState:NSOnState]; } - (void)browserWillHide:(NSNotification *)note { if ([note object] == browserController) [_browserToggleButton setState:NSOffState]; } - (void)showWindow { [scriptWindow makeKeyAndOrderFront:nil]; [scriptWindow makeFirstResponder:outputTextView]; } - (void)hideWindow { [scriptWindow performClose:nil]; } - (void)displayStartupMessage { NSDictionary *statusAttrs = [NSDictionary eidosTextAttributesWithColor:[NSColor textColor] size:11.0]; NSString *statusString = [NSString stringWithFormat:@"Eidos %s, %@ build.", EIDOS_VERSION_STRING, #if DEBUG @"debug" #else @"release" #endif ]; #ifdef _OPENMP statusString = [statusString stringByAppendingFormat:@" Running Eidos in parallel with %d threads maximum.", gEidosMaxThreads]; #endif NSMutableAttributedString *statusAttrString = [[[NSMutableAttributedString alloc] initWithString:statusString attributes:statusAttrs] autorelease]; [statusAttrString addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [statusAttrString length])]; [statusTextField setAttributedStringValue:statusAttrString]; } - (void)cleanup { delete global_symbols; global_symbols = nil; if (global_function_map_owned) delete global_function_map; global_function_map = nil; [bottomSplitView setDelegate:nil]; [self setBottomSplitView:nil]; [scriptWindow setDelegate:nil]; [self setScriptWindow:nil]; [browserController cleanup]; [self setBrowserController:nil]; [self setDelegate:nil]; } - (EidosConsoleTextView *)textView; { return outputTextView; } - (void)invalidateSymbolTableAndFunctionMap { if (global_symbols) { delete global_symbols; global_symbols = nil; } if (global_function_map) { if (global_function_map_owned) delete global_function_map; global_function_map = nil; } [browserController reloadBrowser]; } - (void)validateSymbolTableAndFunctionMap { if (!global_symbols || !global_function_map) { NSString *errorString = nil; [self _executeScriptString:@";" tokenString:NULL parseString:NULL executionString:NULL errorString:&errorString withOptionalSemicolon:NO]; if (errorString) NSLog(@"Error in validateSymbolTableAndFunctionMap: %@", errorString); } [browserController reloadBrowser]; } - (NSString *)_executeScriptString:(NSString *)scriptString tokenString:(NSString **)tokenString parseString:(NSString **)parseString executionString:(NSString **)executionString errorString:(NSString **)errorString withOptionalSemicolon:(BOOL)semicolonOptional { std::string script_string([scriptString UTF8String]); EidosScript script(script_string); std::string output; // Unfortunately, running readFromPopulationFile() is too much of a shock for SLiMgui. It invalidates variables that are being displayed in // the variable browser, in such an abrupt way that it causes a crash. Basically, the code in readFromPopulationFile() that "cleans" all // references to mutations and such does not have any way to clean SLiMgui's references, and so those stale references cause a crash. // There is probably a better solution, but for now, we look for code containing readFromPopulationFile() and special-case it. The user // could circumvent this and trigger a crash, so this is just a band-aid; a proper solution is needed. Another problem with this band-aid // is that SLiMgui's display does not refresh to show the new population state. Indeed, that is an issue with anything that changes the // visible state, such as adding new mutations. There needs to be some way for Eidos code to tell SLiMgui that UI refreshing is needed, // and to clean references to variables that are about to invalidated. FIXME BOOL safeguardReferences = NO; if (scriptString && ([scriptString rangeOfString:@"readFromPopulationFile"].location != NSNotFound)) // BCH 4/7/2016: containsString: added in 10.10 safeguardReferences = YES; if (safeguardReferences) [self invalidateSymbolTableAndFunctionMap]; // Make the final semicolon optional if requested; this allows input like "6+7" in the console if (semicolonOptional) script.SetFinalSemicolonOptional(true); // Tokenize try { script.Tokenize(); if (tokenString) { std::ostringstream token_stream; script.PrintTokens(token_stream); std::string &&token_string = token_stream.str(); *tokenString = [NSString stringWithUTF8String:token_string.c_str()]; #if 0 // Do a checkback on UTF8 token ranges versus UTF16 token ranges by replicating the token string using the UTF16 ranges const std::vector &tokens = script.Tokens(); NSMutableString *replicateTokenString = [NSMutableString string]; BOOL firstToken = YES; for (const EidosToken &token : tokens) { int32_t token_UTF16_start = token.token_UTF16_start_; int32_t token_UTF16_end = token.token_UTF16_end_; NSRange tokenRange = NSMakeRange(token_UTF16_start, token_UTF16_end - token_UTF16_start + 1); if (!firstToken) [replicateTokenString appendString:@" "]; if (tokenRange.location == [scriptString length]) [replicateTokenString appendString:@""]; else { [replicateTokenString appendString:@"<"]; [replicateTokenString appendString:[scriptString substringWithRange:tokenRange]]; [replicateTokenString appendString:@">"]; } firstToken = NO; } [replicateTokenString appendString:@"\n"]; *tokenString = [*tokenString stringByAppendingString:replicateTokenString]; #endif } } catch (...) { std::string &&error_string = Eidos_GetUntrimmedRaiseMessage(); *errorString = [NSString stringWithUTF8String:error_string.c_str()]; // move the error outside of the currentScript context if possible; the ranges // should have already been moved by TranslateErrorContextToUserScript() if (gEidosErrorContext.currentScript == &script) { #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: clearing gEidosErrorContext.currentScript after error in tokenization." << std::endl; #endif gEidosErrorContext.currentScript = nullptr; } else if (gEidosErrorContext.currentScript) { // The error got translated to a script we don't recognize; clear the error info, // all we can do is show the error string to the user, with no position *errorString = [@"A tokenization error occurred in a different script context, so the error position cannot be highlighted in the console:\n" stringByAppendingString:*errorString]; #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: error in tokenization traced to a different script; clearing all error info." << std::endl; #endif ClearErrorContext(); } return nil; } // Parse, an "interpreter block" bounded by an EOF rather than a "script block" that requires braces try { script.ParseInterpreterBlockToAST(true); if (parseString) { std::ostringstream parse_stream; script.PrintAST(parse_stream); std::string &&parse_string = parse_stream.str(); *parseString = [NSString stringWithUTF8String:parse_string.c_str()]; } } catch (...) { std::string &&error_string = Eidos_GetUntrimmedRaiseMessage(); *errorString = [NSString stringWithUTF8String:error_string.c_str()]; // move the error outside of the currentScript context if possible; the ranges // should have already been moved by TranslateErrorContextToUserScript() if (gEidosErrorContext.currentScript == &script) { #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: clearing gEidosErrorContext.currentScript after error in parsing." << std::endl; #endif gEidosErrorContext.currentScript = nullptr; } else if (gEidosErrorContext.currentScript) { // The error got translated to a script we don't recognize; clear the error info, // all we can do is show the error string to the user, with no position *errorString = [@"A parsing error occurred in a different script context, so the error position cannot be highlighted in the console:\n" stringByAppendingString:*errorString]; #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: error in parsing traced to a different script; clearing all error info." << std::endl; #endif ClearErrorContext(); } return nil; } // Get a symbol table and let our delegate add symbols to it if (!global_symbols) { global_symbols = gEidosConstantsSymbolTable; if ([delegate respondsToSelector:@selector(eidosConsoleWindowController:symbolsFromBaseSymbols:)]) global_symbols = [delegate eidosConsoleWindowController:self symbolsFromBaseSymbols:global_symbols]; // With the advant of global versus local symbol tables, the semantics here have gotten a little tricky. In EidosScribe // we want the console to work in the global variables table directly, which we need to create; that will be our symbol // table. In SLiM, we want the console to work in a local variables table; SLiM has its own global variables table, // which we don't want to clutter up, just as if were were in a callback. So here, we now check whether a global variables // table is already in the chain, and if so, we create a local variables table; otherwise we create a global variables table. bool global_variables_table_exists = false; EidosSymbolTable *scan_table = global_symbols; while (scan_table) { if (scan_table->TableType() == EidosSymbolTableType::kGlobalVariablesTable) { global_variables_table_exists = true; break; } scan_table = scan_table->ChainSymbolTable(); } EidosSymbolTableType console_table_type = global_variables_table_exists ? EidosSymbolTableType::kLocalVariablesTable : EidosSymbolTableType::kGlobalVariablesTable; global_symbols = new EidosSymbolTable(console_table_type, global_symbols); // add a table for script-defined variables on top } // Get a function map from our delegate, or make one ourselves if (!global_function_map) { global_function_map_owned = NO; if ([delegate respondsToSelector:@selector(functionMapForEidosConsoleWindowController:)]) global_function_map = [delegate functionMapForEidosConsoleWindowController:self]; if (!global_function_map) { global_function_map = new EidosFunctionMap(*EidosInterpreter::BuiltInFunctionMap()); global_function_map_owned = YES; } } // Get the EidosContext, if any, from the delegate EidosContext *eidos_context = nullptr; if ([delegate respondsToSelector:@selector(eidosConsoleWindowControllerEidosContext:)]) eidos_context = [delegate eidosConsoleWindowControllerEidosContext:self]; // Interpret the parsed block if ([delegate respondsToSelector:@selector(eidosConsoleWindowControllerWillExecuteScript:)]) [delegate eidosConsoleWindowControllerWillExecuteScript:self]; std::ostringstream outstream; // in SLiMguiLegacy, one output stream for both types of output EidosInterpreter interpreter(script, *global_symbols, *global_function_map, eidos_context, outstream, outstream #ifdef SLIMGUI , true #endif ); try { if (executionString) interpreter.SetShouldLogExecution(true); EidosValue_SP result = interpreter.EvaluateInterpreterBlock(true, true); // print output, return the last statement value (result not used) output = outstream.str(); // reload outline view to show new global symbols, in case they have changed [browserController reloadBrowser]; if (executionString) { std::string &&execution_string = interpreter.ExecutionLog(); *executionString = [NSString stringWithUTF8String:execution_string.c_str()]; } } catch (...) { if ([delegate respondsToSelector:@selector(eidosConsoleWindowControllerDidExecuteScript:)]) [delegate eidosConsoleWindowControllerDidExecuteScript:self]; output = outstream.str(); std::string &&error_string = Eidos_GetUntrimmedRaiseMessage(); *errorString = [NSString stringWithUTF8String:error_string.c_str()]; // move the error outside of the currentScript context if possible; the ranges // should have already been moved by TranslateErrorContextToUserScript() if (gEidosErrorContext.currentScript == &script) { #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: clearing gEidosErrorContext.currentScript after error in execution." << std::endl; #endif gEidosErrorContext.currentScript = nullptr; } else if (gEidosErrorContext.currentScript) { // The error got translated to a script we don't recognize; clear the error info, // all we can do is show the error string to the user, with no position *errorString = [@"An execution error occurred in a different script context, so the error position cannot be highlighted in the console:\n" stringByAppendingString:*errorString]; #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: error in execution traced to a different script; clearing all error info." << std::endl; #endif ClearErrorContext(); } return [NSString stringWithUTF8String:output.c_str()]; } if ([delegate respondsToSelector:@selector(eidosConsoleWindowControllerDidExecuteScript:)]) [delegate eidosConsoleWindowControllerDidExecuteScript:self]; // See comment on safeguardReferences above if (safeguardReferences) [self validateSymbolTableAndFunctionMap]; // Flush buffered output to files after every script execution, so the user sees the results // NOTE THAT THE WORKING DIRECTORY HAS BEEN CHANGED BACK AT THIS POINT! if (!Eidos_FlushFiles()) raise(SIGTRAP); // could be improved, but for SLiMguiLegacy this is OK return [NSString stringWithUTF8String:output.c_str()]; } - (void)executeScriptString:(NSString *)scriptString withOptionalSemicolon:(BOOL)semicolonOptional { NSTextStorage *ts = [outputTextView textStorage]; double fontSize = [outputTextView displayFontSize]; NSString *tokenString = nil, *parseString = nil, *executionString = nil, *errorString = nil; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; BOOL showTokens = [defaults boolForKey:EidosDefaultsShowTokensKey]; BOOL showParse = [defaults boolForKey:EidosDefaultsShowParseKey]; BOOL showExecution = [defaults boolForKey:EidosDefaultsShowExecutionKey]; NSUInteger promptEnd = [outputTextView promptRangeEnd]; NSRange scriptRange = NSMakeRange(promptEnd, [scriptString length]); NSString *result = [self _executeScriptString:scriptString tokenString:(showTokens ? &tokenString : NULL) parseString:(showParse ? &parseString : NULL) executionString:(showExecution ? &executionString : NULL) errorString:&errorString withOptionalSemicolon:semicolonOptional]; if (errorString && ([errorString rangeOfString:@"unexpected token 'EOF'"].location != NSNotFound)) // BCH 4/7/2016: containsString: added in 10.10 { // The user has entered an incomplete script line, so we need to append a newline... NSAttributedString *outputString1 = [[NSAttributedString alloc] initWithString:@"\n" attributes:[NSDictionary eidosInputAttrsWithSize:fontSize]]; [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:outputString1]; [ts endEditing]; [outputString1 release]; // ...and issue a continuation prompt to await further input [outputTextView showPrompt:'+']; originalPromptEnd = promptEnd; isContinuationPrompt = YES; } else { // make the attributed strings we will append NSAttributedString *outputString1 = [[NSAttributedString alloc] initWithString:@"\n" attributes:[NSDictionary eidosInputAttrsWithSize:fontSize]]; NSAttributedString *outputString2 = (result ? [[NSAttributedString alloc] initWithString:result attributes:[NSDictionary eidosOutputAttrsWithSize:fontSize]] : nil); NSAttributedString *errorAttrString = (errorString ? [[NSAttributedString alloc] initWithString:errorString attributes:[NSDictionary eidosErrorAttrsWithSize:fontSize]] : nil); NSAttributedString *tokenAttrString = (tokenString ? [[NSAttributedString alloc] initWithString:tokenString attributes:[NSDictionary eidosTokensAttrsWithSize:fontSize]] : nil); NSAttributedString *parseAttrString = (parseString ? [[NSAttributedString alloc] initWithString:parseString attributes:[NSDictionary eidosParseAttrsWithSize:fontSize]] : nil); NSAttributedString *executionAttrString = (executionString ? [[NSAttributedString alloc] initWithString:executionString attributes:[NSDictionary eidosExecutionAttrsWithSize:fontSize]] : nil);; // do the editing [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:outputString1]; [outputTextView appendSpacer]; if (tokenAttrString) { [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:tokenAttrString]; [outputTextView appendSpacer]; } if (parseAttrString) { [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:parseAttrString]; [outputTextView appendSpacer]; } if (executionAttrString) { [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:executionAttrString]; [outputTextView appendSpacer]; } if ([outputString2 length]) { [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:outputString2]; [outputTextView appendSpacer]; } if (errorAttrString) { [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:errorAttrString]; [outputTextView appendSpacer]; } // If we have an error, it is in the user script, and it has a valid position, then we can try to // highlight it in the input. Note that gEidosErrorContext.currentScript is nullptr in the console. if (errorString) { if (!gEidosErrorContext.currentScript || (gEidosErrorContext.currentScript->UserScriptUTF16Offset() == 0)) { if ((gEidosErrorContext.errorPosition.characterStartOfErrorUTF16 >= 0) && (gEidosErrorContext.errorPosition.characterEndOfErrorUTF16 >= gEidosErrorContext.errorPosition.characterStartOfErrorUTF16) && (scriptRange.location != NSNotFound)) { int errorTokenStart = gEidosErrorContext.errorPosition.characterStartOfErrorUTF16 + (int)scriptRange.location; int errorTokenEnd = gEidosErrorContext.errorPosition.characterEndOfErrorUTF16 + (int)scriptRange.location; NSRange charRange = NSMakeRange(errorTokenStart, errorTokenEnd - errorTokenStart + 1); [ts addAttribute:NSBackgroundColorAttributeName value:[NSColor redColor] range:charRange]; [ts addAttribute:NSForegroundColorAttributeName value:[NSColor whiteColor] range:charRange]; } #if EIDOS_DEBUG_ERROR_POSITIONS else { std::cout << "-[EidosConsoleWindowController executeScriptString:...]: an error occurred, but the error range is unusable" << std::endl; } #endif } #if EIDOS_DEBUG_ERROR_POSITIONS else { std::cout << "-[EidosConsoleWindowController executeScriptString:...]: an error occurred, but the script context is unusable" << std::endl; } #endif } [ts endEditing]; // clean up [outputString1 release]; [tokenAttrString release]; [parseAttrString release]; [executionAttrString release]; [outputString2 release]; [errorAttrString release]; // and show a new prompt [outputTextView showPrompt]; } } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; BOOL uiEnabled = [self interfaceEnabled]; if (sel == @selector(checkScript:)) return uiEnabled; if (sel == @selector(prettyprintScript:)) return uiEnabled; if (sel == @selector(executeAll:)) return uiEnabled; if (sel == @selector(executeSelection:)) return uiEnabled; if (sel == @selector(toggleConsoleVisibility:)) [menuItem setTitle:([scriptWindow isVisible] ? @"Hide Eidos Console" : @"Show Eidos Console")]; if (sel == @selector(toggleBrowserVisibility:)) return [browserController validateMenuItem:menuItem]; return YES; } // // Actions // #pragma mark - #pragma mark Actions - (BOOL)checkScriptSuppressSuccessResponse:(BOOL)suppressSuccessResponse { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString *scriptString = [scriptTextView string]; const char *cstr = [scriptString UTF8String]; NSString *errorDiagnostic = nil; if (!cstr) { errorDiagnostic = [@"The script string could not be read, possibly due to an encoding problem." retain]; } else { EidosScript script(cstr); try { script.Tokenize(); script.ParseInterpreterBlockToAST(true); } catch (...) { std::string &&error_diagnostic = Eidos_GetTrimmedRaiseMessage(); errorDiagnostic = [[NSString stringWithUTF8String:error_diagnostic.c_str()] retain]; // move the error outside of the currentScript context if possible; the ranges // should have already been moved by TranslateErrorContextToUserScript() if (gEidosErrorContext.currentScript == &script) { #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController checkScriptSuppressSuccessResponse:]: clearing gEidosErrorContext.currentScript after error in script check." << std::endl; #endif gEidosErrorContext.currentScript = nullptr; } else if (gEidosErrorContext.currentScript) { // The error got translated to a script we don't recognize; clear the error info, // all we can do is show the error string to the user, with no position #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: error in script check traced to a different script; clearing all error info." << std::endl; #endif ClearErrorContext(); } } } BOOL checkDidSucceed = !errorDiagnostic; if (!checkDidSucceed || !suppressSuccessResponse) { if ([delegate respondsToSelector:@selector(eidosConsoleWindowController:checkScriptDidSucceed:)]) [delegate eidosConsoleWindowController:self checkScriptDidSucceed:checkDidSucceed]; else [[NSSound soundNamed:(checkDidSucceed ? @"Bottle" : @"Ping")] play]; if (!checkDidSucceed) { // On failure, we show an alert describing the error, and highlight the relevant script line NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleWarning]; [alert setMessageText:@"Script error"]; [alert setInformativeText:errorDiagnostic]; [alert addButtonWithTitle:@"OK"]; [alert beginSheetModalForWindow:scriptWindow completionHandler:^(NSModalResponse returnCode) { [alert autorelease]; }]; [scriptTextView selectErrorRange]; // Show the error in the status bar also NSString *trimmedError = [errorDiagnostic stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSDictionary *errorAttrs = [NSDictionary eidosTextAttributesWithColor:[NSColor redColor] size:11.0]; NSMutableAttributedString *errorAttrString = [[[NSMutableAttributedString alloc] initWithString:trimmedError attributes:errorAttrs] autorelease]; [errorAttrString addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [errorAttrString length])]; [statusTextField setAttributedStringValue:errorAttrString]; } else { // On success, we optionally show a success alert sheet if (![defaults boolForKey:EidosDefaultsSuppressScriptCheckSuccessPanelKey]) { NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleInformational]; [alert setMessageText:@"No script errors"]; [alert setInformativeText:@"No errors found."]; [alert addButtonWithTitle:@"OK"]; [alert setShowsSuppressionButton:YES]; [alert beginSheetModalForWindow:scriptWindow completionHandler:^(NSModalResponse returnCode) { if ([[alert suppressionButton] state] == NSOnState) [defaults setBool:YES forKey:EidosDefaultsSuppressScriptCheckSuccessPanelKey]; [alert autorelease]; }]; } } } [errorDiagnostic release]; return checkDidSucceed; } - (IBAction)checkScript:(id)sender { [self checkScriptSuppressSuccessResponse:NO]; } - (IBAction)prettyprintScript:(id)sender { if ([scriptTextView isEditable]) { if ([self checkScriptSuppressSuccessResponse:YES]) { // We know the script is syntactically correct, so we can tokenize and parse it without worries NSString *scriptString = [scriptTextView string]; const char *cstr = [scriptString UTF8String]; EidosScript script(cstr); script.Tokenize(false, true); // get whitespace and comment tokens // Then generate a new script string that is prettyprinted const std::vector &tokens = script.Tokens(); NSMutableString *pretty = [NSMutableString string]; if ([EidosPrettyprinter prettyprintTokens:tokens fromScript:script intoString:pretty]) { if ([scriptTextView shouldChangeTextInRange:NSMakeRange(0, [[scriptTextView string] length]) replacementString:pretty]) { [scriptTextView setString:pretty]; [scriptTextView didChangeText]; } else { NSBeep(); } } else NSBeep(); } } else { NSBeep(); } } - (IBAction)showScriptHelp:(id)sender { [[EidosHelpController sharedController] showWindow]; } - (IBAction)clearOutput:(id)sender { if (isContinuationPrompt) { [outputTextView clearOutputToPosition:originalPromptEnd - 2]; originalPromptEnd = 2; } else { [outputTextView clearOutputToPosition:outputTextView->lastPromptRange.location]; } } - (void)fixDumbSelectionBug:(id)unused { // See comment below in toggleConsoleVisibility: [outputTextView setSelectable:YES]; [outputTextView setEditable:YES]; [scriptWindow makeFirstResponder:outputTextView]; } - (IBAction)toggleConsoleVisibility:(id)sender { if ([scriptWindow isVisible]) [scriptWindow performClose:nil]; else { [scriptWindow makeKeyAndOrderFront:nil]; // I have no idea what the heck is going on here, but without this code, the console window comes up – in SLiMgui only – with // the insertion point blinking several lines up from the prompt, in a nonsensical position. It looks to me like an AppKit bug; // I think the Kit is deciding where the insertion point is prior to some view resize or relayout operation, and the position // isn't getting recalculated after that resize/relayout. I couldn't figure out why it was doing that, or why it happens only // in SLiMgui, and this is the cleanest workaround I could find. Sheesh. BCH 9 February 2016. [outputTextView setSelectable:NO]; [scriptWindow makeFirstResponder:nil]; [self performSelector:@selector(fixDumbSelectionBug:) withObject:nil afterDelay:0.0]; } } - (IBAction)toggleBrowserVisibility:(id)sender { [browserController toggleBrowserVisibility:nil]; } - (void)elideContinuationPrompt { // This replaces the continuation prompt, if there is one, with a space, and switches the active prompt back to the original prompt; // the net effect is as if the user entered a newline and two spaces at the original prompt, with no continuation. Note that the // two spaces at the beginning of continuation lines is mirrored in fullInputString, below. if (isContinuationPrompt) { NSTextStorage *ts = [outputTextView textStorage]; NSDictionary *inputAttrs = [NSDictionary eidosInputAttrsWithSize:[outputTextView displayFontSize]]; NSAttributedString *promptString1 = [[NSAttributedString alloc] initWithString:@" " attributes:inputAttrs]; NSUInteger promptEnd = [outputTextView promptRangeEnd]; [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange(promptEnd - 2, 1) withAttributedString:promptString1]; [ts endEditing]; [outputTextView setPromptRangeEnd:originalPromptEnd]; isContinuationPrompt = NO; [promptString1 release]; } } - (NSString *)fullInputString { [self elideContinuationPrompt]; NSString *fullInputString = [outputTextView string]; NSUInteger promptEnd = [outputTextView promptRangeEnd]; return [fullInputString substringFromIndex:promptEnd]; } - (IBAction)executeAll:(id)sender { NSTextStorage *ts = [outputTextView textStorage]; NSDictionary *inputAttrs = [NSDictionary eidosInputAttrsWithSize:[outputTextView displayFontSize]]; NSString *fullScriptString = [scriptTextView string]; NSAttributedString *scriptAttrString = [[[NSAttributedString alloc] initWithString:fullScriptString attributes:inputAttrs] autorelease]; NSUInteger promptEnd = [outputTextView promptRangeEnd]; // The contents of the current prompt line get replaced by the execution block [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange(promptEnd, [ts length] - promptEnd) withAttributedString:scriptAttrString]; [ts endEditing]; // The current prompt might be a continuation prompt, so now we get the full input string from the original prompt NSString *executionString = [self fullInputString]; [outputTextView registerNewHistoryItem:executionString]; [self executeScriptString:executionString withOptionalSemicolon:YES]; } - (IBAction)executeSelection:(id)sender { NSTextStorage *ts = [outputTextView textStorage]; NSString *fullScriptString = [scriptTextView string]; NSUInteger scriptLength = [fullScriptString length]; NSRange selectedRange = [scriptTextView selectedRange]; NSCharacterSet *newlineChars = [NSCharacterSet newlineCharacterSet]; NSRange executionRange = selectedRange; // indices of the first and last characters to execute // If the selection is an insertion point, execute the whole line if (executionRange.length == 0) { // start at the start of the selection and move backwards to the beginning of the line while (executionRange.location > 0) { unichar ch = [fullScriptString characterAtIndex:executionRange.location - 1]; if ([newlineChars characterIsMember:ch]) break; --executionRange.location; ++executionRange.length; } // now move the end of the selection backwards to remove any newlines from the end of the range to execute while (executionRange.length > 0) { unichar ch = [fullScriptString characterAtIndex:executionRange.location + executionRange.length - 1]; if (![newlineChars characterIsMember:ch]) break; --executionRange.length; } // now move the end of the selection forwards to the end of the line, not including the newline while (executionRange.location + executionRange.length < scriptLength) { unichar ch = [fullScriptString characterAtIndex:executionRange.location + executionRange.length]; if ([newlineChars characterIsMember:ch]) break; ++executionRange.length; } } // now execute the range we have found if (executionRange.length > 0) { NSString *scriptString = [fullScriptString substringWithRange:executionRange]; NSDictionary *inputAttrs = [NSDictionary eidosInputAttrsWithSize:[outputTextView displayFontSize]]; NSAttributedString *scriptAttrString = [[[NSAttributedString alloc] initWithString:scriptString attributes:inputAttrs] autorelease]; NSUInteger promptEnd = [outputTextView promptRangeEnd]; // The contents of the current prompt line get replaced by the execution line [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange(promptEnd, [ts length] - promptEnd) withAttributedString:scriptAttrString]; [ts endEditing]; // The current prompt might be a continuation prompt, so now we get the full input string from the original prompt NSString *executionString = [self fullInputString]; [outputTextView registerNewHistoryItem:executionString]; [self executeScriptString:executionString withOptionalSemicolon:YES]; } else { NSBeep(); } } // // VariableBrowserControllerDelegate methods // #pragma mark - #pragma mark VariableBrowserControllerDelegate - (EidosSymbolTable *)symbolTableForEidosVariableBrowserController:(EidosVariableBrowserController *)browserController { return global_symbols; } // // NSTextViewDelegate methods // #pragma mark - #pragma mark NSTextViewDelegate - (NSRange)textView:(NSTextView *)textView willChangeSelectionFromCharacterRange:(NSRange)oldRange toCharacterRange:(NSRange)newRange { if (textView == outputTextView) { // prevent a zero-length selection (i.e. an insertion point) in the history if ((newRange.length == 0) && (newRange.location < [outputTextView promptRangeEnd])) return NSMakeRange([[outputTextView string] length], 0); } return newRange; } - (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString { if (textView == outputTextView) { // prevent the user from changing anything above the current prompt if (affectedCharRange.location < [outputTextView promptRangeEnd]) return NO; } return YES; } // // EidosConsoleTextViewDelegate methods // #pragma mark - #pragma mark EidosConsoleTextViewDelegate - (void)eidosConsoleTextViewExecuteInput:(EidosConsoleTextView *)textView { if (textView == outputTextView) { if (isContinuationPrompt && ([[outputTextView string] length] == [outputTextView promptRangeEnd])) { // If the user has hit return at an empty continuation prompt, we take that as a sign that they want to get out of it NSString *executionString = [self fullInputString]; [outputTextView registerNewHistoryItem:executionString]; NSTextStorage *ts = [outputTextView textStorage]; NSAttributedString *outputString1 = [[NSAttributedString alloc] initWithString:@"\n" attributes:[NSDictionary eidosInputAttrsWithSize:[outputTextView displayFontSize]]]; [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:outputString1]; [ts endEditing]; [outputString1 release]; // show a new prompt [outputTextView showPrompt]; } else { // The current prompt might be a non-empty continuation prompt, so now we get the full input string from the original prompt NSString *executionString = [self fullInputString]; [outputTextView registerNewHistoryItem:executionString]; [self executeScriptString:executionString withOptionalSemicolon:YES]; } } } // // EidosTextViewDelegate methods // #pragma mark - #pragma mark EidosTextViewDelegate - (EidosSymbolTable *)eidosTextView:(EidosTextView *)eidosTextView symbolsFromBaseSymbols:(EidosSymbolTable *)baseSymbols { return global_symbols; // we keep our own symbol table, so we don't call our delegate here } - (EidosFunctionMap *)functionMapForEidosTextView:(EidosTextView *)eidosTextView { // If we have a delegate that has its own function map, use that, otherwise use ours if ([delegate respondsToSelector:@selector(functionMapForEidosConsoleWindowController:)]) return [delegate functionMapForEidosConsoleWindowController:self]; else return global_function_map; } - (void)eidosTextView:(EidosTextView *)eidosTextView addOptionalFunctionsToMap:(EidosFunctionMap *)functionMap { if ([delegate respondsToSelector:@selector(eidosConsoleWindowController:addOptionalFunctionsToMap:)]) [delegate eidosConsoleWindowController:self addOptionalFunctionsToMap:functionMap]; } - (EidosSyntaxHighlightType)eidosTextView:(EidosTextView *)eidosTextView tokenStringIsSpecialIdentifier:(const std::string &)token_string { if ([delegate respondsToSelector:@selector(eidosConsoleWindowController:tokenStringIsSpecialIdentifier:)]) return [delegate eidosConsoleWindowController:self tokenStringIsSpecialIdentifier:token_string]; else return EidosSyntaxHighlightType::kNoSyntaxHighlight; } - (NSString *)eidosTextView:(EidosTextView *)eidosTextView helpTextForClickedText:(NSString *)clickedText { if ([delegate respondsToSelector:@selector(eidosConsoleWindowController:helpTextForClickedText:)]) return [delegate eidosConsoleWindowController:self helpTextForClickedText:clickedText]; else return nil; } - (void)textViewDidChangeSelection:(NSNotification *)notification { NSTextView *textView = (NSTextView *)[notification object]; if (textView == outputTextView) { NSUInteger promptEnd = [outputTextView promptRangeEnd]; NSRange selectedRange = [outputTextView selectedRange]; if ((promptEnd > 0) && (selectedRange.location >= promptEnd)) { NSString *outputString = [outputTextView string]; NSString *scriptString = [outputString substringFromIndex:promptEnd]; selectedRange.location -= promptEnd; [statusTextField setAttributedStringValue:[outputTextView attributedSignatureForScriptString:scriptString selection:selectedRange]]; } else { [statusTextField setStringValue:@""]; } } else if (textView == scriptTextView) { [statusTextField setAttributedStringValue:[scriptTextView attributedSignatureForScriptString:[scriptTextView string] selection:[scriptTextView selectedRange]]]; } } // // NSWindow delegate methods // #pragma mark - #pragma mark NSWindow delegate - (void)windowWillClose:(NSNotification *)notification { NSWindow *closingWindow = [notification object]; if (closingWindow == scriptWindow) { // The variable browser is an inspector on our state, but we don't close it here. In EidosScribe, // we are quitting at this point anyway, so it doesn't matter. In SLiMgui, the var browser is // still meaningful even with our window closed, since it shows the current simulation state. // This may need to be revisited for other Contexts. //if ([[browserController browserWindow] isVisible]) // [browserController toggleBrowserVisibility:self]; // Let our delegate do something; EidosScribe quits, SLiMgui toggles its console button if ([delegate respondsToSelector:@selector(eidosConsoleWindowControllerConsoleWindowWillClose:)]) [delegate eidosConsoleWindowControllerConsoleWindowWillClose:self]; } } // // NSSplitView delegate methods // #pragma mark - #pragma mark NSSplitView delegate - (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview { return YES; } // NSSplitView doesn't like delegates to implement these methods any more; it logs if you do. We can achieve the same // effect using constraints in the nib, which is the new way to do things, so that's what we do now. /* - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)dividerIndex { return proposedMax - 230; } - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)dividerIndex { return proposedMin + 230; } */ @end @implementation EidosConsoleWindowController (CxxAdditions) // provides access to the symbol table of the console window, sometimes used by the Context for completion or other tasks - (EidosSymbolTable *)symbols { return global_symbols; } @end ================================================ FILE: EidosScribe/EidosConsoleWindowControllerDelegate.h ================================================ // // EidosConsoleWindowControllerDelegate.h // EidosScribe // // Created by Ben Haller on 9/10/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #include #include #include "eidos_interpreter.h" #include "EidosTextViewDelegate.h" @class EidosConsoleWindowController; /* This is an Objective-C++ header, and so can only be included by Objective-C++ compilations (.mm files instead of .m files). You should not need to include this header in your .h files, since you can declare protocol conformance in a class-continuation category in your .m file, so only classes that conform to this protocol should need to be Objective-C++. EidosConsoleWindowControllerDelegate is a protocol of optional methods that EidosConsoleWindowController's delegate can implement, to provide various Context-defined behaviors and modifications. */ @protocol EidosConsoleWindowControllerDelegate @optional // If provided, this context object will be handed to EidosInterpreter objects created by the console // controller when interpreting Eidos code; the context can then be obtained by Context implementations // of functions and method using EidosInterpreter::Context(), to recover the context object for their own use - (EidosContext *)eidosConsoleWindowControllerEidosContext:(EidosConsoleWindowController *)eidosConsoleController; // This allows the Context to append its own welcome message to the console window on startup - (void)eidosConsoleWindowControllerAppendWelcomeMessageAddendum:(EidosConsoleWindowController *)eidosConsoleController; // This allows the Context to define its own symbols beyond those in Eidos itself - (EidosSymbolTable *)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController symbolsFromBaseSymbols:(EidosSymbolTable *)baseSymbols; // This allows the Context to define its own functions beyond those in Eidos itself // The returned symbol table is not freed by the caller, since it is assumed to be // an existing object with a lifetime managed by the callee. - (EidosFunctionMap *)functionMapForEidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController; // The functionMapForEidosConsoleWindowController: delegate method returns the current function map // from the state of the delegate. That may not include some optional functions, such as SLiM's // zero-generation functions, that EidosConsoleWindowController wants to know about in some situations. // This delegate method requests those optional functions to be added. - (void)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController addOptionalFunctionsToMap:(EidosFunctionMap *)functionMap; // This notifies the delegate that a script check operation did or did not succeed, allowing custom UI - (void)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController checkScriptDidSucceed:(BOOL)succeeded; // This delegate method is called immediately before a script block is executed, allowing custom setup - (void)eidosConsoleWindowControllerWillExecuteScript:(EidosConsoleWindowController *)eidosConsoleController; // This delegate method is called immediately after a script block is executed, allowing custom tear-down - (void)eidosConsoleWindowControllerDidExecuteScript:(EidosConsoleWindowController *)eidosConsoleController; // This delegate method is called just before a console window is closed - (void)eidosConsoleWindowControllerConsoleWindowWillClose:(EidosConsoleWindowController *)eidosConsoleController; // messages from EidosTextViewDelegate that we essentially forward on to our delegate; see EidosTextView.h - (EidosSyntaxHighlightType)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController tokenStringIsSpecialIdentifier:(const std::string &)token_string; - (NSString *)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController helpTextForClickedText:(NSString *)clickedText; @end // We also provide here a method on EidosConsoleWindowController that returns a C++ object and thus cannot be declared // in the main header. If you need to call this method, you can simply include this header to get its interface. @interface EidosConsoleWindowController (CxxAdditions) // provides access to the symbol table of the console window, sometimes used by the Context for completion or other tasks - (EidosSymbolTable *)symbols; @end ================================================ FILE: EidosScribe/EidosHelpClasses.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2709 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Bold;\f1\fswiss\fcharset0 Optima-Italic;\f2\fnil\fcharset0 Menlo-Italic; \f3\fnil\fcharset0 Menlo-Regular;\f4\fswiss\fcharset0 Optima-Regular;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab720 \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf2 5.1 Class Object\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf2 5.1.1 \f2\fs18 Object \f1\fs22 properties\ 5.1.2 \f2\fs18 Object \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 +\'a0(integer$)length(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the size (e.g., length) of the receiving object. This is equivalent to the \f3\fs18 length() \f4\fs20 (or \f3\fs18 size() \f4\fs20 ) function; in other words, for any \f3\fs18 object \f4\fs20 \f3\fs18 x \f4\fs20 , the return value of the function call \f3\fs18 length(x) \f4\fs20 equals the return value of the class method call \f3\fs18 x.length() \f4\fs20 . This method is provided solely for syntactic convenience. Note that \f3\fs18 +length() \f4\fs20 is a synonym for \f3\fs18 +size() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(void)methodSignature([Ns$\'a0methodName\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Prints the method signature for the method specified by \f3\fs18 methodName \f4\fs20 , or for all methods supported by the receiving object if \f3\fs18 methodName \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(void)propertySignature([Ns$\'a0propertyName\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Prints the property signature for the property specified by \f3\fs18 propertyName \f4\fs20 , or for all properties supported by the receiving object if \f3\fs18 propertyName \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(integer$)size(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the size of the receiving object. This is equivalent to the \f3\fs18 size() \f4\fs20 (or \f3\fs18 length() \f4\fs20 ) function; in other words, for any \f3\fs18 object \f4\fs20 \f3\fs18 x \f4\fs20 , the return value of the function call \f3\fs18 size(x) \f4\fs20 equals the return value of the class method call \f3\fs18 x.size() \f4\fs20 . This method is provided solely for syntactic convenience. Note that \f3\fs18 +length() \f4\fs20 is a synonym for \f3\fs18 +size() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)str(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Prints the internal property structure of the receiving object; in particular, the element type of the object is printed, followed, on successive lines, by all of the properties supported by the object, their types, and a sample of their values.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(string$)stringRepresentation(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a singleton \f3\fs18 string \f4\fs20 value that represents the receiving object. By default, this is simply the name of the class of the receiving object; however, many subclasses of \f3\fs18 Object \f4\fs20 provide a different string representation. The value returned by \f3\fs18 stringRepresentation() \f4\fs20 is the same string that would be printed by \f3\fs18 print() \f4\fs20 for the object, so \f3\fs18 stringRepresentation() \f4\fs20 allows the same representation to be used in other contexts such as \f3\fs18 paste() \f4\fs20 and \f3\fs18 cat() \f4\fs20 .\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf2 5.2 Class DataFrame\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\b0\fs18 \cf2 (object$)DataFrame(...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 DataFrame \f4\fs20 constructor can be called in the same ways as the constructor for \f3\fs18 Dictionary \f4\fs20 (its superclass): with no parameters to make an empty \f3\fs18 DataFrame \f4\fs20 , with key-value pairs, with a singleton \f3\fs18 Dictionary \f4\fs20 (or a subclass of \f3\fs18 Dictionary \f4\fs20 , like \f3\fs18 DataFrame \f4\fs20 ) to make a copy, or with a string in JSON format. See the \f3\fs18 Dictionary \f4\fs20 class for further documentation. However, note that \f3\fs18 DataFrame \f4\fs20 can only use \f3\fs18 string \f4\fs20 keys; \f3\fs18 integer \f4\fs20 keys are not allowed.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.2.1 \f2\fs18 DataFrame \f1\fs22 properties\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 colNames => (string)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A vector containing all of the \f3\fs18 string \f4\fs20 column names in the \f3\fs18 DataFrame \f4\fs20 , in order. This property is currently an alias for the \f3\fs18 Dictionary \f4\fs20 property \f3\fs18 allKeys \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 dim => (integer)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A two-element vector containing the dimensions of the \f3\fs18 DataFrame \f4\fs20 . The \f3\fs18 0 \f4\fs20 th element is the number of rows (as provided by \f3\fs18 nrow \f4\fs20 ), and the \f3\fs18 1 \f4\fs20 st element is the number of columns (as provided by \f3\fs18 ncol \f4\fs20 ).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 ncol => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The number of columns in the \f3\fs18 DataFrame \f4\fs20 ; this will be equal to the length of \f3\fs18 colNames \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 nrow => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The number of rows in the \f3\fs18 DataFrame \f4\fs20 (i.e., the number of elements in a column). This will be the same for every column, by definition.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.2.2 \f2\fs18 DataFrame \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(*)asMatrix(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a matrix representation of the \f3\fs18 DataFrame \f4\fs20 . The matrix will have the same type as the elements of the \f3\fs18 DataFrame \f4\fs20 ; if the \f3\fs18 DataFrame \f4\fs20 contains more than one type of element, an error will be raised. The order of the columns of the \f3\fs18 DataFrame \f4\fs20 will be preserved. This method is useful, for example, if you wish to read in a text file as a matrix; you can use \f3\fs18 readCSV() \f4\fs20 to read the file as a \f3\fs18 DataFrame \f4\fs20 , and then convert it to a matrix with \f3\fs18 asMatrix() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)cbind(object\'a0source, ...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds all of the columns contained by \f3\fs18 source \f4\fs20 (which must be a \f3\fs18 Dictionary \f4\fs20 or a subclass of \f3\fs18 Dictionary \f4\fs20 such as \f3\fs18 DataFrame \f4\fs20 ) to the receiver. This method makes the target \f3\fs18 DataFrame \f4\fs20 wider, by adding new columns. If \f3\fs18 source \f4\fs20 contains a column name that is already defined in the target, an error will result. As always for \f3\fs18 DataFrame \f4\fs20 , the columns of the resulting \f3\fs18 DataFrame \f4\fs20 must all be the same length.\ The \f3\fs18 source \f4\fs20 parameter may be a non-singleton vector containing multiple \f3\fs18 Dictionary \f4\fs20 objects, and additional \f3\fs18 Dictionary \f4\fs20 vectors may be supplied (thus the ellipsis in the signature). Each \f3\fs18 Dictionary \f4\fs20 supplied will be added to the target, in the order supplied.\ This method is similar to the \f3\fs18 Dictionary \f4\fs20 method \f3\fs18 addKeysAndValuesFrom() \f4\fs20 , which may be used instead if replacement of duplicate columns is desired.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)rbind(object\'a0source, ...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Appends all of the columns contained by \f3\fs18 source \f4\fs20 (which must be a \f3\fs18 Dictionary \f4\fs20 or a subclass of \f3\fs18 Dictionary \f4\fs20 such as \f3\fs18 DataFrame \f4\fs20 ) to the receiver. This method makes the \f3\fs18 DataFrame \f4\fs20 taller, by adding new rows. If the source and target do not contain the same column names in the same order, an error will result. As always for \f3\fs18 DataFrame \f4\fs20 , the columns of the resulting \f3\fs18 DataFrame \f4\fs20 must all be the same length.\ The \f3\fs18 source \f4\fs20 parameter may be a non-singleton vector containing multiple \f3\fs18 Dictionary \f4\fs20 objects, and additional \f3\fs18 Dictionary \f4\fs20 vectors may be supplied (thus the ellipsis in the signature). Each \f3\fs18 Dictionary \f4\fs20 supplied will be appended to the target, in the order supplied.\ This method is similar to the \f3\fs18 Dictionary \f4\fs20 method \f3\fs18 appendKeysAndValuesFrom() \f4\fs20 , which may be used instead if one wishes the append to work even when the columns are in different orders, or other such situations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(*)subset([Nli\'a0rows\'a0=\'a0NULL], [Nlis\'a0cols\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the elements in the selected rows and columns of the target \f3\fs18 DataFrame \f4\fs20 . The selection logic is based upon that for \f3\fs18 subsetRows() \f4\fs20 and \f3\fs18 subsetColumns() \f4\fs20 , respectively; in short, rows may be selected by \f3\fs18 integer \f4\fs20 indices or by a \f3\fs18 logical \f4\fs20 vector, and columns may be selected by \f3\fs18 integer \f4\fs20 indices, by a \f3\fs18 logical \f4\fs20 vector, or by a \f3\fs18 string \f4\fs20 vector of column names. In addition, however, \f3\fs18 NULL \f4\fs20 may be passed for either \f3\fs18 rows \f4\fs20 or \f3\fs18 cols \f4\fs20 to select all of the rows or all of the columns, respectively; this is the default for both parameters. If you want entire rows (rather than selecting particular columns), pass \f3\fs18 NULL \f4\fs20 for \f3\fs18 cols \f4\fs20 ; if you want entire columns (rather than selecting particular rows), pass \f3\fs18 NULL \f4\fs20 for \f3\fs18 rows \f4\fs20 .\ The first step performed by \f3\fs18 subset() \f4\fs20 is to produce a \f3\fs18 DataFrame \f4\fs20 that contains the selected rows and columns. If that \f3\fs18 DataFrame \f4\fs20 contains more than one column, it is simply returned, and the behavior of \f3\fs18 subset() \f4\fs20 is identical to calling \f3\fs18 subsetRows() \f4\fs20 and \f3\fs18 subsetColumns() \f4\fs20 in sequence (in either order). If, however, the resulting \f3\fs18 DataFrame \f4\fs20 contains only a single column, then \f3\fs18 subset() \f4\fs20 will return a vector containing the elements in that column \'96 unlike the behavior of \f3\fs18 subsetRows() \f4\fs20 and \f3\fs18 subsetColumns() \f4\fs20 , which always return a \f3\fs18 DataFrame \f4\fs20 . This method is therefore a convenient way to get a single value, or multiple values from the same column, from a \f3\fs18 DataFrame \f4\fs20 . (Note that the \f3\fs18 Dictionary \f4\fs20 method \f3\fs18 getValue() \f4\fs20 can also be used to get all of the values from a given \f3\fs18 DataFrame \f4\fs20 column.)\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)subsetColumns(lis\'a0index)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a new \f3\fs18 DataFrame \f4\fs20 containing values for the selected columns of the target \f3\fs18 DataFrame \f4\fs20 . The selection logic described below is similar to how the subset operator \f3\fs18 [] \f4\fs20 in Eidos works, selecting the columns of the target \f3\fs18 DataFrame \f4\fs20 .\ The index parameter may be either \f3\fs18 integer \f4\fs20 , \f3\fs18 logical \f4\fs20 , or \f3\fs18 string \f4\fs20 ; we will discuss the \f3\fs18 integer \f4\fs20 case first. If \f3\fs18 index \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 , the returned \f3\fs18 DataFrame \f4\fs20 will contain the \f3\fs18 index \f4\fs20 \'92th column of the target (counting from the left, from \f3\fs18 0 \f4\fs20 ). If \f3\fs18 index \f4\fs20 is a non-singleton \f3\fs18 integer \f4\fs20 vector, the returned \f3\fs18 DataFrame \f4\fs20 will contains all of the selected columns, in the order that they are selected by \f3\fs18 index \f4\fs20 . If any \f3\fs18 index \f4\fs20 value is out of range for the target \f3\fs18 DataFrame \f4\fs20 (such that the \f3\fs18 DataFrame \f4\fs20 does not have an \f3\fs18 index \f4\fs20 \'92th column), an error will result. If the same column is specified more than once, unique column names will be automatically generated for the additional copies of the column.\ If \f3\fs18 index \f4\fs20 is a \f3\fs18 string \f4\fs20 vector, the returned \f3\fs18 DataFrame \f4\fs20 will contain copies of the columns in the target named by \f3\fs18 index \f4\fs20 . As with an \f3\fs18 integer \f4\fs20 vector, it is an error if a given column does not exist in the target; and unique column names will be generated for additional copies of a column.\ Finally, if \f3\fs18 index \f4\fs20 is a \f3\fs18 logical \f4\fs20 vector, the length of \f3\fs18 index \f4\fs20 must be equal to the number of columns in the target. In this case, the \f3\fs18 T \f4\fs20 values in \f3\fs18 index \f4\fs20 select the columns which will be included in the returned \f3\fs18 DataFrame \f4\fs20 . The columns in the returned \f3\fs18 DataFrame \f4\fs20 will be in the same order as in the target.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)subsetRows(li\'a0index, [logical$\'a0drop\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a new \f3\fs18 DataFrame \f4\fs20 containing values for selected rows of the target \f3\fs18 DataFrame \f4\fs20 . The selection logic described below works exactly as the subset operator \f3\fs18 [] \f4\fs20 does in Eidos, selecting the rows of the target \f3\fs18 DataFrame \f4\fs20 .\ The \f3\fs18 index \f4\fs20 parameter may be either \f3\fs18 integer \f4\fs20 or \f3\fs18 logical \f4\fs20 ; we will discuss the \f3\fs18 integer \f4\fs20 case first. If \f3\fs18 index \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 , the returned \f3\fs18 DataFrame \f4\fs20 will contain the \f3\fs18 index \f4\fs20 \'92th element of the value of each key of the target, under the same keys; this is a single row of the target \f3\fs18 DataFrame \f4\fs20 . If \f3\fs18 index \f4\fs20 is a non-singleton \f3\fs18 integer \f4\fs20 vector, the returned \f3\fs18 DataFrame \f4\fs20 will contain the values for all of the selected rows, in the order that they are selected by \f3\fs18 index \f4\fs20 . If any index value in \f3\fs18 index \f4\fs20 is out of range for the target \f3\fs18 DataFrame \f4\fs20 (such that that DataFrame does not have an \f3\fs18 index \f4\fs20 \'92th row), an error will result.\ If \f3\fs18 index \f4\fs20 is \f3\fs18 logical \f4\fs20 , the length of \f3\fs18 index \f4\fs20 must be equal to the number of rows in the target. In this case, the \f3\fs18 T \f4\fs20 values in \f3\fs18 index \f4\fs20 select the rows which will be included in the returned \f3\fs18 DataFrame \f4\fs20 . The values of each column in the returned \f3\fs18 DataFrame \f4\fs20 will be in the same order as in the target.\ If the values of \f3\fs18 index \f4\fs20 are such that \f1\i no \f4\i0 value for a given key is selected, the \f3\fs18 drop \f4\fs20 parameter controls the resulting behavior. If \f3\fs18 drop \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), the key will be included in the returned dictionary with a zero-length value of matching type, such as \f3\fs18 integer(0) \f4\fs20 or \f3\fs18 string(0) \f4\fs20 . If \f3\fs18 drop \f4\fs20 is \f3\fs18 T \f4\fs20 , the key will be omitted from the returned dictionary.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf2 5.3 Class Dictionary\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\b0\fs18 \cf2 (object$)Dictionary(...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Creates a new \f3\fs18 Dictionary \f4\fs20 object. Called without arguments, as \f3\fs18 Dictionary() \f4\fs20 , this creates a new empty \f3\fs18 Dictionary \f4\fs20 .\ Alternatively, key-value pairs can be passed to set up the initial state of the new \f3\fs18 Dictionary \f4\fs20 . These are set, sequentially, on the new \f3\fs18 Dictionary \f4\fs20 , just as \f3\fs18 setValue() \f4\fs20 would do. For example, calling \f3\fs18 Dictionary("a", 0:3, "b", c("foo", "bar")) \f4\fs20 is equivalent to calling \f3\fs18 Dictionary() \f4\fs20 and then calling \f3\fs18 setValue("a", 0:3) \f4\fs20 and then \f3\fs18 setValue("b", c("foo", "bar")) \f4\fs20 on it; it is just a shorthand for convenience. Keys may be of type \f3\fs18 string \f4\fs20 or \f3\fs18 integer \f4\fs20 , but must all be of the same type; \f3\fs18 Dictionary \f4\fs20 supports using either \f3\fs18 string \f4\fs20 or \f3\fs18 integer \f4\fs20 keys, but they cannot be mixed in a single \f3\fs18 Dictionary \f4\fs20 object.\ Another alternative is to call \f3\fs18 Dictionary() \f4\fs20 with a singleton \f3\fs18 Dictionary \f4\fs20 as its only argument; this creates a new \f3\fs18 Dictionary \f4\fs20 that is a copy of the \f3\fs18 Dictionary \f4\fs20 passed, containing the same keys and values. This is equivalent to creating a new empty \f3\fs18 Dictionary \f4\fs20 and then calling \f3\fs18 addKeysAndValuesFrom() \f4\fs20 to copy key-value pairs over; it is just a shorthand for convenience.\ A final alternative is to call \f3\fs18 Dictionary() \f4\fs20 with a \f3\fs18 string \f4\fs20 vector as its only argument; this creates a new \f3\fs18 Dictionary \f4\fs20 from the string, assuming that it is a data archive in JSON format. If the \f3\fs18 string \f4\fs20 value is not a singleton, its elements will be joined together by newlines to make a singleton \f3\fs18 string \f4\fs20 value; this allows the result from \f3\fs18 readFile() \f4\fs20 to be passed directly to \f3\fs18 Dictionary() \f4\fs20 even for a multiline (prettyprinted) JSON file. Note that a JSON string can be generated from the \f3\fs18 serialize() \f4\fs20 method of \f3\fs18 Dictionary \f4\fs20 ; together with this way of creating a \f3\fs18 Dictionary \f4\fs20 , this provides the ability to persist arbitrary information to a string (perhaps a file on disk) and back again. The recreated \f3\fs18 Dictionary \f4\fs20 should be identical to the original, except that zero length vectors such as \f3\fs18 integer(0) \f4\fs20 , \f3\fs18 float(0) \f4\fs20 , \f3\fs18 logical(0) \f4\fs20 , and \f3\fs18 string(0) \f4\fs20 will all be serialized as \f3\fs18 "[]" \f4\fs20 and recreated as \f3\fs18 integer(0) \f4\fs20 since JSON does not provide a way to specify the type of a zero-length array.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.3.1 \f2\fs18 Dictionary \f1\fs22 properties\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 allKeys => (is)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A vector containing all of the \f3\fs18 string \f4\fs20 or \f3\fs18 integer \f4\fs20 keys that have been assigned values using \f3\fs18 setValue() \f4\fs20 , in sorted (ascending alphabetic or numeric) order.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.3.2 \f2\fs18 Dictionary \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(void)addKeysAndValuesFrom(object$\'a0source)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds all of the key-value pairs contained by \f3\fs18 source \f4\fs20 (which must be a \f3\fs18 Dictionary \f4\fs20 or a subclass of \f3\fs18 Dictionary \f4\fs20 ) to the receiver. If the target already contains a key that is defined in \f3\fs18 source \f4\fs20 , the target\'92s value for that key will be \f1\i replaced \f4\i0 by the value in \f3\fs18 source \f4\fs20 (contrast this with \f3\fs18 appendKeysAndValuesFrom() \f4\fs20 ).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)appendKeysAndValuesFrom(object\'a0source)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Appends all of the key-value pairs contained by \f3\fs18 source \f4\fs20 (which must be a \f3\fs18 Dictionary \f4\fs20 or a subclass of \f3\fs18 Dictionary \f4\fs20 ) to the receiver. If the target already contains a key that is defined in source, the value from source will be \f1\i appended \f4\i0 to the target\'92s existing value, which must be of the same type (contrast this with \f3\fs18 addKeysAndValuesFrom() \f4\fs20 ); if the target does not already contain a key that is defined in source, that key-value pair will simply be added to the target.\ In the current implementation, it is an error for either of the values involved in an append to be a matrix or array; values in these \f3\fs18 Dictionary \f4\fs20 objects should be simple vectors. This limitation preserves the future option to expand this method\'92s functionality to do smart things with matrices and arrays.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)clearKeysAndValues(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Removes all key-value pairs from the receiver.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(integer)compactIndices([logical$\'a0preserveOrder\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Compacts the receiver, which must use \f3\fs18 integer \f4\fs20 keys. After this operation, the receiver will contain only values that have a length greater than zero (discarding all key\'96value pairs for which the value is a zero-length vector). In addition, the keys used will be compacted down to begin at \f3\fs18 0 \f4\fs20 and count upward sequentially. If \f3\fs18 preserveOrder \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), the keys may end up in a different numerical order; this allows the compaction to be performed more efficiently. If \f3\fs18 preserveOrder \f4\fs20 is \f3\fs18 T \f4\fs20 , on the other hand, the numerical order of the keys will be preserved. The returned \f3\fs18 integer \f4\fs20 vector contains the original keys that were kept across the compaction operation, in the order in which they were used in the compaction; keys that were not kept (because their value was zero-length) are omitted from this result vector.\ For example, with a dictionary that contains key\'96value pairs \f3\fs18 -5="a" \f4\fs20 , \f3\fs18 17="b" \f4\fs20 , \f3\fs18 37="c" \f4\fs20 , \f3\fs18 53=integer(0) \f4\fs20 , and \f3\fs18 82="d" \f4\fs20 , \f3\fs18 compactIndices(preserveOrder=T) \f4\fs20 will transform the dictionary to contain \f3\fs18 0="a" \f4\fs20 , \f3\fs18 1="b" \f4\fs20 , \f3\fs18 2="c" \f4\fs20 , and \f3\fs18 3="d" \f4\fs20 , while key \f3\fs18 53 \f4\fs20 (and its zero-length value) is dropped; the returned vector will be ( \f3\fs18 5 \f4\fs20 , \f3\fs18 17 \f4\fs20 , \f3\fs18 37 \f4\fs20 , \f3\fs18 82 \f4\fs20 ). The result from \f3\fs18 compactIndices(preserveOrder=F) \f4\fs20 has a non-deterministic order, but one possibility for the same example inout is that it would transform the dictionary to contain key\'96value pairs \f3\fs18 0="c" \f4\fs20 , \f3\fs18 1="d" \f4\fs20 , \f3\fs18 2="a" \f4\fs20 , and \f3\fs18 3="b" \f4\fs20 , with a returned vector of ( \f3\fs18 37 \f4\fs20 , \f3\fs18 82 \f4\fs20 , \f3\fs18 5 \f4\fs20 , \f3\fs18 17 \f4\fs20 ); the same key\'96value pairs are kept, and they are again placed in sequential keys beginning with \f3\fs18 0 \f4\fs20 , but their order is no longer preserved across the compaction.\ This method is particularly useful when you have a \f3\fs18 Dictionary \f4\fs20 \f3\fs18 d \f4\fs20 that contains results from some operation on a vector \f3\fs18 x \f4\fs20 , such that each key \f3\fs18 n \f4\fs20 in \f3\fs18 d \f4\fs20 has a value that is the result of processing the \f3\fs18 n \f4\fs20 \'92th element of \f3\fs18 x \f4\fs20 . In this case, \f3\fs18 order=d.compactIndices(preserveOrder=F) \f4\fs20 will transmogrify \f3\fs18 d \f4\fs20 to contain only the non-zero-length results, in sequential indices counting from \f3\fs18 0 \f4\fs20 , and \f3\fs18 x[order] \f4\fs20 provides the elements of \f3\fs18 x \f4\fs20 that produced those results, in the same order as in \f3\fs18 d \f4\fs20 after compaction. Using \f3\fs18 preserveOrder=T \f4\fs20 additionally keeps \f3\fs18 d \f4\fs20 in the same order as the original order of \f3\fs18 x \f4\fs20 , for cases in which that ordering is important.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)getRowValues(li\'a0index, [logical$\'a0drop\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a new \f3\fs18 Dictionary \f4\fs20 containing values for selected \'93rows\'94 of the target \f3\fs18 Dictionary \f4\fs20 , allowing \f3\fs18 Dictionary \f4\fs20 to act similarly to a \f3\fs18 DataFrame \f4\fs20 . See the \f3\fs18 subsetRows() \f4\fs20 method of class \f3\fs18 DataFrame \f4\fs20 for comparison; the main utility of \f3\fs18 getRowValues() \f4\fs20 is that it can be used on a \f3\fs18 Dictionary \f4\fs20 that has ragged \'93rows\'94. The selection logic described below works similarly to the subset operator \f3\fs18 [] \f4\fs20 in Eidos, selecting the \'93rows\'94 of the target \f3\fs18 Dictionary \f4\fs20 .\ The \f3\fs18 index \f4\fs20 parameter may be either \f3\fs18 integer \f4\fs20 or \f3\fs18 logical \f4\fs20 ; we will discuss the \f3\fs18 integer \f4\fs20 case first. If \f3\fs18 index \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 , the returned \f3\fs18 Dictionary \f4\fs20 will contain the \f3\fs18 index \f4\fs20 \'92th element of the value of each key of the target, under the same keys; this is a single \'93row\'94 of the target \f3\fs18 Dictionary \f4\fs20 . If \f3\fs18 index \f4\fs20 is a non-singleton \f3\fs18 integer \f4\fs20 vector, the returned \f3\fs18 Dictionary \f4\fs20 will contain the values for all of the selected rows, in the order that they are selected by \f3\fs18 index \f4\fs20 . If any index value in \f3\fs18 index \f4\fs20 is out of range for any key of the target \f3\fs18 Dictionary \f4\fs20 (such that that key does not have an \f3\fs18 index \f4\fs20 \'92th value), the returned dictionary will simply not have a value for that \'93row\'94 of that key.\ If \f3\fs18 index \f4\fs20 is \f3\fs18 logical \f4\fs20 , the \f3\fs18 T \f4\fs20 values in \f3\fs18 index \f4\fs20 select the \'93rows\'94 which will be included in the returned \f3\fs18 Dictionary \f4\fs20 . The values within each column in the returned \f3\fs18 Dictionary \f4\fs20 will be in the same order as in the target. The length of \f3\fs18 index \f4\fs20 need not match any column of the \f3\fs18 Dictionary \f4\fs20 ; excess \'93rows\'94 beyond the length of \f3\fs18 index \f4\fs20 will not be selected, and excess values in \f3\fs18 index \f4\fs20 beyond the end of the longest \'93column\'94 will have no effect.\ If the values of \f3\fs18 index \f4\fs20 are such that \f1\i no \f4\i0 value for a given key is selected, the \f3\fs18 drop \f4\fs20 parameter controls the resulting behavior. If \f3\fs18 drop \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), the key will be included in the returned dictionary with a zero-length value of matching type, such as \f3\fs18 integer(0) \f4\fs20 or \f3\fs18 string(0) \f4\fs20 . If \f3\fs18 drop \f4\fs20 is \f3\fs18 T \f4\fs20 , the key will be omitted from the returned dictionary.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(*)getValue(is$\'a0key)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the value previously set for the dictionary entry identifier \f3\fs18 key \f4\fs20 using \f3\fs18 setValue() \f4\fs20 , or \f3\fs18 NULL \f4\fs20 if no value has been set.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(logical$)identicalContents(object$\'a0x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns \f3\fs18 T \f4\fs20 if the target \f3\fs18 Dictionary \f4\fs20 is equal to \f3\fs18 x \f4\fs20 in all respects \'96 containing the same keys, with values that are identical in the sense defined by the \f3\fs18 identical() \f4\fs20 function in Eidos \'96 or returns \f3\fs18 F \f4\fs20 otherwise.\ Note that if \f3\fs18 Dictionary \f4\fs20 objects are contained, as values, by the dictionaries being tested for equality, they will be compared according to the standards of \f3\fs18 identical() \f4\fs20 , and must therefore actually be the \f1\i same \f4\i0 \f3\fs18 Dictionary \f4\fs20 object, shared by both dictionaries, for \f3\fs18 isEqual() \f4\fs20 to return \f3\fs18 T \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(string)serialize([string$\'a0format\'a0=\'a0"slim"])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a serialized form of the dictionary\'92s contents as a \f3\fs18 string \f4\fs20 singleton or vector. Five formats are supported at present, as chosen with the \f3\fs18 format \f4\fs20 parameter: \f3\fs18 "slim" \f4\fs20 , \f3\fs18 "pretty" \f4\fs20 , and \f3\fs18 "json" \f4\fs20 produce a singleton string, whereas \f3\fs18 "csv" \f4\fs20 and \f3\fs18 "tsv" \f4\fs20 produce a \f3\fs18 string \f4\fs20 vector. These serializations can be written to disk with \f3\fs18 writeFile() \f4\fs20 or \f3\fs18 writeTempFile() \f4\fs20 , written to the output stream with \f3\fs18 cat() \f4\fs20 , or used in any other way.\ The default \f3\fs18 "slim" \f4\fs20 format is intended for simple, informal use where a very easily parseable string is desired. For a simple dictionary containing only keys with singleton non-object values, this will be a semicolon-delimited string like \f3\fs18 '"string1"=value1;"string2"=value2;' \f4\fs20 or \f3\fs18 'int1=value1;int2=value2;' \f4\fs20 . Values of type \f3\fs18 string \f4\fs20 will be quoted, and will be escaped with backslash escape sequences, including \f3\fs18 \\\\ \f4\fs20 , \f3\fs18 \\" \f4\fs20 , \f3\fs18 \\' \f4\fs20 , \f3\fs18 \\t \f4\fs20 , \f3\fs18 \\r \f4\fs20 , and \f3\fs18 \\n \f4\fs20 . Values that are not singleton will be separated by spaces, such as \f3\fs18 '"string1"=1 2 3;' \f4\fs20 , while values that are themselves dictionaries will be delimited by braces, such as \f3\fs18 '"string1"=\{int1=value1;int2=value2;\};' \f4\fs20 . Keys that are of type \f3\fs18 string \f4\fs20 will be quoted (always; note that this is a change in behavior starting in SLiM 4.1) and backslash-escaped (as needed, as for \f3\fs18 string \f4\fs20 values); keys that are of type \f3\fs18 integer \f4\fs20 are not quoted. No facility for parsing \f3\fs18 "slim" \f4\fs20 serializations back into Eidos is presently provided.\ For a more extended example, here is an input \f3\fs18 Dictionary \f4\fs20 , assigned into a variable \f3\fs18 x \f4\fs20 :\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li907\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 x = Dictionary("a", 17, "b", 1:5, "c", c("foo", "bar"),\uc0\u8232 "d", Dictionary("seq", 1.5:5),\u8232 "e", Dictionary());\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 and here is the result of \f3\fs18 x.serialize("json") \f4\fs20 , omitting the enclosing quotes that would indicate that this is a \f3\fs18 string \f4\fs20 value:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li907\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 "a"=17;"b"=1 2 3 4 5;"c"="foo" "bar";"d"=\{"seq"=1.5 2.5 3.5 4.5;\};"e"=\{\};\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 "pretty" \f4\fs20 format is intended for human-readable output, for purposes such as debugging output. It is similar to the \f3\fs18 "slim" \f4\fs20 format, but (1)\'a0it prints an enclosing set of braces at the top level, (2)\'a0it adds newlines inside braces, (3)\'a0it tracks an indentation level that increments for nested dictionaries, (4)\'a0it adds whitespace it some positions for readability, such as around the equals signs that separate keys from values, and (5)\'a0it omits the semicolon at the end of a value, adding a newline instead. No facility for parsing \f3\fs18 "pretty" \f4\fs20 serializations back into Eidos is presently provided.\ For the same extended example \f3\fs18 Dictionary \f4\fs20 as above, here is the result of \f3\fs18 x.serialize("pretty") \f4\fs20 , again omitting the enclosing quotes that would indicate that this is a \f3\fs18 string \f4\fs20 value:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li907\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 \{\uc0\u8232 "a" = 17\u8232 "b" = 1 2 3 4 5\u8232 "c" = "foo" "bar"\u8232 "d" = \{\u8232 "seq" = 1.5 2.5 3.5 4.5\u8232 \}\u8232 "e" = \{\}\u8232 \}\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 "json" \f4\fs20 format, introduced in Eidos 2.7 (SLiM 3.7), provides serialization of the \f3\fs18 Dictionary \f4\fs20 into the standard JSON format, which may not be quite as brief or human-readable, but which can be used as a standard interchange format and read by the \f3\fs18 Dictionary() \f4\fs20 constructor in Eidos as well as by many other programs. For example, a \f3\fs18 Dictionary \f4\fs20 with a key \f3\fs18 "key1" \f4\fs20 with \f3\fs18 integer \f4\fs20 value \f3\fs18 1:3 \f4\fs20 and key \f3\fs18 "key2" \f4\fs20 with \f3\fs18 string \f4\fs20 value \f3\fs18 "value2" \f4\fs20 would produce the JSON serialization \f3\fs18 '\{"key1":[1,2,3],"key2":["value2"]\}' \f4\fs20 , where the outer single quotes are not part of the serialization itself, but are indicating that the serialization is a \f3\fs18 string \f4\fs20 value. Note that since all Eidos values are vectors, even singleton values are serialized into JSON as arrays by Eidos; the hope is that this will make automated parsing of these JSON strings easier, since the singleton case will not have to be special-cased. For example, \f3\fs18 Dictionary("a", 1, "b", Dictionary("x", 2)) \f4\fs20 would be serialized into JSON as \f3\fs18 '\{"a":[1],"b":[\{"x":[2]\}]\}' \f4\fs20 . Note that dictionaries that use \f3\fs18 integer \f4\fs20 keys cannot be serialized into JSON, because JSON does not support \f3\fs18 integer \f4\fs20 keys. Documentation on the JSON format can be found online.\ The \f3\fs18 "csv" \f4\fs20 and \f3\fs18 "tsv" \f4\fs20 formats produce standard comma-separated value (CSV) or tab-separated value (TSV) data. These formats are primarily intended for output from \f3\fs18 DataFrame \f4\fs20 , since that class is used to represent the sort of data tables that CSV/TSV are typically used for; but it may be used with \f3\fs18 Dictionary \f4\fs20 too, particularly if it is being used to represent a data table with ragged columns (missing values will just be skipped over, producing two commas or two tabs in sequence). Values of type \f3\fs18 string \f4\fs20 will always be quoted, with double quotes (with a repeated double quote used to indicate the presence of a double quote inside a \f3\fs18 string \f4\fs20 value, as usual in CSV); values of other types never will. Decimal points (not decimal commas, regardless of system localization) will always be used for \f3\fs18 float \f4\fs20 values, and will never be used for \f3\fs18 integer \f4\fs20 values. Values of logical type will be serialized as \f3\fs18 TRUE \f4\fs20 or \f3\fs18 FALSE \f4\fs20 , without quotes. A header line providing the names of the columns (i.e., the keys of the target \f3\fs18 Dictionary \f4\fs20 ) will always be generated; those column names will also be quoted (if the keys of the dictionary are type \f3\fs18 string \f4\fs20 ; \f3\fs18 integer \f4\fs20 keys are not quoted). One \f3\fs18 string \f4\fs20 element will be generated for each row of the target, plus one \f3\fs18 string \f4\fs20 element for the header line; newlines will not be present in the resulting \f3\fs18 string \f4\fs20 vector unless newlines were present within the \f3\fs18 string \f4\fs20 values in the \f3\fs18 Dictionary \f4\fs20 . The resulting data, if written to a file, should be readable in Eidos using \f3\fs18 readCSV() \f4\fs20 (as long as there are no ragged columns or missing values), as well as in other software such as R and Excel.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setValue(is$\'a0key, *\'a0value)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Sets a value for the dictionary entry identifier \f3\fs18 key \f4\fs20 . The key may be a \f3\fs18 string \f4\fs20 or an \f3\fs18 integer \f4\fs20 ; either is allowed, unless the target dictionary has already begun using keys of a given type, in which case it must continue using the same key type (a given dictionary cannot have both \f3\fs18 string \f4\fs20 and \f3\fs18 integer \f4\fs20 keys). The value, which may be of any type, can be fetched later using \f3\fs18 getValue() \f4\fs20 . Setting a key to a value of \f3\fs18 NULL \f4\fs20 removes that key from the dictionary.\ If \f3\fs18 value \f4\fs20 is of type \f3\fs18 object \f4\fs20 , any \f3\fs18 object \f4\fs20 class is allowed; all objects may be added as values to a dictionary. However, additional scoping restrictions may apply if the \f3\fs18 object \f4\fs20 class is not under an internal memory-management scheme called \'93retain-release\'94; in particular, it may not be legal to keep an object in a dictionary \'93long term\'94 if it is not under retain-release, where \'93long term\'94 is a scoping semantic defined by the Context. All object classes defined by Eidos itself ( \f3\fs18 Dictionary \f4\fs20 , \f3\fs18 DataFrame \f4\fs20 , \f3\fs18 Image \f4\fs20 ) are under retain-release, so this restriction does not affect pure Eidos code. See the SLiM manual (section \'93SLiM scoping rules\'94) for further discussion of this topic.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(void)setValuesVectorized(is$\'a0key, *\'a0values)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This class method sets a singleton value from \f3\fs18 values \f4\fs20 into each target dictionary, using the same dictionary entry identifier \f3\fs18 key \f4\fs20 for each. The number of elements in \f3\fs18 values \f4\fs20 must be equal to the number of target dictionaries, so that the 0th element of \f3\fs18 values \f4\fs20 is set as the value for the 0th target object, the 1st element of \f3\fs18 values \f4\fs20 is set as the value for the 1st target object, and so forth.\ This is a vectorized version of \f3\fs18 setValue() \f4\fs20 ; \f3\fs18 dicts.setValuesVectorized("key", values) \f4\fs20 is equivalent to \f3\fs18 for (dict in dicts, value in values) dict.setValue("key", value) \f4\fs20 , but is faster since the \f3\fs18 for \f4\fs20 loop is vectorized internally. The speedup is not enormous, however; the larger reason for the existence of this method is convenience.\ The values are set into the target dictionaries in exactly the same way as the \f3\fs18 setValue() \f4\fs20 method would do; see that method for details about \f3\fs18 string \f4\fs20 versus \f3\fs18 integer \f4\fs20 keys, scoping restrictions for values of type \f3\fs18 object \f4\fs20 , and so forth. Note that it is not possible to remove values from the target dictionaries, however, since it is not possible to pass \f3\fs18 NULL \f4\fs20 as a value here.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf2 5.4 Class Image\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\b0\fs18 \cf2 (object$)Image(...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Creates a new \f3\fs18 Image \f4\fs20 object. This can be called in a few different ways.\ Passed a singleton \f3\fs18 string \f4\fs20 , as \f3\fs18 Image(string$ filePath) \f4\fs20 , it creates a new \f3\fs18 Image \f4\fs20 from the PNG file at \f3\fs18 filePath \f4\fs20 . If the file represents a grayscale image, an 8-bit grayscale (K) \f3\fs18 Image \f4\fs20 will be created; all other PNG files will yield a 24-bit color (RGB) \f3\fs18 Image \f4\fs20 .\ Passed an \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 vector, as \f3\fs18 Image(numeric matrix) \f4\fs20 , it creates a new grayscale \f3\fs18 Image \f4\fs20 from the values in \f3\fs18 matrix \f4\fs20 , which must be a matrix as its name suggests. If \f3\fs18 matrix \f4\fs20 is \f3\fs18 integer \f4\fs20 , its values must be in [ \f3\fs18 0 \f4\fs20 , \f3\fs18 255 \f4\fs20 ], and will be used directly as 8-bit pixel values without translation; if \f3\fs18 matrix \f4\fs20 is \f3\fs18 float \f4\fs20 , its values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], and will be translated into 8-bit pixel values. The dimensions of the image, in pixels, will be equal to the dimensions of the matrix. The orientation of the image will match that of the matrix, in the sense that the image will appear as the matrix does when printed in the Eidos console; internally this requires a transposition of values, as discussed further below. For the \f3\fs18 integer \f4\fs20 case, the \f3\fs18 integerK \f4\fs20 property of the resulting image will recover the original matrix exactly; for the \f3\fs18 float \f4\fs20 case, the \f3\fs18 floatK \f4\fs20 property will only approximately recover the original matrix since the translation into 8-bit pixel values involves quantization, but values of \f3\fs18 0.0 \f4\fs20 and \f3\fs18 1.0 \f4\fs20 will be recovered exactly.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.4.1 \f2\fs18 Image \f1\fs22 properties\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 width => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The width of the image, in pixels.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 height => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The height of the image, in pixels.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 isGrayscale => (logical$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This flag is \f3\fs18 T \f4\fs20 if the image is grayscale, with only a K channel; it is \f3\fs18 F \f4\fs20 if the image is color, with R/G/B channels.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 bitsPerChannel => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The number of bits used to represent a single pixel, in one channel of the image. At present this is always 8; grayscale (K) images are 8-bit, color (RGB) images are 24-bit. It could be extended to support 16-bit channels in future.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 integerR => (integer)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The red (R) channel of the image, represented as a 2D \f3\fs18 integer \f4\fs20 matrix. Values will be in [0,255]. See the \f3\fs18 floatR \f4\fs20 property for an alternative representation. If the image is grayscale, this property is unavailable.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 integerG => (integer)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The green (G) channel of the image, represented as a 2D \f3\fs18 integer \f4\fs20 matrix. Values will be in [0,255]. See the \f3\fs18 floatG \f4\fs20 property for an alternative representation. If the image is grayscale, this property is unavailable.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 integerB => (integer)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The blue (R) channel of the image, represented as a 2D \f3\fs18 integer \f4\fs20 matrix. Values will be in [0,255]. See the \f3\fs18 floatB \f4\fs20 property for an alternative representation. If the image is grayscale, this property is unavailable.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 integerK => (integer)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The gray (K) channel of the image, represented as a 2D \f3\fs18 integer \f4\fs20 matrix. Values will be in [0,255]. See the \f3\fs18 floatK \f4\fs20 property for an alternative representation. If the image is color, this property is unavailable.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 floatR => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The red (R) channel of the image, represented as a 2D \f3\fs18 float \f4\fs20 matrix. Values will be in [0,1], obtained by dividing the \f3\fs18 integerR \f4\fs20 layer by 255. See the \f3\fs18 integerR \f4\fs20 property for an alternative representation. If the image is grayscale, this property is unavailable.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 floatG => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The green (G) channel of the image, represented as a 2D \f3\fs18 float \f4\fs20 matrix. Values will be in [0,1], obtained by dividing the \f3\fs18 integerG \f4\fs20 layer by 255. See the \f3\fs18 integerG \f4\fs20 property for an alternative representation. If the image is grayscale, this property is unavailable.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 floatB => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The blue (B) channel of the image, represented as a 2D \f3\fs18 float \f4\fs20 matrix. Values will be in [0,1], obtained by dividing the \f3\fs18 integerB \f4\fs20 layer by 255. See the \f3\fs18 integerB \f4\fs20 property for an alternative representation. If the image is grayscale, this property is unavailable.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 floatK => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The gray (K) channel of the image, represented as a 2D \f3\fs18 float \f4\fs20 matrix. Values will be in [0,1], obtained by dividing the \f3\fs18 integerK \f4\fs20 layer by 255. See the \f3\fs18 integerK \f4\fs20 property for an alternative representation. If the image is color, this property is unavailable.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.4.2 \f2\fs18 Image \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(void)write(string$\'a0filePath)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Writes the image to the given filesystem path \f3\fs18 filePath \f4\fs20 as PNG data. It is suggested, but not required, that \f3\fs18 filePath \f4\fs20 should end in a \f3\fs18 .png \f4\fs20 or \f3\fs18 .PNG \f4\fs20 filename extension. If the file cannot be written, an error will result. At present, since \f3\fs18 bitsPerChannel \f4\fs20 is always 8, grayscale data will be written as an 8-bit grayscale PNG while color (RGB) data will be written as a 24-bit color PNG without alpha.\ } ================================================ FILE: EidosScribe/EidosHelpController.h ================================================ // // EidosHelpController.h // SLiM // // Created by Ben Haller on 9/12/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #include #include #include "eidos_call_signature.h" #include "eidos_property_signature.h" @interface EidosHelpController : NSObject { } + (EidosHelpController *)sharedController; - (void)enterSearchForString:(NSString *)searchString titlesOnly:(BOOL)titlesOnly; - (NSWindow *)window; - (void)showWindow; // Add topics and items drawn from a specially formatted RTF file, under a designated top-level heading. // The signatures found for functions, methods, and prototypes will be checked against the supplied lists, // if they are not NULL, and if matches are found, colorized versions will be substituted. - (void)addTopicsFromRTFFile:(NSString *)rtfFile underHeading:(NSString *)topLevelHeading functions:(const std::vector *)functionList methods:(const std::vector *)methodList properties:(const std::vector *)propertyList; // Check for complete documentation - (void)checkDocumentationOfFunctions:(const std::vector *)functions; - (void)checkDocumentationOfClass:(EidosClass *)classObject; - (void)checkDocumentationForDuplicatePointers; @end ================================================ FILE: EidosScribe/EidosHelpController.mm ================================================ // // EidosHelpController.m // SLiM // // Created by Ben Haller on 9/12/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosHelpController.h" #import "EidosCocoaExtra.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include #include // BCH 4/7/2016: So we can build against the OS X 10.9 SDK #ifndef NS_DESIGNATED_INITIALIZER #define NS_DESIGNATED_INITIALIZER #endif // EidosHelpOutlineView – this lets us colorize rows in the outline, and search for all rows matching an item @interface EidosHelpOutlineView : NSOutlineView - (NSIndexSet *)eidosRowIndicesForItem:(id)item; @end // EidosHelpTextStorage – a little subclass to make line wrapping in the help textview work the way it should, defined below @interface EidosHelpTextStorage : NSTextStorage - (id)init; - (id)initWithAttributedString:(NSAttributedString *)attrStr NS_DESIGNATED_INITIALIZER; @end // EidosDividerTextAttachmentCell – a little subclass to provide a divider between multiple help items, defined below @interface EidosDividerTextAttachmentCell : NSTextAttachmentCell @end // EidosLockingClipView – a little subclass to work around an AppKit bug, see setDescription: below @interface EidosLockingClipView : NSClipView @property (atomic, getter=isEidosScrollingLocked) BOOL eidosScrollingLocked; @end // EidosNSString – an NSString subclass to get around the non-uniqueness of NSTaggedPointerStrings; see guardAgainstNSTaggedPointerString @interface EidosNSString : NSString { NSString *internal_string_; } + (NSString *)stringWithString:(NSString *)aString; - (instancetype)init; - (instancetype)initWithString:(NSString *)aString; - (NSUInteger)length; - (unichar)characterAtIndex:(NSUInteger)index; - (void)getCharacters:(unichar *)buffer range:(NSRange)range; - (id)copyWithZone:(nullable NSZone *)zone; @end // // EidosHelpController // #pragma mark - #pragma mark EidosHelpController @interface EidosHelpController () { NSMutableDictionary *topicRoot; int searchType; // 0 == Title, 1 == Content; equal to the tags on the search type menu items } @property (nonatomic, retain) IBOutlet NSWindow *helpWindow; @property (nonatomic, assign) IBOutlet NSSearchField *searchField; @property (nonatomic, assign) IBOutlet NSOutlineView *topicOutlineView; @property (nonatomic, assign) IBOutlet NSTextView *descriptionTextView; - (IBAction)searchTypeChanged:(id)sender; - (IBAction)searchFieldChanged:(id)sender; @end @implementation EidosHelpController + (EidosHelpController *)sharedController { static EidosHelpController *sharedInstance = nil; if (!sharedInstance) sharedInstance = [[EidosHelpController alloc] init]; return sharedInstance; } - (instancetype)init { if (self = [super init]) { std::vector builtin_properties = EidosClass::RegisteredClassProperties(true, false); std::vector builtin_methods = EidosClass::RegisteredClassMethods(true, false); topicRoot = [NSMutableDictionary new]; // Add Eidos topics; the only methods defined by Eidos come from EidosClass [self addTopicsFromRTFFile:@"EidosHelpFunctions" underHeading:@"1. Eidos Functions" functions:&EidosInterpreter::BuiltInFunctions() methods:nullptr properties:nullptr]; [self addTopicsFromRTFFile:@"EidosHelpClasses" underHeading:@"2. Eidos Classes" functions:&EidosInterpreter::BuiltInFunctions() methods:&builtin_methods properties:&builtin_properties]; // constructors are in BuiltInFunctions() [self addTopicsFromRTFFile:@"EidosHelpOperators" underHeading:@"3. Eidos Operators" functions:nullptr methods:nullptr properties:nullptr]; [self addTopicsFromRTFFile:@"EidosHelpStatements" underHeading:@"4. Eidos Statements" functions:nullptr methods:nullptr properties:nullptr]; [self addTopicsFromRTFFile:@"EidosHelpTypes" underHeading:@"5. Eidos Types" functions:nullptr methods:nullptr properties:nullptr]; //NSLog(@"topicRoot == %@", topicRoot); // Check for completeness of the help documentation, since it's easy to forget to add new functions/properties/methods to the doc [self checkDocumentationOfFunctions:&EidosInterpreter::BuiltInFunctions()]; for (EidosClass *class_object : EidosClass::RegisteredClasses(true, false)) { const std::string &element_type = class_object->ClassName(); if (!Eidos_string_hasPrefix(element_type, "_") && (element_type != "DictionaryRetained")) // internal classes are undocumented [self checkDocumentationOfClass:class_object]; } [self checkDocumentationForDuplicatePointers]; } return self; } - (void)dealloc { [self setHelpWindow:nil]; [super dealloc]; } // So, we have a little problem involving a private NSString subclass called NSTaggedPointerString: // // https://www.mikeash.com/pyblog/friday-qa-2015-07-31-tagged-pointer-strings.html // // The problem is that for short strings, NSTaggedPointerString effectively means that all copies of a given string are the // same exact object (the same pointer value), which means that using pointer equality to test whether two strings are // different objects, even though they contain the same characters, does not work – such tests are always true if the strings // are represented with NSTaggedPointerString. We depend upon pointer equality to look up the right topics, due to the // crappy NSDictionary-based design of this class that I should probably rip out. So we need to prevent NSTaggedPointerString // from getting into our topic tree. As it turns out, at least for now +[NSString stringWithString:] will return a clean // string that is not an NSTaggedPointerString. This is surprising, actually, since there is really no reason for it to make // a copy, but it does. This could break at any point, but for now it works, and if it breaks I have a check elsewhere that // will detect the duplicate topic pointers and log (see -checkDocumentationForDuplicatePointers). BCH 11/5/2017 // Well, that stopped working in OS X 10.13; +[NSString stringWithString:] now makes an NSTaggedPointerString, foiling our // scheme. So my new hack scheme is to use a custom NSString subclass, EidosNSString, for keys in our topic tree. This method // now substitutes an EidosNSString for the original NSString. That EidosNSString responds to copyWithZone: by making a copy // of itself, preventing NSTaggedPointerString from getting in. This seems to work, and I think it ought to be fairly safe // and airtight. We shall see. BCH 2/26/2018 // BCH 11/27/2019: Note that the Qt version of this class now has a much nicer design, in which a proper tree class is used // to represent the hierarchy instead of using NSDictionary. There it is based on QTreeWidgetItem, but we could do the same // here with a custom NSObject subclass that just had a list of children, a display string, and an NSAttributedString for the doc. // This rearchitecture would let us get rid of this hack, and a bunch of other confusing cruft as well. - (NSString *)guardAgainstNSTaggedPointerString:(NSString *)oldString { //return oldString; // can be used to test the efficacy of checkDocumentationForDuplicatePointers; in OS X 10.12.6 we're good return [EidosNSString stringWithString:oldString]; } // The attributed strings straight out of the RTF file need a little reformatting, since they have paragraph indents and small font sizes appropriate for print/PDF. // This function corrects the strings by scanning over effective attribute ranges and patching in new paragraph styles and fonts. - (void)reformatTopicItemString:(NSMutableAttributedString *)topicItemAttrString { NSUInteger topicItemAttrStringLength = [topicItemAttrString length]; NSRange topicItemAttrStringRange = NSMakeRange(0, topicItemAttrStringLength); NSRange effectiveRange; // Fix the paragraph styles to have no head indent for (NSUInteger fixIndex = 0; fixIndex < topicItemAttrStringLength; fixIndex = effectiveRange.location + effectiveRange.length) { NSParagraphStyle *pStyle = [topicItemAttrString attribute:NSParagraphStyleAttributeName atIndex:fixIndex longestEffectiveRange:&effectiveRange inRange:topicItemAttrStringRange]; if (pStyle) { CGFloat oldFirstLineHeadIndent = [pStyle firstLineHeadIndent]; if (oldFirstLineHeadIndent > 0) { NSMutableParagraphStyle *newPStyle = [pStyle mutableCopy]; CGFloat oldHeadIndent = [pStyle headIndent]; [newPStyle setHeadIndent:oldHeadIndent - oldFirstLineHeadIndent]; [newPStyle setFirstLineHeadIndent:0]; [newPStyle setTailIndent:0]; [topicItemAttrString addAttribute:NSParagraphStyleAttributeName value:newPStyle range:effectiveRange]; [newPStyle release]; } } } // Fix the fonts to be a bit larger NSFontManager *fm = [NSFontManager sharedFontManager]; for (NSUInteger fixIndex = 0; fixIndex < topicItemAttrStringLength; fixIndex = effectiveRange.location + effectiveRange.length) { NSFont *font = [topicItemAttrString attribute:NSFontAttributeName atIndex:fixIndex longestEffectiveRange:&effectiveRange inRange:topicItemAttrStringRange]; if (font) { NSFont *newFont = [fm convertFont:font toSize:[font pointSize] + 2]; [topicItemAttrString addAttribute:NSFontAttributeName value:newFont range:effectiveRange]; } } //NSLog(@"attributed string for key %@ == %@", topicItemKey, topicItemAttrString); } // This is a helper method for addTopicsFromRTFFile:... that finds the right parent dictionary to insert a given section index under. // This method makes a lot of assumptions about the layout of the RTF file, such as that section number proceeds in sorted order. - (NSMutableDictionary *)parentDictForSection:(NSString *)sectionString currentTopicDicts:(NSMutableDictionary *)topics topDict:(NSMutableDictionary *)topLevelDict { NSArray *sectionComponents = [sectionString componentsSeparatedByString:@"."]; NSUInteger sectionCount = [sectionComponents count]; if (sectionCount <= 1) { // With an empty section string, or a whole-number section like "3", the parent is always the top level dict return topLevelDict; } else { // We have a section string like "3.1" or "3.1.2"; we want to look for a parent to add it to – like "3" or "3.1", respectively NSArray *parentSectionComponents = [sectionComponents subarrayWithRange:NSMakeRange(0, [sectionComponents count] - 1)]; NSString *parentSectionString = [parentSectionComponents componentsJoinedByString:@"."]; NSMutableDictionary *parentTopicDict = [topics objectForKey:parentSectionString]; if (parentTopicDict) { // Found a parent to add to return parentTopicDict; } else { // Couldn't find a parent to add to, so the parent is the top level return topLevelDict; } } } // This is a helper method for addTopicsFromRTFFile:... that creates a new "topic dictionary" under which items will be placed, and finds the right parent // dictionary to insert it under. This method makes a lot of assumptions about the layout of the RTF file, such as that section number proceeds in sorted order. - (NSMutableDictionary *)topicDictForSection:(NSString *)sectionString title:(NSString *)title currentTopicDicts:(NSMutableDictionary *)topics topDict:(NSMutableDictionary *)topLevelDict { NSArray *sectionComponents = [sectionString componentsSeparatedByString:@"."]; NSMutableDictionary *newTopicDict = [NSMutableDictionary dictionary]; if ([title hasSuffix:@" functions"]) title = [title substringToIndex:[title length] - [@" functions" length]]; NSString *numberedTitle = [NSString stringWithFormat:@"%@. %@", [sectionComponents lastObject], title]; NSMutableDictionary *parentTopicDict = [self parentDictForSection:sectionString currentTopicDicts:topics topDict:topLevelDict]; [parentTopicDict setObject:newTopicDict forKey:[self guardAgainstNSTaggedPointerString:numberedTitle]]; [topics setObject:newTopicDict forKey:[self guardAgainstNSTaggedPointerString:sectionString]]; return newTopicDict; } - (NSMutableDictionary *)effectiveTopicRoot { NSMutableDictionary *effectiveTopicRoot = topicRoot; // If the topic root contains only a single key, collapse that top level and consider the object for that key to be the searchDict if ([effectiveTopicRoot count] == 1) { id topValue = [[effectiveTopicRoot allValues] objectAtIndex:0]; if ([topValue isKindOfClass:[NSDictionary class]]) effectiveTopicRoot = topValue; } return effectiveTopicRoot; } // This is the main RTF doc file reading method; it finds an RTF file of a given name in the main bundle, reads it into an attributed string, and then scans // that string for topic headings, function/method/property signature lines, etc., and creates a hierarchy of help topics from the results. This process // assumes that the RTF doc file is laid out in a standard way that fits the regex patterns used here; it is designed to work directly with content copied // and pasted out of our Word documentation files into RTF in TextEdit. - (void)addTopicsFromRTFFile:(NSString *)rtfFile underHeading:(NSString *)topLevelHeading functions:(const std::vector *)functionList methods:(const std::vector *)methodList properties:(const std::vector *)propertyList { NSString *topicFilePath = [[NSBundle mainBundle] pathForResource:rtfFile ofType:@"rtf"]; NSData *topicFileData = [NSData dataWithContentsOfFile:topicFilePath]; NSAttributedString *topicFileAttrString = [[[NSAttributedString alloc] initWithRTF:topicFileData documentAttributes:NULL] autorelease]; // Set up the top-level dictionary that we will place items under NSMutableDictionary *topLevelDict = [NSMutableDictionary dictionary]; NSMutableDictionary *topics = [NSMutableDictionary dictionary]; // keys are strings like 3.1 or 3.1.2 or whatever NSMutableDictionary *currentTopicDict = topLevelDict; // start out putting new topics in the top level dict [topicRoot setObject:topLevelDict forKey:[self guardAgainstNSTaggedPointerString:topLevelHeading]]; // Set up the current topic item that we are appending content into NSString *topicItemKey = nil; NSMutableAttributedString *topicItemAttrString = nil; // Make regular expressions that we will use below NSRegularExpression *topicHeaderRegex = [NSRegularExpression regularExpressionWithPattern:@"^((?:[0-9]+\\.)*[0-9]+)\\.? (.+)$" options:NSRegularExpressionCaseInsensitive error:NULL]; // 2 captures NSRegularExpression *topicGenericItemRegex = [NSRegularExpression regularExpressionWithPattern:@"^((?:[0-9]+\\.)*[0-9]+)\\.? ITEM: ((?:[0-9]+\\.? )?)(.+)$" options:NSRegularExpressionCaseInsensitive error:NULL]; // 3 captures NSRegularExpression *topicFunctionRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\([a-zA-Z<>\\*+$]+\\)([a-zA-Z_0-9]+)\\(.+$" options:NSRegularExpressionCaseInsensitive error:NULL]; // 1 capture NSRegularExpression *topicMethodRegex = [NSRegularExpression regularExpressionWithPattern:@"^([-–+])[ \u00A0]\\([a-zA-Z<>\\*+$]+\\)([a-zA-Z_0-9]+)\\(.+$" options:NSRegularExpressionCaseInsensitive error:NULL]; // 2 captures NSRegularExpression *topicPropertyRegex = [NSRegularExpression regularExpressionWithPattern:@"^([a-zA-Z_0-9]+)[ \u00A0]((?:<[-–]>)|(?:=>)) \\([a-zA-Z<>\\*+$]+\\)$" options:NSRegularExpressionCaseInsensitive error:NULL]; // 2 captures // Scan through the file one line at a time, parsing out topic headers NSString *topicFileString = [topicFileAttrString string]; NSArray *topicFileLineArray = [topicFileString componentsSeparatedByString:@"\n"]; NSUInteger lineCount = [topicFileLineArray count]; NSUInteger lineStartIndex = 0; // the character index of the current line in topicFileAttrString for (unsigned int lineIndex = 0; lineIndex < lineCount; ++lineIndex) { NSString *line = [topicFileLineArray objectAtIndex:lineIndex]; NSUInteger lineLength = [line length]; NSRange lineRange = NSMakeRange(0, lineLength); NSRange lineRangeInAttrString = NSMakeRange(lineStartIndex, lineLength); NSAttributedString *lineAttrString = [topicFileAttrString attributedSubstringFromRange:lineRangeInAttrString]; // on the assumption (usually true) that we will need the corrected attributed string for this line, we reformat it now { NSMutableAttributedString *correctedLineAttrString = [[lineAttrString mutableCopy] autorelease]; [self reformatTopicItemString:correctedLineAttrString]; lineAttrString = correctedLineAttrString; } // figure out what kind of line we have and handle it lineStartIndex += (lineLength + 1); // +1 to jump over the newline NSUInteger isTopicHeaderLine = ([topicHeaderRegex numberOfMatchesInString:line options:(NSMatchingOptions)0 range:lineRange] > 0); NSUInteger isTopicGenericItemLine = ([topicGenericItemRegex numberOfMatchesInString:line options:(NSMatchingOptions)0 range:lineRange] > 0); NSUInteger isTopicFunctionLine = ([topicFunctionRegex numberOfMatchesInString:line options:(NSMatchingOptions)0 range:lineRange] > 0); NSUInteger isTopicMethodLine = ([topicMethodRegex numberOfMatchesInString:line options:(NSMatchingOptions)0 range:lineRange] > 0); NSUInteger isTopicPropertyLine = ([topicPropertyRegex numberOfMatchesInString:line options:(NSMatchingOptions)0 range:lineRange] > 0); NSUInteger matchCount = isTopicHeaderLine + isTopicFunctionLine + isTopicMethodLine + isTopicPropertyLine; // excludes isTopicGenericItemLine, which is a subtype of isTopicHeaderLine if (matchCount > 1) { NSLog(@"*** line matched more than one regex type: %@", line); return; } if (matchCount == 0) { if ([line length]) { if (topicItemAttrString) { // We have a topic, so this is a content line, to be appended to the current topic item's content [topicItemAttrString replaceCharactersInRange:NSMakeRange([topicItemAttrString length], 0) withString:@"\n"]; [topicItemAttrString replaceCharactersInRange:NSMakeRange([topicItemAttrString length], 0) withAttributedString:lineAttrString]; } else { NSLog(@"orphan line while reading for top level heading %@", topLevelHeading); } } } if ((matchCount == 1) || ((matchCount == 0) && (lineIndex == lineCount - 1))) { // This line starts a new header or item or ends the file, so we need to terminate the current item if (topicItemAttrString && topicItemKey) { [currentTopicDict setObject:topicItemAttrString forKey:[self guardAgainstNSTaggedPointerString:topicItemKey]]; topicItemAttrString= nil; topicItemKey = nil; } } if (isTopicHeaderLine) { // We have hit a new topic header. This might be a subtopic of the current topic, or a sibling, or a sibling of one of our ancestors NSTextCheckingResult *match = [topicHeaderRegex firstMatchInString:line options:(NSMatchingOptions)0 range:lineRange]; NSRange sectionRange = [match rangeAtIndex:1]; NSString *sectionString = [line substringWithRange:sectionRange]; NSRange titleRange = [match rangeAtIndex:2]; NSString *titleString = [line substringWithRange:titleRange]; //NSLog(@"topic header section %@, title %@, line: %@", sectionString, titleString, line); if (isTopicGenericItemLine) { // This line plays two roles: it is both a header (with a period-separated section index at the beginning) and a // topic item starter like function/method/property lines, with item content following it immediately. First we // use the header-line section string to find the right parent section to place it. currentTopicDict = [self parentDictForSection:sectionString currentTopicDicts:topics topDict:topLevelDict]; // Then we extract the item name and create a new item under the parent dict. NSTextCheckingResult *itemMatch = [topicGenericItemRegex firstMatchInString:line options:(NSMatchingOptions)0 range:lineRange]; NSRange itemOrderRange = [itemMatch rangeAtIndex:2]; NSString *itemOrder = [line substringWithRange:itemOrderRange]; NSRange itemNameRange = [itemMatch rangeAtIndex:3]; NSString *itemName = [line substringWithRange:itemNameRange]; topicItemKey = [itemOrder stringByAppendingString:itemName]; topicItemAttrString = [[[lineAttrString attributedSubstringFromRange:itemNameRange] mutableCopy] autorelease]; //NSLog(@" parsed topic header item with currentTopicDict %p, itemName %@, itemNameRange %@", currentTopicDict, itemName, NSStringFromRange(itemNameRange)); } else { // This header line is not also an item line, so we want to create a new expandable category and await items currentTopicDict = [self topicDictForSection:sectionString title:titleString currentTopicDicts:topics topDict:topLevelDict]; } } else if (isTopicFunctionLine) { // We have hit a new topic item line. This will become a key in the current topic dictionary. NSTextCheckingResult *match = [topicFunctionRegex firstMatchInString:line options:(NSMatchingOptions)0 range:lineRange]; NSRange callNameRange = [match rangeAtIndex:1]; NSString *callName = [line substringWithRange:callNameRange]; //NSLog(@"topic function name: %@, line: %@", callName, line); // Check for a built-in function signature that matches and substitute it in if (functionList) { std::string function_name([callName UTF8String]); const EidosFunctionSignature *function_signature = nullptr; for (auto signature_iter = functionList->begin(); signature_iter != functionList->end(); signature_iter++) if ((*signature_iter)->call_name_.compare(function_name) == 0) { function_signature = signature_iter->get(); break; } if (function_signature) { NSAttributedString *attrSig = [NSAttributedString eidosAttributedStringForCallSignature:function_signature size:11.0]; NSString *oldSignatureString = [lineAttrString string]; NSString *newSignatureString = [attrSig string]; if ([oldSignatureString isEqualToString:newSignatureString]) { //NSLog(@"signature match for function %@", callName); // Replace the signature line from the RTF file with the syntax-colored version lineAttrString = attrSig; } else { NSLog(@"*** function signature mismatch:\nold: %@\nnew: %@", oldSignatureString, newSignatureString); } } else { NSLog(@"*** no function signature found for function name %@", callName); } } topicItemKey = [callName stringByAppendingString:@"()"]; topicItemAttrString = [[[NSMutableAttributedString alloc] initWithAttributedString:lineAttrString] autorelease]; } else if (isTopicMethodLine) { // We have hit a new topic item line. This will become a key in the current topic dictionary. NSTextCheckingResult *match = [topicMethodRegex firstMatchInString:line options:(NSMatchingOptions)0 range:lineRange]; NSRange classMethodRange = [match rangeAtIndex:1]; NSString *classMethodString = [line substringWithRange:classMethodRange]; NSRange callNameRange = [match rangeAtIndex:2]; NSString *callName = [line substringWithRange:callNameRange]; //NSLog(@"topic method name: %@, line: %@", callName, line); // Check for a built-in method signature that matches and substitute it in if (methodList) { std::string method_name([callName UTF8String]); EidosMethodSignature_CSP method_signature = nullptr; for (auto signature_iter = methodList->begin(); signature_iter != methodList->end(); signature_iter++) if ((*signature_iter)->call_name_.compare(method_name) == 0) { method_signature = *signature_iter; break; } if (method_signature) { NSAttributedString *attrSig = [NSAttributedString eidosAttributedStringForCallSignature:method_signature.get() size:11.0]; NSString *oldSignatureString = [lineAttrString string]; NSString *newSignatureString = [attrSig string]; if ([oldSignatureString isEqualToString:newSignatureString]) { //NSLog(@"signature match for method %@", callName); // Replace the signature line from the RTF file with the syntax-colored version lineAttrString = attrSig; } else { // BCH 3 April 2022: I don't think there's any reason why we can't have more than one method with the same name, // but with different signatures, as long as they are not in the same class; we can't handle overloading, but // method lookup is within-class. So this code could be generalized as the property lookup code below was; I just // haven't bothered to do so. NSLog(@"*** method signature mismatch:\ndocumentation: %@\ndeclaration : %@", oldSignatureString, newSignatureString); } } else { NSLog(@"*** no method signature found for method name %@", callName); } } topicItemKey = [NSString stringWithFormat:@"%@\u00A0%@()", classMethodString, callName]; topicItemAttrString = [[[NSMutableAttributedString alloc] initWithAttributedString:lineAttrString] autorelease]; } else if (isTopicPropertyLine) { // We have hit a new topic item line. This will become a key in the current topic dictionary. NSTextCheckingResult *match = [topicPropertyRegex firstMatchInString:line options:(NSMatchingOptions)0 range:lineRange]; NSRange callNameRange = [match rangeAtIndex:1]; NSString *callName = [line substringWithRange:callNameRange]; NSRange readOnlyRange = [match rangeAtIndex:2]; NSString *readOnlyName = [line substringWithRange:readOnlyRange]; //NSLog(@"topic property name: %@, line: %@", callName, line); // Check for a built-in property signature that matches and substitute it in. Note that we accept a match from any property in any class // API as long as the signature matches; we do not rigorously check that the API within a given class matches between signature and doc. // This is mostly not a problem because it is quite rare for the same property name to be used with more than one signature. if (propertyList) { std::string property_name([callName UTF8String]); bool found_match = false, found_mismatch = false; NSString *oldSignatureString = nullptr; NSString *newSignatureString = nullptr; for (auto signature_iter = propertyList->begin(); signature_iter != propertyList->end(); signature_iter++) if ((*signature_iter)->property_name_.compare(property_name) == 0) { EidosPropertySignature_CSP candidate_signature = *signature_iter; NSAttributedString *attrSig = [NSAttributedString eidosAttributedStringForPropertySignature:candidate_signature.get() size:11.0]; oldSignatureString = [lineAttrString string]; newSignatureString = [attrSig string]; if ([oldSignatureString isEqualToString:newSignatureString]) { //NSLog(@"signature match for method %@", callName); // Replace the signature line from the RTF file with the syntax-colored version lineAttrString = attrSig; found_match = true; break; } else { // If we find a mismatched signature but no matching signature, that's probably an error in either the doc or // the signature, unless we find a match later on with a different signature for the same property name. found_mismatch = true; } } if (found_mismatch && !found_match) NSLog(@"*** property signature mismatch:\nold: %@\nnew: %@", oldSignatureString, newSignatureString); else if (!found_match) NSLog(@"*** no property signature found for property name %@", callName); } topicItemKey = [NSString stringWithFormat:@"%@ %@", callName, readOnlyName]; topicItemAttrString = [[[NSMutableAttributedString alloc] initWithAttributedString:lineAttrString] autorelease]; } } } - (void)checkDocumentationOfFunctions:(const std::vector *)functions { for (const EidosFunctionSignature_CSP &functionSignature : *functions) { NSString *functionNameString = [NSString stringWithUTF8String:functionSignature->call_name_.c_str()]; if (![functionNameString hasPrefix:@"_"]) { NSString *functionString = [NSString stringWithFormat:@"%@()", functionNameString]; id functionDocumentation = [self findObjectForKeyEqualTo:functionString withinDictionary:[self effectiveTopicRoot]]; if (!functionDocumentation) { NSLog(@"*** no documentation found for function %@", functionString); } } } } - (void)checkDocumentationOfClass:(EidosClass *)classObject { const EidosClass *superclass = classObject->Superclass(); // We're hiding DictionaryRetained, so DictionaryRetained subclasses pretend their superclass is Dictionary if (superclass == gEidosDictionaryRetained_Class) superclass = gEidosDictionaryUnretained_Class; const std::string &className = classObject->ClassName(); NSString *classString = [NSString stringWithUTF8String:className.c_str()]; NSString *classKey = [NSString stringWithFormat:@"Class %@", classString]; id classDocumentation = [self findObjectWithKeySuffix:classKey withinDictionary:[self effectiveTopicRoot]]; if ([classDocumentation isKindOfClass:[NSDictionary class]]) { NSDictionary *classDocDict = (NSDictionary *)classDocumentation; int classDocChildCount = (int)[classDocDict count]; NSArray *keys = [classDocDict allKeys]; NSString *propertiesKey = [NSString stringWithFormat:@"1. %@ properties", classString]; NSString *methodsKey = [NSString stringWithFormat:@"2. %@ methods", classString]; if (((classDocChildCount == 2) || (classDocChildCount == 3)) && [keys containsObject:propertiesKey] && [keys containsObject:methodsKey]) // 3 if there is a constructor function, which we don't presently check { // Check for complete documentation of all properties defined by the class { NSDictionary *propertyDict = [classDocDict objectForKey:propertiesKey]; NSMutableArray *docProperties = [[propertyDict allKeys] mutableCopy]; const std::vector *classProperties = classObject->Properties(); const std::vector *superclassProperties = superclass ? superclass->Properties() : nullptr; for (const EidosPropertySignature_CSP &propertySignature : *classProperties) { std::string &&connector_string = propertySignature->PropertySymbol(); NSString *connectorString = [NSString stringWithUTF8String:connector_string.c_str()]; // "<–>" or "=>" NSString *propertyNameString = [NSString stringWithUTF8String:propertySignature->property_name_.c_str()]; NSString *propertyString = [NSString stringWithFormat:@"%@ %@", propertyNameString, connectorString]; NSUInteger docIndex = [docProperties indexOfObject:propertyString]; if (docIndex != NSNotFound) { // If the property is defined in this class doc, consider it documented [docProperties removeObjectAtIndex:docIndex]; } else { // If the property is not defined in this class doc, then that is an error unless it is a superclass property bool isSuperclassProperty = superclassProperties && (std::find(superclassProperties->begin(), superclassProperties->end(), propertySignature) != superclassProperties->end()); if (!isSuperclassProperty) NSLog(@"*** no documentation found for class %@ property %@", classString, propertyString); } } if ([docProperties count]) NSLog(@"*** excess documentation found for class %@ properties %@", classString, docProperties); [docProperties release]; } // Check for complete documentation of all methods defined by the class { NSDictionary *methodDict = [classDocDict objectForKey:methodsKey]; NSMutableArray *docMethods = [[methodDict allKeys] mutableCopy]; const std::vector *classMethods = classObject->Methods(); const std::vector *superclassMethods = superclass ? superclass->Methods() : nullptr; for (const EidosMethodSignature_CSP &methodSignature : *classMethods) { NSString *methodNameString = [NSString stringWithUTF8String:methodSignature->call_name_.c_str()]; if (![methodNameString hasPrefix:@"_"]) { std::string &&prefix_string = methodSignature->CallPrefix(); NSString *prefixString = [NSString stringWithUTF8String:prefix_string.c_str()]; // "", "–\u00A0", or "+\u00A0" NSString *methodString = [NSString stringWithFormat:@"%@%@()", prefixString, methodNameString]; NSUInteger docIndex = [docMethods indexOfObject:methodString]; if (docIndex != NSNotFound) { // If the method is defined in this class doc, consider it documented [docMethods removeObjectAtIndex:docIndex]; } else { // If the method is not defined in this class doc, then that is an error unless it is a superclass method bool isSuperclassMethod = superclassMethods && (std::find(superclassMethods->begin(), superclassMethods->end(), methodSignature) != superclassMethods->end()); if (!isSuperclassMethod) NSLog(@"*** no documentation found for class %@ method %@", classString, methodString); } } } if ([docMethods count]) NSLog(@"*** excess documentation found for class %@ methods %@", classString, docMethods); [docMethods release]; } } else { NSLog(@"*** documentation for class %@ in unexpected format", classString); } } else { NSLog(@"*** no documentation found for class %@", classString); } } - (void)_gatherKeysWithinDictionary:(NSDictionary *)searchDict intoVector:(std::vector &)vec { [searchDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { vec.emplace_back((pointer_t)key); if ([obj isKindOfClass:[NSDictionary class]]) [self _gatherKeysWithinDictionary:obj intoVector:vec]; }]; } - (void)checkDocumentationForDuplicatePointers { // The goal here is to check that no two topics in the tree have the same pointer for their key. This can happen if // NSStrings are uniqued behind out back, which it turns out Apple actually does now using NSTaggedPointerString; see // guardAgainstNSTaggedPointerString: above. We have a workaround for that problem, for now, but need to be vigilant // in case our workaround breaks, as might happen if Apple makes +[NSString stringWithString:] smart enough to return // an NSTaggedPointerString back to us. std::vector topic_keys; [self _gatherKeysWithinDictionary:[self effectiveTopicRoot] intoVector:topic_keys]; // Now that we've got all the keys, sort, and then find and print duplicates std::sort(topic_keys.begin(), topic_keys.end()); std::vector::iterator topic_iter = topic_keys.begin(); while (YES) { topic_iter = std::adjacent_find(topic_iter, topic_keys.end()); if (topic_iter == topic_keys.end()) break; NSLog(@"*** duplicate topic keys in help tree: %@ (%@)", (NSString *)*topic_iter, [(id)(*topic_iter) class]); ++topic_iter; } } - (NSWindow *)window { if (!_helpWindow) { [[NSBundle mainBundle] loadNibNamed:@"EidosHelpWindow" owner:self topLevelObjects:NULL]; // Replace the text storage of the description textview with our custom subclass [[_descriptionTextView layoutManager] replaceTextStorage:[[[EidosHelpTextStorage alloc] init] autorelease]]; // Fix text container insets to look a bit nicer; {0,0} by default [_descriptionTextView setTextContainerInset:NSMakeSize(0.0, 5.0)]; // Fix outline view to expand/collapse with a click anywhere in a topic row [_topicOutlineView setTarget:self]; [_topicOutlineView setAction:@selector(outlineViewClicked:)]; } return _helpWindow; } - (void)showWindow { [[self window] makeKeyAndOrderFront:nil]; } - (void)outlineViewClicked:(id)sender { NSOutlineView *senderOutlineView = (NSOutlineView *)sender; if (senderOutlineView == _topicOutlineView) { id clickItem = [_topicOutlineView itemAtRow:[_topicOutlineView clickedRow]]; if (clickItem) { BOOL optionPressed = (([[NSApp currentEvent] modifierFlags] & NSEventModifierFlagOption) == NSEventModifierFlagOption); if ([_topicOutlineView isItemExpanded:clickItem]) [_topicOutlineView.animator collapseItem:clickItem collapseChildren:optionPressed]; else [_topicOutlineView.animator expandItem:clickItem expandChildren:optionPressed]; } } } - (IBAction)searchTypeChanged:(id)sender { NSMenuItem *senderMenuItem = (NSMenuItem *)sender; int newSearchType = (int)[senderMenuItem tag]; if (newSearchType != searchType) { searchType = newSearchType; [self searchFieldChanged:_searchField]; } } - (BOOL)findItemsUnderRoot:(NSDictionary *)root withKey:(NSString *)rootKey matchingSearchString:(NSString *)searchString titlesOnly:(BOOL)titlesOnly addingToMatchKeys:(NSMutableArray *)matchKeys andItemsToExpand:(NSMutableArray *)expandItems { __block BOOL anyChildMatches = NO; [root enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { if ([obj isKindOfClass:[NSDictionary class]]) { // If the object for the key is a dictionary, the child is an expandable item, so we need to recurse BOOL result = [self findItemsUnderRoot:obj withKey:key matchingSearchString:searchString titlesOnly:titlesOnly addingToMatchKeys:matchKeys andItemsToExpand:expandItems]; if (result) anyChildMatches = YES; } else { // If the item is not a dictionary, then the child is a leaf, so we need to test for a match BOOL isMatch = NO; if ([key rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound) isMatch = YES; if (!titlesOnly) { if ([obj isKindOfClass:[NSString class]]) { if ([obj rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound) isMatch = YES; } else if ([obj isKindOfClass:[NSAttributedString class]]) { if ([[obj string] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound) isMatch = YES; } } if (isMatch) { [matchKeys addObject:key]; anyChildMatches = YES; } } }]; if (anyChildMatches && rootKey) [expandItems addObject:rootKey]; return anyChildMatches; } - (IBAction)searchFieldChanged:(id)sender { NSString *searchString = [_searchField stringValue]; // Do a depth-first search under the topic root that matches the search pattern, and gather tasks to perform NSMutableArray *matchKeys = [NSMutableArray array]; NSMutableArray *expandItems = [NSMutableArray array]; [self findItemsUnderRoot:[self effectiveTopicRoot] withKey:nil matchingSearchString:searchString titlesOnly:(searchType == 0) addingToMatchKeys:matchKeys andItemsToExpand:expandItems]; if ([matchKeys count]) { // First collapse everything, as an initial state [_topicOutlineView collapseItem:nil collapseChildren:YES]; // Expand all nodes that have a search hit; reverse order so parents expand before their children [expandItems enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [_topicOutlineView expandItem:obj]; }]; // Select all of the items that matched; rowForItem: only returns the first hit, so we have a custom method that gets all hits [matchKeys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [_topicOutlineView selectRowIndexes:[(EidosHelpOutlineView *)_topicOutlineView eidosRowIndicesForItem:obj] byExtendingSelection:YES]; }]; // The outline view occasionally seems to mis-update; I think it is an AppKit bug but it's hard to be sure. // In any case, telling it to completely redraw here seems to fix the problem. [_topicOutlineView setNeedsDisplay:YES]; } else { NSBeep(); } } - (void)enterSearchForString:(NSString *)searchString titlesOnly:(BOOL)titlesOnly { // Load our nib if it is not already loaded [self window]; // Set the search string per the request [_searchField setStringValue:searchString]; // Set the search type per the request searchType = titlesOnly ? 0 : 1; // Then execute the search by firing the search field's action [self searchFieldChanged:_searchField]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL selector = [menuItem action]; if (selector == @selector(searchTypeChanged:)) { NSInteger tag = [menuItem tag]; [menuItem setState:(searchType == tag) ? NSOnState : NSOffState]; return YES; } return YES; // no super } - (id)findObjectWithKeySuffix:(NSString *)searchKeySuffix withinDictionary:(NSDictionary *)searchDict { __block id value = nullptr; if (value) return value; [searchDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { // Search by substring matching; we have to be careful to use this method to search only for unique keys if ([key hasSuffix:searchKeySuffix]) value = obj; else if ([obj isKindOfClass:[NSDictionary class]]) value = [self findObjectWithKeySuffix:searchKeySuffix withinDictionary:obj]; if (value) *stop = YES; }]; return value; } - (id)findObjectForKeyEqualTo:(NSString *)searchKey withinDictionary:(NSDictionary *)searchDict { __block id value = nullptr; if (value) return value; [searchDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { // Search using isEqualToString:; we have to be careful to use this method to search only for unique keys if ([key isEqualToString:searchKey]) value = obj; else if ([obj isKindOfClass:[NSDictionary class]]) value = [self findObjectForKeyEqualTo:searchKey withinDictionary:obj]; if (value) *stop = YES; }]; return value; } - (id)findObjectForKey:(NSString *)searchKey withinDictionary:(NSDictionary *)searchDict { __block id value = nullptr; if (value) return value; [searchDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { // Search by pointer equality, not by hash/isEqual:, so that items with identical (but not pointer-equal) keys // can refer to different values in our topic tree; the "subpopID" property, for example, has the same name // in both Mutation and Substitution, but has different descriptions. if (key == searchKey) value = obj; else if ([obj isKindOfClass:[NSDictionary class]]) value = [self findObjectForKey:searchKey withinDictionary:obj]; if (value) *stop = YES; }]; return value; } - (id)findObjectForKey:(NSString *)searchKey { // The datasource methods below deal in keys; the child returned at a given index is an NSString key. We often need // to find the corresponding value for a given key, but this is not simple since we don't have a reference to the // dictionary that it is a key within. So we do a depth-first search through our topic tree to find it. This is // going to be a bit slow, but the topic tree should be small so it shouldn't matter. The alternative would be to // develop a better data structure for storing our topics, rather than using NSDictionary. // Start at the topic root NSDictionary *searchDict = [self effectiveTopicRoot]; // If the search key is nil, return the searchDict; this makes the searchDict correspond to the root object of the outline view if (!searchKey) return searchDict; // Otherwise, search within the searchDict return [self findObjectForKey:searchKey withinDictionary:searchDict]; } // // NSOutlineView datasource / delegate methods // #pragma mark NSOutlineView delegate - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { // According to our design, outline items are always NSStrings, and we have to look up their values in our topic tree id itemValue = [self findObjectForKey:item]; if ([itemValue isKindOfClass:[NSDictionary class]]) return [itemValue count]; return 0; } - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { // According to our design, outline items are always NSStrings, and we have to look up their values in our topic tree id itemValue = [self findObjectForKey:item]; if ([itemValue isKindOfClass:[NSDictionary class]]) return [[[itemValue allKeys] sortedArrayUsingSelector:@selector(localizedStandardCompare:)] objectAtIndex:index]; return (id _Nonnull)nil; // get rid of the static analyzer warning } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { // According to our design, outline items are always NSStrings, and we have to look up their values in our topic tree id itemValue = [self findObjectForKey:item]; if ([itemValue isKindOfClass:[NSDictionary class]]) return YES; return NO; } - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { if (item) { static NSRegularExpression *sortOrderStripRegex = nullptr; BOOL isString = [item isKindOfClass:[NSString class]]; NSString *itemString = isString ? item : [item string]; if (!sortOrderStripRegex) sortOrderStripRegex = [[NSRegularExpression regularExpressionWithPattern:@"^(?:[0-9]+\\. )(.+)$" options:NSRegularExpressionCaseInsensitive error:NULL] retain]; NSTextCheckingResult *match = [sortOrderStripRegex firstMatchInString:itemString options:(NSMatchingOptions)0 range:NSMakeRange(0, [itemString length])]; if (match) { // If the item starts with a sort-order prefix, strip it off for display NSRange captureRange = [match rangeAtIndex:1]; if (isString) return [item substringWithRange:captureRange]; else return [item attributedSubstringFromRange:captureRange]; } return item; } return @""; } - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item { // We want items under the true topic root to look like groups; if we have collapsed the top level, we have no group items if ([topicRoot objectForKey:item]) return YES; return NO; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item { return ![self outlineView:outlineView isItemExpandable:item]; } - (void)lockClipView:(id)sender { // See comment in setDescription: below NSView *superview = [_descriptionTextView superview]; if ([superview isKindOfClass:[EidosLockingClipView class]]) [(EidosLockingClipView *)superview setEidosScrollingLocked:YES]; } - (void)unlockClipView:(id)sender { // See comment in setDescription: below NSView *superview = [_descriptionTextView superview]; if ([superview isKindOfClass:[EidosLockingClipView class]]) [(EidosLockingClipView *)superview setEidosScrollingLocked:NO]; } - (void)setDescription:(NSAttributedString *)newDescription { NSTextStorage *ts = [_descriptionTextView textStorage]; [ts beginEditing]; [ts setAttributedString:newDescription]; [ts endEditing]; // scroll to where we actually want to be and then lock it down [_descriptionTextView scrollPoint:NSZeroPoint]; [self lockClipView:nil]; // NSTextView will scroll the content down just slightly, annoyingly, if the content is too long to fit in the view; // to prevent this, we have to subclass NSClipView, as far as I can tell (forcing layout here does not work because // more work is still done by background layout later that moves the scroll point, and doing a delayed perform to // fix the scroll position is both unreliable and causes a visible jump). Ugh. [self performSelector:@selector(unlockClipView:) withObject:nil afterDelay:0.1]; } - (void)outlineViewSelectionDidChange:(NSNotification *)notification { NSOutlineView *outlineView = [notification object]; if (outlineView == _topicOutlineView) { NSIndexSet *selectedIndices = [_topicOutlineView selectedRowIndexes]; NSUInteger indexCount = [selectedIndices count]; if (indexCount == 0) { // Empty selection, so clear the description textview [_descriptionTextView setString:@""]; } else if (indexCount == 1) { // Single-topic selection, so show the topic's description NSString *selectedKey = [_topicOutlineView itemAtRow:[selectedIndices firstIndex]]; id selectedValue = [self findObjectForKey:selectedKey]; if ([selectedValue isKindOfClass:[NSString class]]) { [_descriptionTextView setString:selectedValue]; } else if ([selectedValue isKindOfClass:[NSAttributedString class]]) { [self setDescription:(NSAttributedString *)selectedValue]; } } else { // Multiple items are selected, so we want to put together an attributed string that shows them all, with dividers __block NSMutableAttributedString *conglomerate = [[[NSMutableAttributedString alloc] initWithString:@""] autorelease]; __block BOOL firstIndex = YES; [selectedIndices enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { NSString *selectedKey = [_topicOutlineView itemAtRow:idx]; id selectedValue = [self findObjectForKey:selectedKey]; if ([selectedValue isKindOfClass:[NSAttributedString class]]) { if (!firstIndex) { NSTextAttachment *attachment = [[[NSTextAttachment alloc] init] autorelease]; EidosDividerTextAttachmentCell *dividerCell = [[[EidosDividerTextAttachmentCell alloc] init] autorelease]; [attachment setAttachmentCell:dividerCell]; NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:attachment]; [conglomerate replaceCharactersInRange:NSMakeRange([conglomerate length], 0) withString:@"\n"]; [conglomerate replaceCharactersInRange:NSMakeRange([conglomerate length], 0) withAttributedString:attachmentString]; [conglomerate replaceCharactersInRange:NSMakeRange([conglomerate length], 0) withString:@"\n"]; } firstIndex = NO; [conglomerate replaceCharactersInRange:NSMakeRange([conglomerate length], 0) withAttributedString:selectedValue]; } }]; [self setDescription:conglomerate]; } } } @end // // EidosHelpOutlineView // #pragma mark - #pragma mark EidosHelpOutlineView @implementation EidosHelpOutlineView - (void)drawRow:(NSInteger)rowIndex clipRect:(NSRect)clipRect { [super drawRow:rowIndex clipRect:clipRect]; NSRect rowRect = [self rectOfRow:rowIndex]; id item = [self itemAtRow:rowIndex]; id delegate = [self delegate]; // We want to colorize group rows green if they are from the Eidos doc, blue otherwise (Context doc) if ([delegate outlineView:self isGroupItem:item]) { if (item && ([item rangeOfString:@"Eidos"].location != NSNotFound)) // BCH 4/7/2016: containsString: added in 10.10 { [[NSColor colorWithCalibratedRed:0 green:1.0 blue:0 alpha:0.04] set]; NSRectFillUsingOperation(rowRect, NSCompositingOperationSourceOver); } else { [[NSColor colorWithCalibratedRed:0 green:0 blue:1.0 alpha:0.04] set]; NSRectFillUsingOperation(rowRect, NSCompositingOperationSourceOver); } } // Add little colored boxes to the WF-only and nonWF-only API; this is pretty inefficient, but we // don't have very many strings to check so it should be fine, and is simpler than the alternatives... BOOL draw_WF_box = NO, draw_nonWF_box = NO, draw_nucmut_box = NO; static NSArray *stringsWF = nullptr; static NSArray *stringsNonWF = nullptr; static NSArray *stringsNucmut = nullptr; if (!stringsWF) stringsWF = [@[@"–\u00A0addSubpopSplit()", @"–\u00A0registerMateChoiceCallback()", @"cloningRate =>", @"immigrantSubpopFractions =>", @"immigrantSubpopIDs =>", @"selfingRate =>", @"sexRatio =>", @"–\u00A0setCloningRate()", @"–\u00A0setMigrationRates()", @"–\u00A0setSelfingRate()", @"–\u00A0setSexRatio()", @"–\u00A0setSubpopulationSize()", @"4. mateChoice() callbacks" ] retain]; if (!stringsNonWF) stringsNonWF = [@[@"initializeSLiMModelType()", @"age =>", @"modelType =>", @"–\u00A0registerReproductionCallback()", @"–\u00A0registerSurvivalCallback()", @"–\u00A0addCloned()", @"–\u00A0addCrossed()", @"–\u00A0addEmpty()", @"–\u00A0addMultiRecombinant()", @"–\u00A0addRecombinant()", @"–\u00A0addSelfed()", @"–\u00A0removeSubpopulation()", @"–\u00A0killIndividuals()", @"–\u00A0takeMigrants()", @"8. reproduction() callbacks", @"10. survival() callbacks" ] retain]; if (!stringsNucmut) stringsNucmut = [@[@"initializeAncestralNucleotides()", @"initializeMutationTypeNuc()", @"initializeHotspotMap()", @"codonsToAminoAcids()", @"randomNucleotides()", @"mm16To256()", @"mmJukesCantor()", @"mmKimura()", @"nucleotideCounts()", @"nucleotideFrequencies()", @"nucleotidesToCodons()", @"codonsToNucleotides()", @"nucleotideBased =>", @"nucleotide <–>", @"nucleotideValue <–>", @"nucleotide =>", @"nucleotideValue =>", @"mutationMatrix =>", @"–\u00A0setMutationMatrix()", @"–\u00A0ancestralNucleotides()", @"–\u00A0setAncestralNucleotides()", @"–\u00A0nucleotides()", @"hotspotEndPositions =>", @"hotspotEndPositionsF =>", @"hotspotEndPositionsM =>", @"hotspotMultipliers =>", @"hotspotMultipliersF =>", @"hotspotMultipliersM =>", @"–\u00A0setHotspotMap()" ] retain]; if ([stringsWF containsObject:item]) draw_WF_box = YES; if ([stringsNonWF containsObject:item]) draw_nonWF_box = YES; if ([stringsNucmut containsObject:item]) draw_nucmut_box = YES; if (draw_WF_box || draw_nonWF_box || draw_nucmut_box) { NSRect boxRect = NSMakeRect(NSMaxX(rowRect) - 13, rowRect.origin.y + 6, 8, 8); if (draw_WF_box) [[NSColor colorWithCalibratedRed:66/255.0 green:255/255.0 blue:53/255.0 alpha:1.0] set]; // WF-only color (green) else if (draw_nonWF_box) [[NSColor colorWithCalibratedRed:88/255.0 green:148/255.0 blue:255/255.0 alpha:1.0] set]; // nonWF-only color (blue) else //if (draw_nucmut_box) [[NSColor colorWithCalibratedRed:228/255.0 green:118/255.0 blue:255/255.0 alpha:1.0] set]; // nucmut color (purple) NSRectFill(boxRect); [[NSColor blackColor] set]; NSFrameRect(boxRect); } } - (NSIndexSet *)eidosRowIndicesForItem:(id)item { NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet]; NSInteger rowCount = [self numberOfRows]; for (NSInteger rowIndex = 0; rowIndex < rowCount; ++rowIndex) { id rowItem = [self itemAtRow:rowIndex]; if ([item isEqual:rowItem]) [indexSet addIndex:rowIndex]; } return indexSet; } @end // // EidosHelpTextStorage // #pragma mark - #pragma mark EidosHelpTextStorage @interface EidosHelpTextStorage () { NSMutableAttributedString *contents; } @end @implementation EidosHelpTextStorage - (id)initWithAttributedString:(NSAttributedString *)attrStr { if (self = [super init]) { contents = attrStr ? [attrStr mutableCopy] : [[NSMutableAttributedString alloc] init]; } return self; } - init { return [self initWithAttributedString:nil]; } - (void)dealloc { [contents release]; [super dealloc]; } // The next set of methods are the primitives for attributed and mutable attributed string... - (NSString *)string { return [contents string]; } - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRange *)range { return [contents attributesAtIndex:location effectiveRange:range]; } - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str { NSUInteger origLen = [self length]; [contents replaceCharactersInRange:range withString:str]; [self edited:NSTextStorageEditedCharacters range:range changeInLength:[self length] - origLen]; } - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range { [contents setAttributes:attrs range:range]; [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; } // And now the actual reason for this subclass: to provide code-aware line break points - (NSUInteger)lineBreakBeforeIndex:(NSUInteger)index withinRange:(NSRange)aRange { NSUInteger breakpoint = [super lineBreakBeforeIndex:index withinRange:aRange]; NSString *string = [self string]; // until we hit the beginning of the string, demand earlier breakpoints if the breakpoint we're given splits in the middle of a type identifier while ((breakpoint != NSNotFound) && (breakpoint > aRange.location) && (breakpoint < aRange.location + aRange.length)) { unichar uch1 = [string characterAtIndex:breakpoint - 1]; unichar uch2 = [string characterAtIndex:breakpoint]; if (uch1 != ' ') { static BOOL beenHere = NO; static unichar nbs = 'x'; if (!beenHere) { beenHere = YES; nbs = [@"\u00A0" characterAtIndex:0]; // "\u00A0" is a non-breaking space } if ((uch2 == '$') || (uch2 == nbs)) { breakpoint = [super lineBreakBeforeIndex:breakpoint withinRange:aRange]; continue; } } break; } // if we failed to find a breakpoint, allow breaking after a non-breaking space; this is better than forcing a mid-word break if (((breakpoint == NSNotFound) || (breakpoint == aRange.location)) && (index > aRange.location)) { NSRange nbsSearchRange; if (index - aRange.location < aRange.length) nbsSearchRange = NSMakeRange(aRange.location, index - aRange.location); // don't include index else nbsSearchRange = NSMakeRange(aRange.location, aRange.length - 1); // don't include last character // break after a non-breaking space if necessary, but not right at the start; avoid "- (" NSRange nbsRange = [string rangeOfString:@"\u00A0" options:(NSStringCompareOptions)(NSLiteralSearch | NSBackwardsSearch) range:nbsSearchRange]; if ((nbsRange.location != NSNotFound) && (nbsRange.location > aRange.location + 2)) breakpoint = nbsRange.location + 1; else { // If that didn't work, break after an opening parenthesis if necessary, but not right at the start; avoid "- (" nbsRange = [string rangeOfString:@"(" options:(NSStringCompareOptions)(NSLiteralSearch | NSBackwardsSearch) range:nbsSearchRange]; if ((nbsRange.location != NSNotFound) && (nbsRange.location > aRange.location + 3)) breakpoint = nbsRange.location + 1; else { // If that didn't work, break after a closing parenthesis if necessary nbsRange = [string rangeOfString:@")" options:(NSStringCompareOptions)(NSLiteralSearch | NSBackwardsSearch) range:nbsSearchRange]; if ((nbsRange.location != NSNotFound) && (nbsRange.location != aRange.location)) breakpoint = nbsRange.location + 1; } } } return breakpoint; } @end // // EidosDividerTextAttachmentCell // #pragma mark - #pragma mark EidosDividerTextAttachmentCell @interface EidosDividerTextAttachmentCell () @end @implementation EidosDividerTextAttachmentCell - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { const CGFloat widthFraction = 0.85; const CGFloat spaceFraction = 1.0 - widthFraction; CGFloat cellLeft = cellFrame.origin.x; CGFloat cellRight = cellFrame.origin.x + cellFrame.size.width; CGFloat weightedLeft = round(cellLeft * widthFraction + cellRight * spaceFraction); CGFloat weightedRight = round(cellLeft * spaceFraction + cellRight * widthFraction); CGFloat middleHeight = round(cellFrame.origin.y + cellFrame.size.height * 0.5 + 1.0); NSRect dividerRect = NSMakeRect(weightedLeft, middleHeight, weightedRight - weightedLeft, 2); [[NSColor colorWithCalibratedWhite:0.4 alpha:1.0] set]; NSRectFill(dividerRect); [[NSColor colorWithCalibratedWhite:0.7 alpha:1.0] set]; NSRectFill(NSMakeRect(dividerRect.origin.x + 1, dividerRect.origin.y + 1, dividerRect.size.width - 1, 1)); } - (void)highlight:(BOOL)flag withFrame:(NSRect)cellFrame inView:(NSView *)controlView { [self drawWithFrame:cellFrame inView:controlView]; } - (NSRect)cellFrameForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(NSRect)lineFrag glyphPosition:(NSPoint)position characterIndex:(NSUInteger)charIndex { // the -10.0 appears to be necessary because the text container inset is not subtracted out of the lineFrag rect; is this an AppKit bug? return NSMakeRect(0, 0, lineFrag.size.width - 10.0, lineFrag.size.height); } - (BOOL)wantsToTrackMouse { return NO; } @end // // EidosLockingClipView // #pragma mark - #pragma mark EidosLockingClipView @implementation EidosLockingClipView - (NSRect)constrainBoundsRect:(NSRect)proposedBounds { NSRect modifiedBounds = [super constrainBoundsRect:proposedBounds]; if (_eidosScrollingLocked) modifiedBounds.origin.y = 0.0; //NSLog(@"proposedBounds == %@, modifiedBounds == %@, locking is %@", NSStringFromRect(proposedBounds), NSStringFromRect(modifiedBounds), _eidosScrollingLocked ? @"ON" : @"OFF"); return modifiedBounds; } @end // // EidosNSString // #pragma mark - #pragma mark EidosNSString @implementation EidosNSString + (NSString *)stringWithString:(NSString *)aString { return [[[EidosNSString alloc] initWithString:aString] autorelease]; } - (instancetype)init { if (self = [super init]) { internal_string_ = [[NSString alloc] init]; } return self; } - (instancetype)initWithString:(NSString *)aString { if (self = [super init]) { internal_string_ = [[NSString alloc] initWithString:aString]; } return self; } - (void)dealloc { if (internal_string_) { [internal_string_ release]; internal_string_ = nil; } [super dealloc]; } - (NSUInteger)length { return [internal_string_ length]; } - (unichar)characterAtIndex:(NSUInteger)index { return [internal_string_ characterAtIndex:index]; } - (void)getCharacters:(unichar *)buffer range:(NSRange)range { [internal_string_ getCharacters:buffer range:range]; } - (id)copyWithZone:(nullable NSZone *)zone { NSString *copiedString = [internal_string_ copyWithZone:zone]; EidosNSString *copy = [EidosNSString stringWithString:copiedString]; [copiedString release]; return [copy retain]; } @end ================================================ FILE: EidosScribe/EidosHelpFunctions.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2761 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Bold;\f1\fnil\fcharset0 Menlo-Regular;\f2\froman\fcharset0 TimesNewRomanPSMT; \f3\fswiss\fcharset0 Optima-Regular;\f4\fswiss\fcharset0 Optima-BoldItalic;\f5\fnil\fcharset0 Menlo-Italic; \f6\fnil\fcharset0 AppleSymbols;\f7\fswiss\fcharset0 Optima-Italic;\f8\froman\fcharset0 TimesNewRomanPS-ItalicMT; \f9\fnil\fcharset0 Menlo-Bold;\f10\fswiss\fcharset0 Helvetica-Oblique;\f11\fswiss\fcharset0 Helvetica; \f12\ftech\fcharset77 Symbol;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red0\green0\blue255;\red213\green0\blue5; \red150\green150\blue150;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c0\c0\c100000;\cssrgb\c87536\c0\c0; \cssrgb\c65500\c65500\c65500;} {\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1} {\list\listtemplateid2\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid101\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid2} {\list\listtemplateid3\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid201\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid3}} {\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab397 \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.1. Math functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf0 (numeric)abs(numeric \f2 \'a0 \f1 x) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b absolute value \f3\b0 of \f1\fs18 x \f3\fs20 . If \f1\fs18 x \f3\fs20 is \f1\fs18 integer \f3\fs20 , the C++ function \f1\fs18 llabs() \f3\fs20 is used and an \f1\fs18 integer \f3\fs20 vector is returned; if \f1\fs18 x \f3\fs20 is \f1\fs18 float \f3\fs20 , the C++ function \f1\fs18 fabs() \f3\fs20 is used and a \f1\fs18 float \f3\fs20 vector is returned. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)acos(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b arc cosine \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 acos() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)asin(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b arc sine \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 asin() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)atan(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b arc tangent \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 atan() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)atan2(numeric \f2 \'a0 \f1 x, numeric \f2 \'a0 \f1 y)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b arc tangent \f3\b0 of \f1\fs18 y/x \f3\fs20 using the C++ function \f1\fs18 atan2() \f3\fs20 , which uses the signs of both x and y to determine the correct quadrant for the result. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)ceil(float \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b ceiling \f3\b0 of \f1\fs18 x \f3\fs20 : the smallest integral value greater than or equal to \f1\fs18 x \f3\fs20 . Note that the return value is \f1\fs18 float \f3\fs20 even though integral values are guaranteed, because values could be outside of the range representable by \f1\fs18 integer \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)cos(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b cosine \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 cos() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (numeric)cumProduct(numeric \f2 \'a0 \f1 x) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b cumulative product \f3\b0 of \f1\fs18 x \f3\fs20 : a vector of equal length as \f1\fs18 x \f3\fs20 , in which the element at index \f1\fs18 i \f3\fs20 is equal to the product of the elements of \f1\fs18 x \f3\fs20 across the range \f1\fs18 0:i \f2\fs20 . \f3 The return type will match the type of \f1\fs18 x \f2\fs20 . \f3 If \f1\fs18 x \f3\fs20 is of type \f1\fs18 integer \f3\fs20 , but all of the values of the cumulative product vector cannot be represented in that type, an error condition will result. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (numeric)cumSum(numeric \f2 \'a0 \f1 x) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b cumulative sum \f3\b0 of \f1\fs18 x \f3\fs20 : a vector of equal length as \f1\fs18 x \f3\fs20 , in which the element at index \f1\fs18 i \f3\fs20 is equal to the sum of the elements of \f1\fs18 x \f3\fs20 across the range \f1\fs18 0:i \f2\fs20 . \f3 The return type will match the type of \f1\fs18 x \f2\fs20 . \f3 If \f1\fs18 x \f3\fs20 is of type \f1\fs18 integer \f3\fs20 , but all of the values of the cumulative sum vector cannot be represented in that type, an error condition will result. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)exp(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b base- \f4\i e \f0\i0 exponential \f3\b0 of \f1\fs18 x, \f5\i e \f1\i0 \super x \f3\fs20 \nosupersub , using the C++ function \f1\fs18 exp() \f2\fs20 . \f3 This may be somewhat faster than \f1\fs18 E^x \f3\fs20 for large vectors. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)floor(float \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b floor \f3\b0 of \f1\fs18 x \f3\fs20 : the largest integral value less than or equal to \f1\fs18 x \f2\fs20 . \f3 Note that the return value is \f1\fs18 float \f3\fs20 even though integral values are guaranteed, because values could be outside of the range representable by \f1\fs18 integer \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)integerDiv(integer\'a0x, integer\'a0y)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the result of \f0\b integer division \f3\b0 of \f1\fs18 x \f3\fs20 by \f1\fs18 y \f3\fs20 . The \f1\fs18 / \f3\fs20 operator in Eidos always produces a \f1\fs18 float \f3\fs20 result; if you want an \f1\fs18 integer \f3\fs20 result you may use this function instead. If any value of \f1\fs18 y \f3\fs20 is \f1\fs18 0 \f3\fs20 , an error will result. The parameters \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must either be of equal length, or one of the two must be a singleton. The precise behavior of \f1\fs18 integer \f3\fs20 division, in terms of how rounding and negative values are handled, may be platform dependent; it will be whatever the C++ behavior of \f1\fs18 integer \f3\fs20 division is on the given platform. Eidos does not guarantee any particular behavior, so use this function with caution. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)integerMod(integer\'a0x \f2 , \f1 integer\'a0y)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the result of \f0\b integer modulo \f3\b0 of \f1\fs18 x \f3\fs20 by \f1\fs18 y \f3\fs20 . The \f1\fs18 % \f3\fs20 operator in Eidos always produces a \f1\fs18 float \f3\fs20 result; if you want an \f1\fs18 integer \f3\fs20 result you may use this function instead. If any value of \f1\fs18 y \f3\fs20 is \f1\fs18 0 \f3\fs20 , an error will result. The parameters \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must either be of equal length, or one of the two must be a singleton. The precise behavior of \f1\fs18 integer \f3\fs20 modulo, in terms of how rounding and negative values are handled, may be platform dependent; it will be whatever the C++ behavior of \f1\fs18 integer \f3\fs20 modulo is on the given platform. Eidos does not guarantee any particular behavior, so use this function with caution. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical)isFinite(float \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b finiteness \f3\b0 of \f1\fs18 x \f3\fs20 : \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 is not \f1\fs18 INF \f3\fs20 or \f1\fs18 NAN \f3\fs20 , \f1\fs18 F \f3\fs20 if \f1\fs18 x \f3\fs20 is \f1\fs18 INF \f3\fs20 or \f1\fs18 NAN \f2\fs20 . \f3 \f1\fs18 INF \f3\fs20 and \f1\fs18 NAN \f3\fs20 are defined only for type \f1\fs18 float \f3\fs20 , so x is required to be a \f1\fs18 float \f2\fs20 . \f3 Note that \f1\fs18 isFinite() \f3\fs20 is not the opposite of \f1\fs18 isInfinite() \f3\fs20 , because \f1\fs18 NAN \f3\fs20 is considered to be neither finite nor infinite. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical)isInfinite(float \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b infiniteness \f3\b0 of \f1\fs18 x \f3\fs20 : \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 is \f1\fs18 INF \f3\fs20 , \f1\fs18 F \f3\fs20 otherwise. \f1\fs18 INF \f3\fs20 is defined only for type \f1\fs18 float \f3\fs20 , so x is required to be a \f1\fs18 float \f3\fs20 . Note that \f1\fs18 isInfinite() \f3\fs20 is not the opposite of \f1\fs18 isFinite() \f3\fs20 , because \f1\fs18 NAN \f3\fs20 is considered to be neither finite nor infinite. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical)isNAN(float \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b undefinedness \f3\b0 of \f1\fs18 x \f3\fs20 : \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 is not \f1\fs18 NAN \f3\fs20 , \f1\fs18 F \f3\fs20 if \f1\fs18 x \f3\fs20 is \f1\fs18 NAN \f2\fs20 . \f3 \f1\fs18 NAN \f3\fs20 is defined only for type \f1\fs18 float \f3\fs20 , so x is required to be a \f1\fs18 float \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)log(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b base- \f4\i e \f0\i0 logarithm \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 log() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)log10(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b base-10 logarithm \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 log10() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)log2(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b base-2 logarithm \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 log2() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (numeric$)product(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b product \f3\b0 of \f1\fs18 x \f3\fs20 : the result of multiplying all of the elements of \f1\fs18 x \f3\fs20 together. If \f1\fs18 x \f3\fs20 is \f1\fs18 float \f3\fs20 , the result will be \f1\fs18 float \f3\fs20 . If \f1\fs18 x \f3\fs20 is \f1\fs18 integer \f3\fs20 , things are a bit more complex; the result will be \f1\fs18 integer \f3\fs20 if it can fit into the \f1\fs18 integer \f3\fs20 type without overflow issues (including during intermediate stages of the computation), otherwise it will be \f1\fs18 float \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)round(float \f2 \'a0 \f1 x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b round \f3\b0 of \f1\fs18 x \f3\fs20 : the integral value nearest to \f1\fs18 x \f3\fs20 , rounding half-way cases away from \f1\fs18 0 \f3\fs20 (different from the rounding policy of R, which rounds halfway cases toward the nearest even number). Note that the return value is \f1\fs18 float \f3\fs20 even though integral values are guaranteed, because values could be outside of the range representable by \f1\fs18 integer \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)setDifference(* \f2 \'a0 \f1 x, * \f2 \'a0 \f1 y)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b set-theoretic (asymmetric) difference \f3\b0 of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 , denoted \f1\fs18 x \f3\fs20 \f6 \uc0\u8726 \f3 \f1\fs18 y \f3\fs20 : a vector containing all elements that are in \f1\fs18 x \f3\fs20 but are not in \f1\fs18 y \f3\fs20 . Duplicate elements will be stripped out, in the same manner as the \f1\fs18 unique() \f3\fs20 function. The order of elements in the returned vector is arbitrary and should not be relied upon. The returned vector will be of the same type as \f1\fs18 x \f3\fs20 and \f1\fs18 y \f2\fs20 , \f3 and \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must be of the same type.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)setIntersection(* \f2 \'a0 \f1 x, * \f2 \'a0 \f1 y)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b set-theoretic intersection \f3\b0 of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 , denoted \f1\fs18 x \f3\fs20 \f6 \uc0\u8745 \f3 \f1\fs18 y \f3\fs20 : a vector containing all elements that are in both \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 (but not in \f7\i only \f3\i0 \f1\fs18 x \f3\fs20 or \f1\fs18 y \f3\fs20 ). Duplicate elements will be stripped out, in the same manner as the \f1\fs18 unique() \f3\fs20 function. The order of elements in the returned vector is arbitrary and should not be relied upon. The returned vector will be of the same type as \f1\fs18 x \f3\fs20 and \f1\fs18 y \f2\fs20 , \f3 and \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must be of the same type.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)setSymmetricDifference(* \f2 \'a0 \f1 x, * \f2 \'a0 \f1 y)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b set-theoretic symmetric difference \f3\b0 of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 , denoted \f1\fs18 x \f3\fs20 \f6 \uc0\u8710 \f3 \f1\fs18 y \f3\fs20 : a vector containing all elements that are in \f1\fs18 x \f3\fs20 or \f1\fs18 y \f3\fs20 , but not in both. Duplicate elements will be stripped out, in the same manner as the \f1\fs18 unique() \f3\fs20 function. The order of elements in the returned vector is arbitrary and should not be relied upon. The returned vector will be of the same type as \f1\fs18 x \f3\fs20 and \f1\fs18 y \f2\fs20 , \f3 and \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must be of the same type.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)setUnion(* \f2 \'a0 \f1 x, * \f2 \'a0 \f1 y)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b set-theoretic union \f3\b0 of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 , denoted \f1\fs18 x \f3\fs20 \f6 \uc0\u8746 \f3 \f1\fs18 y \f3\fs20 : a vector containing all elements that are in \f1\fs18 x \f3\fs20 and/or \f1\fs18 y \f3\fs20 . Duplicate elements will be stripped out, in the same manner as the \f1\fs18 unique() \f3\fs20 function. This function is therefore roughly equivalent to \f1\fs18 unique(c(x, y)) \f3\fs20 , but this function will probably be faster. The order of elements in the returned vector is arbitrary and should not be relied upon. The returned vector will be of the same type as \f1\fs18 x \f3\fs20 and \f1\fs18 y \f2\fs20 , \f3 and \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must be of the same type.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (numeric)sign(numeric\'a0x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b sign \f3\b0 of \f1\fs18 x \f3\fs20 , meaning that for each element of x, a value of either \f1\fs18 -1 \f3\fs20 , \f1\fs18 0 \f3\fs20 , or \f1\fs18 1 \f3\fs20 will be returned as the corresponding element in the returned vector depending upon whether the original element was (respectively) negative, zero, or positive. If \f1\fs18 x \f3\fs20 is \f1\fs18 integer \f3\fs20 , an \f1\fs18 integer \f3\fs20 vector is returned; if \f1\fs18 x \f3\fs20 is \f1\fs18 float \f3\fs20 , a \f1\fs18 float \f3\fs20 vector is returned.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)sin(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b sine \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 sin() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)sqrt(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b square root \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 sqrt() \f2\fs20 . \f3 This may be somewhat faster than \f1\fs18 x^0.5 \f3\fs20 for large vectors. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (numeric$)sum(lif\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b sum \f3\b0 of \f1\fs18 x \f3\fs20 : the result of adding all of the elements of \f1\fs18 x \f3\fs20 together. The unusual parameter type signature \f1\fs18 lif \f3\fs20 indicates that \f1\fs18 x \f3\fs20 can be \f1\fs18 logical \f3\fs20 , \f1\fs18 integer \f3\fs20 , or \f1\fs18 float \f2\fs20 . \f3 If \f1\fs18 x \f3\fs20 is \f1\fs18 float \f3\fs20 , the result will be \f1\fs18 float \f3\fs20 . If \f1\fs18 x \f3\fs20 is \f1\fs18 logical \f3\fs20 , the result will be \f1\fs18 integer \f3\fs20 (the number of \f1\fs18 T \f3\fs20 values in \f1\fs18 x \f3\fs20 , since the \f1\fs18 integer \f3\fs20 values of \f1\fs18 T \f3\fs20 and \f1\fs18 F \f3\fs20 are \f1\fs18 1 \f3\fs20 and \f1\fs18 0 \f3\fs20 respectively). If \f1\fs18 x \f3\fs20 is \f1\fs18 integer \f3\fs20 , things are a bit more complex; in this case, the result will be \f1\fs18 integer \f3\fs20 if it can fit into the \f1\fs18 integer \f3\fs20 type without overflow issues (including during intermediate stages of the computation), otherwise it will be \f1\fs18 float \f2\fs20 . \f3 Note that floating-point roundoff issues can cause this function to return inexact results when \f1\fs18 x \f3\fs20 is \f1\fs18 float \f3\fs20 type; this is rarely an issue, but see the \f1\fs18 sumExact() \f3\fs20 function for an alternative. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float$)sumExact(float\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b exact sum \f3\b0 of \f1\fs18 x \f3\fs20 : the exact result of adding all of the elements of \f1\fs18 x \f3\fs20 together. Unlike the \f1\fs18 sum() \f3\fs20 function, \f1\fs18 sumExact() \f3\fs20 accepts only type \f1\fs18 float \f3\fs20 , since the \f1\fs18 sum() \f3\fs20 function is already exact for other types. When summing floating-point values \'96 particularly values that vary across many orders of magnitude \'96 the precision limits of floating-point numbers can lead to roundoff errors that cause the \f1\fs18 sum() \f3\fs20 function to return an inexact result. This function does additional work to ensure that the final result is exact within the possible limits of the \f1\fs18 float \f3\fs20 type; some roundoff may still inevitably occur, in other words, but a more exact result could not be represented with a value of type \f1\fs18 float \f2\fs20 . \f3 The disadvantage of using this function instead of \f1\fs18 sum() \f3\fs20 is that it is much slower \'96 about 35 times slower, according to one test on macOS, but that will vary across operating systems and hardware. This function is rarely truly needed, but apart from the performance consequences there is no disadvantage to using it. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)tan(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b tangent \f3\b0 of \f1\fs18 x \f3\fs20 using the C++ function \f1\fs18 tan() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)trunc(float \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b truncation \f3\b0 of \f1\fs18 x \f3\fs20 : the integral value nearest to, but no larger in magnitude than, \f1\fs18 x \f3\fs20 . Note that the return value is \f1\fs18 float \f3\fs20 even though integral values are guaranteed, because values could be outside of the range representable by \f1\fs18 integer \f2\fs20 .\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.2. Statistics functions\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf2 \expnd0\expndtw0\kerning0 (float)cor(numeric\'a0x, \kerning1\expnd0\expndtw0 [Nif\'a0y\'a0=\'a0NULL]\expnd0\expndtw0\kerning0 )\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \kerning1\expnd0\expndtw0 Returns the \f0\b sample Pearson\'92s correlation coefficient \f3\b0 between vectors \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 , usually denoted \f7\i r \f3\i0 . If \f1\fs18 y \f3\fs20 is \f1\fs18 NULL \f3\fs20 , it is considered to have the same value as \f1\fs18 x \f3\fs20 ; for vector \f1\fs18 x \f3\fs20 this is not very useful (since the correlation of \f1\fs18 x \f3\fs20 with itself is \f1\fs18 1.0 \f3\fs20 by definition), but it is more useful for calculating a correlation matrix using the columns of \f1\fs18 x \f3\fs20 (see below). The sizes of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must be identical. If \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 have a size of \f1\fs18 0 \f3\fs20 or \f1\fs18 1 \f3\fs20 , \f1\fs18 NAN \f3\fs20 will be returned (a change in behavior from Eidos 4.0; it used to return \f1\fs18 NULL \f3\fs20 ). The return value will be a singleton \f1\fs18 float \f3\fs20 .\ It is also legal to call \f1\fs18 cor() \f3\fs20 with matrix \f1\fs18 x \f3\fs20 and/or \f1\fs18 y \f3\fs20 . In this case the return value will be a correlation matrix between x and y. Each column of \f1\fs18 x \f3\fs20 will be represented by one row of the result (or if \f1\fs18 x \f3\fs20 is a vector, the result will simply have one row representing \f1\fs18 x \f3\fs20 ), and each column of \f1\fs18 y \f3\fs20 will be represented by one column of the result (or if \f1\fs18 y \f3\fs20 is a vector, the result will simply have one column representing \f1\fs18 y \f3\fs20 ). Each element in the result matrix will therefore represent the correlation between a column of matrix \f1\fs18 x \f3\fs20 (or the entirety of vector \f1\fs18 x \f3\fs20 ) and a column of matrix \f1\fs18 y \f3\fs20 (or the entirety of vector y). Calling \f1\fs18 cor(x, x) \f3\fs20 , or equivalently \f1\fs18 cor(x) \f3\fs20 , thus produces a symmetric correlation matrix among the columns of \f1\fs18 x \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (float)cov(numeric\'a0x, \kerning1\expnd0\expndtw0 [Nif\'a0y\'a0=\'a0NULL]\expnd0\expndtw0\kerning0 )\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \kerning1\expnd0\expndtw0 Returns the \f0\b corrected sample covariance \f3\b0 between vectors \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 . If \f1\fs18 y \f3\fs20 is \f1\fs18 NULL \f3\fs20 , it is considered to have the same value as \f1\fs18 x \f3\fs20 ; for vector \f1\fs18 x \f3\fs20 this is equivalent to calling \f1\fs18 var(x) \f3\fs20 , but it is more useful for calculating a variance-covariance matrix using the columns of \f1\fs18 x \f3\fs20 (see below). The sizes of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must be identical. If \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 have a size of \f1\fs18 0 \f3\fs20 or \f1\fs18 1 \f3\fs20 , \f1\fs18 NAN \f3\fs20 will be returned (a change in behavior from Eidos 4.0; it used to return \f1\fs18 NULL \f3\fs20 ). The return value will be a singleton \f1\fs18 float \f3\fs20 .\ It is also legal to call \f1\fs18 cov() \f3\fs20 with matrix \f1\fs18 x \f3\fs20 and/or \f1\fs18 y \f3\fs20 . In this case the return value will be a covariance matrix between x and y. Each column of \f1\fs18 x \f3\fs20 will be represented by one row of the result (or if \f1\fs18 x \f3\fs20 is a vector, the result will simply have one row representing \f1\fs18 x \f3\fs20 ), and each column of \f1\fs18 y \f3\fs20 will be represented by one column of the result (or if \f1\fs18 y \f3\fs20 is a vector, the result will simply have one column representing \f1\fs18 y \f3\fs20 ). Each element in the result matrix will therefore represent the covariance between a column of matrix \f1\fs18 x \f3\fs20 (or the entirety of vector \f1\fs18 x \f3\fs20 ) and a column of matrix \f1\fs18 y \f3\fs20 (or the entirety of vector y). Calling \f1\fs18 cov(x, x) \f3\fs20 , or equivalently \f1\fs18 cov(x) \f3\fs20 , thus produces a symmetric variance-covariance matrix among the columns of \f1\fs18 x \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)filter(numeric\'a0x, float\'a0filter, [lif$\'a0outside\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the result of convolving \f1\fs18 x \f3\fs20 with \f1\fs18 filter \f3\fs20 . The returned vector will be the same length as \f1\fs18 x \f3\fs20 . The convolution is performed by centering \f1\fs18 filter \f3\fs20 on each position of \f1\fs18 x \f3\fs20 to produce a corresponding result element that is the sum over the products of each \f1\fs18 filter \f3\fs20 value with each \f1\fs18 x \f3\fs20 value within the filter\'92s range. The length of \f1\fs18 filter \f3\fs20 is required to be odd, so that the filter has a central value (and can thus be centered over each value of \f1\fs18 x \f3\fs20 ).\ If the filter, centered over a given value of \f1\fs18 x \f3\fs20 , extends beyond the end of \f1\fs18 x \f3\fs20 then the calculation of the corresponding element of the result is governed by the \f1\fs18 outside \f3\fs20 parameter. When \f1\fs18 outside \f3\fs20 is \f1\fs18 F \f3\fs20 (the default), the corresponding element in the result will be \f1\fs18 NAN \f3\fs20 ; this matches the behavior of the R \f1\fs18 filter() \f3\fs20 function (except that R uses \f1\fs18 NA \f3\fs20 ). If \f1\fs18 outside \f3\fs20 is \f1\fs18 T \f3\fs20 , values outside \f1\fs18 x \f3\fs20 will be excluded from the calculation (the filter value covering that position will be considered to be \f1\fs18 0 \f3\fs20 ), and the other values in the filter will be adjusted so that the sum of the absolute values of the filter weights used is unchanged, to compensate for the excluded values by giving the positions inside \f1\fs18 x \f3\fs20 more weight. Finally, if \f1\fs18 outside \f3\fs20 is \f1\fs18 integer \f3\fs20 or \f1\fs18 float \f3\fs20 , that value will be used as the value of \f1\fs18 x \f3\fs20 for all positions outside \f1\fs18 x \f3\fs20 ; one might pass an expected value or mean value in this way, to be used for all outside positions.\ This function is useful for computing running means and similar transformations of an input vector. For a simple running mean of width \f1\fs18 w \f3\fs20 , pass r \f1\fs18 ep(1/w, w) \f3\fs20 for \f1\fs18 filter \f3\fs20 . That case is automatically detected and handled efficiently; otherwise, the runtime of this function is proportional to the length of \f1\fs18 x \f3\fs20 times the length of \f1\fs18 filter \f3\fs20 , and so will be slow for long filters.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (+$)max(+\'a0x, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b maximum \f3\b0 of \f1\fs18 x \f3\fs20 and the other arguments supplied: the single greatest value contained by all of them. All of the arguments must be the same type as \f1\fs18 x \f3\fs20 , and the return type will match that of \f1\fs18 x \f2\fs20 . \f3 If all of the arguments have a size of \f1\fs18 0 \f3\fs20 , the return value will be \f1\fs18 NULL \f3\fs20 ; note that this means that \f1\fs18 max(x,\'a0max(y)) \f3\fs20 may produce an error, if \f1\fs18 max(y) \f3\fs20 is \f1\fs18 NULL \f3\fs20 , in cases where \f1\fs18 max(x,\'a0y) \f3\fs20 does not. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float$)mean(lif \f2 \'a0 \f1 x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b arithmetic mean \f3\b0 of \f1\fs18 x \f3\fs20 : the sum of \f1\fs18 x \f3\fs20 divided by the number of values in \f1\fs18 x \f2\fs20 . \f3 If \f1\fs18 x \f3\fs20 has a size of \f1\fs18 0 \f3\fs20 , the return value will be \f1\fs18 NULL \f2\fs20 . \f3 \cf2 \expnd0\expndtw0\kerning0 The unusual parameter type signature \f1\fs18 lif \f3\fs20 indicates that \f1\fs18 x \f3\fs20 can be \f1\fs18 logical \f3\fs20 , \f1\fs18 integer \f3\fs20 , or \f1\fs18 float \f3\fs20 ; if \f1\fs18 x \f3\fs20 is \f1\fs18 logical \f3\fs20 , it is coerced to \f1\fs18 integer \f3\fs20 internally (with \f1\fs18 F \f3\fs20 being \f1\fs18 0 \f3\fs20 and \f1\fs18 T \f3\fs20 being \f1\fs18 1 \f3\fs20 , as always), allowing \f1\fs18 mean() \f3\fs20 to calculate the average truth value of a \f1\fs18 logical \f3\fs20 vector. \f2 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (+$)min(+\'a0x, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b minimum \f3\b0 of \f1\fs18 x \f3\fs20 and the other arguments supplied: the single smallest value contained by all of them. All of the arguments must be the same type as \f1\fs18 x \f3\fs20 , and the return type will match that of \f1\fs18 x \f2\fs20 . \f3 If all of the arguments have a size of \f1\fs18 0 \f3\fs20 , the return value will be \f1\fs18 NULL \f3\fs20 ; note that this means that \f1\fs18 min(x,\'a0min(y)) \f3\fs20 may produce an error, if \f1\fs18 min(y) \f3\fs20 is \f1\fs18 NULL \f3\fs20 , in cases where \f1\fs18 min(x,\'a0y) \f3\fs20 does not. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (+)pmax(+\'a0x, +\'a0y)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b parallel maximum \f3\b0 of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 : the element-wise maximum for each corresponding pair of elements in \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 . The type of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must match, and the returned value will have the same type. In one usage pattern the size of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 match, in which case the returned value will have the same size. In the other usage pattern either \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 is a singleton, in which case the returned value will match the size of the non-singleton argument, and pairs of elements for comparison will be formed between the singleton\'92s element and each of the elements in the non-singleton. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (+)pmin(+\'a0x, +\'a0y)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b parallel minimum \f3\b0 of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 : the element-wise minimum for each corresponding pair of elements in \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 . The type of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must match, and the returned value will have the same type. In one usage pattern the size of \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 match, in which case the returned value will have the same size. In the other usage pattern either \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 is a singleton, in which case the returned value will match the size of the non-singleton argument, and pairs of elements for comparison will be formed between the singleton\'92s element and each of the elements in the non-singleton. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)quantile(numeric\'a0x, [Nf\'a0probs\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns \f0\b sample quantiles \f3\b0 of \f1\fs18 x \f3\fs20 for the given probabilities. The smallest value in \f1\fs18 x \f3\fs20 corresponds to a probability of \f1\fs18 0 \f3\fs20 , and the largest value in \f1\fs18 x \f3\fs20 to a probability of \f1\fs18 1 \f3\fs20 . The \f1\fs18 probs \f3\fs20 vector should be a vector of probabilities in \f1\fs18 [0, 1] \f3\fs20 , or \f1\fs18 NULL \f3\fs20 , which is equivalent to \f1\fs18 c(0.0, 0.25, 0.5, 0.75, 1.0) \f3\fs20 , requesting sample quartiles.\ The quantile function linearly interpolates between the points of the empirical cumulative distribution function. In other words, if \f1\fs18 x \f3\fs20 is a vector of length \f7\i n \f3\i0 +1, then the quantiles with \f1\fs18 probs \f3\fs20 equal to (0, 1/ \f7\i n \f3\i0 , 2/ \f7\i n \f3\i0 , ..., ( \f7\i n \f3\i0 \uc0\u8722 1)/ \f7\i n \f3\i0 , 1) are equal to the sorted values of \f1\fs18 x \f3\fs20 , and the quantile is a linear function of \f1\fs18 probs \f3\fs20 otherwise. Note that there are many ways to compute quantiles; this algorithm corresponds to R\'92s default \'93type 7\'94 algorithm.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (numeric)range(numeric \f2 \'a0 \f1 x, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b range \f3\b0 of \f1\fs18 x \f3\fs20 and the other arguments supplied: a vector of length \f1\fs18 2 \f3\fs20 composed of the minimum and maximum values contained by all of them, at indices \f1\fs18 0 \f3\fs20 and \f1\fs18 1 \f3\fs20 respectively. All of the arguments must be the same type as \f1\fs18 x \f3\fs20 , and the return type will match that of \f1\fs18 x \f2\fs20 . \f3 If all of the arguments have a size of \f1\fs18 0 \f3\fs20 , the return value will be \f1\fs18 NULL \f3\fs20 ; note that this means that \f1\fs18 range(x,\'a0range(y)) \f3\fs20 may produce an error, if \f1\fs18 range(y) \f3\fs20 is \f1\fs18 NULL \f3\fs20 , in cases where \f1\fs18 range(x,\'a0y) \f3\fs20 does not. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (numeric)rank(numeric\'a0x, [string$\'a0tiesMethod\'a0=\'a0"average"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b ranks \f3\b0 of the elements of \f1\fs18 x \f3\fs20 : a vector of length \f1\fs18 L \f3\fs20 (the length of \f1\fs18 x \f3\fs20 ), composed of the relative ranks, from \f1\fs18 1 \f3\fs20 to \f1\fs18 L \f3\fs20 , of each corresponding element of \f1\fs18 x \f3\fs20 . The \f1\fs18 tiesMethod \f3\fs20 parameter may be any of \f1\fs18 "average" \f3\fs20 (the default), \f1\fs18 "first" \f3\fs20 , \f1\fs18 "last" \f3\fs20 , \f1\fs18 "max" \f3\fs20 , or \f1\fs18 "min" \f3\fs20 ( \f1\fs18 "random" \f3\fs20 , supported by R, is not supported by Eidos at this time but could be added if needed). For \f1\fs18 "average" \f3\fs20 , the return value is of type \f1\fs18 float \f3\fs20 ; for all others, it is of type \f1\fs18 integer \f3\fs20 . (Note that the return type does \f7\i not \f3\i0 depend upon the type of \f1\fs18 x \f3\fs20 .)\ The result for all of these \f1\fs18 tiesMethod \f3\fs20 values is identical (except for type) if the elements of \f1\fs18 x \f3\fs20 are unique; the difference between these methods is in how ties are resolved. Suppose that \f7\i n \f3\i0 elements of \f1\fs18 x \f3\fs20 are tied (because they are equal), corresponding to ranks \f7\i k \f3\i0 through \f7\i k \f3\i0 + \f7\i n\uc0\u8722 \f3\i0 1. For \f1\fs18 tiesMethod \f3\fs20 \f1\fs18 "average" \f3\fs20 , all \f7\i n \f3\i0 tied elements receive the same rank, ( \f7\i k \f3\i0 + ( \f7\i n\uc0\u8722 \f3\i0 1)/2), which is the average of the ranks. For \f1\fs18 "first" \f3\fs20 , the first tied element receives rank \f7\i k \f3\i0 , upward to the last tied element receiving rank \f7\i k \f3\i0 + \f7\i n\uc0\u8722 \f3\i0 1. For \f1\fs18 "last" \f3\fs20 , the last tied element receives rank \f7\i k \f3\i0 , downward to the first tied element receiving rank \f7\i k \f3\i0 + \f7\i n\uc0\u8722 \f3\i0 1. For \f1\fs18 "max" \f3\fs20 , all \f7\i n \f3\i0 tied element receive the maximum rank, \f7\i k \f3\i0 + \f7\i n\uc0\u8722 \f3\i0 1. For \f1\fs18 "min" \f3\fs20 , all \f7\i n \f3\i0 tied element receive the minimum rank, \f7\i k \f3\i0 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float$)sd(numeric \f2 \'a0 \f1 x)\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b corrected sample standard deviation \f3\b0 of \f1\fs18 x \f3\fs20 . If \f1\fs18 x \f3\fs20 has a size of \f1\fs18 0 \f3\fs20 or \f1\fs18 1 \f3\fs20 , \f1\fs18 NAN \f3\fs20 will be returned (a change in behavior from Eidos 4.0; it used to return \f1\fs18 NULL \f3\fs20 ). Matrix/array dimensions are ignored by \f1\fs18 sd() \f3\fs20 ; it simply uses all of the elements of \f1\fs18 x \f3\fs20 for its calculation.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float$)ttest(float \f2 \'a0 \f1 x, [Nf\'a0y\'a0=\'a0NULL], [Nf$\'a0mu\'a0=\'a0NULL])\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f7\i p \f3\i0 -value resulting from running a \f7\i t \f3\i0 -test with the supplied data. Two types of \f7\i t \f2\i0 - \f3 tests can be performed. If \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 are supplied (i.e., \f1\fs18 y \f3\fs20 is non- \f1\fs18 NULL \f3\fs20 ), a two-sample unpaired two-sided Welch\'92s \f7\i t \f3\i0 -test is conducted using the samples in \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 , each of which must contain at least two elements. The null hypothesis for this test is that the two samples are drawn from populations with the same mean. Other options, such as pooled-variance \f7\i t \f3\i0 -tests, paired \f7\i t \f3\i0 -tests, and one-sided \f7\i t \f3\i0 -tests, are not presently available. If \f1\fs18 x \f3\fs20 and \f1\fs18 mu \f3\fs20 are supplied (i.e., \f1\fs18 mu \f3\fs20 is non- \f1\fs18 NULL \f3\fs20 ), a one-sample \f7\i t \f3\i0 -test is conducted in which the null hypothesis is that the sample is drawn from a population with mean \f1\fs18 mu \f2\fs20 .\ \f3 Note that the results from this function are substantially different from those produced by R. The Eidos \f1\fs18 ttest() \f3\fs20 function uses uncorrected sample statistics, which means they will be biased for small sample sizes, whereas R probably uses corrected, unbiased sample statistics. This is an Eidos bug, and might be fixed if anyone complains. If large sample sizes are used, however, the bias is likely to be small, and uncorrected statistics are simpler and faster to compute.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (float$)var(numeric\'a0x)\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \kerning1\expnd0\expndtw0 Returns the \f0\b corrected sample variance \f3\b0 of \f1\fs18 x \f3\fs20 . If \f1\fs18 x \f3\fs20 has a size of \f1\fs18 0 \f3\fs20 or \f1\fs18 1 \f3\fs20 , \f1\fs18 NAN \f3\fs20 will be returned (a change in behavior from Eidos 4.0; it used to return \f1\fs18 NULL \f3\fs20 ). This is the square of the standard deviation calculated by \f1\fs18 sd() \f3\fs20 . It is illegal to call \f1\fs18 var() \f3\fs20 with a matrix or array argument; use \f1\fs18 cov() \f3\fs20 to calculate a variance-covariance matrix.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.3. Distribution drawing and density functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf2 \expnd0\expndtw0\kerning0 (float)dmvnorm(float\'a0x, numeric\'a0mu, numeric\'a0sigma)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f0\b probability densities for a \f4\i k \f0\i0 -dimensional multivariate normal distribution \f3\b0 with a length \f7\i k \f3\i0 mean vector \f1\fs18 mu \f3\fs20 and a \f7\i k \f3\i0 \'d7 \f7\i k \f3\i0 variance-covariance matrix \f1\fs18 sigma \f3\fs20 . The \f1\fs18 mu \f3\fs20 and \f1\fs18 sigma \f3\fs20 parameters are used for all densities. The quantile values, \f1\fs18 x \f3\fs20 , should be supplied as a matrix with one row per vector of quantile values and \f7\i k \f3\i0 columns (one column per dimension); for convenience, a single quantile may be supplied as a vector rather than a matrix with just one row. The number of dimensions \f7\i k \f3\i0 must be at least two; for \f7\i k \f3\i0 =1, use \f1\fs18 dnorm() \f3\fs20 .\ Cholesky decomposition of the variance-covariance matrix \f1\fs18 sigma \f3\fs20 is involved as an internal step, and this requires that \f1\fs18 sigma \f3\fs20 be positive-definite; if it is not, an error will result. When more than one density is needed, it is much more efficient to call \f1\fs18 dmvnorm() \f3\fs20 once to generate all of the densities, since the Cholesky decomposition of \f1\fs18 sigma \f3\fs20 can then be done just once.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)dbeta(float\'a0x, numeric\'a0alpha, numeric\'a0beta)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f0\b probability densities for a beta distribution \f3\b0 at quantiles \f1\fs18 x \f3\fs20 with parameters \f1\fs18 alpha \f3\fs20 and \f1\fs18 beta \f3\fs20 . The \f1\fs18 alpha \f3\fs20 and \f1\fs18 beta \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of the same length as \f1\fs18 x \f3\fs20 , specifying a value for each density computation. The probability density function is \f8\i P \f2\i0 ( \f8\i s \f2\i0 \'a0|\'a0 \f8\i \uc0\u945 \f2\i0 , \f8\i \uc0\u946 \f2\i0 )\'a0= [\uc0\u915 ( \f8\i \uc0\u945 \f2\i0 + \f8\i \uc0\u946 \f2\i0 )/\uc0\u915 ( \f8\i \uc0\u945 \f2\i0 )\uc0\u915 ( \f8\i \uc0\u946 \f2\i0 )] \f8\i s \fs13\fsmilli6667 \super \uc0\u945 \f2\i0 \uc0\u8722 1 \fs20 \nosupersub (1\uc0\u8722 \f8\i s \f2\i0 ) \f8\i\fs13\fsmilli6667 \super \uc0\u946 \f2\i0 \uc0\u8722 1 \f3\fs20 \nosupersub , where \f8\i \uc0\u945 \f3\i0 is \f1\fs18 alpha \f3\fs20 and \f8\i \uc0\u946 \f3\i0 is \f1\fs18 beta \f3\fs20 . Both parameters must be greater than \f1\fs18 0 \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)dexp(float\'a0x, [numeric\'a0mu\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f0\b probability densities for an exponential distribution \f3\b0 at quantiles \f1\fs18 x \f3\fs20 with mean \f1\fs18 mu \f3\fs20 (i.e. rate \f1\fs18 1/mu \f3\fs20 ). The \f1\fs18 mu \f3\fs20 parameter may either be a singleton, specifying a single value to be used for all of the draws, or they may be vectors of the same length as \f1\fs18 x \f3\fs20 , specifying a value for each density computation.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)dgamma(float\'a0x, numeric\'a0mean, numeric\'a0shape)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f0\b probability densities for a gamma distribution \f3\b0 at quantiles \f1\fs18 x \f3\fs20 with mean \f1\fs18 mean \f3\fs20 and shape parameter \f1\fs18 shape \f3\fs20 . The \f1\fs18 mean \f3\fs20 and \f1\fs18 shape \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of the same length as \f1\fs18 x \f3\fs20 , specifying a value for each density computation. The probability density function is \f8\i P \f2\i0 ( \f8\i s \f2\i0 \'a0|\'a0 \f8\i \uc0\u945 \f2\i0 , \f8\i \uc0\u946 \f2\i0 )\'a0= [\uc0\u915 ( \f8\i \uc0\u945 \f2\i0 ) \f8\i \uc0\u946 \fs13\fsmilli6667 \super \uc0\u945 \f2\i0\fs20 \nosupersub ] \fs13\fsmilli6667 \super \uc0\u8722 1 \f8\i\fs20 \nosupersub s \fs13\fsmilli6667 \super \uc0\u945 \f2\i0 \uc0\u8722 1 \fs20 \nosupersub exp(\uc0\u8722 \f8\i s \f2\i0 / \f8\i \uc0\u946 \f2\i0 ) \f3 , where \f8\i \uc0\u945 \f3\i0 is the shape parameter \f1\fs18 shape \f3\fs20 , and the mean of the distribution given by \f1\fs18 mean \f3\fs20 is equal to \f8\i \uc0\u945 \u946 \f3\i0 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (float)dnorm(float\'a0x, [numeric\'a0mean\'a0=\'a00], [numeric\'a0sd\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f0\b probability densities for a normal distribution \f3\b0 at quantiles \f1\fs18 x \f3\fs20 with mean \f1\fs18 mean \f3\fs20 and standard deviation \f1\fs18 sd \f3\fs20 . The \f1\fs18 mean \f3\fs20 and \f1\fs18 sd \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the densities, or they may be vectors of the same length as \f1\fs18 x \f3\fs20 , specifying a value for each density computation. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)findInterval(numeric\'a0x, numeric\'a0vec, [logical$\'a0rightmostClosed\'a0=\'a0F], [logical$\'a0allInside\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f0\b interval indices \f3\b0 for the values in \f1\fs18 x \f3\fs20 within a vector of non-decreasing breakpoints \f1\fs18 vec \f3\fs20 . The returned \f1\fs18 integer \f3\fs20 vector contains, for each corresponding element of \f1\fs18 x \f3\fs20 , the index of the interval in \f1\fs18 vec \f3\fs20 within which that element of \f1\fs18 x \f3\fs20 is contained.\ More precisely, if \f1\fs18 i \f3\fs20 is the returned \f1\fs18 integer \f3\fs20 vector from \f1\fs18 findInterval(x, v) \f3\fs20 , and \f1\fs18 N \f3\fs20 is \f1\fs18 length(v) \f3\fs20 , then for each index \f1\fs18 j \f3\fs20 in \f1\fs18 x \f3\fs20 , it will be true that \f1\fs18 v[i[j]] \f3\fs20 \uc0\u8804 \f1\fs18 x[j] \f3\fs20 < \f1\fs18 v[i[j]+1] \f3\fs20 , treating \f1\fs18 v[-1] \f3\fs20 as \f1\fs18 -INF \f3\fs20 and \f1\fs18 v[N] \f3\fs20 as \f1\fs18 INF \f3\fs20 , \f7\i assuming \f3\i0 that the two flags \f1\fs18 rightmostClosed \f3\fs20 and \f1\fs18 allInside \f3\fs20 have their default value of \f1\fs18 F \f3\fs20 . The effects of the flags will be discussed below. Note that \f1\fs18 vec \f3\fs20 must be non-decreasing; in other words, it must be sorted in ascending order, although it may have duplicate values. The returned vector will thus be equal in length to \f1\fs18 x \f3\fs20 , and each of its elements will be in the interval [ \f1\fs18 -1 \f3\fs20 , \f1\fs18 N-1 \f3\fs20 ].\ The \f1\fs18 rightmostClosed \f3\fs20 flag, if \f1\fs18 T \f3\fs20 , alters the above behavior to treat the rightmost interval, \f1\fs18 vec[N-2] \f3\fs20 .. \f1\fs18 vec[N-1] \f3\fs20 , as closed. This means that if \f1\fs18 x[j]==vec[N-1] \f3\fs20 (i.e., equals \f1\fs18 max(vec) \f3\fs20 ), the corresponding result \f1\fs18 i[j] \f3\fs20 will be \f1\fs18 N-2 \f3\fs20 as for all other values in the last interval.\ The \f1\fs18 allInside \f3\fs20 flag, if \f1\fs18 T \f3\fs20 , alters the above behavior to coerce returned indices into \f1\fs18 0 \f3\fs20 .. \f1\fs18 N-2 \f3\fs20 . In other words, \f1\fs18 -1 \f3\fs20 is mapped to \f1\fs18 0 \f3\fs20 , and \f1\fs18 N-1 \f3\fs20 is mapped to \f1\fs18 N-2 \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (float)pnorm(float\'a0q, [numeric\'a0mean\'a0=\'a00], [numeric\'a0sd\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f0\b cumulative distribution function values for a normal distribution \f3\b0 at quantiles \f1\fs18 q \f3\fs20 with mean \f1\fs18 mean \f3\fs20 and standard deviation \f1\fs18 sd \f3\fs20 . The \f1\fs18 mean \f3\fs20 and \f1\fs18 sd \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the quantiles, or they may be vectors of the same length as \f1\fs18 q \f3\fs20 , specifying a value for each quantile.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)qnorm(float\'a0p, [numeric\'a0mean\'a0=\'a00], [numeric\'a0sd\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f0\b quantiles for a normal distribution \f3\b0 with lower tail probabilities less than \f1\fs18 p \f3\fs20 , with mean \f1\fs18 mean \f3\fs20 and standard deviation \f1\fs18 sd \f3\fs20 . The \f1\fs18 mean \f3\fs20 and \f1\fs18 sd \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the quantiles, or they may be vectors of the same length as \f1\fs18 p \f3\fs20 , specifying a value for each quantile computation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)rbeta(integer$\'a0n, numeric\'a0alpha, numeric\'a0beta)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a beta distribution \f3\b0 with parameters \f1\fs18 alpha \f3\fs20 and \f1\fs18 beta \f3\fs20 . The \f1\fs18 alpha \f3\fs20 and \f1\fs18 beta \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. Draws are made from a beta distribution with probability density \f8\i P \f2\i0 ( \f8\i s \f2\i0 \'a0|\'a0 \f8\i \uc0\u945 \f2\i0 , \f8\i \uc0\u946 \f2\i0 )\'a0= [\uc0\u915 ( \f8\i \uc0\u945 \f2\i0 + \f8\i \uc0\u946 \f2\i0 )/\uc0\u915 ( \f8\i \uc0\u945 \f2\i0 )\uc0\u915 ( \f8\i \uc0\u946 \f2\i0 )] \f8\i s \fs13\fsmilli6667 \super \uc0\u945 \f2\i0 \uc0\u8722 1 \fs20 \nosupersub (1\uc0\u8722 \f8\i s \f2\i0 ) \f8\i\fs13\fsmilli6667 \super \uc0\u946 \f2\i0 \uc0\u8722 1 \f3\fs20 \nosupersub , where \f8\i \uc0\u945 \f3\i0 is \f1\fs18 alpha \f3\fs20 and \f8\i \uc0\u946 \f3\i0 is \f1\fs18 beta \f3\fs20 . Both parameters must be greater than \f1\fs18 0 \f3\fs20 . The values drawn are in the interval [0, 1].\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (integer)rbinom(integer$ \f2 \'a0 \f1 n, integer\'a0size, float\'a0prob)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a binomial distribution \f3\b0 with a number of trials specified by \f1\fs18 size \f3\fs20 and a probability of success specified by \f1\fs18 prob \f3\fs20 . The \f1\fs18 size \f3\fs20 and \f1\fs18 prob \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (float)rcauchy(integer$\'a0n, [numeric\'a0location\'a0=\'a00], [numeric\'a0scale\'a0=\'a01])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a Cauchy distribution \f3\b0 with location \f1\fs18 location \f3\fs20 and scale \f1\fs18 scale \f3\fs20 . The \f1\fs18 location \f3\fs20 and \f1\fs18 scale \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (float)rdirichlet(integer$\'a0n, numeric\'a0alpha)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a matrix of \f1\fs18 n \f3\fs20 \f0\b random draws from a Dirichlet distribution \f3\b0 with vector of shape parameters \f1\fs18 alpha \f3\fs20 . The Dirichlet distribution is a multidimensional generalization of the beta distribution, sometimes called the multivariate beta distribution; see also \f1\fs18 rbeta() \f3\fs20 . All values in \f1\fs18 alpha \f3\fs20 must be positive and finite, and \f1\fs18 alpha \f3\fs20 must be of length >= 2. The return value is a matrix with \f1\fs18 n \f3\fs20 rows and \f1\fs18 size(alpha) \f3\fs20 columns, each row containing a single Dirichlet random deviate.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)rdunif(integer$\'a0n, [integer\'a0min\'a0=\'a00], [integer\'a0max \f2 \'a0 \f1 =\'a01])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a discrete uniform distribution \f3\b0 from \f1\fs18 min \f3\fs20 to \f1\fs18 max \f3\fs20 , inclusive. The \f1\fs18 min \f3\fs20 and \f1\fs18 max \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. See \f1\fs18 runif() \f3\fs20 for draws from a continuous uniform distribution\kerning1\expnd0\expndtw0 , and \f1\fs18 runif64() \f3\fs20 for uniform draws from the full 64-bit \f1\fs18 integer \f3\fs20 range\expnd0\expndtw0\kerning0 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (integer)rdunif64(integer$\'a0n)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a discrete uniform distribution spanning the full 64-bit \f9\fs18 integer \f0\fs20 range \f3\b0 . See \f1\fs18 rdunif() \f3\fs20 for draws from a discrete uniform distribution with a specified range.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)rexp(integer$ \f2 \'a0 \f1 n, [numeric\'a0mu\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from an exponential distribution \f3\b0 with mean \f1\fs18 mu \f3\fs20 (i.e. rate \f1\fs18 1/mu \f3\fs20 ). The \f1\fs18 mu \f3\fs20 parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)rf(integer$\'a0n, numeric\'a0d1, numeric\'a0d2)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from an \f4\i F \f0\i0 -distribution \f3\b0 with degrees of freedom \f1\fs18 d1 \f3\fs20 and \f1\fs18 d2 \f3\fs20 . The \f1\fs18 d1 \f3\fs20 and \f1\fs18 d2 \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)rgamma(integer$ \f2 \'a0 \f1 n, numeric\'a0mean, numeric\'a0shape)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a gamma distribution \f3\b0 with mean \f1\fs18 mean \f3\fs20 and shape parameter \f1\fs18 shape \f3\fs20 . The \f1\fs18 mean \f3\fs20 and \f1\fs18 shape \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. Draws are made from a gamma distribution with probability density \f8\i P \f2\i0 ( \f8\i s \f2\i0 \'a0|\'a0 \f10\i \uc0\u945 \f2\i0 , \f10\i \uc0\u946 \f2\i0 )\'a0= [ \f11 \uc0\u915 \f2 ( \f10\i \uc0\u945 \f2\i0 ) \f10\i \uc0\u946 \u945 \f2\i0 ]\super \uc0\u8722 1\nosupersub exp(\uc0\u8722 \f8\i s \f2\i0 / \f10\i \uc0\u946 \f2\i0 ) \f3 , where \f10\i \uc0\u945 \f3\i0 is the shape parameter \f1\fs18 shape \f3\fs20 , and the mean of the distribution given by \f1\fs18 mean \f3\fs20 is equal to \f10\i \uc0\u945 \u946 \f2\i0 . \f3 Values of \f1\fs18 mean \f3\fs20 less than zero are allowed, and are equivalent (in principle) to the negation of a draw from a gamma distribution with the same \f1\fs18 shape \f3\fs20 parameter and the negation of the \f1\fs18 mean \f3\fs20 parameter. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (integer)rgeom(integer$\'a0n, float\'a0p)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a geometric distribution \f3\b0 with parameter \f1\fs18 p \f3\fs20 . The \f1\fs18 p \f3\fs20 parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. Eidos follows R in using the geometric distribution with support on the set \{0, 1, 2, \'85\}, where the drawn value indicates the number of failures prior to success. There is an alternative definition, based upon the number of trial required to get one success, so beware.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (float)rlaplace(integer$\'a0n, [numeric\'a0b\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a Laplace distribution \f3\b0 with shape parameter \f1\fs18 b \f3\fs20 . The \f1\fs18 b \f3\fs20 parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length \f1\fs18 n \f3\fs20 , specifying a value for each draw.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)rlnorm(integer$ \f2 \'a0 \f1 n, [numeric\'a0meanlog\'a0=\'a00], [numeric\'a0sdlog\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a lognormal distribution \f3\b0 with mean \f1\fs18 meanlog \f3\fs20 and standard deviation \f1\fs18 sdlog \f3\fs20 , specified on the log scale. The \f1\fs18 meanlog \f3\fs20 and \f1\fs18 sdlog \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)rmultinom(integer$\'a0n, integer$\'a0size, numeric\'a0prob)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a matrix of \f1\fs18 n \f3\fs20 \f0\b random vectors from the specified multinomial distribution \f3\b0 . For each random vector drawn from the multinomial distribution, \f1\fs18 size \f3\fs20 objects will be put into \f7\i k \f3\i0 boxes, where \f1\fs18 prob \f3\fs20 is a vector of probabilities of length \f7\i k \f3\i0 specifying the probability for each box. If \f1\fs18 prob \f3\fs20 is not normalized to sum to \f1\fs18 1.0 \f3\fs20 , its entries are treated as weights and normalized appropriately. The draws are returned as a matrix with \f7\i k \f3\i0 rows (one row per box) and \f1\fs18 n \f3\fs20 columns (one column per drawn random vector).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (float)rmvnorm(integer$\'a0n, numeric\'a0mu, numeric\'a0sigma)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a matrix of \f1\fs18 n \f3\fs20 \f0\b random draws from a \f4\i k \f0\i0 -dimensional multivariate normal distribution \f3\b0 with a length \f7\i k \f3\i0 mean vector \f1\fs18 mu \f3\fs20 and a \f7\i k \f3\i0 \'d7 \f7\i k \f3\i0 variance-covariance matrix \f1\fs18 sigma \f3\fs20 . The \f1\fs18 mu \f3\fs20 and \f1\fs18 sigma \f3\fs20 parameters are used for all \f1\fs18 n \f3\fs20 draws. The draws are returned as a matrix with \f1\fs18 n \f3\fs20 rows (one row per draw) and \f7\i k \f3\i0 columns (one column per dimension). The number of dimensions \f7\i k \f3\i0 must be at least two; for \f7\i k \f3\i0 =1, use \f1\fs18 rnorm() \f3\fs20 .\ Cholesky decomposition of the variance-covariance matrix \f1\fs18 sigma \f3\fs20 is involved as an internal step, and this requires that \f1\fs18 sigma \f3\fs20 be positive-definite; if it is not, an error will result. When more than one draw is needed, it is much more efficient to call \f1\fs18 rmvnorm() \f3\fs20 once to generate all of the draws, since the Cholesky decomposition of \f1\fs18 sigma \f3\fs20 can then be done just once.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (integer)rnbinom(integer$\'a0n, numeric\'a0size, float\'a0prob)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a negative binomial distribution \f3\b0 representing the number of failures which occur in a sequence of Bernoulli trials before reaching a target number of successful trials specified by \f1\fs18 size \f3\fs20 , given a probability of success specified by \f1\fs18 prob \f3\fs20 . The mean of this distribution for \f1\fs18 size \f3\fs20 \f7\i s \f3\i0 and \f1\fs18 prob \f3\fs20 \f7\i p \f3\i0 is \f7\i s \f3\i0 (1\uc0\u8722 \f7\i p \f3\i0 )/ \f7\i p \f3\i0 , with variance \f7\i s \f3\i0 (1\uc0\u8722 \f7\i p \f3\i0 )/ \f7\i p \f3\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub . The \f1\fs18 size \f3\fs20 and \f1\fs18 prob \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)rnorm(integer$ \f2 \'a0 \f1 n, [numeric\'a0mean\'a0=\'a00], [numeric\'a0sd\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a normal distribution \f3\b0 with mean \f1\fs18 mean \f3\fs20 and standard deviation \f1\fs18 sd \f3\fs20 . The \f1\fs18 mean \f3\fs20 and \f1\fs18 sd \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)rpois(integer$ \f2 \'a0 \f1 n, numeric\'a0lambda)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a Poisson distribution \f3\b0 with parameter \f1\fs18 lambda \f3\fs20 (not to be confused with the language concept of a \'93lambda\'94; \f1\fs18 lambda \f3\fs20 here is just the name of a parameter, because the symbol typically used for the parameter of a Poisson distribution is the Greek letter \f11 \uc0\u955 \f3 ). The \f1\fs18 lambda \f3\fs20 parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)runif(integer$\'a0n, [numeric\'a0min\'a0=\'a00], [numeric\'a0max \f2 \'a0 \f1 =\'a01])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a continuous uniform distribution \f3\b0 from \f1\fs18 min \f3\fs20 to \f1\fs18 max \f3\fs20 , inclusive. The \f1\fs18 min \f3\fs20 and \f1\fs18 max \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. See \f1\fs18 rdunif() \f3\fs20 for draws from a discrete uniform distribution.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (float)rweibull(integer$ \f2 \'a0 \f1 n, numeric\'a0lambda, numeric\'a0k)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a Weibull distribution \f3\b0 with scale parameter \f1\fs18 lambda \f3\fs20 and shape parameter \f1\fs18 k \f3\fs20 , both greater than zero. The \f1\fs18 lambda \f3\fs20 and \f1\fs18 k \f3\fs20 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. Draws are made from a Weibull distribution with probability distribution \f8\i P \f2\i0 ( \f8\i s \f2\i0 \'a0|\'a0 \f10\i \uc0\u955 \f2\i0 , \f8\i k \f2\i0 )\'a0=\'a0( \f8\i k \f2\i0 / \f10\i \uc0\u955 \f8 \super k \f2\i0 \nosupersub )\'a0 \f8\i s\super k \f2\i0 \uc0\u8722 1\nosupersub \'a0exp(-( \f8\i s \f2\i0 / \f10\i \uc0\u955 \f2\i0 ) \f8\i \super k \f2\i0 \nosupersub ).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)rztpois(integer$\'a0n, numeric\'a0lambda)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of \f1\fs18 n \f3\fs20 \f0\b random draws from a zero-truncated Poisson distribution \f3\b0 with parameter \f1\fs18 lambda \f3\fs20 (not to be confused with the language concept of a \'93lambda\'94; \f1\fs18 lambda \f3\fs20 here is just the name of a parameter, because the symbol typically used for the parameter of a Poisson distribution is the Greek letter \f8\i \uc0\u955 \f3\i0 ). The zero-truncated Poisson distribution is the conditional probability distribution of a Poisson-distributed random variable, given that the value of the random variable is not zero. The values returned by \f1\fs18 rztpois() \f3\fs20 will therefore never be zero.\ The \f1\fs18 lambda \f3\fs20 parameter, \f8\i \uc0\u955 \f3\i0 , may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length \f1\fs18 n \f3\fs20 , specifying a value for each draw. It is important to note that for \f1\fs18 rpois() \f3\fs20 the expected mean of the distribution is \f8\i \uc0\u955 \f3\i0 , but for \f1\fs18 rztpois() \f3\fs20 the expected mean of the distribution is \f8\i \uc0\u955 \f3\i0 \'a0/\'a0(1\'a0\uc0\u8722 \'a0exp(\u8722 \f8\i \uc0\u955 \f3\i0 )), precisely because the zero-truncated Poisson distribution is conditional upon being non-zero.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.4. Vector construction functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf0 (*)c(...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b concatenation \f3\b0 of all of its parameters into a single vector\cf2 \expnd0\expndtw0\kerning0 , stripped of all matrix/array dimensions (see \f1\fs18 rbind() \f3\fs20 and \f1\fs18 cbind() \f3\fs20 for concatenation that does not strip this information)\cf0 \kerning1\expnd0\expndtw0 . The parameters will be promoted to the highest type represented among them, and that type will be the return type. \f1\fs18 NULL \f3\fs20 values are ignored; they have no effect on the result. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)float(integer$ \f2 \'a0 \f1 length)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b new \f9\fs18 float \f0\fs20 vector \f3\b0 of the length specified by \f1\fs18 length \f3\fs20 , filled with \f1\fs18 0.0 \f3\fs20 values. This can be useful for pre-allocating a vector which you then fill with values by subscripting. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)integer(integer$ \f2 \'a0 \f1 length, [integer$\'a0fill1\'a0=\'a00], [integer$\'a0fill2\'a0=\'a01], [Ni\'a0fill2Indices\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b new \f9\fs18 integer \f0\fs20 vector \f3\b0 of the length specified by \f1\fs18 length \f3\fs20 , filled with \f1\fs18 0 \f3\fs20 values by default. This can be useful for pre-allocating a vector which you then fill with values by subscripting.\ If a value is supplied for \f1\fs18 fill1 \f3\fs20 , the new vector will be filled with that value instead of the default of \f1\fs18 0 \f2\fs20 . \f3 Additionally, if a non- \f1\fs18 NULL \f3\fs20 vector is supplied for \f1\fs18 fill2Indices \f3\fs20 , the indices specified by \f1\fs18 fill2Indices \f3\fs20 will be filled with the value provided by \f1\fs18 fill2 \f2\fs20 . \f3 For example, given the default values of \f1\fs18 0 \f3\fs20 and \f1\fs18 1 \f3\fs20 for \f1\fs18 fill1 \f3\fs20 and \f1\fs18 fill2 \f3\fs20 , the returned vector will contain \f1\fs18 1 \f3\fs20 at all positions specified by \f1\fs18 fill2Indices \f3\fs20 , and will contain \f1\fs18 0 \f3\fs20 at all other positions. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical)logical(integer$ \f2 \'a0 \f1 length)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b new \f9\fs18 logical \f0\fs20 vector \f3\b0 of the length specified by \f1\fs18 length \f3\fs20 , filled with \f1\fs18 F \f3\fs20 values. This can be useful for pre-allocating a vector which you then fill with values by subscripting. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (object)object(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b new empty \f9\fs18 object \f0\fs20 vector \f3\b0 . Unlike \f1\fs18 float() \f3\fs20 , \f1\fs18 integer() \f3\fs20 , \f1\fs18 logical() \f3\fs20 , and \f1\fs18 string() \f3\fs20 , a length cannot be specified and the new vector contains no elements. This is because there is no default value for the object type. Adding to such a vector is typically done with \f1\fs18 c() \f2\fs20 . \f3 Note that the return value is of type \f1\fs18 object \f3\fs20 ; this method creates an \f1\fs18 object \f3\fs20 vector that does not know what element type it contains. Such \f1\fs18 object \f3\fs20 vectors may be mixed freely with other \f1\fs18 object \f3\fs20 vectors in \f1\fs18 c() \f3\fs20 and similar contexts; the result of such mixing will take its \f1\fs18 object \f3\fs20 -element type from the \f1\fs18 object \f3\fs20 vector with a defined \f1\fs18 object \f3\fs20 -element type (if any). \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)rep(* \f2 \'a0 \f1 x, integer$ \f2 \'a0 \f1 count)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b repetition \f3\b0 of \f1\fs18 x \f3\fs20 : the entirety of \f1\fs18 x \f3\fs20 is repeated \f1\fs18 count \f3\fs20 times. The return type matches the type of \f1\fs18 x \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)repEach(* \f2 \'a0 \f1 x, integer \f2 \'a0 \f1 count)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b repetition of elements \f3\b0 of \f1\fs18 x \f3\fs20 : each element of \f1\fs18 x \f3\fs20 is repeated. If \f1\fs18 count \f3\fs20 is a singleton, it specifies the number of times that each element of \f1\fs18 x \f3\fs20 will be repeated. Otherwise, the length of \f1\fs18 count \f3\fs20 must be equal to the length of \f1\fs18 x \f3\fs20 ; in this case, each element of \f1\fs18 x \f3\fs20 is repeated a number of times specified by the corresponding value of \f1\fs18 count \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)sample(*\'a0x, integer$\'a0size, [logical$ \f2 \'a0 \f1 replace\'a0=\'a0F], [Nif\'a0weights\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f1\fs18 size \f3\fs20 containing a \f0\b sample from the elements of \f9\fs18 x \f3\b0\fs20 . If \f1\fs18 replace \f3\fs20 is \f1\fs18 T \f3\fs20 , sampling is conducted with replacement (the same element may be drawn more than once); if it is \f1\fs18 F \f3\fs20 (the default), then sampling is done without replacement. A vector of weights may be supplied in the optional parameter \f1\fs18 weights \f3\fs20 ; if not \f1\fs18 NULL \f3\fs20 , it must be equal in size to \f1\fs18 x \f3\fs20 , all weights must be non-negative, and the sum of the weights must be greater than \f1\fs18 0 \f3\fs20 . If \f1\fs18 weights \f3\fs20 is \f1\fs18 NULL \f3\fs20 (the default), then equal weights are used for all elements of \f1\fs18 x \f3\fs20 . An error occurs if \f1\fs18 sample() \f3\fs20 runs out of viable elements from which to draw; most notably, if sampling is done without replacement then \f1\fs18 size \f3\fs20 must be at most equal to the size of \f1\fs18 x \f3\fs20 , but if weights of zero are supplied then the restriction on \f1\fs18 size \f3\fs20 will be even more stringent. The draws are obtained from the standard Eidos random number generator, which might be shared with the Context. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (numeric)seq(numeric$ \f2 \'a0 \f1 from, numeric$ \f2 \'a0 \f1 to, [Nif$ \f2 \'a0 \f1 by \f2 \'a0 \f1 =\'a0NULL], [Ni$ \f2 \'a0 \f1 length\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b sequence \f3\b0 , starting at \f1\fs18 from \f3\fs20 and proceeding in the direction of \f1\fs18 to \f3\fs20 until the next value in the sequence would fall beyond \f1\fs18 to \f3\fs20 . If the optional parameters \f1\fs18 by \f3\fs20 and \f1\fs18 length \f3\fs20 are both \f1\fs18 NULL \f3\fs20 (the default), the sequence steps by values of \f1\fs18 1 \f3\fs20 or \f1\fs18 -1 \f3\fs20 (as needed to proceed in the direction of \f1\fs18 to \f3\fs20 ). A different step value may be supplied with \f1\fs18 by \f3\fs20 , but must have the necessary sign. Alternatively, a sequence length may be supplied in \f1\fs18 length \f3\fs20 , in which case the step magnitude will be chosen to produce a sequence of the requested length (with the necessary sign, as before). If \f1\fs18 from \f3\fs20 and \f1\fs18 to \f3\fs20 are both \f1\fs18 integer \f3\fs20 then the return type will be \f1\fs18 integer \f3\fs20 when possible (but this may not be possible, depending upon values supplied for \f1\fs18 by \f3\fs20 or \f1\fs18 length \f3\fs20 ), otherwise it will be \f1\fs18 float \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (integer)seqAlong(*\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns an \f0\b index sequence along \f9\fs18 x \f3\b0\fs20 , from \f1\fs18 0 \f3\fs20 to \f1\fs18 size(x) - 1 \f3\fs20 , with a step of \f1\fs18 1 \f3\fs20 . This is a convenience function for easily obtaining a set of indices to address or iterate through a vector. Any matrix/array dimension information is ignored; the index sequence is suitable for indexing into \f1\fs18 x \f3\fs20 as a vector.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)seqLen(integer$\'a0length)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns an \f0\b index sequence of \f9\fs18 length \f3\b0\fs20 , from \f1\fs18 0 \f3\fs20 to \f1\fs18 length - 1 \f3\fs20 , with a step of \f1\fs18 1 \f3\fs20 ; if \f1\fs18 length \f3\fs20 is \f1\fs18 0 \f3\fs20 the sequence will be zero-length. This is a convenience function for easily obtaining a set of indices to address or iterate through a vector. Note that when \f1\fs18 length \f3\fs20 is \f1\fs18 0 \f3\fs20 , using the sequence operator with \f1\fs18 0:(length-1) \f3\fs20 will produce \f1\fs18 0\'a0-1 \f3\fs20 , and calling \f1\fs18 seq(a,\'a0b,\'a0length=length) \f3\fs20 will raise an error, but \f1\fs18 seqLen(length) \f3\fs20 will return \f1\fs18 integer(0) \f3\fs20 , making \f1\fs18 seqLen() \f3\fs20 particularly useful for generating a sequence of a given length that might be zero.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (string)string(integer$ \f2 \'a0 \f1 length)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b new \f9\fs18 string \f0\fs20 vector \f3\b0 of the length specified by \f1\fs18 length \f3\fs20 , filled with \f1\fs18 "" \f3\fs20 values. This can be useful for pre-allocating a vector which you then fill with values by subscripting. \f2 \ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.5. Value inspection & manipulation functions\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf0 (logical$)all(logical \f2 \'a0 \f1 x, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f0\b all values are \f9\fs18 T \f3\b0\fs20 in \f1\fs18 x \f3\fs20 and in any other arguments supplied; if any value is \f1\fs18 F \f3\fs20 , returns \f1\fs18 F \f2\fs20 . \f3 All arguments must be of \f1\fs18 logical \f3\fs20 type. If all arguments are zero-length, \f1\fs18 T \f3\fs20 is returned. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical$)allClose(float\'a0x, float\'a0y, [float$\'a0rtol\'a0=\'a01.0e-05], [float$\'a0atol\'a0=\'a01.0e-08], [logical$\'a0equalNAN\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns \f1\fs18 T \f3\fs20 if \f0\b all pairs of values between \f9\fs18 x \f0\fs20 and y are \'93close\'94 \f3\b0 ; if any pair of values is not close, returns \f1\fs18 F \f3\fs20 . The definition of \'93close\'94 matches that used in the \f1\fs18 isClose() \f3\fs20 function; see that documentation for all further details, including the way that values in \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 are paired, as well as the meaning of the \f1\fs18 rtol \f3\fs20 , \f1\fs18 atol \f3\fs20 , and \f1\fs18 equalNAN \f3\fs20 parameters. This function is essentially equivalent to \f1\fs18 all(isClose(x, y, rtol, atol, equalNAN)) \f3\fs20 , but is more efficient.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)any(logical \f2 \'a0 \f1 x, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f0\b any value is \f9\fs18 T \f3\b0\fs20 in \f1\fs18 x \f3\fs20 or in any other arguments supplied; if all values are \f1\fs18 F \f3\fs20 , returns \f1\fs18 F \f2\fs20 . \f3 All arguments must be of \f1\fs18 logical \f3\fs20 type. If all arguments are zero-length, \f1\fs18 F \f3\fs20 is returned. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)cat(* \f2 \'a0 \f1 x, [string$ \f2 \'a0 \f1 sep \f2 \'a0 \f1 =\'a0" "]\cf2 , [logical$\'a0error\'a0=\'a0F]\cf0 )\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Concatenates output \f3\b0 to Eidos\'92s output stream, joined together by \f1\fs18 sep \f3\fs20 . The value \f1\fs18 x \f3\fs20 that is output may be of any type. A newline is not appended to the output, unlike the behavior of \f1\fs18 print() \f3\fs20 ; if a trailing newline is desired, you can use \f1\fs18 "\\n" \f3\fs20 (or use the \f1\fs18 catn() \f3\fs20 function). Also unlike \f1\fs18 print() \f3\fs20 , \f1\fs18 cat() \f3\fs20 tends to emit very literal output; \f1\fs18 print(logical(0)) \f3\fs20 will emit \'93 \f1\fs18 logical(0) \f3\fs20 \'94, for example \'96 showing a semantic interpretation of the value \'96 whereas \f1\fs18 cat(logical(0)) \f3\fs20 will emit nothing at all, since there are no elements in the value (it is zero-length). Similarly, \f1\fs18 print(NULL) \f3\fs20 will emit \'93 \f1\fs18 NULL \f3\fs20 \'94, but \f1\fs18 cat(NULL) \f3\fs20 will emit nothing. \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 By default (when \f1\fs18 error \f3\fs20 is \f1\fs18 F \f3\fs20 ), the output is sent to the standard Eidos output stream. When running at the command line, this sends it to \f1\fs18 stdout \f3\fs20 ; when running in SLiMgui, this sends it to the simulation window\'92s output textview. If \f1\fs18 error \f3\fs20 is \f1\fs18 T \f3\fs20 , the output is instead sent to the Eidos error stream. When running at the command line, this sends it to \f1\fs18 stderr \f3\fs20 ; when running in SLiMgui, the output is routed to the simulation\'92s debugging output window.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)catn([* \f2 \'a0 \f1 x \f2 \'a0 \f1 =\'a0""], [string$ \f2 \'a0 \f1 sep \f2 \'a0 \f1 =\'a0" "]\cf2 , [logical$\'a0error\'a0=\'a0F]\cf0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Concatenates output (with a trailing newline) \f3\b0 to Eidos\'92s output stream, joined together by \f1\fs18 sep \f3\fs20 . The behavior of \f1\fs18 catn() \f3\fs20 is identical to that of \f1\fs18 cat() \f3\fs20 , except that a final newline character is appended to the output for convenience. For \f1\fs18 catn() \f3\fs20 a default value of \f1\fs18 "" \f3\fs20 is supplied for \f1\fs18 x \f3\fs20 , to allow a simple \f1\fs18 catn() \f3\fs20 call with no parameters to emit a newline. \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 By default (when \f1\fs18 error \f3\fs20 is \f1\fs18 F \f3\fs20 ), the output is sent to the standard Eidos output stream. When running at the command line, this sends it to \f1\fs18 stdout \f3\fs20 ; when running in SLiMgui, this sends it to the simulation window\'92s output textview. If \f1\fs18 error \f3\fs20 is \f1\fs18 T \f3\fs20 , the output is instead sent to the Eidos error stream. When running at the command line, this sends it to \f1\fs18 stderr \f3\fs20 ; when running in SLiMgui, the output is routed to the simulation\'92s debugging output window.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string)format(string$ \f2 \'a0 \f1 format, numeric\'a0x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of \f0\b formatted strings \f3\b0 generated from \f1\fs18 x \f3\fs20 , based upon the formatting string \f1\fs18 format \f2\fs20 . \f3 The \f1\fs18 format \f3\fs20 parameter may be any \f1\fs18 string \f3\fs20 value, but must contain exactly one escape sequence beginning with the \f1\fs18 % \f3\fs20 character. This escape sequence specifies how to format a single value from the vector \f1\fs18 x \f3\fs20 . The returned vector contains one \f1\fs18 string \f3\fs20 value for each element of \f1\fs18 x \f3\fs20 ; each \f1\fs18 string \f3\fs20 value is identical to the string supplied in \f1\fs18 format \f3\fs20 , except with a formatted version of the corresponding value from \f1\fs18 x \f3\fs20 substituted in place of the escape sequence.\ The syntax for \f1\fs18 format \f3\fs20 is a subset of the standard C/C++ \f1\fs18 printf() \f3\fs20 -style format strings (e.g., http://en.cppreference.com/w/c/io/fprintf). The escape sequence used to format each value of x is composed of several elements:\ \pard\pardeftab397\li907\fi-187\ri720\sb60\sa60\partightenfactor0 \ls1\ilvl0\cf0 \'96 A \f1\fs18 % \f3\fs20 character at the beginning, initiating the escape sequence (if an actual \f1\fs18 % \f3\fs20 character is desired, rather than an escape sequence, \f1\fs18 %% \f3\fs20 may be used)\ \'96 Optional flags that modify the style of formatting:\ \pard\tx1170\tx1890\pardeftab397\li1890\fi-990\ri720\sb60\sa60\partightenfactor0 \ls2\ilvl0 \f12\fs18 \cf0 \'a5 \f1 - \f3\fs20 : The value is left-justified with the field (as opposed to the default of right-justification).\ \ls2\ilvl0 \f12\fs18 \'a5 \f1 + \f3\fs20 : The sign of the value is always prepended, even if the value is positive (as opposed to the default of appending the sign only if the value is negative).\ \pard\tx1170\tx1890\pardeftab397\li1890\fi-990\ri720\sb60\sa60\partightenfactor0 \ls2\ilvl0 \f12 \cf0 \'a5 \f7\i space \f3\i0 : The value is prepended by a space when a sign is not prepended. This is ignored if the \f1\fs18 + \f3\fs20 flag is present, since values are then always prepended by a sign.\ \pard\tx1170\tx1890\pardeftab397\li1890\fi-990\ri720\sb60\sa60\partightenfactor0 \ls2\ilvl0 \f12\fs18 \cf0 \'a5 \f1 # \f3\fs20 : An alternative format is used. For \f1\fs18 %o \f3\fs20 , at least one leading zero is always produced. For \f1\fs18 %x \f3\fs20 and \f1\fs18 %X \f3\fs20 , \f1\fs18 0x \f3\fs20 or \f1\fs18 0X \f3\fs20 (respectively) is prepended if the value is nonzero. For \f1\fs18 %f \f3\fs20 , \f1\fs18 %F \f3\fs20 , \f1\fs18 %e \f3\fs20 , \f1\fs18 %E \f3\fs20 , \f1\fs18 %g \f3\fs20 , and \f1\fs18 %G \f3\fs20 , a decimal point is forced even if no zeros follow.\ \ls2\ilvl0 \f12\fs18 \'a5 \f1 0 \f3\fs20 : Leading zeros are used to pad the field instead of spaces. This flag is ignored if the left-justification flag, \f1\fs18 - \f3\fs20 , is present. It is also ignored for \f1\fs18 integer \f3\fs20 values, if a precision is specified.\ \pard\pardeftab397\li907\fi-187\ri720\sb60\sa60\partightenfactor0 \ls3\ilvl0\cf0 \'96 An optional minimum field width, specified as an integer value. Fields will be padded out to this minimum width. Padding will be done with space characters by default (or with zeros, if the \f1\fs18 0 \f3\fs20 flag is used), on the left by default (or on the right, if the \f1\fs18 - \f3\fs20 flag is used).\ \'96 An optional precision, given as an integer value preceded by a \f1\fs18 . \f3\fs20 character. If no integer value follows the \f1\fs18 . \f3\fs20 character, a precision of zero will be used. For integer values of \f1\fs18 x \f3\fs20 (formatted with \f1\fs18 %d \f3\fs20 , \f1\fs18 %i \f3\fs20 , \f1\fs18 %o \f3\fs20 , \f1\fs18 %x \f3\fs20 , or \f1\fs18 %X \f3\fs20 ) the precision specifies the minimum number of digits that will appear (with extra zeros on the left if necessary), with a default precision of \f1\fs18 1 \f2\fs20 . \f3 For float values of \f1\fs18 x \f3\fs20 formatted with \f1\fs18 %f \f3\fs20 , \f1\fs18 %F \f3\fs20 , \f1\fs18 %e \f3\fs20 , \f1\fs18 %E \f3\fs20 , \f1\fs18 %g \f3\fs20 , or \f1\fs18 %G \f3\fs20 , the precision specifies the minimum number of digits that will appear to the right of the decimal point (with extra zeros on the right if necessary), with a default precision of \f1\fs18 6 \f2\fs20 .\ \ls3\ilvl0 \f3 \'96 A format specifier. For \f1\fs18 integer \f3\fs20 values, this may be \f1\fs18 %d \f3\fs20 or \f1\fs18 %i \f3\fs20 (producing base-10 output; there is no difference between the two), \f1\fs18 %o \f3\fs20 (producing base-8 or octal output), \f1\fs18 %x \f3\fs20 (producing base-16 hexadecimal output using lowercase letters), or \f1\fs18 %X \f3\fs20 (producing base-16 hexadecimal output using uppercase letters). For \f1\fs18 float \f3\fs20 values, this may be \f1\fs18 %f \f3\fs20 or \f1\fs18 %F \f3\fs20 to produce decimal notation (of the form \f1\fs18 [\uc0\u8722 ]ddd.ddd \f3\fs20 ; there is no difference between the two), \f1\fs18 %e \f3\fs20 or \f1\fs18 %E \f3\fs20 to produce scientific notation (of the form \f1\fs18 [\uc0\u8722 ]d.ddde\'b1dd \f3\fs20 or \f1\fs18 [\uc0\u8722 ]d.dddE\'b1dd \f3\fs20 , respectively), or \f1\fs18 %g \f3\fs20 or \f1\fs18 %G \f3\fs20 to produce either decimal notation or scientific notation (using the formatting of \f1\fs18 %f \f3\fs20 / \f1\fs18 %e \f3\fs20 or \f1\fs18 %F \f3\fs20 / \f1\fs18 %E \f3\fs20 , respectively) on a per-value basis, depending upon the range of the value.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 Note that relative to the standard C/C++ \f1\fs18 printf() \f3\fs20 -style behavior, there are a few differences: (1) only a single escape sequence may be present in the format string, (2) the use of \f1\fs18 * \f3\fs20 to defer field width and precision values to a passed parameter is not supported, (3) only \f1\fs18 integer \f3\fs20 and \f1\fs18 float \f3\fs20 values of \f1\fs18 x \f3\fs20 are supported, (4) only the \f1\fs18 %d \f3\fs20 , \f1\fs18 %i \f3\fs20 , \f1\fs18 %o \f3\fs20 , \f1\fs18 %x \f3\fs20 , \f1\fs18 %X \f3\fs20 , \f1\fs18 %f \f3\fs20 , \f1\fs18 %F \f3\fs20 , \f1\fs18 %e \f3\fs20 , \f1\fs18 %E \f3\fs20 , \f1\fs18 %g \f3\fs20 , and \f1\fs18 %G \f3\fs20 format specifiers are supported, and (5) no length modifiers may be supplied, since Eidos does not support different sizes of the \f1\fs18 integer \f3\fs20 and \f1\fs18 float \f3\fs20 types. Note also that the Eidos conventions of emitting \f1\fs18 INF \f3\fs20 and \f1\fs18 NAN \f3\fs20 for infinities and Not-A-Number values respectively is not honored by this function; the strings generated for such values are platform-dependent, following the implementation definition of the C++ compiler used to build Eidos, since \f1\fs18 format() \f3\fs20 calls through to \f1\fs18 snprintf() \f3\fs20 to assemble the final string values.\ For example, \f1\fs18 format("A number: %+7.2f", c(-4.1, 15.375, 8)) \f3\fs20 will produce a vector with three elements: \f1\fs18 "A number: -4.10" "A number: +15.38" "A number: +8.00" \f2\fs20 . \f3 The precision of \f1\fs18 .2 \f3\fs20 results in two digits after the decimal point, the minimum field width of \f1\fs18 7 \f3\fs20 results in padding of the values on the left (with spaces) to a minimum of seven characters, the flag \f1\fs18 + \f3\fs20 causes a sign to be shown on positive values as well as negative values, and the format specifier \f1\fs18 f \f3\fs20 leads to the \f1\fs18 float \f3\fs20 values of \f1\fs18 x \f3\fs20 being formatted in base-10 decimal. One \f1\fs18 string \f3\fs20 value is produced in the result vector for each value in the parameter \f1\fs18 x \f2\fs20 . \f3 These values could then be merged into a single string with \f1\fs18 paste() \f3\fs20 , for example, or printed with \f1\fs18 print() \f3\fs20 or \f1\fs18 cat() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical$)identical(*\'a0x, *\'a0y, ...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a \f1\fs18 logical \f3\fs20 value indicating \f0\b whether two or more values are identical \f3\b0 . For two values \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 , this will return \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 have exactly the same type and size, and all of their corresponding elements are exactly the same, and (for matrices and arrays) their dimensions are identical; otherwise it will return \f1\fs18 F \f3\fs20 . Additional parameters beyond \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 are compared to x in the same manner, and \f1\fs18 T \f3\fs20 is returned only if all of the parameters are identical.\ The test here is for \f7\i exact \f3\i0 equality; an \f1\fs18 integer \f3\fs20 value of \f1\fs18 1 \f3\fs20 is not considered identical to a \f1\fs18 float \f3\fs20 value of \f1\fs18 1.0 \f3\fs20 , for example. Elements in \f1\fs18 object \f3\fs20 values must be literally the same element, not simply identical in all of their properties. Type promotion is never done. For testing whether two values are the same, this is generally preferable to the use of operator\'a0 \f1\fs18 == \f3\fs20 or operator\'a0 \f1\fs18 != \f3\fs20 ; see the discussion at section 2.5.1. Note that \f1\fs18 identical(NULL,NULL) \f3\fs20 and \f1\fs18 identical(NAN, NAN) \f3\fs20 are both \f1\fs18 T \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)ifelse(logical\'a0test, *\'a0trueValues, *\'a0falseValues)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the result of a \f0\b vector conditional \f3\b0 operation: a vector composed of values from \f1\fs18 trueValues \f3\fs20 , for indices where \f1\fs18 test \f3\fs20 is \f1\fs18 T \f3\fs20 , and values from \f1\fs18 falseValues \f3\fs20 , for indices where \f1\fs18 test \f3\fs20 is \f1\fs18 F \f3\fs20 . The lengths of \f1\fs18 trueValues \f3\fs20 and \f1\fs18 falseValues \f3\fs20 must either be equal to \f1\fs18 1 \f3\fs20 or to the length of \f1\fs18 test \f3\fs20 ; however, \f1\fs18 trueValues \f3\fs20 and \f1\fs18 falseValues \f3\fs20 don\'92t need to be the same length as each other. Furthermore, the type of \f1\fs18 trueValues \f3\fs20 and \f1\fs18 falseValues \f3\fs20 must be the same (including, if they are \f1\fs18 object \f3\fs20 type, their element type). The return will be of the same length as \f1\fs18 test \f3\fs20 , and of the same type as \f1\fs18 trueValues \f3\fs20 and \f1\fs18 falseValues \f3\fs20 . Each element of the return vector will be taken from the corresponding element of \f1\fs18 trueValues \f3\fs20 if the corresponding element of \f1\fs18 test \f3\fs20 is \f1\fs18 T \f3\fs20 , or from the corresponding element of \f1\fs18 falseValues \f3\fs20 if the corresponding element of \f1\fs18 test \f3\fs20 is \f1\fs18 F \f3\fs20 ; if the vector from which the value is to be taken (i.e., \f1\fs18 trueValues \f3\fs20 or \f1\fs18 falseValues \f3\fs20 ) has a length of \f1\fs18 1 \f3\fs20 , that single value is used repeatedly, recycling the vector.\cf2 \expnd0\expndtw0\kerning0 If \f1\fs18 test \f3\fs20 , \f1\fs18 trueValues \f3\fs20 , and/or \f1\fs18 falseValues \f3\fs20 are matrices or arrays, that will be ignored by \f1\fs18 ifelse() \f3\fs20 \f7\i except \f3\i0 that the result will be of the same dimensionality as \f1\fs18 test \f3\fs20 .\cf0 \kerning1\expnd0\expndtw0 \ This is quite similar to a function in R of the same name; note, however, that Eidos evaluates all arguments to functions calls immediately, so \f1\fs18 trueValues \f3\fs20 and \f1\fs18 falseValues \f3\fs20 will be evaluated fully regardless of the values in \f1\fs18 test \f3\fs20 , unlike in R. Value expressions without side effects are therefore recommended. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical)isClose(float\'a0x, float\'a0y, [float$\'a0rtol\'a0=\'a01.0e-05], [float$\'a0atol\'a0=\'a01.0e-08], [logical$\'a0equalNAN\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a logical vector indicating \f0\b whether each pair of values in \f9\fs18 x \f0\fs20 and \f9\fs18 y \f0\fs20 are \'93close\'94 \f3\b0 ; if any pair of values is not close, returns \f1\fs18 F \f3\fs20 . See the \f1\fs18 allClose() \f3\fs20 function for an efficient way to test whether \f7\i all \f3\i0 pairs of values in \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 are close.\ A pair of values \f1\fs18 a \f3\fs20 and \f1\fs18 b \f3\fs20 is considered \'93close\'94 according to the following criteria. If both \f1\fs18 a \f3\fs20 and \f1\fs18 b \f3\fs20 are finite, they are close if \f1\fs18 abs(a \uc0\u8722 b) <= (atol + rtol * abs(b)) \f3\fs20 . If both \f1\fs18 a \f3\fs20 and \f1\fs18 b \f3\fs20 are infinite, they are close if they have the same sign. If both \f1\fs18 a \f3\fs20 and \f1\fs18 b \f3\fs20 are \f1\fs18 NAN \f3\fs20 , they are close if \f1\fs18 equalNAN \f3\fs20 is \f1\fs18 T \f3\fs20 . In all other cases, \f1\fs18 a \f3\fs20 and \f1\fs18 b \f3\fs20 are not close. For finite values, \f1\fs18 rtol \f3\fs20 thus defines a relative tolerance, and \f1\fs18 atol \f3\fs20 an absolute tolerance; the relative difference \f1\fs18 rtol * abs(b) \f3\fs20 and the absolute difference \f1\fs18 atol \f3\fs20 are added together and compared against the absolute difference between \f1\fs18 a \f3\fs20 and \f1\fs18 b \f3\fs20 to determine closeness.\ Note that the default value for \f1\fs18 atol \f3\fs20 is not appropriate when comparing numbers with magnitudes much smaller than one; be sure to select \f1\fs18 atol \f3\fs20 for the use case at hand, especially for defining the threshold below which a non-zero value \f1\fs18 a \f3\fs20 will be considered \'93close\'94 to a very small or zero value \f1\fs18 b \f3\fs20 . Note also that \f1\fs18 isClose() \f3\fs20 is not symmetric in \f1\fs18 a \f3\fs20 and \f1\fs18 b \f3\fs20 ; it assumes that b is the reference value for calculating the relative difference.\ Regarding how values in \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 are paired, three cases are supported. If \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 are the same length, then \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 are paired element-wise; if \f1\fs18 x \f3\fs20 is singleton, the single \f1\fs18 x \f3\fs20 value is paired with each value in \f1\fs18 y \f3\fs20 ; or if \f1\fs18 y \f3\fs20 is singleton, each value in \f1\fs18 x \f3\fs20 is paired with the single \f1\fs18 y \f3\fs20 value.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (integer$)length(*\'a0x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b size \f3\b0 (e.g., length) of \f1\fs18 x \f3\fs20 : the number of elements contained in \f1\fs18 x \f3\fs20 . Note that \f1\fs18 length() \f3\fs20 is a synonym for \f1\fs18 size() \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (integer)match(*\'a0x, *\'a0table)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a vector of the \f0\b positions of (first) matches \f3\b0 of \f1\fs18 x \f3\fs20 in \f1\fs18 table \f3\fs20 . Type promotion is not performed; x and \f1\fs18 table \f3\fs20 must be of the same type. For each element of \f1\fs18 x \f3\fs20 , the corresponding element in the result will give the position of the first match for that element of \f1\fs18 x \f3\fs20 in \f1\fs18 table \f3\fs20 ; if the element has no match in \f1\fs18 table \f3\fs20 , the element in the result vector will be \f1\fs18 -1 \f3\fs20 . The result is therefore a vector of the same length as \f1\fs18 x \f3\fs20 . If a \f1\fs18 logical \f3\fs20 result is desired, with \f1\fs18 T \f3\fs20 indicating that a match was found for the corresponding element of \f1\fs18 x \f3\fs20 , use \f1\fs18 (match(x, table) >= 0) \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)order(+\'a0x, [logical$ \f2 \'a0 \f1 ascending \f2 \'a0 \f1 =\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b vector of sorting indices \f3\b0 for \f1\fs18 x \f3\fs20 : a new \f1\fs18 integer \f3\fs20 vector of the same length as \f1\fs18 x \f3\fs20 , containing the indices into \f1\fs18 x \f3\fs20 that would sort \f1\fs18 x \f3\fs20 . In other words, \f1\fs18 x[order(x)]==sort(x) \f3\fs20 . This can be useful for more complex sorting problems, such as sorting several vectors in parallel by a sort order determined by one of the vectors. If the optional \f1\fs18 logical \f3\fs20 parameter \f1\fs18 ascending \f3\fs20 is \f1\fs18 T \f3\fs20 (the default), then the sorted order will be ascending; if it is \f1\fs18 F \f3\fs20 , the sorted order will be descending. The ordering is determined according to the same logic as the \f1\fs18 < \f3\fs20 and \f1\fs18 > \f3\fs20 operators in Eidos. To easily sort vectors in a single step, use \f1\fs18 sort() \f3\fs20 or \f1\fs18 sortBy() \f3\fs20 , for non- \f1\fs18 object \f3\fs20 and \f1\fs18 object \f3\fs20 vectors respectively. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string$)paste(..., [string$\'a0sep\'a0=\'a0" "])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a \f0\b joined string \f3\b0 composed from the \f1\fs18 string \f3\fs20 representations of the elements of the parameters passed in, taken in order, joined together by \f1\fs18 sep \f3\fs20 . Although this function is based upon the R \f1\fs18 paste() \f3\fs20 function of the same name, note that it is much simpler and less powerful; in particular, the result is always a singleton \f1\fs18 string \f3\fs20 , rather than returning a non-singleton \f1\fs18 string \f3\fs20 vector when one of the parameters is a non-singleton. The string representation used by \f1\fs18 paste() \f3\fs20 is the same as that emitted by \f1\fs18 cat() \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string$)paste0(...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a \f0\b joined string \f3\b0 composed from the \f1\fs18 string \f3\fs20 representations of the elements of the parameters passed in, taken in order, joined together with no separator. This function is identical to \f1\fs18 paste() \f3\fs20 , except that no separator is used. Note that this differs from the semantics of \f1\fs18 paste0() \f3\fs20 in R.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)print(* \f2 \'a0 \f1 x\cf2 , [logical$\'a0error\'a0=\'a0F]\cf0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Prints output \f3\b0 to Eidos\'92s output stream. The value \f1\fs18 x \f3\fs20 that is output may be of any type. A newline is appended to the output. See \f1\fs18 cat() \f3\fs20 for a discussion of the differences between \f1\fs18 print() \f3\fs20 and \f1\fs18 cat() \f2\fs20 .\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 By default (when \f1\fs18 error \f3\fs20 is \f1\fs18 F \f3\fs20 ), the output is sent to the standard Eidos output stream. When running at the command line, this sends it to \f1\fs18 stdout \f3\fs20 ; when running in SLiMgui, this sends it to the simulation window\'92s output textview. If \f1\fs18 error \f3\fs20 is \f1\fs18 T \f3\fs20 , the output is instead sent to the Eidos error stream. When running at the command line, this sends it to \f1\fs18 stderr \f3\fs20 ; when running in SLiMgui, the output is routed to the simulation\'92s debugging output window.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)rev(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b reverse \f3\b0 of \f1\fs18 x \f3\fs20 : a new vector with the same elements as \f1\fs18 x \f3\fs20 , but in the opposite order. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer$)size(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b size \f3\b0 of \f1\fs18 x \f3\fs20 : the number of elements contained in \f1\fs18 x \f2\fs20 . \f3 \cf2 \expnd0\expndtw0\kerning0 Note that \f1\fs18 length() \f3\fs20 is a synonym for \f1\fs18 size() \f3\fs20 . \f2 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (+)sort(+\'a0x, [logical$ \f2 \'a0 \f1 ascending \f2 \'a0 \f1 =\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b sorted copy \f3\b0 of \f1\fs18 x \f3\fs20 : a new vector with the same elements as \f1\fs18 x \f3\fs20 , but in sorted order. If the optional \f1\fs18 logical \f3\fs20 parameter \f1\fs18 ascending \f3\fs20 is \f1\fs18 T \f3\fs20 (the default), then the sorted order will be ascending; if it is \f1\fs18 F \f3\fs20 , the sorted order will be descending. The ordering is determined according to the same logic as the \f1\fs18 < \f3\fs20 and \f1\fs18 > \f3\fs20 operators in Eidos. To sort an \f1\fs18 object \f3\fs20 vector, use \f1\fs18 sortBy() \f2\fs20 . \f3 To obtain indices for sorting, use \f1\fs18 order() \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (object)sortBy(object\'a0x, string$ \f2 \'a0 \f1 property, [logical$ \f2 \'a0 \f1 ascending \f2 \'a0 \f1 =\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b sorted copy \f3\b0 of \f1\fs18 x \f3\fs20 : a new vector with the same elements as \f1\fs18 x \f3\fs20 , but in sorted order. If the optional \f1\fs18 logical \f3\fs20 parameter \f1\fs18 ascending \f3\fs20 is \f1\fs18 T \f3\fs20 (the default), then the sorted order will be ascending; if it is \f1\fs18 F \f3\fs20 , the sorted order will be descending. The ordering is determined according to the same logic as the \f1\fs18 < \f3\fs20 and \f1\fs18 > \f3\fs20 operators in Eidos. The \f1\fs18 property \f3\fs20 argument gives the name of the property within the elements of \f1\fs18 x \f3\fs20 according to which sorting should be done. This must be a simple property name; it cannot be a property path. For example, to sort a \f1\fs18 Mutation \f3\fs20 vector by the selection coefficients of the mutations, you would simply pass \f1\fs18 "selectionCoeff" \f3\fs20 , including the quotes, for \f1\fs18 property \f2\fs20 . \f3 To sort a non- \f1\fs18 object \f3\fs20 vector, use \f1\fs18 sort() \f3\fs20 . To obtain indices for sorting, use \f1\fs18 order() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)str(* \f2 \'a0 \f1 x\cf2 , [logical$\'a0error\'a0=\'a0F]\cf0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Prints the structure \f3\b0 of \f1\fs18 x \f3\fs20 : a summary of its type and the values it contains. If \f1\fs18 x \f3\fs20 is an \f1\fs18 object \f2\fs20 , \f3 note that \f1\fs18 str() \f3\fs20 produces different results from the \f1\fs18 str() \f3\fs20 method of \f1\fs18 x \f3\fs20 ; the \f1\fs18 str() \f3\fs20 function prints the external structure of \f1\fs18 x \f3\fs20 (the fact that it is an object, and the number and type of its elements), whereas the \f1\fs18 str() \f3\fs20 method prints the internal structure of \f1\fs18 x \f3\fs20 (the external structure of all the properties contained by \f1\fs18 x \f3\fs20 ). \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 By default (when \f1\fs18 error \f3\fs20 is \f1\fs18 F \f3\fs20 ), the output is sent to the standard Eidos output stream. When running at the command line, this sends it to \f1\fs18 stdout \f3\fs20 ; when running in SLiMgui, this sends it to the simulation window\'92s output textview. If \f1\fs18 error \f3\fs20 is \f1\fs18 T \f3\fs20 , the output is instead sent to the Eidos error stream. When running at the command line, this sends it to \f1\fs18 stderr \f3\fs20 ; when running in SLiMgui, the output is routed to the simulation\'92s debugging output window.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)tabulate(integer\'a0bin, [Ni$\'a0maxbin\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns \f0\b occurrence counts \f3\b0 for each non-negative integer in \f1\fs18 bin \f3\fs20 . Occurrence counts are tabulated into bins for each value \f1\fs18 0:maxbin \f3\fs20 in \f1\fs18 bin \f3\fs20 ; values outside that range are ignored. The default value of \f1\fs18 maxbin \f3\fs20 , \f1\fs18 NULL \f3\fs20 , is equivalent to passing \f1\fs18 maxbin=max(0, bin) \f3\fs20 ; in other words, by default the result vector will be exactly large enough to accommodate counts for every integer in \f1\fs18 bin \f3\fs20 . In any case, the result vector will contain \f1\fs18 maxbin+1 \f3\fs20 elements (some or all of which might be zero, if the occurrence count of that integer in \f1\fs18 bin \f3\fs20 is zero).\ Note that the semantics of this function differ slightly from the \f1\fs18 tabulate() \f3\fs20 function in R, because R is \f1\fs18 1 \f3\fs20 -based and Eidos is \f1\fs18 0 \f3\fs20 -based.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)unique(*\'a0x, [logical$\'a0preserveOrder\'a0=\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b unique values \f3\b0 in \f1\fs18 x \f3\fs20 . In other words, for each value \f1\fs18 k \f3\fs20 in \f1\fs18 x \f3\fs20 that occurs at least once, the vector returned will contain \f1\fs18 k \f3\fs20 exactly once. If \f1\fs18 preserveOrder \f3\fs20 is \f1\fs18 T \f3\fs20 (the default), the order of values in \f1\fs18 x \f3\fs20 is preserved, taking the first instance of each value; this is relatively slow, with O( \f7\i n \f3\i0 ^2) performance. If \f1\fs18 preserveOrder \f3\fs20 if \f1\fs18 F \f3\fs20 instead, the order of values in \f1\fs18 x \f3\fs20 is not preserved, and no particular ordering should be relied upon; this is relatively fast, with O( \f7\i n \f3\i0 log \f7\i n \f3\i0 ) performance. This performance difference will only matter for large vectors, however; for most applications the default behavior can be retained whether the order of the result matters or not. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)which(logical \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b indices of \f9\fs18 T \f0\fs20 values \f3\b0 in \f1\fs18 x \f3\fs20 . In other words, if an index \f1\fs18 k \f3\fs20 in \f1\fs18 x \f3\fs20 is \f1\fs18 T \f3\fs20 , then the vector returned will contain \f1\fs18 k \f3\fs20 ; if index \f1\fs18 k \f3\fs20 in \f1\fs18 x \f3\fs20 is \f1\fs18 F \f3\fs20 , the vector returned will omit \f1\fs18 k \f3\fs20 . One way to look at this is that it converts from a \f1\fs18 logical \f3\fs20 subsetting vector to an \f1\fs18 integer \f3\fs20 (index-based) subsetting vector, without changing which subset positions would be selected. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer$)whichMax(+ \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b index of the (first) maximum value \f3\b0 in \f1\fs18 x \f3\fs20 . In other words, if \f1\fs18 k \f3\fs20 is equal to the maximum value in \f1\fs18 x \f3\fs20 , then the vector returned will contain the index of the first occurrence of \f1\fs18 k \f3\fs20 in \f1\fs18 x \f2\fs20 . \f3 If the maximum value is unique, the result is the same as (but more efficient than) the expression \f1\fs18 which(x==max(x)) \f3\fs20 , which returns the indices of \f7\i all \f3\i0 of the occurrences of the maximum value in \f1\fs18 x \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer$)whichMin(+ \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b index of the (first) minimum value \f3\b0 in \f1\fs18 x \f3\fs20 . In other words, if \f1\fs18 k \f3\fs20 is equal to the minimum value in \f1\fs18 x \f3\fs20 , then the vector returned will contain the index of the first occurrence of \f1\fs18 k \f3\fs20 in \f1\fs18 x \f3\fs20 . If the minimum value is unique, the result is the same as (but more efficient than) the expression \f1\fs18 which(x==min(x)) \f3\fs20 , which returns the indices of \f7\i all \f3\i0 of the occurrences of the minimum value in \f1\fs18 x \f2\fs20 .\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.6. Value type testing and coercion functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf0 (float)asFloat(+ \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b conversion to \f9\fs18 float \f3\b0\fs20 of \f1\fs18 x \f3\fs20 . If \f1\fs18 x \f3\fs20 is \f1\fs18 string \f3\fs20 and cannot be converted to \f1\fs18 float \f3\fs20 , Eidos will throw an error. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)asInteger(+ \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b conversion to \f9\fs18 integer \f3\b0\fs20 of \f1\fs18 x \f3\fs20 . If \f1\fs18 x \f3\fs20 is of type \f1\fs18 string \f3\fs20 or \f1\fs18 float \f3\fs20 and cannot be converted to \f1\fs18 integer \f3\fs20 , Eidos will throw an error. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical)asLogical(+ \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b conversion to \f9\fs18 logical \f3\b0\fs20 of \f1\fs18 x \f3\fs20 . Recall that in Eidos the empty \f1\fs18 string \f3\fs20 \f1\fs18 "" \f3\fs20 is considered \f1\fs18 F \f3\fs20 , and all other \f1\fs18 string \f3\fs20 values are considered \f1\fs18 T \f2\fs20 . \f3 Converting \f1\fs18 INF \f3\fs20 or \f1\fs18 -INF \f3\fs20 to \f1\fs18 logical \f3\fs20 yields \f1\fs18 T \f3\fs20 (since those values are not equal to zero); converting \f1\fs18 NAN \f3\fs20 to \f1\fs18 logical \f3\fs20 throws an error. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string)asString(+ \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b conversion to \f9\fs18 string \f3\b0\fs20 of \f1\fs18 x \f2\fs20 . \f3 \cf2 \expnd0\expndtw0\kerning0 Note that \f1\fs18 asString(NULL) \f3\fs20 returns \f1\fs18 "NULL" \f3\fs20 even though \f1\fs18 NULL \f3\fs20 is zero-length. \f2 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string$)elementType(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b element type \f3\b0 of \f1\fs18 x \f3\fs20 , as a \f1\fs18 string \f3\fs20 . For the non- \f1\fs18 object \f3\fs20 types, the element type is the same as the type: \f1\fs18 "NULL" \f3\fs20 , \f1\fs18 "logical" \f3\fs20 , \f1\fs18 "integer" \f3\fs20 , \f1\fs18 "float" \f3\fs20 , or \f1\fs18 "string" \f3\fs20 . For \f1\fs18 object \f3\fs20 type, however, \f1\fs18 elementType() \f3\fs20 returns the name of the type of element contained by the object, such as \f1\fs18 "Species" \f3\fs20 or \f1\fs18 "Mutation" \f3\fs20 in the Context of SLiM. Contrast this with \f1\fs18 type() \f2\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)isFloat(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 \f0\b is \f9\fs18 float \f0\fs20 type \f3\b0 , \f1\fs18 F \f3\fs20 otherwise. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)isInteger(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 \f0\b is \f9\fs18 integer \f0\fs20 type \f3\b0 , \f1\fs18 F \f3\fs20 otherwise. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)isLogical(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 \f0\b is \f9\fs18 logical \f0\fs20 type \f3\b0 , \f1\fs18 F \f3\fs20 otherwise. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)isNULL(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 \f0\b is \f9\fs18 NULL \f0\fs20 type \f3\b0 , \f1\fs18 F \f3\fs20 otherwise. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)isObject(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 \f0\b is \f9\fs18 object \f0\fs20 type \f3\b0 , \f1\fs18 F \f3\fs20 otherwise. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)isString(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns \f1\fs18 T \f3\fs20 if \f1\fs18 x \f3\fs20 \f0\b is \f9\fs18 string \f0\fs20 type \f3\b0 , \f1\fs18 F \f3\fs20 otherwise.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string$)type(* \f2 \'a0 \f1 x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b type \f3\b0 of \f1\fs18 x \f3\fs20 , as a \f1\fs18 string \f3\fs20 : \f1\fs18 "NULL" \f3\fs20 , \f1\fs18 "logical" \f3\fs20 , \f1\fs18 "integer" \f3\fs20 , \f1\fs18 "float" \f3\fs20 , \f1\fs18 "string" \f3\fs20 , or \f1\fs18 "object" \f3\fs20 . Contrast this with \f1\fs18 elementType() \f2\fs20 .\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.7. String manipulation functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf2 (lis)grep(string$\'a0pattern, string\'a0x, [logical$\'a0ignoreCase\'a0=\'a0F], [string$\'a0grammar\'a0=\'a0"ECMAScript"], [string$\'a0value\'a0=\'a0"indices"], [logical$\'a0fixed\'a0=\'a0F], [logical$\'a0invert\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Searches for \f0\b regular expression matches \f3\b0 in the string-elements of \f1\fs18 x \f3\fs20 . Regular expressions (regexes) express patterns that strings can either match or not match; they are very widely used in programming languages and terminal shells. The topic of regexes is very complex, and a great deal of information about them can be found online, including examples and tutorials; this manual will not attempt to document the topic in detail.\ The \f1\fs18 grep() \f3\fs20 function uses a regex supplied in \f1\fs18 pattern \f3\fs20 , looking for matches for the regex in each element of \f1\fs18 x \f3\fs20 . If \f1\fs18 ignoreCase \f3\fs20 is \f1\fs18 F \f3\fs20 (the default), the pattern matching will be case sensitive (i.e., uppercase versus lowercase will matter); if it is T, the pattern matching will be case-insensitive.\ The \f1\fs18 grammar \f3\fs20 parameter determines the regex grammar used to find matches. Several options are available. The default, \f1\fs18 "ECMAScript" \f3\fs20 , is a straightforward regex grammar, the specification for which can be found at {\field{\*\fldinst{HYPERLINK "https://www.cplusplus.com/reference/regex/ECMAScript/"}}{\fldrslt \cf3 \ul \ulc3 https://www.cplusplus.com/reference/regex/ECMAScript/}} among many other links. The \f1\fs18 "basic" \f3\fs20 grammar uses POSIX basic regular expressions, often called BRE; this is documented at {\field{\*\fldinst{HYPERLINK "https://en.wikibooks.org/wiki/Regular_Expressions/POSIX_Basic_Regular_Expressions"}}{\fldrslt \cf3 \ul \ulc3 https://en.wikibooks.org/wiki/Regular_Expressions/POSIX_Basic_Regular_Expressions}}. The \f1\fs18 "extended" \f3\fs20 grammar uses POSIX extended regular expressions, often called ERE; this is documented at {\field{\*\fldinst{HYPERLINK "https://en.wikibooks.org/wiki/Regular_Expressions/POSIX-Extended_Regular_Expressions"}}{\fldrslt \cf3 \ul \ulc3 https://en.wikibooks.org/wiki/Regular_Expressions/POSIX-Extended_Regular_Expressions}}. The \f1\fs18 "awk" \f3\fs20 grammar is based upon the \f1\fs18 "extended" \f3\fs20 grammar, with more escapes for non-printing characters. The \f1\fs18 "grep" \f3\fs20 and \f1\fs18 "egrep" \f3\fs20 grammars are based upon the \f1\fs18 "basic" \f3\fs20 and \f1\fs18 "extended" \f3\fs20 grammars, respectively, but also allow newline characters ( \f1\fs18 "\\n" \f3\fs20 ) to separate alternations. If you are not sure which grammar you want to use, \f1\fs18 "ECMAScript" \f3\fs20 is recommended. All of these grammars are implemented internally in Eidos using the C++ \f1\fs18 \f3\fs20 library, so if you need clarification on the details of a grammar, you can search for related C++ materials online.\ Information about the matches found is returned in one of four ways. If \f1\fs18 value \f3\fs20 is \f1\fs18 "indices" \f3\fs20 (the default), an \f1\fs18 integer \f3\fs20 vector is returned containing the index in \f1\fs18 x \f3\fs20 for each match. If \f1\fs18 value \f3\fs20 is \f1\fs18 "elements" \f3\fs20 , a \f1\fs18 string \f3\fs20 vector is returned containing the actual string-elements of \f1\fs18 x \f3\fs20 for each match. If \f1\fs18 value \f3\fs20 is \f1\fs18 "matches" \f3\fs20 , a \f1\fs18 string \f3\fs20 vector is returned containing only the substring that matched, within each string-element in \f1\fs18 x \f3\fs20 that matched (if more than one substring in a given element matched, the \f7\i first \f3\i0 match is returned). Finally, if \f1\fs18 value \f3\fs20 is \f1\fs18 "logical" \f3\fs20 a \f1\fs18 logical \f3\fs20 vector is returned, of the same length as \f1\fs18 x \f3\fs20 , containing \f1\fs18 T \f3\fs20 where the corresponding element of \f1\fs18 x \f3\fs20 matched, or \f1\fs18 F \f3\fs20 where it did not match. This function therefore encapsulates the functionality of both the \f1\fs18 grep() \f3\fs20 and \f1\fs18 grepl() \f3\fs20 functions of R; use \f1\fs18 value="logical" \f3\fs20 for functionality like that of R\'92s \f1\fs18 grepl() \f3\fs20 .\ If \f1\fs18 fixed \f3\fs20 is \f1\fs18 F \f3\fs20 (the default), matching is determined using \f1\fs18 pattern \f3\fs20 following the specified regex grammar as described above. If \f1\fs18 fixed \f3\fs20 is \f1\fs18 T \f3\fs20 , matching is instead determined using \f1\fs18 pattern \f3\fs20 as a \f1\fs18 string \f3\fs20 value to be matched \'93as is\'94, rather than as a regular expression; the \f1\fs18 grammar \f3\fs20 specified does not matter in this case, but \f1\fs18 ignoreCase \f3\fs20 still applies. This could be thought of as another \f1\fs18 grammar \f3\fs20 value, really, meaning \'93no grammar\'94, but it is supplied as a separate flag following R.\ Finally, if \f1\fs18 invert \f3\fs20 if \f1\fs18 F \f3\fs20 (the default) matching proceeds as normal for the chosen regex grammar, whereas if \f1\fs18 invert \f3\fs20 if \f1\fs18 T \f3\fs20 matching is inverted: indices, elements, or \f1\fs18 logical \f3\fs20 values are returned for the elements of \f1\fs18 x \f3\fs20 that did \f7\i not \f3\i0 match. If \f1\fs18 invert \f3\fs20 is \f1\fs18 T \f3\fs20 , the \f1\fs18 value \f3\fs20 parameter may not be \f1\fs18 "matches" \f3\fs20 .\ Note that there is not presently any way to extract subpattern matches, nor is there any way to perform replacements of matches.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)nchar(string\'a0x)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns a vector of the \f0\b number of characters \f3\b0 in the string-elements of \f1\fs18 x \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical)strcontains(string\'a0x, string$\'a0s, [integer$\'a0pos\'a0=\'a00])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b occurrence of a string \f3\b0 specified by \f1\fs18 s \f3\fs20 in each of the elements of \f1\fs18 x \f3\fs20 , starting at position \f1\fs18 pos \f3\fs20 . Position \f1\fs18 0 \f3\fs20 , the default, is the beginning of \f1\fs18 x \f3\fs20 ; a position of \f1\fs18 0 \f3\fs20 means the entire string is searched. A starting search position that is at or beyond the end of a given element of \f1\fs18 x \f3\fs20 is not an error; it just implies that a match will not be found in that element. The existences of matches are returned as a \f1\fs18 logical \f3\fs20 vector; if a match was found in a given element, the corresponding value in the returned vector is \f1\fs18 T \f3\fs20 , otherwise it is \f1\fs18 F \f3\fs20 . This function is a simplified version of \f1\fs18 strfind() \f3\fs20 , which returns the positions of matches. The \f1\fs18 strprefix() \f3\fs20 and \f1\fs18 strsuffix() \f3\fs20 functions are also related.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)strfind(string\'a0x, string$\'a0s, [integer$\'a0pos\'a0=\'a00])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b first occurrence of a string \f3\b0 specified by \f1\fs18 s \f3\fs20 in each of the elements of \f1\fs18 x \f3\fs20 , starting at position \f1\fs18 pos \f3\fs20 . Position \f1\fs18 0 \f3\fs20 , the default, is the beginning of \f1\fs18 x \f3\fs20 ; a position of \f1\fs18 0 \f3\fs20 means the entire string is searched. A starting search position that is at or beyond the end of a given element of \f1\fs18 x \f3\fs20 is not an error; it just implies that a match will not be found in that element. The positions of matches are returned as an \f1\fs18 integer \f3\fs20 vector; if no match was found in a given element, the corresponding value in the returned vector is \f1\fs18 -1 \f3\fs20 . The \f1\fs18 strcontains() \f3\fs20 function may be used when a logical value (found / not found) is desired.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical)strprefix(string\'a0x, string$\'a0s)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b occurrence of a prefix string \f3\b0 specified by \f1\fs18 s \f3\fs20 at the beginning of each of the elements of \f1\fs18 x \f3\fs20 . The existences of prefixes are returned as a \f1\fs18 logical \f3\fs20 vector; if a given element begins with the prefix, the corresponding value in the returned vector is \f1\fs18 T \f3\fs20 , otherwise it is \f1\fs18 F \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string)strsplit(string$\'a0x, [string$\'a0sep\'a0=\'a0" "])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns \f0\b substrings \f3\b0 of \f1\fs18 x \f3\fs20 that were separated by the separator string \f1\fs18 sep \f3\fs20 . Every substring defined by an occurrence of the separator is included, and thus zero-length substrings may be returned. For example, \f1\fs18 strsplit(".foo..bar.", ".") \f3\fs20 returns a string vector containing \f1\fs18 "" \f3\fs20 , \f1\fs18 "foo" \f3\fs20 , \f1\fs18 "" \f3\fs20 , \f1\fs18 "bar" \f3\fs20 , \f1\fs18 "" \f3\fs20 . In that example, the empty string between \f1\fs18 "foo" \f3\fs20 and \f1\fs18 "bar" \f3\fs20 in the returned vector is present because there were two periods between \f1\fs18 foo \f3\fs20 and \f1\fs18 bar \f3\fs20 in the input string \'96 the empty string is the substring between those two separators. If \f1\fs18 sep \f3\fs20 is \f1\fs18 "" \f3\fs20 , a vector of single characters will be returned, resulting from splitting \f1\fs18 x \f3\fs20 at every position. Note that \f1\fs18 paste() \f3\fs20 performs the inverse operation of \f1\fs18 strsplit() \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical)strsuffix(string\'a0x, string$\'a0s)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b occurrence of a suffix string \f3\b0 specified by \f1\fs18 s \f3\fs20 at the end of each of the elements of \f1\fs18 x \f3\fs20 . The existences of suffixes are returned as a \f1\fs18 logical \f3\fs20 vector; if a given element ends with the suffix, the corresponding value in the returned vector is \f1\fs18 T \f3\fs20 , otherwise it is \f1\fs18 F \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string)substr(string\'a0x, integer\'a0first, [Ni\'a0last\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns \f0\b substrings \f3\b0 extracted from the elements of \f1\fs18 x, \f3\fs20 spanning character position \f1\fs18 first \f3\fs20 to character position \f1\fs18 last \f3\fs20 (inclusive). Character positions are numbered from \f1\fs18 0 \f3\fs20 to \f1\fs18 nchar(x)-1 \f3\fs20 . Positions that fall outside of that range are legal; a substring range that encompasses no characters will produce an empty string. If \f1\fs18 first \f3\fs20 is greater than \f1\fs18 last \f3\fs20 , an empty string will also result. If \f1\fs18 last \f3\fs20 is NULL (the default), then the substring will extend to the end of the string. The parameters \f1\fs18 first \f3\fs20 and \f1\fs18 last \f3\fs20 may either be singletons, specifying a single value to be used for all of the substrings, or they may be vectors of the same length as \f1\fs18 x \f3\fs20 , specifying a value for each substring.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.8. Matrix and array functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf0 (*)apply(*\'a0x, integer\'a0margin, string$\'a0lambdaSource)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f7\i\fs20 \cf4 \expnd0\expndtw0\kerning0 Prior to Eidos 1.6 / SLiM 2.6, \f5\fs18 sapply() \f7\fs20 was named \f5\fs18 apply() \f7\fs20 , and this function did not yet exist\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\i0\b \cf2 Applies a block of Eidos code to margins of x \f3\b0 . This function is essentially an extension of \f1\fs18 sapply() \f3\fs20 for use with matrices and arrays; it is recommended that you fully understand \f1\fs18 sapply() \f3\fs20 before tackling this function. As with \f1\fs18 sapply() \f3\fs20 , the lambda specified by \f1\fs18 lambdaSource \f3\fs20 will be executed for subsets of \f1\fs18 x \f3\fs20 , and the results will be concatenated together with type-promotion in the style of \f1\fs18 c() \f3\fs20 to produce a result. Unlike \f1\fs18 sapply() \f3\fs20 , however, the subsets of \f1\fs18 x \f3\fs20 used might be rows, columns, or higher-dimensional slices of \f1\fs18 x \f3\fs20 , rather than just single elements, depending upon the value of \f1\fs18 margin \f3\fs20 . For \f1\fs18 apply() \f3\fs20 , \f1\fs18 x \f3\fs20 must be a matrix or array. The \f1\fs18 apply() \f3\fs20 function in Eidos is patterned directly after the \f1\fs18 apply() \f3\fs20 function in R, and should behave identically, except that dimension indices in Eidos are zero-based whereas in R they are one-based.\ The \f1\fs18 margin \f3\fs20 parameter gives the indices of dimensions of \f1\fs18 x \f3\fs20 that will be iterated over when assembling values to supply to lambdaSource. If \f1\fs18 x \f3\fs20 is a matrix it has two dimensions: rows, of dimension index \f1\fs18 0 \f3\fs20 , and columns, of dimension index \f1\fs18 1 \f3\fs20 . These are the indices of the dimension sizes returned by \f1\fs18 dim() \f3\fs20 ; \f1\fs18 dim(x)[0] \f3\fs20 gives the number of rows of \f1\fs18 x \f3\fs20 , and \f1\fs18 dim(x)[1] \f3\fs20 gives the number of columns. These dimension indices are also apparent when subsetting \f1\fs18 x \f3\fs20 ; a subset index in position \f1\fs18 0 \f3\fs20 , such as \f1\fs18 x[m,] \f3\fs20 , gives row \f1\fs18 m \f3\fs20 of \f1\fs18 x \f3\fs20 , whereas a subset index in position \f1\fs18 1 \f3\fs20 , such as \f1\fs18 x[,n] \f3\fs20 , gives column \f1\fs18 n \f3\fs20 of \f1\fs18 x \f3\fs20 . In the same manner, supplying \f1\fs18 0 \f3\fs20 for \f1\fs18 margin \f3\fs20 specifies that subsets of \f1\fs18 x \f3\fs20 from \f1\fs18 x[0,] \f3\fs20 to \f1\fs18 x[m,] \f3\fs20 should be \'93passed\'94 to \f1\fs18 lambdaSource \f3\fs20 , through the \f1\fs18 applyValue \f3\fs20 \'93parameter\'94; dimension \f1\fs18 0 \f3\fs20 is iterated over, whereas dimension \f1\fs18 1 \f3\fs20 is taken in aggregate since it is not included in \f1\fs18 margin \f3\fs20 . The final effect of this is that whole rows of \f1\fs18 x \f3\fs20 are passed to \f1\fs18 lambdaSource \f3\fs20 through \f1\fs18 applyValue \f3\fs20 . Similarly, \f1\fs18 margin=1 \f3\fs20 would specify that subsets of \f1\fs18 x \f3\fs20 from \f1\fs18 x[,0] \f3\fs20 to \f1\fs18 x[,n] \f3\fs20 should be passed to \f1\fs18 lambdaSource \f3\fs20 , resulting in whole columns being passed. Specifying \f1\fs18 margin=c(0,1) \f3\fs20 would indicate that dimensions \f1\fs18 0 \f3\fs20 and \f1\fs18 1 \f3\fs20 should both be iterated over (dimension \f1\fs18 0 \f3\fs20 more rapidly), so for a matrix each each individual value of \f1\fs18 x \f3\fs20 would be passed to l \f1\fs18 ambdaSource \f3\fs20 . Specifying \f1\fs18 margin=c(1,0) \f3\fs20 would similarly iterate over both dimensions, but dimension \f1\fs18 1 \f3\fs20 more rapidly; the traversal order would therefore be different, and the dimensionality of the result would also differ (see below). For higher-dimensional arrays dimension indices beyond \f1\fs18 1 \f3\fs20 exist, and so \f1\fs18 margin=c(0,1) \f3\fs20 or \f1\fs18 margin=c(1,0) \f3\fs20 would provide slices of \f1\fs18 x \f3\fs20 to \f1\fs18 lambdaSource \f3\fs20 , each slice having a specific row and column index. Slices are generated by subsetting in the same way as operator \f1\fs18 [] \f3\fs20 , but additionally, redundant dimensions are dropped as by \f1\fs18 drop() \f3\fs20 .\ The return value from \f1\fs18 apply() \f3\fs20 is built up from the type-promoted concatenated results, as if by the \f1\fs18 c() \f3\fs20 function, from the iterated execution of \f1\fs18 lambdaSource \f3\fs20 ; the only question is what dimensional structure is imposed upon that vector of values. If the results from \f1\fs18 lambdaSource \f3\fs20 are not of a consistent length, or are of length zero, then the concatenated results are returned as a plain vector. If all results are of length \f1\fs18 n > 1 \f3\fs20 , the return value is an array of dimensions \f1\fs18 c(n, dim(x)[margin]); \f3\fs20 in other words, each \f1\fs18 n \f3\fs20 -vector provides the lowest dimension of the result, and the sizes of the marginal dimensions are imposed upon the data above that. If all results are of length \f1\fs18 n == 1 \f3\fs20 , then if a single margin was specified the result is a vector (of length equal to the size of that marginal dimension), or if more than one margin was specified the result is an array of dimension \f1\fs18 dim(x)[margin] \f3\fs20 ; in other words, the sizes of the marginal dimensions are imposed upon the data. Since \f1\fs18 apply() \f3\fs20 iterates over the marginal dimensions in the same manner, these structures follows the structure of the data.\ The above explanation may not be entirely clear, so let\'92s look at an example. If \f1\fs18 x \f3\fs20 is a matrix with two rows and three columns, such as defined by \f1\fs18 x = matrix(1:6, nrow=2); \f3\fs20 , then executing \f1\fs18 apply(x,\'a00,\'a0"sum(applyValue);"); \f3\fs20 would cause each row of \f1\fs18 x \f3\fs20 to be supplied to the lambda through \f1\fs18 applyValue \f3\fs20 , and the values in each row would thus be summed to produce \f1\fs18 9 12 \f3\fs20 as a result. The call \f1\fs18 apply(x,\'a01,\'a0"sum(applyValue);"); \f3\fs20 would instead sum columns of \f1\fs18 x \f3\fs20 , producing \f1\fs18 3 7 11 \f3\fs20 as a result. Now consider using \f1\fs18 range() \f3\fs20 rather than \f1\fs18 sum() \f3\fs20 in the lambda, thus producing two values for each row or column. The call \f1\fs18 apply(x,\'a00,\'a0"range(applyValue);"); \f3\fs20 produces a result of \f1\fs18 matrix(c(1,5,2,6), nrow=2) \f3\fs20 , with the range of the first row of \f1\fs18 x \f3\fs20 , 1\'965, in the first column of the result, and the range of the second row of \f1\fs18 x \f3\fs20 , 2\'966, in the second column. Although visualization becomes more difficult, these same patterns extend to higher dimensions and arbitrary margins of \f1\fs18 x \f3\fs20 .\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 For efficiently obtaining the sums of the rows or columns of a matrix, see \f1\fs18 rowSums() \f3\fs20 and \f1\fs18 colSums() \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)array(*\'a0data, integer\'a0dim) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Creates a new array \f3\b0 from the data specified by \f1\fs18 data \f3\fs20 , with the dimension sizes specified by \f1\fs18 dim \f3\fs20 . The first dimension size in \f1\fs18 dim \f3\fs20 is the number of rows, and the second is the number of columns; further entries specify the sizes of higher-order dimensions. As many dimensions may be specified as desired, but with a minimum of two dimensions. An array with two dimensions is a matrix (by definition); note that \f1\fs18 matrix() \f3\fs20 may provide a more convenient way to make a new matrix. Each dimension must be of size \f1\fs18 1 \f3\fs20 or greater; \f1\fs18 0 \f3\fs20 -size dimensions are not allowed.\ The elements of \f1\fs18 data \f3\fs20 are used to populate the new array; the size of \f1\fs18 data \f3\fs20 must therefore be equal to the size of the new array, which is the product of all the values in \f1\fs18 dim \f3\fs20 . The new array will be filled in dimension order: one element in each row until a column is filled, then on to the next column in the same manner until all columns are filled, and then onward into the higher-order dimensions in the same manner.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (*)asVector(*\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Creates a new vector \f3\b0 from the elements of \f1\fs18 x \f3\fs20 , stripping off any dimensional information associated with \f1\fs18 x \f3\fs20 being a vector or array. The values of the resulting vector are read out from \f1\fs18 x \f3\fs20 in dimension order: one element from each row until a column is completed, then on to the next column in the same manner until all columns are completed, and then onward into the higher-order dimensions in the same manner. If \f1\fs18 x \f3\fs20 is already a vector, it is returned unmodified. See \f1\fs18 drop() \f3\fs20 for a similar method that drops only matrix/array dimensions that are redundant.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)cbind(...) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Combines vectors or matrices by column \f3\b0 to produce a single matrix. The parameters must be vectors (which are interpreted by \f1\fs18 cbind() \f3\fs20 as if they were one-column matrices) or matrices. They must be of the same type, of the same class if they are of type \f1\fs18 object \f3\fs20 , and have the same number of rows. If these conditions are met, the result is a single matrix with the parameters joined together, left to right. Parameters may instead be \f1\fs18 NULL \f3\fs20 , in which case they are ignored; or if all parameters are \f1\fs18 NULL \f3\fs20 , the result is \f1\fs18 NULL \f3\fs20 . A sequence of vectors, matrices, and \f1\fs18 NULL \f3\fs20 s may thus be concatenated with the \f1\fs18 NULL \f3\fs20 values removed, analogous to \f1\fs18 c() \f3\fs20 . Calling \f1\fs18 cbind(x) \f3\fs20 is an easy way to create a one-column matrix from a vector.\ To combine vectors or matrices by row instead, see \f1\fs18 rbind() \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (numeric)colSums(lif\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the sums of the columns \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a matrix. The result is a vector of elements, each providing the sum of the corresponding column of \f1\fs18 x \f3\fs20 . If \f1\fs18 x \f3\fs20 is of type \f1\fs18 logical \f3\fs20 or \f1\fs18 integer \f3\fs20 the result will be of type \f1\fs18 integer \f3\fs20 ; unlike the \f1\fs18 sum() \f3\fs20 function, \f1\fs18 colSums() \f3\fs20 does not promote the return type to \f1\fs18 float \f3\fs20 if \f1\fs18 integer \f3\fs20 overflow occurs, but instead throws an error. If \f1\fs18 x \f3\fs20 is of type \f1\fs18 float \f3\fs20 the result will be of type \f1\fs18 float \f3\fs20 . Except for the change in the treatment of \f1\fs18 integer \f3\fs20 overflow noted above, this is equivalent to using \f1\fs18 apply() \f3\fs20 with \f1\fs18 sum() \f3\fs20 to sum the columns of \f1\fs18 x \f3\fs20 , but is much faster.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (numeric$)det(numeric\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the determinant \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a square matrix (otherwise an error is raised). The determinant is a scalar-valued function of the entries of the matrix, and characterizes some properties of the matrix. In particular, the determinant is nonzero if and only if the matrix is invertible. If the determinant is zero, the matrix does not have an inverse and is referred to as \'93singular\'94. In Eidos the determinant is calculated from the \f7\i LU \f3\i0 decomposition of the matrix. The return type will match the type of \f1\fs18 x \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (*)diag([*\'a0x\'a0=\'a01], [Ni$\'a0nrow\'a0=\'a0NULL], [Ni$\'a0ncol\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the diagonal \f3\b0 of \f1\fs18 x \f3\fs20 . This function has four distinct usage patterns (matching R). First, if \f1\fs18 x \f3\fs20 is a matrix of any type, it returns the diagonal elements of \f1\fs18 x \f3\fs20 as a vector; in this case, \f1\fs18 nrow \f3\fs20 and \f1\fs18 ncol \f3\fs20 must be \f1\fs18 NULL \f3\fs20 . Second, if \f1\fs18 x \f3\fs20 is \f1\fs18 1 \f3\fs20 (the default) and \f1\fs18 nrow \f3\fs20 is non- \f1\fs18 NULL \f3\fs20 , it returns an identity matrix with the requested number of rows (and, if \f1\fs18 ncol \f3\fs20 is also non- \f1\fs18 NULL \f3\fs20 , the requested number of columns, otherwise the matrix will be square). Third, if \f1\fs18 x \f3\fs20 is a singleton \f1\fs18 integer \f3\fs20 value and \f1\fs18 nrow \f3\fs20 and \f1\fs18 ncol \f3\fs20 are \f1\fs18 NULL \f3\fs20 , it returns a square identity matrix of size \f1\fs18 x \f3\fs20 . Fourth, if x is a \f1\fs18 logical \f3\fs20 , \f1\fs18 integer \f3\fs20 , or \f1\fs18 float \f3\fs20 vector of length at least \f1\fs18 2 \f3\fs20 , it returns a matrix that uses the values of \f1\fs18 x \f3\fs20 as its diagonal (without recycling or truncation, unlike R) and has \f1\fs18 F \f3\fs20 , \f1\fs18 0 \f3\fs20 , or \f1\fs18 0.0 \f3\fs20 off-diagonal entries as appropriate.\ Note that using \f1\fs18 diag(x) \f3\fs20 , without \f1\fs18 nrow \f3\fs20 or \f1\fs18 ncol \f3\fs20 , can have unexpected effects if \f1\fs18 x \f3\fs20 is a vector that could be of length one. Use \f1\fs18 diag(x, nrow=length(x)) \f3\fs20 for consistent behavior.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer)dim(*\'a0x) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns the dimensions \f3\b0 of matrix or array \f1\fs18 x \f3\fs20 . The first dimension value is the number of rows, the second is the number of columns, and further values indicate the sizes of higher-order dimensions, identically to how dimensions are supplied to \f1\fs18 array() \f3\fs20 . \f1\fs18 NULL \f3\fs20 is returned if \f1\fs18 x \f3\fs20 is not a matrix or array.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (*)drop(*\'a0x) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns the result of dropping redundant dimensions \f3\b0 from matrix or array \f1\fs18 x \f3\fs20 . Redundant dimensions are those with a size of exactly 1. Non-redundant dimensions are retained. If only one non-redundant dimension is present, the result is a vector; if more than one non-redundant dimension is present, the result will be a matrix or array. If \f1\fs18 x \f3\fs20 is not a matrix or array, it is returned unmodified.\kerning1\expnd0\expndtw0 See \f1\fs18 asVector() \f3\fs20 for a way to drop all dimensions of a matrix or array, whether redundant or not.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (float)inverse(numeric\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the (multiplicative) inverse \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a square non-singular matrix (otherwise an error is raised). If matrix \f0\b B \f3\b0 is the inverse of \f7\i n \f3\i0 -by- \f7\i n \f3\i0 matrix \f0\b A \f3\b0 , then \f0\b AB \f3\b0 = \f0\b BA \f3\b0 = \f0\b I \f7\i\b0\fs13\fsmilli6667 \sub n \f3\i0\fs20 \nosupersub , where \f0\b I \f7\i\b0\fs13\fsmilli6667 \sub n \f3\i0\fs20 \nosupersub denotes the \f7\i n \f3\i0 -by- \f7\i n \f3\i0 identity matrix and the multiplication used is ordinary matrix multiplication as performed by \f1\fs18 matrixMult() \f3\fs20 . If \f1\fs18 x \f3\fs20 might be singular (and thus non-invertible), and you wish to avoid the possibility of an error, you can call \f1\fs18 det() \f3\fs20 first to find the determinant of the matrix; if the determinant is zero, the matrix is singular and does not have an inverse, and so \f1\fs18 inverse() \f3\fs20 should not be called. In Eidos the inverse is calculated from the \f7\i LU \f3\i0 decomposition of the matrix.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical)lowerTri(*\'a0x, [logical$\'a0diag\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the lower triangle \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a matrix. The return value will be a \f1\fs18 logical \f3\fs20 matrix of the same dimensions as \f1\fs18 x \f3\fs20 , with elements \f1\fs18 T \f3\fs20 in the lower triangle, \f1\fs18 F \f3\fs20 elsewhere. If \f1\fs18 diag \f3\fs20 is \f1\fs18 F \f3\fs20 (the default), the diagonal is not included in the lower triangle; if \f1\fs18 diag \f3\fs20 is \f1\fs18 T \f3\fs20 , the diagonal is included in the lower triangle (i.e., its elements will be \f1\fs18 T \f3\fs20 ).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)matrix(*\'a0data, [Ni$\'a0nrow\'a0=\'a0NULL], [Ni$\'a0ncol\'a0=\'a0NULL], [logical$\'a0byrow\'a0=\'a0F]) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Creates a new matrix \f3\b0 from the data specified by \f1\fs18 data \f3\fs20 . By default this creates a one-column matrix. If non- \f1\fs18 NULL \f3\fs20 values are supplied for \f1\fs18 nrow \f3\fs20 and/or \f1\fs18 ncol \f3\fs20 , a matrix will be made with the requested number of rows and/or columns if possible; if the length of \f1\fs18 data \f3\fs20 is not compatible with the requested dimensions, an error will result. By default, values from data will populate the matrix by columns, filling each column sequentially before moving on to the next column; if \f1\fs18 byrow \f3\fs20 is \f1\fs18 T \f3\fs20 the matrix will be populated by rows instead.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (numeric)matrixMult(numeric\'a0x, numeric\'a0y) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns the result of matrix multiplication \f3\b0 of \f1\fs18 x \f3\fs20 with \f1\fs18 y \f3\fs20 . In Eidos (as in R), with two matrices \f1\fs18 A \f3\fs20 and \f1\fs18 B \f3\fs20 the simple product \f1\fs18 A\'a0*\'a0B \f3\fs20 multiplies the corresponding elements of the matrices; in other words, if \f1\fs18 X \f3\fs20 is the result of \f1\fs18 A\'a0*\'a0B \f3\fs20 , then \f1\fs18 X \f7\i\fs13\fsmilli6667 \sub ij \f3\i0\fs20 \nosupersub \'a0=\'a0 \f1\fs18 A \f7\i\fs13\fsmilli6667 \sub ij \f3\i0\fs20 \nosupersub \'a0* \f1\fs18 B \f7\i\fs13\fsmilli6667 \sub ij \f3\i0\fs20 \nosupersub . This is parallel to the definition of other operators; A\'a0+\'a0B adds the corresponding elements of the matrices ( \f1\fs18 X \f7\i\fs13\fsmilli6667 \sub ij \f3\i0\fs20 \nosupersub \'a0=\'a0 \f1\fs18 A \f7\i\fs13\fsmilli6667 \sub ij \f3\i0\fs20 \nosupersub \'a0+\'a0 \f1\fs18 B \f7\i\fs13\fsmilli6667 \sub ij \f3\i0\fs20 \nosupersub ), etc. In R, true matrix multiplication is achieved with a special operator, \f1\fs18 %*% \f3\fs20 ; in Eidos, the \f1\fs18 matrixMult() \f3\fs20 function is used instead.\ Both \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 must be matrices, and must be conformable according to the standard definition of matrix multiplication (i.e., if \f1\fs18 x \f3\fs20 is an \f7\i n \f3\i0 \'a0\'d7\'a0 \f7\i m \f3\i0 matrix then \f1\fs18 y \f3\fs20 must be a \f7\i m \f3\i0 \'a0\'d7\'a0 \f7\i p \f3\i0 matrix, and the result will be a \f7\i n \f3\i0 \'a0\'d7\'a0 \f7\i p \f3\i0 matrix). Vectors will not be promoted to matrices by this function, even if such promotion would lead to a conformable matrix.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (numeric)matrixPow(numeric\'a0x, integer$\'a0power)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the result of raising matrix \f9\fs18 x \f0\fs20 to an \f9\fs18 integer \f0\fs20 \f9\fs18 power \f0\fs20 . \f3\b0 The parameter x must be a square matrix (or an error will be raised). This operation is performed by repeated matrix multiplication with \f1\fs18 matrixMult() \f3\fs20 , and uses \f1\fs18 inverse() \f3\fs20 to compute the inverse of the matrix if \f1\fs18 power \f3\fs20 is negative.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer$)nrow(*\'a0x) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns the number of rows \f3\b0 in matrix or array \f1\fs18 x \f3\fs20 . For vector \f1\fs18 x \f3\fs20 , \f1\fs18 nrow() \f3\fs20 returns \f1\fs18 NULL \f3\fs20 ; \f1\fs18 size() \f3\fs20 should be used. An equivalent of R\'92s \f1\fs18 NROW() \f3\fs20 function, which treats vectors as \f1\fs18 1 \f3\fs20 -column matrices, is not provided but would be trivial to implement as a user-defined function.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (integer$)ncol(*\'a0x) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns the number of columns \f3\b0 in matrix or array \f1\fs18 x \f3\fs20 . For vector \f1\fs18 x \f3\fs20 , \f1\fs18 ncol() \f3\fs20 returns \f1\fs18 NULL \f3\fs20 ; \f1\fs18 size() \f3\fs20 should be used. An equivalent of R\'92s \f1\fs18 NCOL() \f3\fs20 function, which treats vectors as \f1\fs18 1 \f3\fs20 -column matrices, is not provided but would be trivial to implement as a user-defined function.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (numeric)outerProduct(numeric\'a0x, numeric\'a0y)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the outer product \f3\b0 of vectors \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 . The outer product, \f1\fs18 x \f3\fs20 \f6 \uc0\u8855 \f3 \f1\fs18 y \f3\fs20 , is the result of matrix multiplication of \f1\fs18 x \f3\fs20 with the transpose of \f1\fs18 y \f3\fs20 , or \f1\fs18 xy \f3\fs13\fsmilli6667 \super T \fs20 \nosupersub . It will be a matrix with a number of rows equal to the length of \f1\fs18 x \f3\fs20 , and a number of columns equal to the length of \f1\fs18 y \f3\fs20 . It is required that \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 be vectors, not matrices or arrays, that they have non-zero lengths, and that they be the same type \'96 both \f1\fs18 integer \f3\fs20 or both \f1\fs18 float \f3\fs20 . The return value will be of the same type as \f1\fs18 x \f3\fs20 and \f1\fs18 y \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)rbind(...) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Combines vectors or matrices by row \f3\b0 to produce a single matrix. The parameters must be vectors (which are interpreted by \f1\fs18 rbind() \f3\fs20 as if they were one-row matrices) or matrices. They must be of the same type, of the same class if they are of type \f1\fs18 object \f3\fs20 , and have the same number of columns. If these conditions are met, the result is a single matrix with the parameters joined together, top to bottom. Parameters may instead be \f1\fs18 NULL \f3\fs20 , in which case they are ignored; or if all parameters are \f1\fs18 NULL \f3\fs20 , the result is \f1\fs18 NULL \f3\fs20 . A sequence of vectors, matrices, and \f1\fs18 NULL \f3\fs20 s may thus be concatenated with the \f1\fs18 NULL \f3\fs20 values removed, analogous to \f1\fs18 c() \f3\fs20 . Calling \f1\fs18 rbind(x) \f3\fs20 is an easy way to create a one-row matrix from a vector.\ To combine vectors or matrices by column instead, see \f1\fs18 cbind() \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (numeric)rowSums(lif\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the sums of the rows \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a matrix. The result is a vector of elements, each providing the sum of the corresponding row of \f1\fs18 x \f3\fs20 . If \f1\fs18 x \f3\fs20 is of type \f1\fs18 logical \f3\fs20 or \f1\fs18 integer \f3\fs20 the result will be of type \f1\fs18 integer \f3\fs20 ; unlike the \f1\fs18 sum() \f3\fs20 function, \f1\fs18 rowSums() \f3\fs20 does not promote the return type to \f1\fs18 float \f3\fs20 if \f1\fs18 integer \f3\fs20 overflow occurs, but instead throws an error. If \f1\fs18 x \f3\fs20 is of type \f1\fs18 float \f3\fs20 the result will be of type \f1\fs18 float \f3\fs20 . Except for the change in the treatment of \f1\fs18 integer \f3\fs20 overflow noted above, this is equivalent to using \f1\fs18 apply() \f3\fs20 with \f1\fs18 sum() \f3\fs20 to sum the rows of \f1\fs18 x \f3\fs20 , but is much faster.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)t(*\'a0x) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns the transpose \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a matrix. This is the matrix reflected across its diagonal; or alternatively, the matrix with its columns written out instead as rows in the same order.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (numeric$)tr(numeric\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the trace \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a square matrix (otherwise an error is raised). The trace is the sum of the diagonal elements of the matrix. The return type will match the type of \f1\fs18 x \f3\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical)upperTri(*\'a0x, [logical$\'a0diag\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns the upper triangle \f3\b0 of \f1\fs18 x \f3\fs20 , which must be a matrix. The return value will be a \f1\fs18 logical \f3\fs20 matrix of the same dimensions as \f1\fs18 x \f3\fs20 , with elements \f1\fs18 T \f3\fs20 in the upper triangle, \f1\fs18 F \f3\fs20 elsewhere. If \f1\fs18 diag \f3\fs20 is \f1\fs18 F \f3\fs20 (the default), the diagonal is not included in the upper triangle; if \f1\fs18 diag \f3\fs20 is \f1\fs18 T \f3\fs20 , the diagonal is included in the upper triangle (i.e., its elements will be \f1\fs18 T \f3\fs20 ).\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.9. Filesystem access functions\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf0 (logical$)createDirectory(string$\'a0path) \f2 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Creates a new filesystem directory \f3\b0 at the path specified by \f1\fs18 path \f3\fs20 and returns a \f1\fs18 logical \f3\fs20 value indicating if the creation succeeded ( \f1\fs18 T \f3\fs20 ) or failed ( \f1\fs18 F \f3\fs20 ). If the path already exists, \f1\fs18 createDirectory() \f3\fs20 will do nothing to the filesystem, will emit a warning, and will return \f1\fs18 T \f3\fs20 to indicate success if the existing path is a directory, or \f1\fs18 F \f3\fs20 to indicate failure if the existing path is not a directory. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)deleteFile(string$\'a0filePath) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Deletes the file \f3\b0 specified by \f1\fs18 filePath \f3\fs20 and returns a \f1\fs18 logical \f3\fs20 value indicating if the deletion succeeded ( \f1\fs18 T \f3\fs20 ) or failed ( \f1\fs18 F \f3\fs20 ).\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 This function might also be able to delete a directory at \f1\fs18 filePath \f3\fs20 , but only if it is empty (apart from the \f1\fs18 . \f3\fs20 and \f1\fs18 .. \f3\fs20 directory entries that exist on Un*x filesystems). If other files (including invisible files) exist in the directory, \f1\fs18 deleteFile() \f3\fs20 will probably fail as a safety measure, in which case the contained files must be deleted individually first. This is vague because the actual policy regarding deletion of directories will depend upon the operating system, since Eidos achieves the deletion by calling an operating-system function. \f2 \cf0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (logical$)fileExists(string$\'a0filePath)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Checks the existence of the file \f3\b0 specified by \f1\fs18 filePath \f3\fs20 and returns a \f1\fs18 logical \f3\fs20 value indicating if it exists ( \f1\fs18 T \f3\fs20 ) or does not exist ( \f1\fs18 F \f3\fs20 ). This also works for directories.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (string)filesAtPath(string$\'a0path, [logical$\'a0fullPaths\'a0=\'a0F]) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f1\fs18 string \f3\fs20 vector containing the \f0\b names of all files in a directory \f3\b0 specified by \f1\fs18 path \f2\fs20 . \f3 If the optional parameter \f1\fs18 fullPaths \f3\fs20 is \f1\fs18 T \f3\fs20 , full filesystem paths are returned for each file; if \f1\fs18 fullPaths \f3\fs20 is \f1\fs18 F \f3\fs20 (the default), then only the filenames relative to the specified directory are returned. This list includes directories (i.e. subfolders), including the \f1\fs18 "." \f3\fs20 and \f1\fs18 ".." \f3\fs20 directories on Un*x systems. The list also includes invisible files, such as those that begin with a \f1\fs18 "." \f3\fs20 on Un*x systems. This function does not descend recursively into subdirectories. If an error occurs during the read, \f1\fs18 NULL \f3\fs20 will be returned. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (logical$)flushFile(string$\'a0filePath)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Flushes buffered content to a file \f3\b0 specified by \f1\fs18 filePath \f3\fs20 . Normally, written data is buffered by \f1\fs18 writeFile() \f3\fs20 if the \f1\fs18 compress \f3\fs20 option of that function is \f1\fs18 T \f3\fs20 , holding the data in memory rather than writing it to disk immediately. This buffering improves both performance and file size; however, sometimes it is desirable to flush the buffered data to disk with \f1\fs18 flush() \f3\fs20 so that the filesystem is up to date. Note that flushing after every write is not recommended, since it will lose all of the benefits of buffering. Calling \f1\fs18 flushFile() \f3\fs20 for a path that has not been written to, or is not being buffered, will do nothing. If the flush is successful, \f1\fs18 T \f3\fs20 will be returned; if not, \f1\fs18 F \f3\fs20 will be returned (but at present, an error will result instead).\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (string$)getwd(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Gets the current filesystem working directory \f3\b0 . The filesystem working directory is the directory which will be used as a base path for relative filesystem paths. For example, if the working directory is \f1\fs18 "~/Desktop" \f3\fs20 (the \f1\fs18 Desktop \f3\fs20 subdirectory within the current user\'92s home directory, as represented by \f1\fs18 ~ \f3\fs20 ), then the filename \f1\fs18 "foo.txt" \f3\fs20 would correspond to the filesystem path \f1\fs18 "~/Desktop/foo.txt" \f3\fs20 , and the relative path \f1\fs18 "bar/baz/" \f3\fs20 would correspond to the filesystem path \f1\fs18 \'93~/Desktop/bar/baz/\'93 \f3\fs20 .\ Note that the path returned may not be identical to the path previously set with \f1\fs18 setwd() \f3\fs20 , if for example symbolic links are involved; but it ought to refer to the same actual directory in the filesystem.\ The initial working directory is \'96 as is generally the case on Un*x \'96 simply the directory given to the running Eidos process by its parent process (the operating system, a shell, a job scheduler, a debugger, or whatever the case may be). If you launch Eidos (or SLiM) from the command line in a Un*x shell, it is typically the current directory in that shell. Before relative filesystem paths are used, you may therefore wish check what the initial working directory is on your platform, with \f1\fs18 getwd() \f3\fs20 , if you are not sure. Alternatively, you can simply use \f1\fs18 setwd() \f3\fs20 to set the working directory to a known path.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (object$)readCSV(string$\'a0filePath, [ls\'a0colNames\'a0=\'a0T], [Ns$\'a0colTypes\'a0=\'a0NULL], [string$\'a0sep\'a0=\'a0","], [string$\'a0quote\'a0=\'a0'"'], [string$\'a0dec\'a0=\'a0"."], [string$\'a0comment\'a0=\'a0""])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Reads data from a CSV or other delimited file \f3\b0 specified by \f1\fs18 filePath \f3\fs20 and returns a \f1\fs18 DataFrame \f3\fs20 object containing the data in a tabular form. CSV (comma-separated value) files use a somewhat standard file format in which a table of data is provided, with values within a row separated by commas, while rows in the table are separated by newlines. Software from R to Excel (and Eidos; see the \f1\fs18 serialize() \f3\fs20 method of \f1\fs18 Dictionary \f3\fs20 ) can export data in CSV format. This function can actually also read files that use a delimiter other than commas; TSV (tab-separated value) files are a popular alternative. Since there is substantial variation in the exact file format for CSV files, this documentation will try to specify the precise format expected by this function. Note that CSV files represent values differently that Eidos usually does, and some of the format options allowed by \f1\fs18 readCSV() \f3\fs20 , such as decimal commas, are not otherwise available in Eidos.\ If \f1\fs18 colNames \f3\fs20 is \f1\fs18 T \f3\fs20 (the default), the first row of data is taken to be a header, containing the string names of the columns in the data table; those names will be used by the resulting \f1\fs18 DataFrame \f3\fs20 . If \f1\fs18 colNames \f3\fs20 is \f1\fs18 F \f3\fs20 , a header row is not expected and column names are auto-generated as \f1\fs18 X1 \f3\fs20 , \f1\fs18 X2 \f3\fs20 , etc. If \f1\fs18 colNames \f3\fs20 is a \f1\fs18 string \f3\fs20 vector, a header row is not expected and \f1\fs18 colNames \f3\fs20 will be used as the column names; if additional columns exist beyond the length of \f1\fs18 colNames \f3\fs20 their names will be auto-generated. Duplicate column names will generate a warning and be made unique.\ If \f1\fs18 colTypes \f3\fs20 is \f1\fs18 NULL \f3\fs20 (the default), the value type for each column will be guessed from the values it contains, as described below. If \f1\fs18 colTypes \f3\fs20 is a singleton \f1\fs18 string \f3\fs20 , it should contain single-letter codes indicating the desired type for each column, from left to right. The letters \f1\fs18 lifs \f3\fs20 have the same meaning as in Eidos signatures ( \f1\fs18 logical \f3\fs20 , \f1\fs18 integer \f3\fs20 , \f1\fs18 float \f3\fs20 , and \f1\fs18 string \f3\fs20 ); in addition, \f1\fs18 ? \f3\fs20 may be used to indicate that the type for that column should be guessed as by default, and \f1\fs18 _ \f3\fs20 or \f1\fs18 - \f3\fs20 may be used to indicate that that column should be skipped \'96 omitted from the returned \f1\fs18 DataFrame \f3\fs20 . Other characters in \f1\fs18 colTypes \f3\fs20 will result in an error. If additional columns exist beyond the end of the \f1\fs18 colTypes \f3\fs20 string their types will be guessed as by default.\ The separator between values is supplied by \f1\fs18 sep \f3\fs20 ; it is a comma by default, but a tab can be used instead by supplying tab ( \f1\fs18 "\\t" \f3\fs20 in Eidos), or another character may also be used. If \f1\fs18 sep \f3\fs20 is the empty string \f1\fs18 "" \f3\fs20 , the separator between values is \'93whitespace\'94, meaning one or more spaces or tabs. When the separator is whitespace, whitespace at the beginning or the end of a line will be ignored.\ Similarly, the character used to quote string values is a double quote ( \f1\fs18 '"' \f3\fs20 in Eidos), by default, but another character may be supplied in \f1\fs18 quote \f3\fs20 . When the string delimiter is encountered, \f7\i all \f3\i0 following characters are considered to be part of the string until another string delimiter is encountered, terminating the string; this includes spaces, comment characters, newlines, and everything else. Within a string value, the string delimiter itself is used twice in a row to indicate that the delimiter itself is present within the string; for example, if the string value (shown without the usual surrounding quotes to try to avoid confusion) is \f1\fs18 she said "hello" \f3\fs20 , and the string delimiter is the double quote as it is by default, then in the CSV file the value would be given as \f1\fs18 "she said ""hello""" \f3\fs20 . The usual Eidos style of escaping characters using a backslash is \f7\i not \f3\i0 part of the CSV standard followed here. (When a string value is provided \f7\i without \f3\i0 using the string delimiter, all following characters are considered part of the string except a newline, the value separator \f1\fs18 sep \f3\fs20 , the quote separator \f1\fs18 quote \f3\fs20 , and the comment separator \f1\fs18 comment \f3\fs20 ; if none of those characters are present in the string value, the quote delimiter may be omitted.)\ The character used to indicate a decimal delimiter in numbers may be supplied with \f1\fs18 dec \f3\fs20 ; by default this is \f1\fs18 "." \f3\fs20 (and so \f1\fs18 10.0 \f3\fs20 would be ten, written with a decimal point), but \f1\fs18 "," \f3\fs20 is common in European data files (and so \f1\fs18 10,0 \f3\fs20 would be ten, written with a decimal comma). Note that \f1\fs18 dec \f3\fs20 and \f1\fs18 sep \f3\fs20 may not be the same, so that it is unambiguous whether \f1\fs18 10,0 \f3\fs20 is two numbers ( \f1\fs18 10 \f3\fs20 and \f1\fs18 0 \f3\fs20 ) or one number ( \f1\fs18 10.0 \f3\fs20 ). For this reason, European CSV files that use a decimal comma typically use a semicolon as the value separator, which may be supplied with \f1\fs18 sep=";" \f3\fs20 to \f1\fs18 readCSV() \f3\fs20 .\ Finally, the remainder of a line following a comment character will be ignored when the file is read; by default \f1\fs18 comment \f3\fs20 is the empty string, \f1\fs18 "" \f3\fs20 , indicating that comments do not exist at all, but \f1\fs18 "#" \f3\fs20 is a popular comment prefix.\ To translate the CSV data into a \f1\fs18 DataFrame \f3\fs20 , it is necessary for Eidos to guess what value type each column is unless a column type is specified by \f1\fs18 colTypes \f3\fs20 . Quotes surrounding a value are irrelevant to this guess; for example, \f1\fs18 1997 \f3\fs20 and \f1\fs18 "1997" \f3\fs20 are both candidates to be \f1\fs18 integer \f3\fs20 values (because some programs generate CSV output in which \f7\i every \f3\i0 value is quoted regardless of type). If \f7\i every \f3\i0 value in a column is either \f1\fs18 true \f3\fs20 , \f1\fs18 false \f3\fs20 , \f1\fs18 TRUE \f3\fs20 , \f1\fs18 FALSE \f3\fs20 , \f1\fs18 T \f3\fs20 , or \f1\fs18 F \f3\fs20 , the column will be taken to be \f1\fs18 logical \f3\fs20 . Otherwise, if \f7\i every \f3\i0 value in a column is an integer (here defined as an optional \f1\fs18 + \f3\fs20 or \f1\fs18 - \f3\fs20 , followed by nothing but decimal digits \f1\fs18 0123456789 \f3\fs20 ), the column will be taken to be \f1\fs18 integer \f3\fs20 . Otherwise, if \f7\i every \f3\i0 value in a column is a floating-point number (here defined as an optional \f1\fs18 + \f3\fs20 or \f1\fs18 - \f3\fs20 , followed by decimal digits \f1\fs18 0123456789 \f3\fs20 , optionally a decimal separator and then optionally more decimal digits, and ending with an optional exponent like \f1\fs18 e7 \f3\fs20 , \f1\fs18 E+05 \f3\fs20 , or \f1\fs18 e-2 \f3\fs20 ), the column will be taken to be \f1\fs18 float \f3\fs20 ; the special values \f1\fs18 NAN \f3\fs20 , \f1\fs18 INF \f3\fs20 , \f1\fs18 INFINITY \f3\fs20 , \f1\fs18 -INF \f3\fs20 , and \f1\fs18 -INFINITY \f3\fs20 (not case-sensitive) are also candidates to be \f1\fs18 float \f3\fs20 (if the rest of the column is also convertible to \f1\fs18 float \f3\fs20 ), representing the corresponding \f1\fs18 float \f3\fs20 constants. Otherwise, the column will be taken to be \f1\fs18 string \f3\fs20 . \f1\fs18 NULL \f3\fs20 and \f1\fs18 NA \f3\fs20 are not recognized by \f1\fs18 readCSV() \f3\fs20 in CSV files and will be read as strings. Every line in a CSV file must contain the same number of values (forming a rectangular data table); missing values are not allowed by \f1\fs18 readCSV() \f3\fs20 since there is no way to represent them in \f1\fs18 DataFrame \f3\fs20 (since Eidos has no equivalent of R\'92s \f1\fs18 NA \f3\fs20 value). Spaces are considered part of a data field and are not trimmed, following the RFC 4180 standard. These choices are an attempt to provide optimal behavior for most clients, but given the lack of any universal standard for CSV files, and the lack of any type information in the CSV format, they will not always work as desired; in such cases, it should be reasonably straightforward to preprocess input files using standard Unix text-processing tools like \f1\fs18 sed \f3\fs20 and \f1\fs18 awk \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string)readFile(string$\'a0filePath) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Reads in the contents of a file \f3\b0 specified by \f1\fs18 filePath \f3\fs20 and returns a \f1\fs18 string \f3\fs20 vector containing the lines (separated by \f1\fs18 \\n \f3\fs20 and \f1\fs18 \\r \f3\fs20 characters) of the file. Reading files other than text files is not presently supported. If an error occurs during the read, \f1\fs18 NULL \f3\fs20 will be returned. \f2 \ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (string$)setwd(string$\'a0path)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Sets the current filesystem working directory \f3\b0 . The filesystem working directory is the directory which will be used as a base path for relative filesystem paths (see \f1\fs18 getwd() \f3\fs20 for further discussion). An error will result if the working directory cannot be set to the given path.\ The current working directory prior to the change will be returned as an invisible \f1\fs18 string \f3\fs20 value; the value returned is identical to the value that would have been returned by \f1\fs18 getwd() \f3\fs20 , apart from its invisibility.\ See \f1\fs18 getwd() \f3\fs20 for discussion regarding the initial working directory, before it is set with \f1\fs18 setwd() \f3\fs20 .\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (string$)tempdir(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns a path to a directory appropriate for saving temporary files \f3\b0 . The path returned by \f1\fs18 tempdir() \f3\fs20 is platform-specific, and is not guaranteed to be the same from one run of SLiM to the next. It is guaranteed to end in a slash, so further path components should be appended without a leading slash. At present, on macOS and Linux systems, the path will be \f1\fs18 "/tmp/" \f3\fs20 ; this may change in future Eidos versions without warning.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical$)writeFile(string$\'a0filePath, string\'a0contents, [logical$\'a0append\'a0=\'a0F], [logical$\'a0compress\'a0=\'a0F]) \f2 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Writes or appends to a file \f3\b0 specified by \f1\fs18 filePath \f3\fs20 with contents specified by \f1\fs18 contents \f3\fs20 , a \f1\fs18 string \f3\fs20 vector of lines. If \f1\fs18 append \f3\fs20 is \f1\fs18 T \f3\fs20 , the write will be appended to the existing file (if any) at \f1\fs18 filePath \f3\fs20 ; if it is \f1\fs18 F \f3\fs20 (the default), then the write will replace an existing file at that path.\cf2 If the write is successful, \f1\fs18 T \f3\fs20 will be returned; if not, \f1\fs18 F \f3\fs20 will be returned (but at present, an error will result instead).\cf0 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 If \f1\fs18 compress \f3\fs20 is \f1\fs18 T \f3\fs20 , the contents will be compressed with \f1\fs18 zlib \f3\fs20 as they are written, and the standard \f1\fs18 .gz \f3\fs20 extension for \f1\fs18 gzip \f3\fs20 -compressed files will be appended to the filename in \f1\fs18 filePath \f3\fs20 if it is not already present. If the \f1\fs18 compress \f3\fs20 option is used in conjunction with \f1\fs18 append==T \f3\fs20 , Eidos will buffer data to append and flush it to the file in a delayed fashion (for performance reasons), and so appended data may not be visible in the file until later \'96 potentially not until the process ends (i.e., the end of the SLiM simulation, for example). If that delay if undesirable, buffered data can be explicitly flushed to the filesystem with \f1\fs18 flushFile() \f3\fs20 . The \f1\fs18 compress \f3\fs20 option was added in Eidos 2.4 (SLiM 3.4). Note that \f1\fs18 readFile() \f3\fs20 does not currently support reading in compressed data.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf0 Note that newline characters will be added at the ends of the lines in \f1\fs18 contents \f3\fs20 . If you do not wish to have newlines added, you should use \f1\fs18 paste() \f3\fs20 to assemble the elements of \f1\fs18 contents \f3\fs20 together into a singleton \f1\fs18 string \f2\fs20 .\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string$)writeTempFile(string$\'a0prefix, string$\'a0suffix, string\'a0contents, [logical$\'a0compress\'a0=\'a0F]) \f2 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Writes to a unique temporary file \f3\b0 with contents specified by \f1\fs18 contents \f3\fs20 , a \f1\fs18 string \f3\fs20 vector of lines. The filename used will begin with \f1\fs18 prefix \f3\fs20 and end with \f1\fs18 suffix \f3\fs20 , and will contain six random characters in between; for example, if \f1\fs18 prefix \f3\fs20 is \f1\fs18 "plot1_" \f3\fs20 and \f1\fs18 suffix \f3\fs20 is \f1\fs18 ".pdf" \f3\fs20 , the generated filename might look like \f1\fs18 "plot1_r5Mq0t.pdf" \f3\fs20 . It is legal for \f1\fs18 prefix \f3\fs20 , \f1\fs18 suffix \f3\fs20 , or both to be the empty string, \f1\fs18 "" \f3\fs20 , but supplying a file extension is usually advisable at minimum. The file will be created inside the \f1\fs18 /tmp/ \f3\fs20 directory of the system, which is provided by Un*x systems as a standard location for temporary files; the \f1\fs18 /tmp/ \f3\fs20 directory should not be specified as part of prefix (nor should any other directory information). The filename generated is guaranteed not to already exist in \f1\fs18 /tmp/ \f3\fs20 . The file is created with Un*x permissions \f1\fs18 0600 \f3\fs20 , allowing reading and writing only by the user for security. If the write is successful, the full path to the temporary file will be returned; if not, \f1\fs18 "" \f3\fs20 will be returned.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 If \f1\fs18 compress \f3\fs20 is \f1\fs18 T \f3\fs20 , the contents will be compressed with \f1\fs18 zlib \f3\fs20 as they are written, and the standard \f1\fs18 .gz \f3\fs20 extension for \f1\fs18 gzip \f3\fs20 -compressed files will be appended to the filename suffix in \f1\fs18 suffix \f3\fs20 if it is not already present. The \f1\fs18 compress \f3\fs20 option was added in Eidos 2.4 (SLiM 3.4). Note that \f1\fs18 readFile() \f3\fs20 does not currently support reading in compressed data.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf0 \kerning1\expnd0\expndtw0 Note that newline characters will be added at the ends of the lines in \f1\fs18 contents \f3\fs20 . If you do not wish to have newlines added, you should use \f1\fs18 paste() \f3\fs20 to assemble the elements of \f1\fs18 contents \f3\fs20 together into a singleton \f1\fs18 string \f2\fs20 .\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.10. Color manipulation functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf2 \expnd0\expndtw0\kerning0 (string)cmColors(integer$\'a0n)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \kerning1\expnd0\expndtw0 This method has been deprecated, and may be removed in a future release of Eidos. \f3\b0 In SLiM 3.5 and later, use \f1\fs18 colors(n, "cm") \f3\fs20 instead.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf5 Generate colors in a \'93cyan-magenta\'94 color palette.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string)colors(numeric\'a0x, string$\'a0name)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Generate colors in a standard color palette \f3\b0 . If \f1\fs18 x \f3\fs20 is a singleton \f1\fs18 integer \f3\fs20 , the returned vector will contain \f1\fs18 x \f3\fs20 color strings representing \f1\fs18 x \f3\fs20 colors equidistant along the named palette, spanning its full extent. Alternatively, if \f1\fs18 x \f3\fs20 is a \f1\fs18 float \f3\fs20 vector of values in [ \f1\fs18 0 \f3\fs20 , \f1\fs18 1 \f3\fs20 ], the returned vector will contain one color string for each value in \f1\fs18 x \f3\fs20 , representing the color at the corresponding fraction along the named palette (values outside [ \f1\fs18 0 \f3\fs20 , \f1\fs18 1 \f3\fs20 ] will be clamped to that range). (Note that the function signature states the type of \f1\fs18 x \f3\fs20 as \f1\fs18 numeric \f3\fs20 , but in this function the \f1\fs18 integer \f3\fs20 and \f1\fs18 float \f3\fs20 cases have completely different semantic meanings.)\ The color palette specified by \f1\fs18 name \f3\fs20 may be any of the following color palettes based upon color palettes in R: \f1\fs18 "cm" \f3\fs20 , \f1\fs18 "heat" \f3\fs20 , and \f1\fs18 "terrain" \f3\fs20 .\ It may also be one of the following color palettes based on color palettes in MATLAB (and the Turbo palette from Anton Mikhailov of the Google AI group, based upon the Jet palette provided by MATLAB): \f1\fs18 "parula" \f3\fs20 , \f1\fs18 "hot" \f3\fs20 , \f1\fs18 "jet" \f3\fs20 , \f1\fs18 "turbo" \f3\fs20 , and \f1\fs18 "gray" \f3\fs20 .\ Finally, it may be one of the following color palettes based upon color palettes in Matplotlib, also available in the \f1\fs18 viridis \f3\fs20 R package: \f1\fs18 "magma" \f3\fs20 , \f1\fs18 "inferno" \f3\fs20 , \f1\fs18 "plasma" \f3\fs20 , \f1\fs18 "viridis" \f3\fs20 , and \f1\fs18 "cividis" \f3\fs20 . These color palettes are designed to be perceptually uniform, changing continuously and linearly. They are also designed to perform well even for users with red-green colorblindness; the \f1\fs18 "cividis" \f3\fs20 palette, in particular, is designed to look nearly identical to those with and without red-green colorblindness, to be perceptually uniform in both hue and brightness, and to increase linearly in brightness.\ This function replaces the deprecated \f1\fs18 cmColors() \f3\fs20 , \f1\fs18 heatColors() \f3\fs20 , and \f1\fs18 terrainColors() \f3\fs20 functions, and adds several several additional color palettes to Eidos. See \f1\fs18 rainbow() \f3\fs20 for another color palette function.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)color2rgb(string\'a0color) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Converts a color string to RGB \f3\b0 . The color string specified in \f1\fs18 color \f3\fs20 may be either a named color or a color in hexadecimal format such as \f1\fs18 "#007FC0" \f3\fs20 . The equivalent RGB color is returned as a \f1\fs18 float \f3\fs20 vector of length three (red, green, blue). Returned RGB values will be in the interval [0, 1].\ This function can also be called with a non-singleton vector of color strings in \f1\fs18 color \f3\fs20 . In this case, the returned \f1\fs18 float \f3\fs20 value will be a matrix of RGB values, with three columns (red, green, blue) and one row per element of \f1\fs18 color \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string)heatColors(integer$\'a0n)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \kerning1\expnd0\expndtw0 This method has been deprecated, and may be removed in a future release of Eidos. \f3\b0 In SLiM 3.5 and later, use \f1\fs18 colors(n, "heat") \f3\fs20 instead.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf5 Generate colors in a \'93heat map\'94 color palette.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (float)hsv2rgb(float\'a0hsv) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Converts an HSV color to RGB \f2\b0 . \f3 The HSV color is specified in \f1\fs18 hsv \f3\fs20 as a \f1\fs18 float \f3\fs20 vector of length three (hue, saturation, value), and the equivalent RGB color is returned as a \f1\fs18 float \f3\fs20 vector of length three (red, green, blue). HSV values will be clamped to the interval [0, 1], and returned RGB values will also be in the interval [0, 1]. \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 \expnd0\expndtw0\kerning0 This function can also be called with a matrix of HSV values, with three columns (hue, saturation, value). In this case, the returned \f1\fs18 float \f3\fs20 value will be a matrix of RGB values, with three columns (red, green, blue) and one row per row of \f1\fs18 hsv \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string)rainbow(integer$\'a0n, [float$\'a0s\'a0=\'a01.0], [float$\'a0v\'a0=\'a01.0], [float$\'a0start\'a0=\'a00.0], [Nf$\'a0end\'a0=\'a0NULL], [logical$\'a0ccw\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Generate colors in a \'93rainbow\'94 color palette \f3\b0 . The number of colors desired is passed in \f1\fs18 n \f3\fs20 , and the returned vector will contain \f1\fs18 n \f3\fs20 color strings. Parameters \f1\fs18 s \f3\fs20 and \f1\fs18 v \f3\fs20 control the saturation and value of the rainbow colors generated. The color sequence begins with the hue \f1\fs18 start \f3\fs20 , and ramps to the hue \f1\fs18 end \f3\fs20 , in a counter-clockwise direction around the standard HSV color wheel if \f1\fs18 ccw \f3\fs20 is \f1\fs18 T \f3\fs20 (the default, following R), otherwise in a clockwise direction. If \f1\fs18 end \f3\fs20 is \f1\fs18 NULL \f3\fs20 (the default), a value of \f1\fs18 (n-1)/n \f3\fs20 is used, producing a complete rainbow around the color wheel when \f1\fs18 start \f3\fs20 is also the default value of \f1\fs18 0.0 \f3\fs20 . See \f1\fs18 colors() \f3\fs20 for other color palettes.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (string)rgb2color(float\'a0rgb) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Converts an RGB color to a color string \f2\b0 . \f3 The RGB color is specified in \f1\fs18 rgb \f3\fs20 as a \f1\fs18 float \f3\fs20 vector of length three (red, green, blue). The equivalent color string is returned as singleton \f1\fs18 string \f3\fs20 specifying the color in the format \f1\fs18 "#RRGGBB" \f3\fs20 , such as \f1\fs18 "#007FC0" \f2\fs20 . \f3 RGB values will be clamped to the interval [0, 1]. \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 \expnd0\expndtw0\kerning0 This function can also be called with a matrix of RGB values, with three columns (red, green, blue). In this case, the returned \f1\fs18 string \f3\fs20 value will be a vector of color strings, with one element per row of \f1\fs18 rgb \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (float)rgb2hsv(float\'a0rgb) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Converts an RGB color to HSV \f2\b0 . \f3 The RGB color is specified in \f1\fs18 rgb \f3\fs20 as a \f1\fs18 float \f3\fs20 vector of length three (red, green, blue), and the equivalent HSV color is returned as a \f1\fs18 float \f3\fs20 vector of length three (hue, saturation, value). RGB values will be clamped to the interval [0, 1], and returned HSV values will also be in the interval [0, 1]. \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 \expnd0\expndtw0\kerning0 This function can also be called with a matrix of RGB values, with three columns (red, green, blue). In this case, the returned \f1\fs18 float \f3\fs20 value will be a matrix of HSV values, with three columns (hue, saturation, value) and one row per row of \f1\fs18 rgb \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string)terrainColors(integer$\'a0n)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \kerning1\expnd0\expndtw0 This method has been deprecated, and may be removed in a future release of Eidos. \f3\b0 In SLiM 3.5 and later, use \f1\fs18 colors(n, "terrain") \f3\fs20 instead.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf5 Generate colors in a \'93terrain\'94 color palette.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.11. Miscellaneous functions\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf2 (void)assert(logical\'a0assertions, [Ns$\'a0message\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Assert that a condition or conditions are true \f3\b0 . If any element of \f1\fs18 assertions \f3\fs20 is \f1\fs18 F \f3\fs20 , execution will be stopped. A message, \'93assertion failed\'94, will be printed before stopping; if \f1\fs18 message \f3\fs20 is not \f1\fs18 NULL \f3\fs20 ; its value will then be printed.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)beep([Ns$\'a0soundName\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Plays a sound or beeps. \f3\b0 On macOS in a GUI environment (i.e., in EidosScribe or SLiMgui), the optional parameter \f1\fs18 soundName \f3\fs20 can be the name of a sound file to play; in other cases (if \f1\fs18 soundName \f3\fs20 is \f1\fs18 NULL \f3\fs20 , or at the command line, or on platforms other than OS X) \f1\fs18 soundName \f3\fs20 is ignored and a standard system beep is played.\ When \f1\fs18 soundName \f3\fs20 is not \f1\fs18 NULL \f2\fs20 , \f3 a sound file in a supported format (such as \f1\fs18 .aiff \f3\fs20 or \f1\fs18 .mp3 \f3\fs20 ) is searched for sequentially in four standard locations, in this order: \f1\fs18 ~/Library/Sounds \f3\fs20 , \f1\fs18 /Library/Sounds \f3\fs20 , \f1\fs18 /Network/Library/Sounds \f3\fs20 , and finally \f1\fs18 /System/Library/Sounds \f3\fs20 . Standard OS X sounds located in \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 /System/Library/Sounds \f3\fs20 \cf0 \kerning1\expnd0\expndtw0 include \f1\fs18 "Basso" \f2\fs20 , \f3 \f1\fs18 "Blow" \f2\fs20 , \f3 \f1\fs18 "Bottle" \f2\fs20 , \f3 \f1\fs18 "Frog" \f2\fs20 , \f3 \f1\fs18 "Funk" \f2\fs20 , \f3 \f1\fs18 "Glass" \f2\fs20 , \f3 \f1\fs18 "Hero" \f2\fs20 , \f3 \f1\fs18 "Morse" \f2\fs20 , \f3 \f1\fs18 "Ping" \f2\fs20 , \f3 \f1\fs18 "Pop" \f2\fs20 , \f3 \f1\fs18 "Purr" \f2\fs20 , \f3 \f1\fs18 "Sosumi" \f2\fs20 , \f3 \f1\fs18 "Submarine" \f3\fs20 , and \f1\fs18 "Tink" \f3\fs20 . Do not include the file extension, such as \f1\fs18 .aiff \f3\fs20 or \f1\fs18 .mp3 \f3\fs20 , in \f1\fs18 soundName \f2\fs20 .\ \f0\b CAUTION: \f3\b0 When not running in EidosScribe or SLiMgui, it is often the case that the only simple means available to play a beep is to send a \f1\fs18 BEL \f3\fs20 character (ASCII 7) to the standard output. Unfortunately, when this is the case, it means that (1) no beep will be audible if output is being redirected into a file, and (2) a control character, \f1\fs18 ^G \f3\fs20 , will occur in the output at the point when the beep was requested. It is therefore recommended that \f1\fs18 beep() \f3\fs20 be used only when doing interactive work in a terminal shell (or in a GUI), not when producing output files. However, this issue is platform-specific; on some platforms \f1\fs18 beep() \f3\fs20 may result in a beep, and no emitted \f1\fs18 ^G \f3\fs20 , even when output is redirected. When a \f1\fs18 ^G \f3\fs20 must be emitted to the standard output to generate the beep, a warning message will also be emitted to make any associated problems easier to diagnose. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)citation(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Prints citation information for Eidos \f3\b0 to Eidos\'92s output stream. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (float$)clock([string$\'a0type\'a0=\'a0"cpu"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the value of a \f0\b system clock \f3\b0 . If \f1\fs18 type \f3\fs20 is \f1\fs18 "cpu" \f3\fs20 , this returns the current value of the CPU usage clock. This is the amount of CPU time used by the current process, in seconds; it is unrelated to the current time of day (for that, see the \f1\fs18 time() \f3\fs20 function). This is useful mainly for determining how much processor time a given section of code takes; \f1\fs18 clock() \f3\fs20 can be called before and after a block of code, and the end clock minus the start clock gives the elapsed CPU time consumed in the execution of the block of code. See also the \f1\fs18 timed \f3\fs20 parameter of \f1\fs18 executeLambda() \f3\fs20 , which automates this procedure. Note that if multiple cores are utilized by the process, the CPU usage clock will be the sum of the CPU usage across all cores, and may therefore run faster than the wall clock.\ If \f1\fs18 type \f3\fs20 is \f1\fs18 "mono" \f3\fs20 , this returns the value of the system\'92s monotonic clock. This represents user-perceived (\'93wall clock\'94) elapsed time from some arbitrary timebase (which will not change during the execution of the program), but it will not jump if the time zone or the wall clock time are changed for the system. This clock is useful for measuring user-perceived elapsed time, as described above, and may provide a more useful metric for performance than CPU time if multiple cores are being utilized. \f2 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string$)date(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b standard date string \f3\b0 for the current date in the local time of the executing machine. The format is \f1\fs18 %d-%m-%Y \f3\fs20 (day in two digits, then month in two digits, then year in four digits, zero-padded and separated by dashes) regardless of the localization of the executing machine, for predictability and consistency. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string$)debugIndent(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Returns the \f0\b indentation string \f3\b0 currently being used to start lines in the debugging output stream. In a pure Eidos context this will currently be the empty string, \f1\fs18 "" \f3\fs20 . In specific Contexts, such as SLiM, the debugging output stream may be structured with nested indentation, in which case this string will typically be a series of spaces or tabs. To make your debugging output (such as from \f1\fs18 cat() \f3\fs20 , \f1\fs18 catn() \f3\fs20 , or \f1\fs18 print() \f3\fs20 with the \f1\fs18 error=T \f3\fs20 optional argument set) line up with other output at the current level of execution nesting, you can start your new lines of output with this string if you wish.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)defineConstant(string$\'a0symbol, *\'a0value)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Defines a new constant \f3\b0 with the name \f1\fs18 symbol \f3\fs20 and the value specified by \f1\fs18 value \f3\fs20 . The name cannot previously be defined in any way (i.e., as either a variable or a constant). The defined constant acts identically to intrinsic Eidos constants such as \f1\fs18 T \f3\fs20 , \f1\fs18 NAN \f3\fs20 , and \f1\fs18 PI \f3\fs20 , and will remain defined for as long as the Eidos context lives even if it is defined inside a block being executed by \f1\fs18 executeLambda() \f3\fs20 , \f1\fs18 apply() \f3\fs20 , \f1\fs18 sapply() \f3\fs20 , or a Context-defined script block.\ Syntactically, \f1\fs18 value \f3\fs20 may be any value at all; semantically, however, if \f1\fs18 value \f3\fs20 is of \f1\fs18 object \f3\fs20 type then \f1\fs18 value \f3\fs20 \'92s class must be under an internal memory-management scheme called \'93retain-release\'94. Objects that are not under retain-release can cease to exist whenever the Context is finished using them, and thus a defined constant referencing such an object could become invalid, which must be prevented. Objects that are under retain-release will not cease to exist if they are referenced by a global constant; the reference to them from the global constant \'93retains\'94 them and keeps them in existence. All object classes built into Eidos are under retain-release; see the SLiM manual (section \'93SLiM scoping rules\'94) for discussion of which SLiM object classes are under retain-release.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)defineGlobal(string$\'a0symbol, *\'a0value)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Defines a new global variable \f3\b0 with the name \f1\fs18 symbol \f3\fs20 and the value specified by \f1\fs18 value \f3\fs20 . The name cannot previously be defined as a constant. The result is similar to a standard variable assignment with operator \f1\fs18 = \f3\fs20 , except that the variable is always defined in the global scope (even if the \f1\fs18 defineGlobal() \f3\fs20 call is made inside a user-defined function or other locally-scoped block, such as a SLiM event or callback). This means that the variable will remain defined even after the current scope is exited. Note that global variables can be hidden by local variables with the same name; unlike defined constants, such scoped masking is allowed.\ Syntactically, \f1\fs18 value \f3\fs20 may be any value at all; semantically, however, if \f1\fs18 value \f3\fs20 is of \f1\fs18 object \f3\fs20 type then \f1\fs18 value \f3\fs20 \'92s class must be under an internal memory-management scheme called \'93retain-release\'94. Objects that are not under retain-release can cease to exist whenever the Context is finished using them, and thus a global variable referencing such an object could become invalid, which must be prevented. Objects that are under retain-release will not cease to exist if they are referenced by a global variable; the reference to them from the global variable \'93retains\'94 them and keeps them in existence. All object classes built into Eidos are under retain-release; see the SLiM manual (section \'93SLiM scoping rules\'94) for discussion of which SLiM object classes are under retain-release.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (\cf2 \expnd0\expndtw0\kerning0 vNlifso\cf0 \kerning1\expnd0\expndtw0 )doCall(string$\'a0functionName, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the results from a \f0\b call to a specified function \f3\b0 . The function named by the parameter \f1\fs18 functionName \f3\fs20 is called, and the remaining parameters to \f1\fs18 doCall() \f3\fs20 are forwarded on to that function verbatim. This can be useful for calling one of a set of similar functions, such as \f1\fs18 sin() \f3\fs20 , \f1\fs18 cos() \f3\fs20 , etc., to perform a math function determined at runtime, or one of the \f1\fs18 as...() \f3\fs20 family of functions to convert to a type determined at runtime. Note that named arguments and default arguments, beyond the \f1\fs18 functionName \f3\fs20 argument, are not supported by \f1\fs18 doCall() \f3\fs20 ; all arguments to the target function must be specified explicitly, without names. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (\cf2 \expnd0\expndtw0\kerning0 vNlifso\cf0 \kerning1\expnd0\expndtw0 )executeLambda(string$\'a0lambdaSource, [ls$\'a0timed\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Executes a block of Eidos code \f3\b0 defined by \f1\fs18 lambdaSource \f3\fs20 . Eidos allows you to execute \f7\i lambdas \f3\i0 : blocks of Eidos code which can be called directly within the same scope as the caller. Eidos lambdas do not take arguments; for this reason, they are not first-class functions. (Since they share the scope of the caller, however, you may effectively pass values in and out of a lambda using variables.) The \f1\fs18 string \f3\fs20 argument \f1\fs18 lambdaSource \f3\fs20 may contain one or many Eidos statements as a single \f1\fs18 string \f3\fs20 value. Lambdas are represented, to the caller, only as the source code \f1\fs18 string \f3\fs20 \f1\fs18 lambdaSource \f3\fs20 ; the executable code is not made available programmatically. If an error occurs during the tokenization, parsing, or execution of the lambda, that error is raised as usual; executing code inside a lambda does not provide any additional protection against exceptions raised. The return value produced by the code in the lambda is returned by \f1\fs18 executeLambda() \f3\fs20 . If the optional parameter \f1\fs18 timed \f3\fs20 is \f1\fs18 T \f3\fs20 , the total (CPU clock) execution time for the lambda will be printed after the lambda has completed (see \f1\fs18 clock() \f3\fs20 ); if it is \f1\fs18 F \f3\fs20 (the default), no timing information will be printed. The \f1\fs18 timed \f3\fs20 parameter may also be \f1\fs18 "cpu" \f3\fs20 or \f1\fs18 "mono" \f3\fs20 to specifically request timing with the CPU clock (which will count the usage across all cores, and may thus run faster than wall clock time if multiple cores are being utilized) or the monotonic clock (which will correspond, more or less, to elapsed wall clock time regardless of multithreading); see the documentation for \f1\fs18 clock() \f3\fs20 for further discussion of these timing options.\cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 The current implementation of \f1\fs18 executeLambda() \f3\fs20 caches a tokenized and parsed version of \f1\fs18 lambdaSource \f3\fs20 , so calling \f1\fs18 executeLambda() \f3\fs20 repeatedly on a single source \f1\fs18 string \f3\fs20 is much more efficient than calling \f1\fs18 executeLambda() \f3\fs20 with a newly constructed \f1\fs18 string \f3\fs20 each time. If you can use a \f1\fs18 string \f3\fs20 literal for \f1\fs18 lambdaSource \f3\fs20 , or reuse a constructed source \f1\fs18 string \f3\fs20 stored in a variable, that will improve performance considerably. \f2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (logical)exists(string\'a0symbol)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns a \f1\fs18 logical \f3\fs20 vector indicating \f0\b whether symbols exist \f3\b0 . If a symbol has been defined as an intrinsic Eidos constant like \f1\fs18 T \f3\fs20 , \f1\fs18 INF \f3\fs20 , and \f1\fs18 PI \f3\fs20 , or as a Context-defined constant like \f1\fs18 sim \f3\fs20 in SLiM, or as a user-defined constant using \f1\fs18 defineConstant() \f3\fs20 , or as a variable by assignment, this function returns \f1\fs18 T \f3\fs20 . Otherwise, the symbol has not been defined, and \f1\fs18 exists() \f3\fs20 returns \f1\fs18 F \f3\fs20 . This is commonly used to check whether a user-defined constant already exists, with the intention of defining the constant if it has not already been defined. A vector of symbols may be passed, producing a vector of corresponding results. \f2 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)functionSignature([Ns$ \f2 \'a0 \f1 functionName \f2 \'a0 \f1 =\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Prints function signatures \f3\b0 for all functions (if \f1\fs18 functionName \f3\fs20 is \f1\fs18 NULL \f3\fs20 , the default), or for the function named by \f1\fs18 functionName \f3\fs20 , to Eidos\'92s output stream. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)functionSource(string$\'a0functionName)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Prints the Eidos source code \f3\b0 for the function specified by \f1\fs18 functionName \f3\fs20 , or prints a diagnostic message if the function is implemented in C++ rather than Eidos.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (integer$)getSeed(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns the \f0\b random number seed \f3\b0 . This is the last seed value set using \f1\fs18 setSeed() \f3\fs20 ; if \f1\fs18 setSeed() \f3\fs20 has not been called, it will be a seed value chosen based on the process-id and the current time when Eidos was initialized, unless the Context has set a different seed value. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)license(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Prints Eidos\'92s license terms \f3\b0 to Eidos\'92s output stream. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)ls(\cf2 [logical$\'a0showSymbolTables\'a0=\'a0F]\cf0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Prints all currently defined variables \f3\b0 to Eidos\'92s output stream. \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 Beginning in Eidos 2.5 (SLiM 3.5), the \f1\fs18 showSymbolTables \f3\fs20 optional argument can be set to \f1\fs18 T \f3\fs20 to request full information on the current symbol table chain. This will show which symbol table a given symbol is defined in, as well as revealing whether there are other symbols with the same name that have been masked by a local definition. This is mostly useful for debugging.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer$)parallelGetNumThreads(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Gets the number of threads \f3\b0 that is requested be used in subsequent parallel (i.e., multithreaded) regions, as set with \f1\fs18 parallelSetNumThreads() \f3\fs20 . If Eidos is not configured to run multithreaded, this function will return \f1\fs18 1 \f3\fs20 . See also \f1\fs18 parallelGetMaxThreads() \f3\fs20 , which returns the maximum number of threads that can be used. Note that if this function returns the maximum number of threads, as returned by \f1\fs18 parallelGetMaxThreads() \f3\fs20 , then there are \f7\i two possible semantic meanings \f3\i0 of that return value, which cannot be distinguished using this function; see \f1\fs18 parallelSetNumThreads() \f3\fs20 for discussion.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer$)parallelGetMaxThreads(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Gets the maximum number of threads \f3\b0 that can be used in parallel (i.e., multithreaded) regions. This is configured externally; it may be OpenMP\'92s default number of threads for the hardware platform being used, or may be set by an environment variable or command-line option. If Eidos is not configured to run multithreaded, this function will return \f1\fs18 1 \f3\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (object$)parallelGetTaskThreadCounts(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Gets the number of threads \f3\b0 that is requested to be used for specific tasks in Eidos and SLiM. Returns a new \f1\fs18 Dictionary \f3\fs20 containing values for all of the tasks for which a number of threads can be specified; see \f1\fs18 parallelSetTaskThreadCounts() \f3\fs20 for a list of all such tasks. Note that the specified number of threads will not necessarily be used in practice; in particular, a thread count set by \f1\fs18 parallelSetNumThreads() \f3\fs20 will override these per-task counts. Also, if the task size is below a certain task-specific threshold the task will not be executed in parallel regardless of these settings.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)parallelSetNumThreads([Ni$\'a0numThreads\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Sets the number of threads \f3\b0 that is requested to be used in subsequent parallel (i.e., multithreaded) regions. If Eidos is not configured to run multithreaded, this function will have no effect. The requested number of threads will be clamped to the interval [ \f1\fs18 1 \f3\fs20 , \f1\fs18 maxThreads \f3\fs20 ], where \f1\fs18 maxThreads \f3\fs20 is the maximum number of threads configured externally (either by OpenMP\'92s default, or by an environment variable or command-line option). That maximum number of threads (the value of \f1\fs18 maxThreads \f3\fs20 ) can be obtained from \f1\fs18 parallelGetMaxThreads() \f3\fs20 .\ There is an important wrinkle in the semantics of this method that must be explained. Passing \f1\fs18 NULL \f3\fs20 (the default) resets Eidos to the default number of threads for which it is configured to run. In this configuration, \f1\fs18 parallelGetNumThreads() \f3\fs20 will return \f1\fs18 maxThreads \f3\fs20 , but the number of threads used for any given parallel operation might not, in fact, be equal to \f1\fs18 maxThreads \f3\fs20 ; Eidos might use fewer threads if it determines that that would improve performance. Passing the value of \f1\fs18 maxThreads \f3\fs20 explicitly, on the other hand, sets Eidos to always use \f1\fs18 maxThreads \f3\fs20 threads, even if it may result in lower performance; but in this configuration, too, \f1\fs18 parallelGetNumThreads() \f3\fs20 will return \f1\fs18 maxThreads \f3\fs20 . For example, suppose \f1\fs18 maxThreads \f3\fs20 is \f1\fs18 16 \f3\fs20 . Passing \f1\fs18 NULL \f3\fs20 requests that Eidos use \f7\i up to \f3\i0 \f1\fs18 16 \f3\fs20 threads, as it sees fit; in contrast, explicitly passing \f1\fs18 16 \f3\fs20 requests that Eidos use \f7\i exactly \f3\i0 16 threads. In both cases, however, \f1\fs18 parallelGetNumThreads() \f3\fs20 will return \f1\fs18 16 \f3\fs20 .\ If you wish to temporarily change the number of threads used, the standard pattern is to call \f1\fs18 parallelSetNumThreads() \f3\fs20 with the number of threads you want to use, do the operation you wish to control, and then call \f1\fs18 parallelSetNumThreads(NULL) \f3\fs20 to return to the default behavior of Eidos.\ Note that the number of threads requested here overrides any per-task request set with \f1\fs18 parallelSetTaskThreadCounts() \f3\fs20 . Also, if the task size is below a certain task-specific threshold the task will not be executed in parallel regardless of these settings.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)parallelSetTaskThreadCounts(No$\'a0dict)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Sets the number of threads \f3\b0 that is requested to be used for specific tasks in Eidos and SLiM. The dictionary \f1\fs18 dict \f3\fs20 should contain \f1\fs18 string \f3\fs20 keys that identify tasks, and \f1\fs18 integer \f3\fs20 values that provide the number of threads to be used when performing those tasks. For example, a key of \f1\fs18 "LOG10_FLOAT" \f3\fs20 identifies the task of performing the \f1\fs18 log10() \f3\fs20 function on a \f1\fs18 float \f3\fs20 vector, and a value of \f1\fs18 8 \f3\fs20 for that key would tell Eidos to use eight threads when performing that task. The number of threads actually used will never be greater than the maximum thread count as returned by \f1\fs18 parallelGetMaxThreads() \f3\fs20 . Furthermore, a thread count set with \f1\fs18 parallelSetNumThreads() \f3\fs20 overrides the per-task setting, so if you wish to set specific per-task thread counts you should not set an overall thread count with \f1\fs18 parallelSetNumThreads() \f3\fs20 . If \f1\fs18 dict \f3\fs20 is \f1\fs18 NULL \f3\fs20 , all task thread counts will be reset to their default values.\ The currently requested thread counts for all tasks can be obtained with \f1\fs18 parallelGetTaskThreadCounts() \f3\fs20 . Note that the counts returned by that function may not match the counts requested with \f1\fs18 parallelSetTaskThreadCounts() \f3\fs20 ; in particular, they may be clipped to the maximum number of threads as returned by \f1\fs18 parallelGetMaxThreads() \f3\fs20 .\ The task keys recognized, and the tasks they govern, are:\ \pard\tx4320\pardeftab720\li1080\sa180\partightenfactor0 \f1\fs18 \cf2 "ABS_FLOAT" abs(float x)\uc0\u8232 "CEIL" ceil()\u8232 "EXP_FLOAT" exp(float x)\u8232 "FLOOR" floor()\u8232 "LOG_FLOAT" log(float x)\u8232 "LOG10_FLOAT" log10(float x)\u8232 "LOG2_FLOAT" log2(float x)\u8232 "ROUND" round()\u8232 "SQRT_FLOAT" sqrt(float x)\u8232 "SUM_INTEGER" sum(integer x)\u8232 "SUM_FLOAT" sum(float x)\u8232 "SUM_LOGICAL" sum(logical x)\u8232 "TRUNC" trunc()\ "MAX_INT" max(integer x)\uc0\u8232 "MAX_FLOAT" max(float x)\u8232 "MIN_INT" min(integer x)\u8232 "MIN_FLOAT" min(float x)\u8232 "PMAX_INT_1" pmax(i$ x, i y) / pmax(i x, i$ y)\u8232 "PMAX_INT_2" pmax(integer x, integer y)\u8232 "PMAX_FLOAT_1" pmax(f$ x, f y) / pmax(f x, f$ y)\u8232 "PMAX_FLOAT_2" pmax(float x, float y)\u8232 "PMIN_INT_1" pmin(i$ x, i y) / pmax(i x, i$ y)\u8232 "PMIN_INT_2" pmin(integer x, integer y)\u8232 "PMIN_FLOAT_1" pmin(f$ x, f y) / pmin(f x, f$ y)\u8232 "PMIN_FLOAT_2" pmin(float x, float y)\ "MATCH_INT" match(integer x, integer table)\uc0\u8232 "MATCH_FLOAT" match(float x, float table)\u8232 "MATCH_STRING" match(string x, string table)\u8232 "MATCH_OBJECT" match(object x, object table)\u8232 "SAMPLE_INDEX" sample() \f3\fs20 index buffer generation (internal) \f1\fs18 \uc0\u8232 "SAMPLE_R_INT" sample(integer x, weights=NULL)\u8232 "SAMPLE_R_FLOAT" sample(float x, weights=NULL)\u8232 "SAMPLE_R_OBJECT" sample(object x, weights=NULL)\u8232 "SAMPLE_WR_INT" sample(integer x, if weights)\u8232 "SAMPLE_WR_FLOAT" sample(float x, if weights)\u8232 "SAMPLE_WR_OBJECT" sample(object x, if weights)\u8232 "TABULATE_MAXBIN" tabulate() \f3\fs20 determination of \f1\fs18 maxbin \f3\fs20 (if not supplied) \f1\fs18 \uc0\u8232 "TABULATE" tabulate() \f3\fs20 main loop \f1\fs18 \ "DNORM_1" dnorm(numeric$ mean, numeric$ sd)\uc0\u8232 "DNORM_2" dnorm() \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RBINOM_1" rbinom(i$ size = 1, f$ prob = 0.5)\u8232 "RBINOM_2" rbinom(i$ size, f$ prob) \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RBINOM_3" rbinom() \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RDUNIF_1" rdunif(i$ min = 0, i$ max = 1) \f3\fs20 and similar \f1\fs18 \uc0\u8232 "RDUNIF_2" rdunif(i$ min, i$ max) \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RDUNIF_3" rdunif() \f3\fs20 other cases \f1\fs18 \uc0\u8232 "REXP_1" rexp(numeric$ mu)\u8232 "REXP_2" rexp() \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RNORM_1" rnorm(numeric$ mean, numeric$ sd)\u8232 "RNORM_2" rnorm(numeric$ sigma)\u8232 "RNORM_3" rnorm() \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RPOIS_1" rpois(numeric$ lambda)\u8232 "RPOIS_2" rpois() \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RUNIF_1" runif(numeric$ min = 0, numeric$ max = 1)\u8232 "RUNIF_2" runif(numeric$ min, numeric$ max) \f3\fs20 other cases \f1\fs18 \uc0\u8232 "RUNIF_3" runif() \f3\fs20 other cases \f1\fs18 \ "SORT_INT" sort(integer x)\uc0\u8232 "SORT_FLOAT" sort(float x)\u8232 "SORT_STRING" sort(string x)\ "CLIPPEDINTEGRAL_1S" clippedIntegral() \f3\fs20 for \f1\fs18 "x" \f3\fs20 , \f1\fs18 "y" \f3\fs20 , \f1\fs18 "z"\uc0\u8232 "CLIPPEDINTEGRAL_2S" clippedIntegral() \f3\fs20 for \f1\fs18 "xy" \f3\fs20 , \f1\fs18 "xz" \f3\fs20 , \f1\fs18 "yz"\uc0\u8232 "DRAWBYSTRENGTH" drawByStrength(returnDict=T)\u8232 "INTNEIGHCOUNT" interactingNeighborSount()\u8232 "LOCALPOPDENSITY" localPopulationDensity()\u8232 "NEARESTINTNEIGH" nearestInteractingNeighbors(returnDict=T)\u8232 "NEARESTNEIGH" nearestNeighbors(returnDict=T)\u8232 "NEIGHCOUNT" neighborCount()\u8232 "TOTNEIGHSTRENGTH" totalOfNeighborsStrengths()\ "POINT_IN_BOUNDS_1D" pointInBounds() \f3\fs20 , 1D case \f1\fs18 \uc0\u8232 "POINT_IN_BOUNDS_2D" pointInBounds() \f3\fs20 , 2D case \f1\fs18 \uc0\u8232 "POINT_IN_BOUNDS_3D" pointInBounds() \f3\fs20 , 3D case \f1\fs18 \uc0\u8232 "POINT_PERIODIC_1D" pointPeriodic() \f3\fs20 , 1D case \f1\fs18 \uc0\u8232 "POINT_PERIODIC_2D" pointPeriodic() \f3\fs20 , 2D case \f1\fs18 \uc0\u8232 "POINT_PERIODIC_3D" pointPeriodic() \f3\fs20 , 3D case \f1\fs18 \uc0\u8232 "POINT_REFLECTED_1D" pointReflected() \f3\fs20 , 1D case \f1\fs18 \uc0\u8232 "POINT_REFLECTED_2D" pointReflected() \f3\fs20 , 2D case \f1\fs18 \uc0\u8232 "POINT_REFLECTED_3D" pointReflected() \f3\fs20 , 3D case \f1\fs18 \uc0\u8232 "POINT_STOPPED_1D" pointStopped() \f3\fs20 , 1D case \f1\fs18 \uc0\u8232 "POINT_STOPPED_2D" pointStopped() \f3\fs20 , 2D case \f1\fs18 \uc0\u8232 "POINT_STOPPED_3D" pointStopped() \f3\fs20 , 3D case \f1\fs18 \uc0\u8232 "POINT_UNIFORM_1D" pointUniform() \f3\fs20 , 1D case \f1\fs18 \uc0\u8232 "POINT_UNIFORM_2D" pointUniform() \f3\fs20 , 2D case \f1\fs18 \uc0\u8232 "POINT_UNIFORM_3D" pointUniform() \f3\fs20 , 3D case \f1\fs18 \uc0\u8232 "SET_SPATIAL_POS_1_1D" setSpatialPosition() \f3\fs20 with one point, 1D case \f1\fs18 \uc0\u8232 "SET_SPATIAL_POS_1_2D" setSpatialPosition() \f3\fs20 with one point, 2D case \f1\fs18 \uc0\u8232 "SET_SPATIAL_POS_1_3D" setSpatialPosition() \f3\fs20 with one point, 3D case \f1\fs18 \uc0\u8232 "SET_SPATIAL_POS_2_1D" setSpatialPosition() \f3\fs20 with \f7\i N \f3\i0 points, 1D case \f1\fs18 \uc0\u8232 "SET_SPATIAL_POS_2_2D" setSpatialPosition() \f3\fs20 with \f7\i N \f3\i0 points, 2D case \f1\fs18 \uc0\u8232 "SET_SPATIAL_POS_2_3D" setSpatialPosition() \f3\fs20 with \f7\i N \f3\i0 points, 3D case \f1\fs18 \uc0\u8232 "SPATIAL_MAP_VALUE" spatialMapValue()\ "CONTAINS_MARKER_MUT" containsMarkerMutation(returnMutation = F)\uc0\u8232 "I_COUNT_OF_MUTS_OF_TYPE" countOfMutationsOfType() (Individual)\u8232 "H_COUNT_OF_MUTS_OF_TYPE" countOfMutationsOfType() (Haplosome)\u8232 "INDS_W_PEDIGREE_IDS" individualsWithPedigreeIDs()\u8232 "RELATEDNESS" relatedness()\u8232 "SAMPLE_INDIVIDUALS_1" sampleIndividuals() \f3\fs20 simple case with replace=T \f1\fs18 \uc0\u8232 "SAMPLE_INDIVIDUALS_2" sampleIndividuals() \f3\fs20 base case with replace=T \f1\fs18 \uc0\u8232 "SET_FITNESS_SCALE_1" Individual.fitness = \f3\fs20 one value \f1\fs18 \uc0\u8232 "SET_FITNESS_SCALE_2" Individual.fitness = \f7\i\fs20 N \f3\i0 values \f1\fs18 \uc0\u8232 "SUM_OF_MUTS_OF_TYPE" sumOfMutationsOfType()\ "AGE_INCR" \f3\fs20 incrementing \f1\fs18 Individual age \f3\fs20 values \f1\fs18 \uc0\u8232 "DEFERRED_REPRO" \f3\fs20 deferred nonWF reproduction \f1\fs18 \uc0\u8232 "WF_REPRO" \f3\fs20 WF reproduction (no callbacks) \f1\fs18 \uc0\u8232 "FITNESS_ASEX_1" \f3\fs20 fitness eval, asex, individual \f1\fs18 fitnessScaling\uc0\u8232 "FITNESS_ASEX_2" \f3\fs20 fitness eval, asex, no \f1\fs18 fitnessScaling \f3\fs20 or mutations \f1\fs18 \uc0\u8232 "FITNESS_ASEX_3" \f3\fs20 fitness eval, asex, \f1\fs18 fitnessScaling \f3\fs20 and mutations \f1\fs18 \uc0\u8232 "FITNESS_SEX_1" \f3\fs20 fitness eval, sexual, individual \f1\fs18 fitnessScaling\uc0\u8232 "FITNESS_SEX_2" \f3\fs20 fitness eval, sexual, no \f1\fs18 fitnessScaling \f3\fs20 or mutations \f1\fs18 \uc0\u8232 "FITNESS_SEX_3" \f3\fs20 fitness eval, sexual, \f1\fs18 fitnessScaling \f3\fs20 and mutations \f1\fs18 \uc0\u8232 "MIGRANT_CLEAR" \f3\fs20 clearing the \f1\fs18 migrant \f3\fs20 property at tick end \f1\fs18 \uc0\u8232 "SIMPLIFY_SORT_PRE" \f3\fs20 preparation for simplification sorting (internal) \f1\fs18 \uc0\u8232 "SIMPLIFY_SORT" \f3\fs20 simplification sorting \f1\fs18 \uc0\u8232 "SIMPLIFY_SORT_POST" \f3\fs20 cleanup after simplification sorting (internal) \f1\fs18 \uc0\u8232 "PARENTS_CLEAR" \f3\fs20 clearing parental haplosomes at tick end in WF models \f1\fs18 \uc0\u8232 "UNIQUE_MUTRUNS" \f3\fs20 uniquing mutation runs (internal bookkeeping) \f1\fs18 \uc0\u8232 "SURVIVAL" \f3\fs20 survival evaluation (no callbacks) \f1\fs18 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 Typically, a dictionary of task keys and thread counts is read from a file and set up with this function at initialization time, but it is also possible to change new task thread counts dynamically. If Eidos is not configured to run multithreaded, this function has no effect.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (string$)readLine(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Reads a line of input from the \'93standard input\'94 file. \f3\b0 This function is intended to allow for communication between a running Eidos script (such as a SLiM simulation) and an external process that is sending input to it. It reads one line from the standard input and returns it as a singleton \f1\fs18 string \f3\fs20 . This is done with the C++ function \f1\fs18 std::getline() \f3\fs20 , using the \f1\fs18 std::cin \f3\fs20 file. If that call returns false (typically because the end-of-file was reached or there was a read error of some kind), the empty string \f1\fs18 "" \f3\fs20 will be returned. The \f1\fs18 system() \f3\fs20 function can also be helpful for related situations.\ This function will raise an error if called in a GUI environment such as EidosScribe, SLiMgui, or SLiMguiLegacy, since there is no standard input set up for the running script in those environments.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)rm([Ns\'a0variableNames\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Removes variables \f3\b0 from the Eidos namespace; in other words, it causes the variables to become undefined. Variables are specified by their \f1\fs18 string \f3\fs20 name in the \f1\fs18 variableNames \f3\fs20 parameter. If the optional \f1\fs18 variableNames \f3\fs20 parameter is \f1\fs18 NULL \f3\fs20 (the default), \f7\i all \f3\i0 variables will be removed (be careful!).\ In SLiM 3, there was an optional parameter \f1\fs18 removeConstants \f3\fs20 that, if \f1\fs18 T \f3\fs20 , allowed you to remove defined constants (and then potentially redefine them to have a different value). The \f1\fs18 removeConstants \f3\fs20 parameter was removed in SLiM 4, since the \f1\fs18 defineGlobal() \f3\fs20 function now provides the ability to define (and redefine) global variables that are not constant.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (*)sapply(*\'a0x, string$\'a0lambdaSource, [string$\'a0simplify\'a0=\'a0"vector"])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f7\i\fs20 \cf4 \expnd0\expndtw0\kerning0 Named \f5\fs18 apply() \f7\fs20 prior to Eidos 1.6 / SLiM 2.6\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\i0\b \cf2 Applies a block of Eidos code to the elements of x \f3\b0 . This function is sort of a hybrid between \f1\fs18 c() \f3\fs20 and \f1\fs18 executeLambda() \f3\fs20 ; it might be useful to consult the documentation for both of those functions to better understand what \f1\fs18 sapply() \f3\fs20 does. For each element in \f1\fs18 x \f3\fs20 , the lambda defined by \f1\fs18 lambdaSource \f3\fs20 will be called. For the duration of that callout, a variable named \f1\fs18 applyValue \f3\fs20 will be defined to have as its value the element of \f1\fs18 x \f3\fs20 currently being processed. The expectation is that the lambda will use \f1\fs18 applyValue \f3\fs20 in some way, and will return either \f1\fs18 NULL \f3\fs20 or a new value (which need not be a singleton, and need not be of the same type as \f1\fs18 x \f3\fs20 ). The return value of \f1\fs18 sapply() \f3\fs20 is generated by concatenating together all of the individual vectors returned by the lambda, in exactly the same manner as the \f1\fs18 c() \f3\fs20 function (including the possibility of type promotion).\ Since this function can be hard to understand at first, here is an example:\ \f1\fs18 sapply(1:10, "if (applyValue % 2) applyValue ^ 2; else NULL;");\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \kerning1\expnd0\expndtw0 This produces the output \f1\fs18 1 9 25 49 81 \f3\fs20 . The \f1\fs18 sapply() \f3\fs20 operation begins with the vector \f1\fs18 1:10 \f3\fs20 . For each element of that vector, the lambda is called and \f1\fs18 applyValue \f3\fs20 is defined with the element value. In this respect, \f1\fs18 sapply() \f3\fs20 is actually very much like a \f1\fs18 for \f3\fs20 loop. If \f1\fs18 applyValue \f3\fs20 is even (as evaluated by the modulo operator, \f1\fs18 % \f3\fs20 ), the condition of the \f1\fs18 if \f3\fs20 statement is \f1\fs18 F \f3\fs20 and so \f1\fs18 NULL \f3\fs20 is returned by the lambda; this must be done explicitly, since a \f1\fs18 void \f3\fs20 return is not allowed by \f1\fs18 sapply() \f3\fs20 . If \f1\fs18 applyValue \f3\fs20 is odd, on the other hand, the lambda returns its square (as calculated by the exponential operator, \f1\fs18 ^ \f3\fs20 ). Just as with the \f1\fs18 c() \f3\fs20 function, \f1\fs18 NULL \f3\fs20 values are dropped during concatenation, so the final result contains only the squares of the odd values.\expnd0\expndtw0\kerning0 \ This example illustrates that the lambda can \'93drop\'94 values by returning \f1\fs18 NULL \f3\fs20 , so \f1\fs18 sapply() \f3\fs20 can be used to select particular elements of a vector that satisfy some condition, much like the subscript operator, \f1\fs18 [] \f3\fs20 . The example also illustrates that input and result types do not have to match; the vector passed in is \f1\fs18 integer \f3\fs20 , whereas the result vector is \f1\fs18 float \f3\fs20 .\ Beginning in Eidos 1.6, a new optional parameter named \f1\fs18 simplify \f3\fs20 allows the result of \f1\fs18 sapply() \f3\fs20 to be a matrix or array in certain cases, better organizing the elements of the result. If the \f1\fs18 simplify \f3\fs20 parameter is \f1\fs18 "vector" \f3\fs20 , the concatenated result value is returned as a plain vector in all cases; this is the default behavior, for backward compatibility. Two other possible values for \f1\fs18 simplify \f3\fs20 are presently supported. If \f1\fs18 simplify \f3\fs20 is \f1\fs18 "matrix" \f3\fs20 , the concatenated result value will be turned into a matrix with one column for each non- \f1\fs18 NULL \f3\fs20 value returned by the lambda, as if the values were joined together with \f1\fs18 cbind() \f3\fs20 , as long as all of the lambda\'92s return values are either (a) \f1\fs18 NULL \f3\fs20 or (b) the same length as the other non- \f1\fs18 NULL \f3\fs20 values returned. If \f1\fs18 simplify \f3\fs20 is \f1\fs18 "match" \f3\fs20 , the concatenated result value will be turned into a vector, matrix, or array that exactly matches the dimensions as \f1\fs18 x \f3\fs20 , with a one-to-one correspondence between \f1\fs18 x \f3\fs20 and the elements of the return value just like a unary operator, as long as all of the lambda\'92s return values are singletons (with no \f1\fs18 NULL \f3\fs20 values). Both \f1\fs18 "matrix" \f3\fs20 and \f1\fs18 "match" \f3\fs20 will raise an error if their preconditions are not met, to avoid unexpected behavior, so care should be taken that the preconditions are always met when these options are used.\ As with \f1\fs18 executeLambda() \f3\fs20 , all defined variables are accessible within the lambda, and changes made to variables inside the lambda will persist beyond the end of the \f1\fs18 sapply() \f3\fs20 call; the lambda is executing in the same scope as the rest of your code.\ The \f1\fs18 sapply() \f3\fs20 function can seem daunting at first, but it is an essential tool in the Eidos toolbox. It combines the iteration of a \f1\fs18 for \f3\fs20 loop, the ability to select elements like operator \f1\fs18 [] \f3\fs20 , and the ability to assemble results of mixed type together into a single vector like \f1\fs18 c() \f3\fs20 , all with the power of arbitrary Eidos code execution like \f1\fs18 executeLambda() \f3\fs20 . It is relatively fast, compared to other ways of achieving similar results such as a \f1\fs18 for \f3\fs20 loop that accumulates results with \f1\fs18 c() \f3\fs20 . Like \f1\fs18 executeLambda() \f3\fs20 , \f1\fs18 sapply() \f3\fs20 is most efficient if it is called multiple times with a single \f1\fs18 string \f3\fs20 script variable, rather than with a newly constructed \f1\fs18 string \f3\fs20 for \f1\fs18 lambdaSource \f3\fs20 each time.\ Prior to Eidos 1.6 (SLiM 2.6), \f1\fs18 sapply() \f3\fs20 was instead named \f1\fs18 apply() \f3\fs20 ; it was renamed to \f1\fs18 sapply() \f3\fs20 in order to more closely match the naming of functions in R. This renaming allowed a new \f1\fs18 apply() \f3\fs20 function to be added to Eidos that operates on the margins of matrices and arrays, similar to the \f1\fs18 apply() \f3\fs20 function of R (see \f1\fs18 apply() \f3\fs20 , above).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (void)setSeed(integer$\'a0seed) \f2 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Set the random number seed \f3\b0 . Future random numbers will be based upon the seed value set, and the random number sequence generated from a particular seed value is guaranteed to be reproducible. The last seed set can be recovered with the \f1\fs18 getSeed() \f3\fs20 function.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)source(string$ \f2 \'a0 \f1 filePath\cf2 , [logical$\'a0chdir\'a0=\'a0F]\cf0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Executes the contents of an Eidos source file \f3\b0 found at the filesystem path \f1\fs18 filePath \f3\fs20 . This is essentially shorthand for calling \f1\fs18 readFile() \f3\fs20 , joining the read lines with newlines to form a single string using \f1\fs18 paste() \f3\fs20 , and then passing that string to \f1\fs18 executeLambda() \f3\fs20 . The source file must consist of complete Eidos statements. Regardless of what the last executed source line evaluates to, \f1\fs18 source() \f3\fs20 has no return value. If no file exists at \f1\fs18 filePath \f3\fs20 , an error will be raised.\ The \f1\fs18 chdir \f3\fs20 parameter controls the current working directory in effect while the source file is executed. If \f1\fs18 chdir \f3\fs20 is \f1\fs18 F \f3\fs20 (the default), the current working directory will remain unchanged. If \f1\fs18 chdir \f3\fs20 is \f1\fs18 T \f3\fs20 , the current working directory will be temporarily changed to the filesystem path at which the source file is located, and restored after execution of the source file is complete.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)stop([Ns$ \f2 \'a0 \f1 message \f2 \'a0 \f1 =\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Stops execution \f3\b0 of Eidos (and of the Context, such as the running SLiM simulation, if applicable), in the event of an error. If the optional \f1\fs18 message \f3\fs20 parameter is not \f1\fs18 NULL \f3\fs20 , it will be printed to Eidos\'92s output stream prior to stopping. \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (logical$)suppressWarnings(logical$\'a0suppress)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Turns suppression of warning messages on or off \f3\b0 . The \f1\fs18 suppress \f3\fs20 flag indicates whether suppression of warnings should be enabled ( \f1\fs18 T \f3\fs20 ) or disabled ( \f1\fs18 F \f3\fs20 ). The previous warning-suppression value is returned by \f1\fs18 suppressWarnings() \f3\fs20 , making it easy to suppress warnings from a given call and then return to the previous suppression state afterwards. It is recommended that warnings be suppressed only around short blocks of code (not all the time), so that unexpected but perhaps important warnings are not missed. And of course warnings are generally emitted for good reasons; before deciding to disregard a given warning, make sure that you understand exactly why it is being issued, and are certain that it does not represent a serious problem.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (*)sysinfo(string$\'a0key)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 Returns information about the system \f3\b0 . The information returned by \f1\fs18 tempdir() \f3\fs20 depends upon the value of \f1\fs18 key \f3\fs20 , which selects one of the pieces of information listed:\ \pard\pardeftab2160\li547\fi360\sa60\partightenfactor0 \f0\b \cf2 \ul \ulc2 key \f3\b0 \ulnone \f0\b \ul value \f3\b0 \ulnone \ \pard\pardeftab2160\li547\fi360\sa60\partightenfactor0 \f1\fs18 \cf2 os \f3\fs20 the name of the OS; \f1\fs18 "macOS" \f3\fs20 or \f1\fs18 "Windows" \f3\fs20 , or \f1\fs18 "Unix" \f3\fs20 for all others\ \f1\fs18 sysname \f3\fs20 the name of the kernel\ \f1\fs18 release \f3\fs20 the operating system (kernel) release\ \f1\fs18 version \f3\fs20 the operating system (kernel) version\ \f1\fs18 nodename \f3\fs20 the name by which the machine is known on the network\ \f1\fs18 machine \f3\fs20 the hardware type; often the CPU type (e.g., \f1\fs18 "x86_64" \f3\fs20 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 The value \f1\fs18 "unknown" \f3\fs20 will be returned for a key if the correct value cannot be ascertained. Note that the values of keys that refer to the kernel may not be what you expect; for example, on one particular macOS 10.15.7 system, \f1\fs18 sysname \f3\fs20 returns \f1\fs18 "Darwin" \f3\fs20 , \f1\fs18 release \f3\fs20 returns \f1\fs18 "19.6.0" \f3\fs20 , and \f1\fs18 version \f3\fs20 returns \f1\fs18 "Darwin Kernel Version 19.6.0: Thu Sep 16 20:58:47 PDT 2021; root:xnu-6153.141.40.1~1/RELEASE_X86_64" \f3\fs20 .\ Further keys can be added if there is information that would be useful, particularly if a cross-platform way to obtain the information can be found.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (string)system(string$\'a0command, [string\'a0args\'a0=\'a0""], [string\'a0input\'a0=\'a0""], [logical$\'a0stderr\'a0=\'a0F], [logical$\'a0wait\'a0=\'a0T])\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf0 Runs a Un*x command in a \f9\fs18 /bin/sh \f0\fs20 shell \f3\b0 with optional arguments and input, and returns the result as a vector of output lines. The \f1\fs18 args \f3\fs20 parameter may contain a vector of arguments to \f1\fs18 command \f3\fs20 ; they will be passed directly to the shell without any quoting, so applying the appropriate quoting as needed by \f1\fs18 /bin/sh \f3\fs20 is the caller\'92s responsibility. The arguments are appended to \f1\fs18 command \f3\fs20 , separated by spaces, and the result is passed to the shell as a single command string, so arguments may simply be given as part of \f1\fs18 command \f3\fs20 instead, if preferred. By default no input is supplied to \f1\fs18 command \f3\fs20 ; if \f1\fs18 input \f3\fs20 is non-empty, however, it will be written to a temporary file (one line per \f1\fs18 string \f3\fs20 element) and the standard input of \f1\fs18 command \f3\fs20 will be redirected to that temporary file (using standard \f1\fs18 /bin/sh \f3\fs20 redirection with \f1\fs18 < \f3\fs20 , appended to the command string passed to the shell). By default, output sent to standard error will not be captured (and thus may end up in the output of the SLiM process, or may be lost); if \f1\fs18 stderr \f3\fs20 is \f1\fs18 T \f3\fs20 , however, the standard error stream will be redirected into standard out (using standard \f1\fs18 /bin/sh \f3\fs20 redirection with \f1\fs18 2>&1 \f3\fs20 , appended to the command string passed to the shell).\ Arbitrary command strings involving multiple commands, pipes, redirection, etc., may be used with \f1\fs18 system() \f3\fs20 , but may be incompatible with the way that \f1\fs18 args \f2\fs20 , \f3 \f1\fs18 input \f3\fs20 , and \f1\fs18 stderr \f3\fs20 are handled by this function, so in this case supplying the whole command string in \f1\fs18 command \f3\fs20 may be the simplest course. You may redirect standard error into standard output yourself in \f1\fs18 command \f3\fs20 with \f1\fs18 2>&1 \f2\fs20 . \f3 Supplying input to a complex command line can often be facilitated by the use of parentheses to create a subshell; for example,\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab543\li900\ri1440\sb180\sa180\partightenfactor0 \f1\fs18 \cf0 system("(wc -l | sed 's/ //g')", input=c('foo', 'bar', 'baz')); \f2 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 will supply the input lines to \f1\fs18 wc \f3\fs20 courtesy of the subshell started for the \f1\fs18 () \f3\fs20 operator. If this strategy doesn\'92t work for the command line you want to execute, you can always write a temporary file yourself using \f1\fs18 writeFile() \f3\fs20 or \f1\fs18 writeTempFile() \f3\fs20 and redirect that file to standard input in \f1\fs18 command \f3\fs20 with \f1\fs18 < \f2\fs20 .\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f3 \cf2 \expnd0\expndtw0\kerning0 If \f1\fs18 wait \f3\fs20 is \f1\fs18 T \f3\fs20 (the default), \f1\fs18 system() \f3\fs20 will wait for the command to finish, and return the output generated as a \f1\fs18 string \f3\fs20 vector, as described above. If \f1\fs18 wait \f3\fs20 is \f1\fs18 F \f3\fs20 , \f1\fs18 system() \f3\fs20 will instead append \f1\fs18 " &" \f3\fs20 to the end of the command line to request that it be run in the background, and it will not collect and return the output from the command; instead it will return \f1\fs18 string(0) \f3\fs20 immediately. If the output from the command is needed, it could be redirected to a file, and that file could be checked periodically in Eidos for some indication that the command had completed; if output is not redirected to a file, it may appear in SLiM\'92s output stream. If the final command line executed by \f1\fs18 system() \f3\fs20 ends in \f1\fs18 " &" \f3\fs20 , the behavior of \f1\fs18 system() \f3\fs20 should be just as if \f1\fs18 wait=T \f3\fs20 had been supplied, but it is recommended to use \f1\fs18 wait=T \f3\fs20 instead to ensure that the command line is correctly assembled.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 There is an example at {\field{\*\fldinst{HYPERLINK "https://github.com/MesserLab/SLiM-Extras/blob/master/functions/rgnorm.slim"}}{\fldrslt \cf3 \ul \ulc3 https://github.com/MesserLab/SLiM-Extras/blob/master/functions/rgnorm.slim}} that demonstrates the use of \f1\fs18 system() \f3\fs20 , calling out to Python, to obtain draws from a generalized normal distribution (which is not supported intrinsically by Eidos). That example even includes internal buffering of a large number of draws, making it a reasonably efficient solution.\expnd0\expndtw0\kerning0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (string$)time(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf0 Returns a \f0\b standard time string \f3\b0 for the current time in the local time of the executing machine. The format is \f1\fs18 %H:%M:%S \f3\fs20 (hour in two digits, then minute in two digits, then seconds in two digits, zero-padded and separated by dashes) regardless of the localization of the executing machine, for predictability and consistency. The 24-hour clock time is used (i.e., no AM/PM). \f2 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (float$)usage(\kerning1\expnd0\expndtw0 [ls$\'a0type\'a0=\'a0"rss"]\expnd0\expndtw0\kerning0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs20 \cf2 \kerning1\expnd0\expndtw0 Returns the \f0\b memory usage \f3\b0 . This is the amount of memory used by the current process, in MB (megabytes); multiply by \f1\fs18 1024*1024 \f3\fs20 to get the usage in bytes.\ Memory usage is a surprisingly complex topic. One metric reported by \f1\fs18 usage() \f3\fs20 is the resident set size, or RSS, which includes memory usage from shared libraries, but does not include memory that is swapped out or has never been used. For most purposes, RSS is a useful metric of memory usage from a practical perspective. On some platforms (AIX, BSD, Solaris) the memory usage reported may be zero, but it should be correct on both macOS and Linux platforms. On macOS, memory pages that have not been used for a while may get compressed by the kernel to reduce the RSS of the process; the RSS metric reported by \f1\fs18 usage() \f3\fs20 will reflect the compressed size of such pages, not their original size, so surprising decreases in memory usage may be observed when the kernel decides to compress some memory pages. The RSS is requested with a \f1\fs18 type \f3\fs20 of \f1\fs18 "rss" \f3\fs20 , which is the default; for historical reasons, it can also be requested with a \f1\fs18 type \f3\fs20 of \f1\fs18 F \f3\fs20 .\ Another metric reported by \f1\fs18 usage() \f3\fs20 is the peak RSS. This is just the highest RSS value that has ever been recorded by the kernel. It should generally mirror the behavior of RSS, except that it ratchets upward monotonically. The peak RSS is requested with a \f1\fs18 type \f3\fs20 of \f1\fs18 "rss_peak" \f3\fs20 ; for historical reasons, it can also be requested with a \f1\fs18 type \f3\fs20 of \f1\fs18 T \f3\fs20 .\ The third metric currently reported by \f1\fs18 usage() \f3\fs20 is the virtual memory usage. This is essentially the amount of memory used by pages that have been assigned to the process, whether those pages are resident, compressed, or swapped. It is typically much larger than the RSS, because it includes various types of memory that are not counted in the RSS; indeed, for some system configurations the virtual memory usage can be reported as being the entire memory space of the computer. Whether it is a useful metric will be platform-dependent; \f7\i caveat emptor \f3\i0 .\ This function can be useful for documenting the memory usage of long runs as they are in progress. In SLiM, the RSS could also be used to trigger tree-sequence simplification with a call to \f1\fs18 treeSeqSimplify() \f3\fs20 , to reduce memory usage when it becomes too large, but keep in mind that the simplification process itself may cause a substantial spike in memory usage, and that page compression and swaps may reduce the RSS even though the memory actually used by tree-sequence recording continues to increase.\ When running under SLiM, other tools for monitoring memory usage include the \f1\fs18 slim \f3\fs20 command-line options \f1\fs18 -m[em] \f3\fs20 and \f1\fs18 -M[emhist] \f3\fs20 , and the \f1\fs18 usage() \f3\fs20 and \f1\fs18 outputUsage() \f3\fs20 methods of \f1\fs18 Community \f3\fs20 ; see the SLiM manual for more information.\expnd0\expndtw0\kerning0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (float)version(\cf2 \expnd0\expndtw0\kerning0 [logical$\'a0print\'a0=\'a0T]\cf0 \kerning1\expnd0\expndtw0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 \expnd0\expndtw0\kerning0 Get Eidos\'92s version. \f3\b0 There are two ways to use this function. If \f1\fs18 print \f3\fs20 is \f1\fs18 T \f3\fs20 , the default, then the version number is printed to the Eidos output stream in a formatted manner, like \'93 \f1\fs18 Eidos version 2.1 \f3\fs20 \'94. If Eidos is attached to a Context that provides a version number, that is also printed, like \'93 \f1\fs18 SLiM version 3.1 \f3\fs20 \'94. In this case, the Eidos version number, and the Context version number if available, are returned as an invisible \f1\fs18 float \f3\fs20 vector. This is most useful when using Eidos interactively. If \f1\fs18 print \f3\fs20 is \f1\fs18 F \f3\fs20 , on the other hand, nothing is printed, but the returned \f1\fs18 float \f3\fs20 vector of version numbers is not invisible. This is useful for scripts that need to test the Eidos or Context version they are running against.\ In both cases, in the \f1\fs18 float \f3\fs20 version numbers returned, a version like 2.4.2 would be returned as \f1\fs18 2.42 \f3\fs20 ; this would not scale well to subversions greater than nine, so that will be avoided in our versioning.} ================================================ FILE: EidosScribe/EidosHelpOperators.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2513 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Italic;\f1\fnil\fcharset0 Menlo-Italic;\f2\froman\fcharset0 TimesNewRomanPS-ItalicMT; \f3\fswiss\fcharset0 Optima-Regular;\f4\fnil\fcharset0 Menlo-Regular;\f5\froman\fcharset0 TimesNewRomanPSMT; } {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab720 \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i\fs22 \cf0 2.2.2 ITEM: 1. Sequences: operator \f1\fs18 : \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 The \f4\fs18 : \f3\fs22 operator is used to construct vectors with (usually) more than one value. In particular, it is used to construct \f0\i sequences \f3\i0 , and so it is called the sequence operator. Given operands \f4\fs18 x \f3\fs22 and \f4\fs18 y \f3\fs22 (standing for any two numbers), the sequence operator starts at \f4\fs18 x \f3\fs22 and counts, by \f4\fs18 1 \f3\fs22 (or \f4\fs18 -1 \f3\fs22 , as appropriate) toward \f4\fs18 y \f3\fs22 without passing it. It yields a vector containing all of the numbers it encounters along the way.\ Note that the sequence operator can count down as well as up, that it can handle \f4\fs18 float \f3\fs22 as well as \f4\fs18 integer \f3\fs22 operands, and that negative numbers are allowed.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.2.4 ITEM: 2. Subsets: operator \f1\fs18 [] \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 The \f4\fs18 [] \f3\fs22 operator selects a subset of the vector upon which it operates; it is thus often called the subset operator. It can work in one of two different ways, depending upon whether it is given an \f4\fs18 integer \f3\fs22 vector of indices, or is given a \f4\fs18 logical \f3\fs22 vector of selectors.\ First of all, a subset can be selected with an \f4\fs18 integer \f3\fs22 vector of indices. These indices are zero-based, like C but unlike R; the first value in a vector is thus at index \f4\fs18 0 \f3\fs22 , not index \f4\fs18 1 \f3\fs22 . Note that a given index can be used multiple times.\ Second, a subset can be selected with a \f4\fs18 logical \f3\fs22 vector of selectors. In this case, the \f4\fs18 logical \f3\fs22 vector must be the same length as the vector being selected; each \f4\fs18 logical \f3\fs22 value indicates whether the corresponding vector value should be selected ( \f4\fs18 T \f3\fs22 ) or not ( \f4\fs18 F \f3\fs22 ).\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.3.1 ITEM: 3. Arithmetic operators: \f1\fs18 + \f0\fs22 , \f1\fs18 - \f0\fs22 , \f1\fs18 * \f0\fs22 , \f1\fs18 / \f0\fs22 , \f1\fs18 % \f0\fs22 , \f1\fs18 ^ \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 These are the standard operators of arithmetic; \f4\fs18 + \f3\fs22 performs addition, \f4\fs18 - \f3\fs22 performs subtraction, \f4\fs18 * \f3\fs22 performs multiplication, \f4\fs18 / \f3\fs22 performs division, \f4\fs18 % \f3\fs22 performs a modulo operation (more on that below), and \f4\fs18 ^ \f3\fs22 performs exponentiation. Not a great deal needs to be said about these operators, which behave according to the standard rules of mathematics. They also follow the standard rules of \'93precedence\'94; exponentiation is the highest precedence, addition and subtraction are the lowest precedence, and the other three are in the middle, so \f4\fs18 4^2+5*6^7 \f3\fs22 is grouped as ( \f4\fs18 4^3)+(5*(6^7)) \f3\fs22 , as expected if you remember your grade-school math.\ There are only a few minor twists to be discussed. One is the meaning of the \f4\fs18 % \f3\fs22 operator, which many people have not previously encountered. This computes the \'93modulo\'94 from a division, which is the remainder left behind after division. For example, \f4\fs18 13%6 \f3\fs22 is \f4\fs18 1 \f3\fs22 , because after \f4\fs18 13 \f3\fs22 is divided evenly by \f4\fs18 6 \f3\fs22 (taking care of \f4\fs18 12 \f3\fs22 of the \f4\fs18 13 \f3\fs22 ), \f4\fs18 1 \f3\fs22 is left as a remainder. Probably the most common use of \f4\fs18 % \f3\fs22 is in determining whether a number is even or odd by looking at the result of a \f4\fs18 %2 \f3\fs22 operation; \f4\fs18 5%2 \f3\fs22 is \f4\fs18 1 \f3\fs22 , indicating that \f4\fs18 5 \f3\fs22 is odd, whereas \f4\fs18 6%2 \f3\fs22 is \f4\fs18 0 \f3\fs22 , indicating that \f4\fs18 6 \f3\fs22 is even.\ Another twist is that both the division and modulo operators in Eidos operate on \f4\fs18 float \f3\fs22 values \'96 even if \f4\fs18 integer \f3\fs22 values are passed \'96 and return \f4\fs18 float \f3\fs22 results. (For those who care, division is performed internally using the C++ division operator \f4\fs18 / \f3\fs22 , and modulo is performed using the C++ \f4\fs18 fmod() \f3\fs22 function). This policy was chosen because the definitions of integer division and modulo vary widely among programming languages and are contested and unclear (see Bantchev 2006, http://www.math.bas.bg/bantchev/articles/divmod.pdf). If you are sure that you want \f4\fs18 integer \f3\fs22 division or modulo, and understand the issues involved, Eidos provides the functions \f4\fs18 integerDiv() \f3\fs22 and \f4\fs18 integerMod() \f3\fs22 for this purpose. Besides side-stepping the vague definitions of the \f4\fs18 integer \f3\fs22 operator, this policy also avoids rather common bugs involving the accidental use of \f4\fs18 integer \f3\fs22 division when \f4\fs18 float \f3\fs22 division was desired \'96 a much more common occurrence than \f0\i vice versa \f5\i0 .\ \f3 A third twist is that \f4\fs18 + \f3\fs22 and \f4\fs18 - \f3\fs22 can both act as \'93unary\'94 operators, meaning that they are happy to take just a single operand. This is standard math notation, as in the expressions \f4\fs18 -6+3 \f3\fs22 or \f4\fs18 7*-5 \f3\fs22 ; but it can sometimes look a bit strange, as in the expression \f4\fs18 5--6 \f3\fs22 (more easily read as \f4\fs18 5 - -6 \f3\fs22 ).\ A fourth twist is that the \f4\fs18 ^ \f3\fs22 operator is right-associative, whereas all other binary Eidos operators are left-associative. For example, \f4\fs18 2-3-4 \f3\fs22 is evaluated as \f4\fs18 (2-3)-4 \f3\fs22 , not as \f4\fs18 2-(3-4) \f3\fs22 ; this is left-associativity. However, \f4\fs18 2^3^4 \f3\fs22 is evaluated as \f4\fs18 2^(3^4) \f3\fs22 , not \f4\fs18 (2^3)^4 \f3\fs22 ; this is right-associativity. Since this follows the standard associativity for these operators, in both mathematics and most other programming languages, the result should generally be intuitive, but if you have never explicitly thought about associativity before you might be taken by surprise.\ A fifth twist is that the arithmetic operators and functions in Eidos are guaranteed to handle overflows safely. The \f4\fs18 float \f3\fs22 type is safe because it uses IEEE-standard arithmetic, including the use of \f4\fs18 INF \f3\fs22 to indicate infinities and the use of \f4\fs18 NAN \f3\fs22 to represent not-a-number results; this is the same as in most languages. In Eidos, however, the \f4\fs18 integer \f3\fs22 type is also safe, unlike in C, C++, and many other languages. All operations on \f4\fs18 integer \f3\fs22 values in Eidos either (1) will always produce \f4\fs18 float \f3\fs22 results, as the \f4\fs18 / \f3\fs22 and \f4\fs18 % \f3\fs22 operators do; (2) will produce \f4\fs18 float \f3\fs22 results when needed to avoid overflow, as the \f4\fs18 product() \f3\fs22 and \f4\fs18 sum() \f3\fs22 functions do; or (3) will raise an error condition on an overflow, as the Eidos operators \f4\fs18 + \f3\fs22 , \f4\fs18 - \f3\fs22 , and \f4\fs18 * \f3\fs22 do, as well as the \f4\fs18 abs() \f3\fs22 and \f4\fs18 asInteger() \f3\fs22 functions. This means that the \f4\fs18 integer \f3\fs22 type in Eidos can be used without fear that overflows might cause results to be incorrect.\ The final twist is really a reminder: \f0\i everything is a vector \f3\i0 . These operators are designed to do something smart, when possible, with vectors of any length, not just with single-valued vectors as shown above. In general, the operands of these arithmetic operators must either be the same length (in which case the elements in the operand vectors are paired off and the operation is performed between each pair), or one or the other vector must be of length \f4\fs18 1 \f3\fs22 (in which case the operation is performed using that single value, paired with each value in the other operand vector).\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.3.2 ITEM: 4. Logical operators: \f1\fs18 | \f0\fs22 , \f1\fs18 & \f0\fs22 , \f1\fs18 ! \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 The \f4\fs18 | \f3\fs22 , \f4\fs18 & \f3\fs22 , and \f4\fs18 ! \f3\fs22 operators act upon \f4\fs18 logical \f3\fs22 values. If they are given operands of other types, those operands will be \'93coerced\'94 to \f4\fs18 logical \f3\fs22 values following the rule mentioned above: zero is \f4\fs18 F \f3\fs22 , non-zero is \f4\fs18 T \f3\fs22 (and for \f4\fs18 string \f3\fs22 operands, a \f4\fs18 string \f3\fs22 that is zero characters long \'96 the empty string, \f4\fs18 "" \f3\fs22 \'96 is considered \f4\fs18 F \f3\fs22 , while all other \f4\fs18 string \f3\fs22 values are considered \f4\fs18 T \f3\fs22 ).\ As to what they do: \f4\fs18 | \f3\fs22 is the \'93or\'94 operation, \f4\fs18 & \f3\fs22 is the \'93and\'94 operation, and \f4\fs18 ! \f3\fs22 is the \'93not\'94 operation. As in common parlance, \'93or\'94 is \f4\fs18 T \f3\fs22 if either of its operands is \f4\fs18 T \f3\fs22 , whereas \'93and\'94 is \f4\fs18 T \f3\fs22 only if both of its operands are \f4\fs18 T \f3\fs22 . The \'93not\'94 operator is unary (it takes only one operand), and it negates its operand; \f4\fs18 T \f3\fs22 becomes \f4\fs18 F \f3\fs22 , \f4\fs18 F \f3\fs22 becomes \f4\fs18 T \f3\fs22 . As with the arithmetic operators, these operators work with vector operands, too \'96 either matching up values pairwise between the two operands, or applying a single value across a multivalued operand.\ Those familiar with programming might wish to know that the \f4\fs18 | \f3\fs22 and \f4\fs18 & \f3\fs22 operators do not \'93short-circuit\'94 \'96 they can\'92t, because they are vector operators. If the \f4\fs18 & \f3\fs22 operator first sees an operand that evaluates to \f4\fs18 F \f3\fs22 , for example, it knows that it will produce \f4\fs18 F \f3\fs22 value(s) as a result; but it does not know what size result vector to make. If a later operand is a multivalued vector, the \f4\fs18 & \f3\fs22 operator will produce a result vector of matching length; if all later operands are also length \f4\fs18 1 \f3\fs22 , however, \f4\fs18 & \f3\fs22 will produce a result vector of length \f4\fs18 1 \f3\fs22 . To know this for sure (and to make sure that there are no illegal length mismatches between later operands), it must evaluate all of its operands; it cannot short-circuit. Similarly for the \f4\fs18 | \f3\fs22 operator.\ These semantics match those in R, for its \f4\fs18 | \f3\fs22 and \f4\fs18 & \f3\fs22 operators, but they might seem a little strange to those used to C and other scalar-based languages. For those used to R, on the other hand, it should be noted here that Eidos does not support the \f4\fs18 && \f3\fs22 and \f4\fs18 || \f3\fs22 operators of R, for reasons of simplicity; it is safer to use the \f4\fs18 any() \f3\fs22 or \f4\fs18 all() \f3\fs22 functions to simplify multivalued \f4\fs18 logical \f3\fs22 vectors before using \f4\fs18 & \f3\fs22 or \f4\fs18 | \f5\fs22 . \f3 If this is gibberish to you, it is not important; the point here is only to prevent confusion among users accustomed to R.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.3.3 ITEM: 5. Comparative operators: \f1\fs18 == \f0\fs22 , \f1\fs18 != \f0\fs22 , \f1\fs18 < \f0\fs22 , \f1\fs18 <= \f0\fs22 , \f1\fs18 > \f0\fs22 , \f1\fs18 >= \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 These operators compare their left and right operand. The operators test for equality ( \f4\fs18 == \f3\fs22 ), inequality ( \f4\fs18 != \f3\fs22 ), less-than ( \f4\fs18 < \f3\fs22 ), less-than-or-equality ( \f4\fs18 <= \f3\fs22 ), greater-than ( \f4\fs18 > \f3\fs22 ), and greater-than-or-equality ( \f4\fs18 >= \f3\fs22 ) relationships. As seen above with the arithmetic and logical operators, this can work in two different ways: if the operands are the same length, their elements are paired up and the comparison is done between each pair, whereas if the operands are not the same length then one operand must be of length one, and its value is compared against all of the values of the other operand.\ Regardless of the types of the operands, these operators all produce a \f4\fs18 logical \f3\fs22 result vector. If the operands are of different types, promotion will be used to coerce them to be the same type (i.e. \f4\fs18 logical \f3\fs22 will be coerced to \f4\fs18 integer \f3\fs22 , \f4\fs18 integer \f3\fs22 to \f4\fs18 float \f3\fs22 , and \f4\fs18 float \f3\fs22 to \f4\fs18 string \f3\fs22 ). Note that this is often not what you want! You might not want the automatic type promotion that makes \f4\fs18 5=="5" \f3\fs22 evaluate as \f4\fs18 T \f3\fs22 , or the vectorized comparison that makes \f4\fs18 1:5==4 \f3\fs22 evaluate as something other than simply \f4\fs18 F \f3\fs22 . You might really want to ask: are two values \f0\i identical? \f3\i0 For such purposes, the \f4\fs18 identical() \f3\fs22 function is a better choice.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.3.4 ITEM: 6. String concatenation: operator \f1\fs18 + \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 The \f4\fs18 + \f3\fs22 operator is often used as an arithmetic operator, but it can also act as a concatenation operator for string operands. Concatenation is pasting together; the \f4\fs18 + \f3\fs22 operator simply pastes its string operands together, end to end.\ In fact, this works with non- \f4\fs18 string \f3\fs22 operands too, as long as a \f4\fs18 string \f3\fs22 operand is nearby; the interpretation of \f4\fs18 + \f3\fs22 as a concatenation operator is preferred by Eidos, and wins out over its arithmetic interpretation, as long as a \f4\fs18 string \f3\fs22 operand is present to suggest doing so. The other non- \f4\fs18 string \f3\fs22 operands will be coerced to \f4\fs18 string \f3\fs22 . However, this does not work retroactively; if Eidos has already done arithmetic addition on some operands, it will not go back and perform concatenation instead. To force concatenation in such situations, you can simply begin the expression with an empty string, \f4\fs18 "" \f3\fs22 .\ The concatenation operator also works with vectors, as usual.\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 Beginning with Eidos 2.2, string concatenation involving \f4\fs18 NULL \f3\fs22 concatenates the \f4\fs18 string \f3\fs22 value \f4\fs18 "NULL" \f3\fs22 , just as if \f4\fs18 NULL \f3\fs22 were a singleton \f4\fs18 string \f3\fs22 vector containing that value.\cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.4.1 ITEM: 7. Assignment: operator \f1\fs18 =\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0\fs22 \cf0 The results of expressions can be saved in variables. As in many languages, this is done with the \f4\fs18 = \f3\fs22 operator, often called the assignment operator.\ The assignment operator, \f4\fs18 = \f3\fs22 , is different from the equality comparison operator, \f4\fs18 == \f3\fs22 . In many languages, confusing the two can cause bugs that are hard to find; in C, for example, it is legal to write:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 if (x=y) ...\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 In C, this would assign the value of \f4\fs18 y \f3\fs22 to \f4\fs18 x \f3\fs22 , and then the expression \f4\fs18 x=y \f3\fs22 would evaluate to the value that was assigned, and that value would be tested by the \f4\fs18 if \f3\fs22 statement. This can be useful as a way of writing extremely compact code; but it is also a very common source of bugs, especially for inexperienced programmers. In Eidos using assignment in this way is simply illegal; assignment is allowed only in the context of a statement like \f4\fs18 x=y; \f3\fs22 to prevent these issues. (This point is mostly of interest to experienced programmers, so if it is unclear, don\'92t worry.)\ Variable names are fairly unrestricted. They may begin with a letter (uppercase or lowercase) or an underscore, and subsequently may contain all of those characters, and numerical digits as well. So \f4\fs18 x_23 \f3\fs22 , \f4\fs18 fooBar \f3\fs22 , and \f4\fs18 MyVariable23 \f3\fs22 are all legal variable names (although not good ones \'96 good variable names explain what the variable represents, such as \f4\fs18 selection_coeff \f3\fs22 ). However, \f4\fs18 4by4 \f3\fs22 would not be a legal variable name, since it begins with a digit.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.3.5 ITEM: 8. The ternary conditional: operator \f1\fs18 ? else \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 Eidos, like many languages, has an \f4\fs18 if \f3\fs22 statement that can be used to specify conditional execution of statements, and an \f4\fs18 if-else \f3\fs22 construct can be used to provide an alternative code path. Sometimes, however, one wishes to have conditional execution of an expression, rather than an entire statement. The \f4\fs18 if-else \f3\fs22 construct is particularly inconvenient with assignments involving complex lvalues, such as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 if (condition)\ x[index].property = a;\ else\ x[index].property = b;\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 It is desirable to provide a way for the user to specify that the choice of rvalue, \f4\fs18 a \f3\fs22 or \f4\fs18 b \f3\fs22 , should depend upon \f4\fs18 condition \f3\fs22 without having to duplicate the lvalue and the assignment. The R language provides this functionality by making \f4\fs18 if-else \f3\fs22 statements result in an rvalue, like an expression. The C language, on the other hand, provides a \f0\i ternary conditional \f3\i0 operator, \f4\fs18 ?: \f5\fs22 , \f3 that can be used in expressions to much the same effect. Eidos straddles the gap with a ternary conditional operator, \f4\fs18 ? else \f3\fs22 , that uses the \f4\fs18 ? \f3\fs22 initiator of C, but the \f4\fs18 else \f3\fs22 token as a continuation as in R. In the syntax of Eidos, the above conditional assignment can be rewritten as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 x[index].property = condition ? a else b;\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 This will evaluate \f4\fs18 condition \f3\fs22 and result in \f4\fs18 a \f3\fs22 if \f4\fs18 condition \f3\fs22 is \f4\fs18 T \f3\fs22 , or \f4\fs18 b \f3\fs22 if \f4\fs18 condition \f3\fs22 is \f4\fs18 F \f3\fs22 . That result is then assigned into the lvalue. Note that, as in C, the precedence of the ternary conditional operator is very low, but higher than operator \f4\fs18 = \f3\fs22 , so that parentheses are often not needed to group statements of this type. The \f4\fs18 else \f3\fs22 clause of the ternary conditional is required; there is no equivalent of an \f4\fs18 if \f3\fs22 statement without an \f4\fs18 else \f3\fs22 , since an rvalue must be produced.\ Just as with \f4\fs18 if-else \f3\fs22 statements, only the selected subexpression, as determined by the condition, is evaluated; the other subexpression will not be evaluated, so any side effects it might have will not occur. For example, with the statement:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 x = condition ? f1() else f2();\ \pard\pardeftab720\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 here \f4\fs18 f1() \f3\fs22 will be called if \f4\fs18 condition \f3\fs22 is \f4\fs18 T \f3\fs22 , \f4\fs18 f2() \f3\fs22 if \f4\fs18 condition \f3\fs22 is \f4\fs18 F \f3\fs22 ; only the subexpression selected by the condition is evaluated, and so it is never the case that both \f4\fs18 f1() \f3\fs22 and \f4\fs18 f2() \f3\fs22 are called.\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \cf0 Ternary conditionals may be nested. Because the operator is right-associative, an expression such as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 z = (a == b ? a else b ? c else d); \f5 \ \pard\pardeftab720\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 is grouped as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 z = (a == b ? a else (b ? c else d)); \f5 \ \pard\pardeftab720\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 rather than\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 z = ((a == b ? a else b) ? c else d); \f5 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 This is generally desirable, since it provides a flow similar to chaining of \f4\fs18 if-else if-else \f3\fs22 statements. In any case, parentheses may be used to change the order to evaluation as usual.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.3.6 ITEM: 9. Grouping: operator \f1\fs18 () \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 All of the discussion above involved simple expressions that allowed the standard precedence rules of mathematics to determine the order of operations; \f4\fs18 1+2*3 \f3\fs22 is evaluated as \f4\fs18 1+(2*3) \f3\fs22 rather than \f4\fs18 (1+2)*3 \f3\fs22 because the \f4\fs18 * \f3\fs22 operator is higher precedence than the \f4\fs18 + \f3\fs22 operator. For the record, here is the full precedence hierarchy for operators in Eidos, from highest to lowest precedence:\ \pard\tx1890\tx2880\pardeftab720\fi547\ri720\sb40\sa40\partightenfactor0 \f4\fs18 \cf0 [] \f3\fs22 , \f4\fs18 () \f3\fs22 , \f4\fs18 . \f5\fs22 \f3 subscript, function call, and member access\ \f4\fs18 ^ \f5\fs22 \f3 exponentiation \f0\i (right-associative) \f5\i0 \ \f4\fs18 + \f3\fs22 , \f4\fs18 - \f3\fs22 , \f4\fs18 ! \f5\fs22 \f3 unary plus, unary minus, logical (Boolean) negation \f0\i (right-associative) \f5\i0 \ \f4\fs18 : \f5\fs22 \f3 sequence construction\ \f4\fs18 * \f3\fs22 , \f4\fs18 / \f3\fs22 , \f4\fs18 % \f5\fs22 \f3 multiplication, division, and modulo\ \f4\fs18 + \f3\fs22 , \f4\fs18 - \f5\fs22 \f3 addition and subtraction\ \f4\fs18 < \f3\fs22 , \f4\fs18 > \f3\fs22 , \f4\fs18 <= \f3\fs22 , \f4\fs18 >= \f5\fs22 \f3 less-than, greater-than, less-than-or-equality, greater-than-or-equality\ \f4\fs18 == \f3\fs22 , \f4\fs18 != \f5\fs22 \f3 equality and inequality\ \f4\fs18 & \f5\fs22 \f3 logical (Boolean) and\ \f4\fs18 | \f5\fs22 \f3 logical (Boolean) or\ \f4\fs18 = \f5\fs22 \f3 assignment\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \cf0 Operators at the same precedence level are generally evaluated in the order in which they are encountered. Put more technically, Eidos operators are generally left-associative; \f4\fs18 3*5%2 \f3\fs22 evaluates as \f4\fs18 (3*5)%2 \f3\fs22 , which is \f4\fs18 1 \f5\fs22 , \f3 not as \f4\fs18 3*(5%2) \f3\fs22 , which is \f4\fs18 3 \f3\fs22 . The only binary operator in Eidos that is an exception to this rule is the \f4\fs18 ^ \f3\fs22 operator, which (following standard mathematical convention) is right-associative; \f4\fs18 2^3^4 \f3\fs22 is evaluated as \f4\fs18 2^(3^4) \f3\fs22 , not \f4\fs18 (2^3)^4 \f3\fs22 . The unary \f4\fs18 + \f3\fs22 , unary \f4\fs18 - \f3\fs22 , and \f4\fs18 ! \f3\fs22 operators are also technically right-associative; for unary operators this is of little practical import, however (it basically just implies that the unary operators must occur to the left of their operand; you write \f4\fs18 -x \f3\fs22 , not \f4\fs18 x- \f3\fs22 , to express the negation of \f4\fs18 x \f3\fs22 ).\ In any case, parentheses can be used to modify the order of operations, just as in math. This works just as you would expect.\ Note that this use of parentheses is distinct from the \f4\fs18 () \f3\fs22 operator as used in making function calls.\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 Finally, note that Eidos 2.4 and earlier (SLiM 3.4 and earlier) had an operator precedence bug: exponentiation was given a lower precedence than unary minus and its siblings, and so the expression \f4\fs18 -2^2 \f3\fs22 would evaluate to \f4\fs18 4 \f3\fs22 , as \f4\fs18 (-2)^2 \f3\fs22 , rather than \f4\fs18 -4 \f3\fs22 , as \f4\fs18 -(2^2) \f3\fs22 . This violated standard mathematical precedence rules, and was fixed in Eidos 2.5 (SLiM 3.5).\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.7.1 ITEM: 10. Function calls: operator \f1\fs18 () \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 A function is simply a block of code which has been given a name. Using that name, you can then cause the execution of that block of code whenever you wish. That is the first major purpose of functions: the \f0\i reuseability \f3\i0 of a useful chunk of code. A function can be supplied with the particular variables upon which it should act, called the function\'92s \'93parameters\'94 or \'93arguments\'94; you can execute a function with the sequence \f4\fs18 5:15 \f3\fs22 as an argument in one place, and with the string \f4\fs18 "foo" \f3\fs22 as an argument in another. That is the second major purpose of functions: the \f0\i generalization \f3\i0 of a useful chunk of code to easily act on different inputs.\ In Eidos, you may define your own functions, or you may execute a \f0\i lambda \f3\i0 (i.e., a snippet of code represented as a \f4\fs18 string \f3\fs22 value) directly in the Eidos interpreter. However, a fairly large set of built-in functions are supplied for your use, and the hope is that they will suffice for most purposes.\ Functions are called using the \f4\fs18 () \f3\fs22 operator. Function arguments go between the parentheses of the \f4\fs18 () \f3\fs22 operator, separated by commas. Most functions expect an exact number of arguments; many functions, in fact, are even fussier than that, requiring each parameter to be of a particular type, a particular size, or both. But some, such as \f4\fs18 c() \f3\fs22 , are more flexible.\ Many functions provide a return value. In other words, a function call like \f4\fs18 c(5,6) \f3\fs22 can evaluate to a particular value, just as an expression like \f4\fs18 5+6 \f3\fs22 evaluates to a particular value. The result from a function call can be used in an expression or assigned to a variable, as you might expect.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.8.3 ITEM: 11. Properties: operator \f1\fs18 . \f2\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 Objects encapsulate behaviors as well as elements. One type of behavior is called a \f0\i property \f3\i0 . A property is a simple attribute of each element in an \f4\fs18 object \f3\fs22 . Properties can be read using the member-access operator, written as \f4\fs18 . \f3\fs22 (a period). The name of a particular property can be used with \f4\fs18 . \f3\fs22 to get that property\'92s value. Operations on \f4\fs18 object \f3\fs22 are vectorized just as they are for all other types in Eidos; the result of the \f4\fs18 . \f3\fs22 operator is a vector containing the value of the property for all of the elements of the \f4\fs18 object \f3\fs22 operand.\ You can also use the member-access operator to write new values to properties that are not read-only, using the \f4\fs18 = \f3\fs22 operator to do the assignment into the property selected by the \f4\fs18 . \f3\fs22 operator.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.8.6 ITEM: 12. Method calls: operator \f1\fs18 () \f0\fs22 and operator \f1\fs18 . \f0\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\i0 \cf0 Objects encapsulate behaviors as well as elements. In addition to properties, another type of behavior is called a \f0\i method \f3\i0 . Methods are very much like functions; they are chunks of code that you can call to perform tasks. However, each type of \f4\fs18 object \f3\fs22 has its own particular methods \'96 unlike functions, which are defined globally. Methods are more heavyweight than properties; they might involve quite a lot of computation, they might create a completely new \f4\fs18 object \f3\fs22 as their result, and they might even modify the \f4\fs18 object \f3\fs22 upon which they are called. Not all methods are heavyweight in this sort of way, however; anything that one might want an \f4\fs18 object \f3\fs22 to do, but that does not feel like a simple property of the \f4\fs18 object \f3\fs22 , can be a method. Methods can also take arguments, just like functions, and they can return whole vectors as their result, unlike (read-write) properties, which must refer to singleton values so that multiplexed assignment can work. Methods are therefore much more powerful than properties.\ Methods are called using the member-access operator, \f4\fs18 . \f3\fs22 , with a syntax that looks a lot like accessing a property, but combined with the function call operator, \f4\fs18 () \f3\fs22 . That might look like:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f4\fs18 \cf0 object.method()\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f3\fs22 \cf0 Naturally, method calls are also vector operations. For a multi-element \f4\fs18 object \f3\fs22 , a single method call will result in the method call being multiplexed out to all of the elements of the \f4\fs18 object \f3\fs22 , and the results from all of those method calls will be concatenated together in the same way that the \f4\fs18 c() \f3\fs22 function performs concatenation (including dropping of \f4\fs18 NULL \f3\fs22 s and type promotion, potentially).\ } ================================================ FILE: EidosScribe/EidosHelpStatements.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2513 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Italic;\f1\fnil\fcharset0 Menlo-Italic;\f2\fswiss\fcharset0 Optima-Regular; \f3\fnil\fcharset0 Menlo-Regular;\f4\froman\fcharset0 TimesNewRomanPSMT;\f5\froman\fcharset0 TimesNewRomanPS-ItalicMT; } {\colortbl;\red255\green255\blue255;\red170\green13\blue145;\red28\green10\blue207;\red255\green0\blue0; \red28\green0\blue207;\red0\green116\blue0;} {\*\expandedcolortbl;;\csgenericrgb\c66667\c5098\c56863;\csgenericrgb\c10980\c3922\c81176;\csgenericrgb\c100000\c0\c0; \csgenericrgb\c10980\c0\c81176;\csgenericrgb\c0\c45490\c0;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab720 \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i\fs22 \cf0 2.5.1 ITEM: 1. \f1\fs18 if \f0\fs22 and \f1\fs18 if\'96else \f0\fs22 statements\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 As in many languages, conditional execution is provided by the \f3\fs18 if \f2\fs22 statement. This statement is supplied with a \f3\fs18 logical \f2\fs22 condition; if the condition is \f3\fs18 T \f2\fs22 , the rest of the \f3\fs18 if \f2\fs22 statement is executed, whereas if the condition is \f3\fs18 F \f2\fs22 , the rest of the \f3\fs18 if \f2\fs22 statement is ignored. An example:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 if (2^2^2^2^2 > 10000) "exponentiation is da bomb!" \f4 \cf0 \ \fs4 \ \f3\fs18 "exponentiation is da bomb!"\ \f4\fs4 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 The only twist here, really, is that the condition must evaluate to a single value, i.e. a vector of \f3\fs18 size() == 1 \f2\fs22 . The \f3\fs18 if \f2\fs22 statement, in other words, is essentially a scalar operator, not a vector operator. If you have a multivalued \f3\fs18 logical \f2\fs22 vector, you can use the \f3\fs18 any() \f2\fs22 or \f3\fs18 all() \f2\fs22 functions to simplify it to a single \f3\fs18 logical \f2\fs22 value. Alternatively, the \f3\fs18 ifelse() \f2\fs22 function provides a vector conditional operation, similar to that in R.\ It is worth exploring this twist with an example. Suppose you have a variable \f3\fs18 x \f2\fs22 which ought to be equal to \f3\fs18 3 \f2\fs22 , and a variable \f3\fs18 y \f2\fs22 which ought to contain two values, \f3\fs18 7 \f2\fs22 and \f3\fs18 8 \f2\fs22 . You might expect to be able to write:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 if (x == 3 & y == c(7,8)) "yes!" \f4 \cf0 \ \fs4 \ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf4 ERROR (EidosInterpreter::Evaluate_If): condition has size() != 1 \f4 .\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \fs4 \cf0 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 The error informs you that the size of condition is not equal to \f3\fs18 1 \f2\fs22 (and that that is a problem). The expression \f3\fs18 y\'a0==\'a0c(7,8) \f2\fs22 produces a \f3\fs18 logical \f2\fs22 vector with two values, the result of testing the first and second values respectively. The \f3\fs18 & \f4\fs22 \'a0 \f2 operator thus produces a two-valued \f3\fs18 logical \f2\fs22 vector as its result, and \f3\fs18 if \f2\fs22 is not happy about that. To resolve this, you could use the \f3\fs18 all() \f2\fs22 function, or in many cases more appropriately, the \f3\fs18 identical() \f2\fs22 function. See the Eidos manual for further discussion of this issue.\ It is also worth noting that the condition for \f3\fs18 if \f2\fs22 does not need to be a \f3\fs18 logical \f2\fs22 value; a value of a different type will be converted to \f3\fs18 logical \f2\fs22 by coercion if possible.\ Often you want to perform an alternative action when the condition of an \f3\fs18 if \f2\fs22 statement is \f3\fs18 F \f2\fs22 ; the \f3\fs18 if\'96else \f2\fs22 statement allows this. It is simplest to just show this with an example:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 if (2/2/2/2/2 > 10000) "division is da bomb!"; else "not so much." \f4 \cf0 \ \fs4 \ \f3\fs18 "not so much."\ \f4\fs4 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 Super simple, right? \f4 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.5.3 ITEM: 3. semicolons and "null statements", \f1\fs18 ; \f5\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 Every statement in Eidos must end with a semicolon (except compound statements, which end with a closing brace). However, when you\'92re working interactively in EidosScribe, EidosScribe will add a trailing semicolon to your statements if necessary, just to make your life simpler. So when you type:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 >\cf0 \cf3 1+1==2 \f4 \cf0 \ \pard\pardeftab720\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 what is really being evaluated behind the scenes is:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 >\cf0 \cf3 1+1==2; \f4 \cf0 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 When you\'92re not working interactively, semicolons are required, and if you forget, you will get an error, like this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 1+1==2 \f4 \cf0 \ \fs4 \ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf4 ERROR (Parse): unexpected token 'EOF' in statement; expected ';'\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 EOF stands for End Of File; it\'92s a standard way of referring to the end of an input buffer, in this case the line of input provided by the user for execution.\ The simplest and shortest possible statement in Eidos is the "null statement", which consists of nothing but a semicolon:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb180\sa180\partightenfactor0 \f3\fs18 \cf0 ;\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 This is not terribly useful, since it does nothing.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.5.4 ITEM: 4. compound statements with \f1\fs18 \{ \} \f5\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The other thing you might wonder about, regarding \f3\fs18 if \f2\fs22 statements, is: what if I want to perform more than one action in response to the condition being \f3\fs18 T \f2\fs22 or \f3\fs18 F \f2\fs22 ? This, then, is an opportune moment to introduce the concept of compound statements. A compound statement is a series of statements (zero or more) enclosed by braces. An example is worth a thousand words:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 if (1+1==2)\ \{\ x = 1;\ x = x + 1;\ x;\ \}\ else\ \{\ "whoah, I'm confused";\ \} \f4 \cf0 \ \fs4 \ \f3\fs18 2\ \f4\fs4 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 Note that the input here is spread across multiple lines for clarity; all of this could be typed on a single line instead. If entered as multiple lines, it cannot presently be entered in EidosScribe\'92s interactive mode because the \f3\fs18 if \f2\fs22 statement would stand on its own and be evaluated as soon as it was completed; instead, the full text would need to be entered in the script area on the left, selected, and executed. All of the blue lines are user input, whereas the final line in black, \f3\fs18 2 \f2\fs22 , shows the output of the execution of the whole \f3\fs18 if\'96else \f2\fs22 statement; the \f3\fs18 if \f2\fs22 clause is executed, the calculations involving \f3\fs18 x \f2\fs22 are performed, and the final statement \f3\fs18 x; \f2\fs22 produces a result which is printed to the console as usual.\ The way that \f3\fs18 x; \f2\fs22 results in output here might seem a bit surprising at first, but it is a consequence of the fact that \f0\i the value of a compound statement is the value of the last statement executed within the compound statement \f2\i0 ; the values of the previous statements are discarded.\ You can use a compound statement in any context in which a single statement would be allowed. For example, compound statements are very commonly used with looping constructs.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.6.1 ITEM: 5. \f1\fs18 while \f0\fs22 statements\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 A \f3\fs18 while \f2\fs22 loop repeats a statement as long as a given condition is true. The condition is tested before the first time that the statement is executed, so the statement will be executed zero or more times. Here is a code snippet to compute the first twenty numbers of the Fibonacci sequence:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 fib = c(1, 1);\ while (size(fib) < 20)\ \{\ next_fib = fib[size(fib) - 1] + fib[size(fib) - 2];\ fib = c(fib, next_fib);\ \}\ fib; \f4 \cf0 \ \fs4 \ \f3\fs18 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765\ \f4\fs4 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 Its use of a \f3\fs18 while \f2\fs22 loop is optimal, because it ensures that if the \f3\fs18 fib \f2\fs22 vector is already long enough to satisfy the length condition \f3\fs18 size(fib) < 20 \f2\fs22 , no further values of \f3\fs18 fib \f2\fs22 will be computed. You could use this \f3\fs18 while \f2\fs22 loop to lengthen the \f3\fs18 fib \f2\fs22 vector on demand within a larger block of code that used the \f3\fs18 fib \f2\fs22 vector repeatedly.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.6.2 ITEM: 6. \f1\fs18 do\'96while \f0\fs22 statements\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 A \f3\fs18 do\'96while \f2\fs22 loop repeats a statement as long as a given condition is true. Unlike \f3\fs18 while \f2\fs22 loops, in this case the condition is tested at the end of the loop, and thus the loop statement is always executed at least once. Here is a code snippet to compute a factorial:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 counter = 5;\ factorial = 1;\ do\ \{\ factorial = factorial * counter;\ counter = counter - 1;\ \}\ while (counter > 0);\ "The factorial of 5 is " + factorial; \f4 \cf0 \ \fs4 \ \f3\fs18 "The factorial of 5 is 120"\ \f4\fs4 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 Note that this example could be rewritten using a \f3\fs18 while \f2\fs22 loop instead, but it might be a bit less intuitive in its operation since it would no longer embody the formal definition of the factorial as explicitly. Note also that computing a factorial could be done much more trivially (and efficiently) using the sequence operator \f3\fs18 : \f2\fs22 and the \f3\fs18 product() \f2\fs22 function, but the code here is useful for the purpose of illustration.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.6.3 ITEM: 7. \f1\fs18 for \f0\fs22 statements (with \f1\fs18 in \f0\fs22 )\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The \f3\fs18 for \f2\fs22 loop is used to loop through all of the elements in a vector. For each value in the given vector, a given variable is set to the value, and a given statement is then executed. For example, the following code computes squares by setting \f3\fs18 element \f2\fs22 to each value of \f3\fs18 my_sequence \f2\fs22 , one by one, and then executing the \f3\fs18 print() \f2\fs22 function for each value:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f4\fs4 \cf0 \ \f3\fs18 \cf2 >\cf0 \cf3 my_sequence = 1:4;\ for (element in my_sequence)\ print("The square of " + element + " is " + element^2); \f4 \cf0 \ \fs4 \ \f3\fs18 "The square of 1 is 1"\ "The square of 2 is 4"\ "The square of 3 is 9"\ "The square of 4 is 16"\ \f4\fs4 \ \pard\tx2340\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 This looping construct is called by various names in other languages, such as the \'93for each\'94 statement (PHP), the \'93range-based for\'94 (C++), \'93fast enumeration\'94 (Objective-C), and so forth. It is different from the traditional \f3\fs18 for \f2\fs22 loop of C and related languages, which entails an initializer expression, a condition expression, and an increment/decrement expression. That type of \f3\fs18 for \f2\fs22 loop does not exist in Eidos (following R); the iterator \f3\fs18 for \f2\fs22 of R and Eidos is a more natural and efficient choice for vector-based languages.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.6.4 ITEM: 8. \f1\fs18 next \f0\fs22 statements\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 Sometimes you might wish to cut short the execution of a given iteration of a loop, skipping the rest of the work that would normally be done and proceeding directly to the next iteration. This is the function of the \f3\fs18 next \f2\fs22 statement. The \f3\fs18 next \f2\fs22 statement can be used within \f3\fs18 for \f2\fs22 , \f3\fs18 while \f2\fs22 , and \f3\fs18 do\'96while \f2\fs22 loops.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.6.5 ITEM: 9. \f1\fs18 break \f0\fs22 statements\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 Often it is necessary to stop the execution of a loop altogether, not just to cut short the current iteration of the loop as \f3\fs18 next \f2\fs22 does. To achieve this \'96 to break out of a loop completely \'96 use the \f3\fs18 break \f2\fs22 statement. The \f3\fs18 break \f2\fs22 statement can be used within \f3\fs18 for \f2\fs22 , \f3\fs18 while \f2\fs22 , and \f3\fs18 do\'96while \f2\fs22 loops.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.6.6 ITEM: 10. \f1\fs18 return \f0\fs22 statements\ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The \f3\fs18 return \f2\fs22 statement returns a value from a block of code, as in other languages such as C and R. In one common case, when defining a user-defined function, \f3\fs18 return \f2\fs22 is used to stop execution of the function and return a given value to the caller. Otherwise, a \f3\fs18 return \f2\fs22 is useful mostly when the Context within which you\'92re using Eidos uses the returned value. When using Eidos in SLiM, for example, SLiM uses the value returned by Eidos scripts such as \f3\fs18 mutationEffect() \f2\fs22 callbacks and \f3\fs18 mateChoice() \f2\fs22 callbacks, making \f3\fs18 return \f2\fs22 very useful in that Context. Apart from such Context-dependent uses, \f3\fs18 return \f2\fs22 is mainly useful as a way to break out of nested loops regardless of the depth of nesting, as illustrated below.\ The \f3\fs18 return \f2\fs22 statement is very simple: the keyword \f3\fs18 return \f2\fs22 , and then, optionally, an expression. When the \f3\fs18 return \f2\fs22 statement is executed, the expression is evaluated and its value is immediately returned as the value of the largest enclosing statement. The \f3\fs18 return \f2\fs22 statement therefore breaks out of all conditionals, loops, and compound statements, regardless of the depth of nesting.\ In some circumstances a \f3\fs18 return \f2\fs22 statement is not necessary, because compound statements evaluate to the value of the last statement evaluated within them, and \f3\fs18 if \f2\fs22 statements behave similarly; as in R, therefore, a \f3\fs18 return \f2\fs22 statement can often be omitted. However, using \f3\fs18 return \f2\fs22 makes the intentions of the programmer more explicit, and so its use is encouraged.\ If the expression for the \f3\fs18 return \f2\fs22 statement is omitted, the return value used is \f3\fs18 NULL \f2\fs22 . In situations where the return value will not be used, such as Eidos events in SLiM, the return value should be omitted to make the intent of the code clear.\ \pard\pardeftab397\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.9.1 ITEM: 11. single-line comments with \f1\fs18 // \f5\fs22 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 Technically, comments are actually a type of whitespace; comments in Eidos code are completely ignored and have no effect whatsoever on the results of the execution of code, just like other kinds of whitespace in most respects. Single-line comments begin with \f3\fs18 // \f2\fs22 and then may consist of any text whatsoever, up to the end of the current line of code. A comment may occur by itself on a line, or it may follow other Eidos code. So for example, you could write:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab397\li547\ri1440\sb180\sa180\partightenfactor0 \f3\fs18 \cf5 1\cf0 + \cf5 1\cf0 == \cf5 2\cf0 ; \cf6 // this is true\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 Comments are never required in Eidos, but using them to annotate your code is nevertheless a very good idea, both so that you remember what your intentions were when you come back to the code weeks or months later, and so that others who might need to understand or maintain your code have a helping hand. \f4 \ \pard\pardeftab397\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.9.2 ITEM: 12. block comments with \f1\fs18 /* */ \f5\fs22 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 It is possible to comment out whole blocks of script, instead of just single lines. This can be useful for writing longer comments that describe a section of code in more detail. In Eidos (as in C and C++), such block comments can be written with a beginning \f3\fs18 /* \f2\fs22 and a terminating \f3\fs18 */ \f2\fs22 . Here\'92s an example of this style of comment:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf6 /*\ This computes the factorial x!, which is\ the product of all values from 1 to x, for\ any positive integer x.\ */ \f4 \cf0 \ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3 \cf0 x_factorial = product(\cf5 1\cf0 :x);\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 A nice feature of Eidos is that block comments nest properly, making it possible to use them to comment out stretches of code that already contain block comments. For example, if the code above was no longer needed, but you didn\'92t want to delete it entirely because you might need it again later, you could use a block comment to disable it:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf6 /* ******* NOT NEEDED ************\ /*\ This computes the factorial x!, which is\ the product of all values from 1 to x, for\ any positive integer x.\ */\ x_factorial = product(1:x);\ */ \f4 \cf0 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 The outer block comment is not terminated by the first \f3\fs18 */ \f2\fs22 because Eidos recognizes that that belongs to the inner block comment; the outer block comment continues until the second \f3\fs18 */ \f2\fs22 is encountered.\ \pard\pardeftab397\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 4.1 ITEM: 13. user-defined functions \f5 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 Suppose we wish to define a function that doubles whatever \f3\fs18 float \f2\fs22 value is passed to it. This is very easy to do:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf2 function\cf0 (float)double(float x)\ \{\ \cf2 return\cf0 \cf5 2\cf0 * x;\ \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 The \f3\fs18 function \f2\fs22 keyword initiates the declaration of a new function. It is followed by the full signature for the new function; here the signature declares that the function is named \f3\fs18 double \f2\fs22 , takes a parameter named \f3\fs18 x \f2\fs22 that is of type \f3\fs18 float \f2\fs22 , and returns type \f3\fs18 float \f2\fs22 . This signature is then followed by the definition of the new function, in the form of a compound statement; here, the \f3\fs18 double() \f2\fs22 function is defined as returning two times the value it was passed. Note that a \f3\fs18 return \f2\fs22 statement is used here to return a specified value from the function; if no \f3\fs18 return \f2\fs22 statement is encountered, the value of the last statement evaluated is automatically returned to the caller (as in R), but generally it is clearer to explicitly use \f3\fs18 return \f4\fs22 .\ \f2 Calling such functions works in exactly the same way as calling built-in functions:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf2 >\cf5 double(5.35)\ \cf0 10.7\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 Functions may be recursive; a simple \f3\fs18 factorial() \f2\fs22 function might be defined recursively as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf2 function\cf0 (integer)factorial(integer x)\ \{\ \cf2 if\cf0 (x <= \cf5 1\cf0 )\ \cf2 return\cf0 \cf5 1\cf0 ;\ \cf2 else \f4 \cf0 \ \f3 \cf2 return\cf0 x * factorial(x - \cf5 1\cf0 );\ \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 This works well enough, as you can see:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\pardeftab720\li547\ri1440\sb40\sa40\partightenfactor0 \f3\fs18 \cf2 >\cf5 factorial(13)\ \cf0 6227020800\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf0 As with the built-in Eidos functions, user-defined functions may take multiple parameters, each of which may be allowed to be one of several different possible types. Parameters to user-defined functions may also be optional, with a default value if left unsupplied. Finally, functions are \f0\i scoped \f2\i0 ; the code inside them executes in a private namespace in which only the parameters to the function are available, and variables defined inside a function will not persist beyond the end of the function\'92s execution.\ \ } ================================================ FILE: EidosScribe/EidosHelpTypes.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2513 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Italic;\f1\fnil\fcharset0 Menlo-Italic;\f2\fswiss\fcharset0 Optima-Regular; \f3\fnil\fcharset0 Menlo-Regular;\f4\froman\fcharset0 TimesNewRomanPSMT;\f5\fnil\fcharset0 LucidaGrande; \f6\froman\fcharset0 TimesNewRomanPS-ItalicMT;\f7\fswiss\fcharset0 Optima-Bold;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab720 \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i\fs22 \cf0 2.1.1 ITEM: 1. type \f1\fs18 integer \f0\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The \f3\fs18 integer \f2\fs22 type is used in Eidos to represent integers \'96 whole numbers, with no fractional component. Unlike in many languages, exponential notation may be used to specify \f3\fs18 integer \f2\fs22 literals (\'93literals\'94 means values stated literally in the script, rather than derived through calculations).\ The \f3\fs18 integer \f2\fs22 type is advantageous primarily because it is exact; it does not suffer from any sort of roundoff error. Exact comparison with integer constants is therefore safe; roundoff error will not lead to problems caused by \f3\fs18 0.999999999 \f2\fs22 being deemed to be unequal to \f3\fs18 1 \f4\fs22 . \f2 However, \f3\fs18 integer \f2\fs22 is disadvantageous because it can only represent a limited range of values, and beyond that range, results will be unpredictable. Eidos uses 64 bits to store \f3\fs18 integer \f2\fs22 values, so that range is quite wide; to \f3\fs18 \uc0\u8722 9223372036854775806 \f2\fs22 to \f3\fs18 9223372036854775807 \f2\fs22 , to be exact. That is broad, but it is still enormously narrower than the range of numbers representable with \f3\fs18 float \f4\fs22 .\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.1.2 ITEM: 2. type \f1\fs18 float \f0\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The \f3\fs18 float \f2\fs22 type is used in Eidos to represent all non- \f3\fs18 integer \f2\fs22 numbers \'96 fractions and real numbers. Exponential notation may be used to specify \f3\fs18 float \f2\fs22 literals; in particular; literals with a decimal point or a negative exponent are taken to be of type \f3\fs18 float \f2\fs22 .\ Note that this rule means that some literals are represented using \f3\fs18 float \f2\fs22 even though they could also be represented using \f3\fs18 integer \f4\fs22 .\ \f2 The \f3\fs18 float \f2\fs22 type is advantageous primarily because it can represent an enormously wide range of values. Eidos uses C++\'92s \f3\fs18 double \f2\fs22 type to represent its \f3\fs18 float \f2\fs22 values; the range of values allowed will depend upon your computer\'92s settings, but it will be vast. If that range is exceeded, or if numerical problems occur, type \f3\fs18 float \f2\fs22 can also represent values as infinity or as \'93Not A Number\'94 ( \f3\fs18 INF \f2\fs22 and \f3\fs18 NAN \f2\fs22 , respectively, in Eidos). The \f3\fs18 float \f2\fs22 type is thus more robust for operations that might produce such values. The disadvantage of \f3\fs18 float \f2\fs22 is that it is inexact; some values cannot be represented exactly (just as 1/3 in base 10 cannot be represented exactly, and must be written as 0.3333333...). Roundoff can thus cause comparison errors, overflow and underflow errors, and the accumulation of numerical error.\ Several \f3\fs18 float \f2\fs22 constants are defined in Eidos; besides \f3\fs18 INF \f2\fs22 and \f3\fs18 NAN \f2\fs22 , \f3\fs18 PI \f2\fs22 is defined as \f5 \uc0\u960 \f2 (3.14159...), and \f3\fs18 E \f2\fs22 is defined as \f6\i e \f2\i0 (2.71828...).\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.1.3 ITEM: 3. type \f1\fs18 logical \f0\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The \f3\fs18 logical \f2\fs22 type represents true and false values, such as those from comparisons. In many languages this type is called something like \f3\fs18 boolean \f2\fs22 or \f3\fs18 BOOL \f2\fs22 ; Eidos follows R in using the name \f3\fs18 logical \f2\fs22 instead.\ There are no \f3\fs18 logical \f2\fs22 literals in Eidos. However, there are defined constants that behave in essentially the same way as literals. In particular, \f3\fs18 T \f2\fs22 is defined as true, and \f3\fs18 F \f2\fs22 is defined as false. These are the only two values that the \f3\fs18 logical \f2\fs22 type can take. As in a great many other languages, these \f3\fs18 logical \f2\fs22 values have equivalent numerical values; \f3\fs18 F \f2\fs22 is \f3\fs18 0 \f2\fs22 , and \f3\fs18 T \f2\fs22 is \f3\fs18 1 \f2\fs22 (and in fact any non-zero value is considered to be true if converted to \f3\fs18 logical \f2\fs22 type). Values of type \f3\fs18 integer \f2\fs22 or \f3\fs18 float \f2\fs22 may therefore be converted to \f3\fs18 logical \f2\fs22 , and vice-versa.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.1.4 ITEM: 4. type \f1\fs18 string \f0\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The \f3\fs18 string \f2\fs22 type represents a string of characters \'96 a word, a sentence, a paragraph, the complete works of Shakespeare. There is no formatting on a \f3\fs18 string \f2\fs22 \'96 no \f4 font \f2 , no \fs16 point size \fs22 , no \f7\b bold \f2\b0 or \f0\i italic \f2\i0 . Instead, it is just a character stream. A \f3\fs18 string \f2\fs22 literal must be enclosed by either single or double quotation marks, \f3\fs18 ' \f2\fs22 or \f3\fs18 " \f2\fs22 . This choice simplifies writing Eidos strings that themselves contain quote characters, because you can delimit the string with the opposite kind of quote. For example, \f3\fs18 'You say, "Ere thrice the sun done salutation to the dawn"' \f2\fs22 is a string that contains double quotes, whereas \f3\fs18 "Quoth the Raven, 'nevermore'.\'94 \f2\fs22 is a string that contains single quotes. Apart from this consideration, it does not matter whether you use single or double quotes; the internal representation is the same. The suggested convention is to prefer double quotes, all else being equal, since they are more universally used in other programming languages.\ A complication arises if one wishes to include both single and double quotation marks within a \f3\fs18 string \f2\fs22 ; whichever delimiter you choose, one or the other quote character will terminate the \f3\fs18 string \f2\fs22 literal. In this case, the quotation mark must be \'93escaped\'94 by preceding it with a backslash, \f3\fs18 \\ \f2\fs22 . The backslash can be used to \'93escape\'94 various other characters; to include a newline in a string, for example, use \f3\fs18 \\n \f2\fs22 , and to include a tab, use \f3\fs18 \\t \f2\fs22 . Since the backslash has this special meaning, backslashes themselves must be escaped as \f3\fs18 \\\\ \f2\fs22 . An alternative to dealing with escape sequences is to use the \'93here document\'94 style of string literal; see the Eidos manual for details on this.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.7.2 ITEM: 5. type \f1\fs18 NULL \f0\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf0 The \f3\fs18 NULL \f2\fs22 type two primary uses: as a return value, and as a parameter.\ As a return value, \f3\fs18 NULL \f2\fs22 is used to indicate that a function had nothing useful to return. Some functions always return \f3\fs18 NULL \f2\fs22 , such as \f3\fs18 print() \f2\fs22 ; \f3\fs18 print() \f2\fs22 sends its output directly to the Eidos console. It has nothing useful to return, so it returns \f3\fs18 NULL \f2\fs22 . (That \f3\fs18 NULL \f2\fs22 value does not normally get printed out by Eidos because it is marked as an \'93invisible\'94 return, a side topic not really worth getting into here; invisible returns work much as they do in R).\ Some functions will return a useful value if they can, but will return \f3\fs18 NULL \f2\fs22 if they can\'92t. Often a \f3\fs18 NULL \f2\fs22 return is a result of passing \f3\fs18 NULL \f2\fs22 in as an argument; garbage in, garbage out, as they say. For example, the \f3\fs18 readFile() \f2\fs22 function will return \f3\fs18 NULL \f2\fs22 if an error occurs that prevents the file read operation from completing. The calling code could then detect that \f3\fs18 NULL \f2\fs22 return and act accordingly \'96 it might try to read from a different path, print an error, or terminate execution with \f3\fs18 stop() \f2\fs22 , or it might just ignore the problem, if reading the file was optional anyway (such as an optional configuration file to modify the default behavior of a script).\ The other use of \f3\fs18 NULL \f2\fs22 , as mentioned above, is as an argument to a function. Passing \f3\fs18 NULL \f2\fs22 is occasionally a way of signaling that you don\'92t want to supply a value for an argument, or that you want a default behavior from the function rather than telling it more specifically what to do.\ \f3\fs18 NULL \f2\fs22 cannot be an element of a vector of some other type; it cannot be used to mark missing or unknown values, for example. Instead, \f3\fs18 NULL \f2\fs22 is its own type of vector in Eidos, always of zero length. (There is also no \f3\fs18 NA \f2\fs22 value in Eidos like the one in R, while we\'92re on the topic of marking missing values. Not having to worry about missing values makes Eidos substantially simpler and faster, and Eidos \'96 unlike R \'96 is not designed to be used for doing statistical analysis, so marking missing values is not expected to be important. Eidos does support \f3\fs18 NAN \f2\fs22 \'96 Not A Number \'96 values in \f3\fs18 float \f2\fs22 vectors, however, which could conceivably be used to mark missing values if necessary.)\ The basic philosophy of how Eidos handles \f3\fs18 NULL \f2\fs22 values in expressions and computations is that \f3\fs18 NULL \f2\fs22 in such situations represents a non-fatal error or an unknown value. If using the \f3\fs18 NULL \f2\fs22 value in some meaningful way could lead to potentially misleading or incorrect results, Eidos will generate a fatal error. The idea is to give Eidos code an opportunity to detect a \f3\fs18 NULL \f2\fs22 , and thus to catch and handle the non-fatal error; but if the code does not handle the \f3\fs18 NULL \f2\fs22 , using the \f3\fs18 NULL \f2\fs22 in further operations will result in a fatal error before the functioning of the code is seriously compromised. \f3\fs18 NULL \f2\fs22 values are thus a sort of third rail; there\'92s a good reason they exist, but you have to be very careful around them. They are a bit like zero-valued pointers in C ( \f3\fs18 NULL \f2\fs22 ), C++ ( \f3\fs18 nullptr \f2\fs22 ), Objective-C ( \f3\fs18 nil \f2\fs22 ), and similar languages; they are widely used, but if you ever use one the wrong way it is an immediate and fatal error. For further details, please consult the Eidos manual.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 2.8.1 ITEM: 6. type \f1\fs18 object \f0\fs22 \ \pard\pardeftab720\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 In addition to \f3\fs18 logical \f2\fs22 , \f3\fs18 integer \f2\fs22 , \f3\fs18 float \f2\fs22 , \f3\fs18 string \f2\fs22 , and \f3\fs18 NULL \f2\fs22 , there is one more type in Eidos left to discuss: \f3\fs18 object \f2\fs22 . A variable of type \f3\fs18 object \f2\fs22 is a vector that contains elements; it is a container, a bag of stuff. In this way, it is similar to Eidos\'92s other types; a \f3\fs18 float \f2\fs22 vector in Eidos contains floating-point elements, whereas an \f3\fs18 object \f2\fs22 vector contains \f3\fs18 object \f2\fs22 -elements (often just called \'93objects\'94; whether one is referring to a single \f3\fs18 object \f2\fs22 -element or a vector of type \f3\fs18 object \f2\fs22 is generally clear from context). An \f3\fs18 object \f2\fs22 vector can also embody \f0\i behavior \f2\i0 : it has operations that it can perform using the elements it contains, which all belong to a \f0\i class \f2\i0 that defines the available behaviors. The \f3\fs18 object \f2\fs22 type in Eidos is thus similar to objects in other languages such as Java, C++, or R \'96 except much more limited. In Eidos you cannot define your own \f3\fs18 object \f2\fs22 classes; you work only with the predefined \f3\fs18 object \f2\fs22 classes supplied by SLiM or whatever other Context you might be using Eidos within. These predefined \f3\fs18 object \f2\fs22 classes generally define Context-dependent \f3\fs18 object \f2\fs22 -elements related to the task performed by the Context; in SLiM, the classes are things such as mutations, genomic elements, and mutation types (described in SLiM\'92s documentation). Eidos itself also supplies a few built-in \f3\fs18 object \f2\fs22 classes, notably \f3\fs18 Dictionary \f2\fs22 and \f3\fs18 Image \f2\fs22 .\ The behaviors of objects in Eidos manifest in two ways: objects can have \f0\i properties \f2\i0 (also called instance variables or member variables, in other languages) that can be read from and written to, and they can have \f0\i methods \f2\i0 (also called member functions, in other languages). The behavior of an \f3\fs18 object \f2\fs22 vector in Eidos is determined by the class of element the \f3\fs18 object \f2\fs22 contains; an Eidos \f3\fs18 object \f2\fs22 will always contain only one class of element (just as a \f3\fs18 float \f2\fs22 cannot contain \f3\fs18 string \f2\fs22 -elements, for example).\ Instances of particular \f3\fs18 object \f2\fs22 classes \'96 particular kinds of objects \'96 are obtained via built-in functions and/or global constants and variables. For example, in SLiM there is a global constant called \f3\fs18 sim \f2\fs22 that represents the simulated species as an instance of the \f3\fs18 Species \f2\fs22 class.} ================================================ FILE: EidosScribe/EidosPrettyprinter.h ================================================ // // EidosPrettyprinter.h // SLiM // // Created by Ben Haller on 7/30/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #include #include "eidos_script.h" @interface EidosPrettyprinter : NSObject { } // Prettyprint the given token stream into an NSMutableString + (BOOL)prettyprintTokens:(const std::vector &)tokens fromScript:(EidosScript &)tokenScript intoString:(NSMutableString *)pretty; @end ================================================ FILE: EidosScribe/EidosPrettyprinter.mm ================================================ // // EidosPrettyprinter.m // SLiM // // Created by Ben Haller on 7/30/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosPrettyprinter.h" #include "eidos_script.h" @implementation EidosPrettyprinter + (int)indentForStack:(std::vector &)indentStack startingNewStatement:(BOOL)startingNewStatement nextTokenType:(EidosTokenType)nextTokenType { // Count the number of indents represented by the indent stack. When a control-flow keyword is // followed by a left brace, the indent stack has two items on it, but we want to only indent // one level. int indent = 0; BOOL previousIndentStackItemWasControlFlow = NO; for (int stackIndex = 0; stackIndex < (int)indentStack.size(); ++stackIndex) { EidosTokenType stackTokenType = indentStack[stackIndex]->token_type_; // skip over ternary conditionals; they do not generate indent, but are on the stack so we can match elses if (stackTokenType == EidosTokenType::kTokenConditional) continue; if (previousIndentStackItemWasControlFlow && (stackTokenType == EidosTokenType::kTokenLBrace)) ; else ++indent; previousIndentStackItemWasControlFlow = !(stackTokenType == EidosTokenType::kTokenLBrace); } BOOL lastIndentIsControlFlow = previousIndentStackItemWasControlFlow; // Indent when continuing a statement, but not after a control-flow token. The idea here is that // if you have a structure like: // // if (x) // if (y) // ; // // the indent stack will already dictate that is indented twice; it does not need to // receive the !startingNewStatement extra indent level that we normally add to cause continuing // statements to be indented like: // // x = a + b + c + // d + e + f; // if (!startingNewStatement && !lastIndentIsControlFlow) indent++; // If the next token is a left brace, outdent one level, conventionally. This reflects usage like: // // if (x) // y; // // if (x) // { // y; // } // // This applies only if the last element on the indent stack is a control-flow indent, not a {. // This is the same rule we used when counting indentStack, but applied to nextTokenType. We also // outdent when we see a left brace if we are mid-statement; this covers SLiM callback syntax. // if ((lastIndentIsControlFlow || !startingNewStatement) && (nextTokenType == EidosTokenType::kTokenLBrace)) indent--; // For similar reasons, if the next token is a right brace, always outdent one level if (nextTokenType == EidosTokenType::kTokenRBrace) indent--; return indent; } + (BOOL)prettyprintTokens:(const std::vector &)tokens fromScript:(EidosScript &)tokenScript intoString:(NSMutableString *)pretty { // We keep a stack of indent-generating tokens: { if else do while for. The purpose of this is // to be able to tell what indent level we're at, and how it changes with a ; or a } token. std::vector indentStack; BOOL startingNewStatement = YES; int tokenCount = (int)tokens.size(); for (int tokenIndex = 0; tokenIndex < tokenCount; ++tokenIndex) { const EidosToken &token = tokens[tokenIndex]; NSString *tokenString = [NSString stringWithUTF8String:token.token_string_.c_str()]; EidosTokenType nextTokenPeek = (tokenIndex + 1 < tokenCount ? tokens[tokenIndex+1].token_type_ : EidosTokenType::kTokenEOF); // Find the next non-whitespace, non-comment token for lookahead int nextSignificantTokenPeekIndex = tokenIndex + 1; EidosTokenType nextSignificantTokenPeek = EidosTokenType::kTokenEOF; while (nextSignificantTokenPeekIndex < tokenCount) { EidosTokenType peek = tokens[nextSignificantTokenPeekIndex].token_type_; if ((peek != EidosTokenType::kTokenWhitespace) && (peek != EidosTokenType::kTokenComment) && (peek != EidosTokenType::kTokenCommentLong)) { nextSignificantTokenPeek = peek; break; } nextSignificantTokenPeekIndex++; } switch (token.token_type_) { // These token types are not used in the AST and should not be present case EidosTokenType::kTokenNone: case EidosTokenType::kTokenBad: return NO; // These are virtual tokens that can be ignored case EidosTokenType::kTokenEOF: case EidosTokenType::kTokenInterpreterBlock: case EidosTokenType::kTokenContextFile: case EidosTokenType::kTokenContextEidosBlock: case EidosTokenType::kFirstIdentifierLikeToken: break; // This is where the rubber meets the road; prettyprinting is all about altering whitespace stretches. // We don't want to alter the user's newline decisions, so we count the number of newlines in this // whitespace stretch and always emit the same number. If there are no newlines, we're in whitespace // inside a given line, with tokens on both sides; for the time being we do not alter those at all. // If there are newlines, though, then each newline is changed to be followed by the appropriate number // of tabs as indentation. The indent depends upon the indent stack and some other state about the // context we are in. case EidosTokenType::kTokenWhitespace: { __block int newlineCount = 0; [tokenString enumerateSubstringsInRange:NSMakeRange(0, [tokenString length]) options:NSStringEnumerationByParagraphs usingBlock:^(NSString * _Nullable paragraph, NSRange paragraphRange, NSRange enclosingRange, BOOL * _Nonnull stop) { if (enclosingRange.location + enclosingRange.length > paragraphRange.location + paragraphRange.length) newlineCount++; }]; if (newlineCount <= 0) { // Normally, whitespace tokens that do not contain a newline occur inside a line, and should be preserved. // A whitespace token that indents the start of a line normally started on the previous line and contains // a newline. However, this is not the case at the very beginning of a script; the first token is special. if (tokenIndex > 0) [pretty appendString:tokenString]; } else { int indent = [EidosPrettyprinter indentForStack:indentStack startingNewStatement:startingNewStatement nextTokenType:nextTokenPeek]; for (int lineCounter = 0; lineCounter < newlineCount; ++lineCounter) { [pretty appendString:@"\n"]; for (int tabCounter = 0; tabCounter < indent; ++tabCounter) [pretty appendString:@"\t"]; } } break; } // We have ended a statement, so we reset our indent levels case EidosTokenType::kTokenSemicolon: { // Pop indent-generating tokens that have expired with the end of this statement; a semicolon terminates // a whole nested series of if else do while for, but does not terminate an enclosing { block. Also, // if there are nested if statements, a semicolon terminates only the first one if the next token is an else. while (indentStack.size() > 0) { const EidosToken *topIndentToken = indentStack.back(); if (topIndentToken->token_type_ == EidosTokenType::kTokenLBrace) break; if ((topIndentToken->token_type_ == EidosTokenType::kTokenIf) && (nextSignificantTokenPeek == EidosTokenType::kTokenElse)) { indentStack.pop_back(); break; } indentStack.pop_back(); } [pretty appendString:tokenString]; break; } // Track braces case EidosTokenType::kTokenLBrace: { indentStack.emplace_back(&token); [pretty appendString:tokenString]; break; } case EidosTokenType::kTokenRBrace: { // First pop the matching left brace if ((indentStack.size() == 0) || (indentStack.back()->token_type_ != EidosTokenType::kTokenLBrace)) { // All other indent-producing tokens should already have been balanced; Eidos has no implicit termination of statements NSLog(@"Unbalanced '}' token in prettyprinting!"); return NO; } indentStack.pop_back(); // Then pop indent-generating tokens above the left brace that have expired with the end of this statement while (indentStack.size() > 0) { const EidosToken *topIndentToken = indentStack.back(); if (topIndentToken->token_type_ == EidosTokenType::kTokenLBrace) break; indentStack.pop_back(); } [pretty appendString:tokenString]; break; } // Control-flow keywords influence our indent level; this might look like the normal statement inner indent, // but it is not, as can be seen when these control-flow keywords are nested like 'if (x) if (y) '. // When an if follows an else, the else is removed by the if, since we don't want two indents; else-if is one indent. case EidosTokenType::kTokenIf: { if (indentStack.size()) { EidosTokenType top_token_type = indentStack.back()->token_type_; if (top_token_type == EidosTokenType::kTokenElse) indentStack.pop_back(); } indentStack.emplace_back(&token); [pretty appendString:tokenString]; break; } case EidosTokenType::kTokenDo: case EidosTokenType::kTokenWhile: case EidosTokenType::kTokenFor: case EidosTokenType::kTokenConditional: // note this does not generate indent, but is put on the stack { indentStack.emplace_back(&token); [pretty appendString:tokenString]; break; } // else can be paired with if or ?. In the former case, the if will be off the stack by the time the else // is encountered, and we put the else on to give us an equivalent indent. In the latter case, we consider // the expressions within the ternary conditional to be statement-level; we don't indent, and we don't push // an else on the stack here, but we remove the conditional that we are completing. case EidosTokenType::kTokenElse: { if (indentStack.size()) { EidosTokenType top_token_type = indentStack.back()->token_type_; if (top_token_type == EidosTokenType::kTokenConditional) { indentStack.pop_back(); [pretty appendString:tokenString]; break; } } indentStack.emplace_back(&token); [pretty appendString:tokenString]; break; } // Comments are preserved verbatim case EidosTokenType::kTokenComment: case EidosTokenType::kTokenCommentLong: [pretty appendString:tokenString]; break; // Tokens for operators are emitted verbatim case EidosTokenType::kTokenColon: case EidosTokenType::kTokenComma: case EidosTokenType::kTokenDot: case EidosTokenType::kTokenPlus: case EidosTokenType::kTokenMinus: case EidosTokenType::kTokenMod: case EidosTokenType::kTokenMult: case EidosTokenType::kTokenExp: case EidosTokenType::kTokenAnd: case EidosTokenType::kTokenOr: case EidosTokenType::kTokenDiv: case EidosTokenType::kTokenAssign: case EidosTokenType::kTokenAssign_R: case EidosTokenType::kTokenEq: case EidosTokenType::kTokenLt: case EidosTokenType::kTokenLtEq: case EidosTokenType::kTokenGt: case EidosTokenType::kTokenGtEq: case EidosTokenType::kTokenNot: case EidosTokenType::kTokenNotEq: [pretty appendString:tokenString]; break; case EidosTokenType::kTokenSingleton: [pretty appendString:tokenString]; break; // Nesting levels of parens and brackets are not tracked at the moment case EidosTokenType::kTokenLParen: case EidosTokenType::kTokenRParen: case EidosTokenType::kTokenLBracket: case EidosTokenType::kTokenRBracket: [pretty appendString:tokenString]; break; // Numbers and identifiers are emitted verbatim case EidosTokenType::kTokenNumber: case EidosTokenType::kTokenIdentifier: [pretty appendString:tokenString]; break; // Strings are emitted verbatim, but their original string needs to be reconstructed; // token_string_ has the outer quotes removed and escape sequences resolved case EidosTokenType::kTokenString: { std::string string_original = tokenScript.String().substr(token.token_start_, token.token_end_ - token.token_start_ + 1); [pretty appendString:[NSString stringWithUTF8String:string_original.c_str()]]; break; } // These keywords have no effect on indent level case EidosTokenType::kTokenIn: case EidosTokenType::kTokenNext: case EidosTokenType::kTokenBreak: case EidosTokenType::kTokenReturn: case EidosTokenType::kTokenFunction: [pretty appendString:tokenString]; break; } // Now that we're done processing that token, update startingNewStatement to reflect whether we are within // a statement, of which we have seen at least one token, or starting a new statement. Nonsignificant // tokens (whitespace and comments) do not alter the state of startingNewStatement. if ((token.token_type_ != EidosTokenType::kTokenWhitespace) && (token.token_type_ != EidosTokenType::kTokenComment) && (token.token_type_ != EidosTokenType::kTokenCommentLong)) startingNewStatement = ((token.token_type_ == EidosTokenType::kTokenSemicolon) || (token.token_type_ == EidosTokenType::kTokenLBrace) || (token.token_type_ == EidosTokenType::kTokenRBrace)); } return YES; } @end ================================================ FILE: EidosScribe/EidosScribe-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 4.2 CFBundleSignature ???? CFBundleVersion 1 LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2016–2025 Benjamin C. Haller, http://messerlab.org/slim/. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: EidosScribe/EidosScribe.entitlements ================================================ ================================================ FILE: EidosScribe/EidosScribe_multi-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 4.2 CFBundleSignature ???? CFBundleVersion 1 LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2016–2025 Benjamin C. Haller, http://messerlab.org/slim/. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: EidosScribe/EidosScribe_multi.entitlements ================================================ com.apple.security.cs.disable-library-validation ================================================ FILE: EidosScribe/EidosTextView.h ================================================ // // EidosTextView.h // EidosScribe // // Created by Ben Haller on 6/14/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import // BCH 4/7/2016: So we can build against the OS X 10.9 SDK #ifndef NS_DESIGNATED_INITIALIZER #define NS_DESIGNATED_INITIALIZER #endif @protocol EidosTextViewDelegate; /* A subclass to provide various niceties for a syntax-colored, autoindenting, tab-stopped text view. This class uses the standard NSTextView delegate object, but will use the optional methods defined by the EidosTextViewDelegate protocol if implemented by that delegate. */ typedef enum EidosSyntaxColoringOption { kEidosSyntaxColoringNone = 0, kEidosSyntaxColoringEidos, kEidosSyntaxColoringOutput } EidosSyntaxColoringOption; @interface EidosTextView : NSTextView { } // A delegate for Eidos functionality; this is the same object as the NSText/NSTextView delegate, and is not declared explicitly // here because overriding properties with a different type doesn't really work. So when you call setDelegate: on EidosTextView, // you will not get the proper type-checking, and you will get a runtime error if your delegate object does not in fact conform // to the EidosTextViewDelegate protocol. But if you declare your conformance to the protocol, you should be fine. This is a // little weird, but the only good alternative is to have a separate delegate object for Eidos, which would just be annoying and // confusing. // //@property (nonatomic, assign) id delegate; // The syntax coloring option being used @property (nonatomic) EidosSyntaxColoringOption syntaxColoring; // The font size (of Menlo) being used @property (nonatomic) int displayFontSize; // A flag to temporarily disable syntax coloring, used to coalesce multiple changes into a single recolor @property (nonatomic) BOOL shouldRecolorAfterChanges; // Same designated initializers as NSTextView - (instancetype)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)container NS_DESIGNATED_INITIALIZER; - (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; // Actions associated with code editing - (IBAction)shiftSelectionLeft:(id)sender; - (IBAction)shiftSelectionRight:(id)sender; - (IBAction)commentUncommentSelection:(id)sender; // If an error occurs while tokenizing/parsing/executing Eidos code in this textview, call this to highlight the error - (void)selectErrorRange; // Called after disabling syntax coloring with shouldRecolorAfterChanges, to provide the coalesced recoloring - (void)recolorAfterChanges; // These are used by the Find Recipe panel in SLiM to highlight matches with search terms - (void)clearHighlightMatches; - (void)highlightMatchesForString:(NSString *)matchString; // This method is used to construct the function/method prototypes shown in the status bar; client code // probably will not call it, but possibly it could be used for context-sensitive help or something - (NSAttributedString *)attributedSignatureForScriptString:(NSString *)scriptString selection:(NSRange)selection; @end ================================================ FILE: EidosScribe/EidosTextView.mm ================================================ // // EidosTextView.mm // EidosScribe // // Created by Ben Haller on 6/14/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosTextView.h" #import "EidosTextViewDelegate.h" #import "EidosConsoleTextView.h" #import "EidosCocoaExtra.h" #import "EidosHelpController.h" #include "eidos_script.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "eidos_type_table.h" #include "eidos_type_interpreter.h" #include "eidos_sorting.h" #include #include // EidosTextStorage – a little subclass to make word selection in EidosTextView work the way it should, defined below @interface EidosTextStorage : NSTextStorage - (id)init; - (id)initWithAttributedString:(NSAttributedString *)attrStr NS_DESIGNATED_INITIALIZER; @end @interface EidosTextView () { // these are used in selectionRangeForProposedRange:granularity: to balance delimiters properly BOOL inEligibleDoubleClick; NSTimeInterval doubleDownTime; } @end @implementation EidosTextView - (instancetype)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)aTextContainer { if (self = [super initWithFrame:frameRect textContainer:aTextContainer]) { _displayFontSize = 11; _shouldRecolorAfterChanges = YES; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { _displayFontSize = 11; _shouldRecolorAfterChanges = YES; } return self; } - (void)awakeFromNib { // Replace the text storage of the description textview with our custom subclass NSLayoutManager *lm = [self layoutManager]; NSTextStorage *ts = [self textStorage]; EidosTextStorage *replacementTextStorage = [[EidosTextStorage alloc] initWithAttributedString:ts]; // copy any existing text [lm replaceTextStorage:replacementTextStorage]; [replacementTextStorage setDelegate:self]; [replacementTextStorage release]; // Turn off all of Cocoa's fancy text editing stuff [self setAutomaticDashSubstitutionEnabled:NO]; [self setAutomaticDataDetectionEnabled:NO]; [self setAutomaticLinkDetectionEnabled:NO]; [self setAutomaticQuoteSubstitutionEnabled:NO]; [self setAutomaticSpellingCorrectionEnabled:NO]; [self setAutomaticTextReplacementEnabled:NO]; [self setContinuousSpellCheckingEnabled:NO]; [self setGrammarCheckingEnabled:NO]; [self turnOffLigatures:nil]; // Fix the font and typing attributes [self setFont:[NSFont fontWithName:@"Menlo" size:_displayFontSize]]; [self setTypingAttributes:[NSDictionary eidosTextAttributesWithColor:nil size:_displayFontSize]]; // Fix text container insets to look a bit nicer; {0,0} by default [self setTextContainerInset:NSMakeSize(0.0, 5.0)]; } // The method signature is inherited from NSTextView, but we want to check that the delegate follows our delegate protocol - (void)setDelegate:(id)delegate { if (delegate && ![delegate conformsToProtocol:@protocol(EidosTextViewDelegate)]) NSLog(@"Delegate %@ assigned to EidosTextView %p does not conform to the EidosTextViewDelegate protocol!", delegate, self); [super setDelegate:delegate]; } // handle flashing of matching delimiters - (void)insertText:(id)insertString replacementRange:(NSRange)replacementRange { //bool replacingSelection = NSEqualRanges(replacementRange, [self selectedRange]); [super insertText:insertString replacementRange:replacementRange]; // if we replaced something other than the selected range, something weird is going on and we shouldn't flash // this is commented out because it doesn't work; AppKit likes to call this method with {NSNotFound,0} for typing... //if (!replacingSelection) // return; // if the insert string isn't one character in length, it cannot be a brace character if ([insertString length] != 1) return; unichar uch = [insertString characterAtIndex:0]; if ((uch == '}') || (uch == ']') || (uch == ')')) { // MAINTENANCE NOTE: This also exists, in slightly altered form, in selectionRangeForProposedRange:granularity:, so please maintain the two in parallel! NSString *scriptString = [[self textStorage] string]; int stringLength = (int)[scriptString length]; int proposedCharacterIndex = (int)[self selectedRange].location - 1; // just inserted a single character BOOL forward = false; unichar incrementChar = ' '; unichar decrementChar = ' '; EidosTokenType token1 = EidosTokenType::kTokenNone; EidosTokenType token2 = EidosTokenType::kTokenNone; // Check for any of the delimiter types we support, and set up to do a scan if (uch == '}') { forward = false; incrementChar = '}'; decrementChar = '{'; token1 = EidosTokenType::kTokenLBrace; token2 = EidosTokenType::kTokenRBrace; } if (uch == ')') { forward = false; incrementChar = ')'; decrementChar = '('; token1 = EidosTokenType::kTokenLParen; token2 = EidosTokenType::kTokenRParen; } if (uch == ']') { forward = false; incrementChar = ']'; decrementChar = '['; token1 = EidosTokenType::kTokenLBracket; token2 = EidosTokenType::kTokenRBracket; } if (token1 != EidosTokenType::kTokenNone) { // We've got a double-click on a character that we want to balance-select. This is complicated mostly // because of strings, which are a pain in the butt. To simplify that issue, we tokenize and search // in the token stream parallel to searching in the text. std::string script_string([scriptString UTF8String]); EidosScript script(script_string); // Tokenize script.Tokenize(true, true); // make bad tokens as needed, keep nonsignificant tokens // Find the token containing the double-click point const std::vector &tokens = script.Tokens(); int token_count = (int)tokens.size(); const EidosToken *click_token = nullptr; int click_token_index; for (click_token_index = 0; click_token_index < token_count; ++click_token_index) { click_token = &tokens[click_token_index]; if ((click_token->token_UTF16_start_ <= proposedCharacterIndex) && (click_token->token_UTF16_end_ >= proposedCharacterIndex)) break; } if (!click_token) return; // OK, this token contains the character that was double-clicked. We could have the actual token // (incrementToken), we could be in a string, or we could be in a comment. We stay in the domain // that we start in; if in a string, for example, only delimiters within strings affect our scan. EidosTokenType click_token_type = click_token->token_type_; const EidosToken *scan_token = click_token; int scan_token_index = click_token_index; NSInteger scanPosition = proposedCharacterIndex; int balanceCount = 0; while (YES) { EidosTokenType scan_token_type = scan_token->token_type_; BOOL isCandidate; if (click_token_type == EidosTokenType::kTokenComment) isCandidate = (scan_token_type == EidosTokenType::kTokenComment); else if (click_token_type == EidosTokenType::kTokenCommentLong) isCandidate = (scan_token_type == EidosTokenType::kTokenCommentLong); else if (click_token_type == EidosTokenType::kTokenString) isCandidate = (scan_token_type == EidosTokenType::kTokenString); else isCandidate = ((scan_token_type == token1) || (scan_token_type == token2)); if (isCandidate) { uch = [scriptString characterAtIndex:scanPosition]; if (uch == incrementChar) balanceCount++; else if (uch == decrementChar) balanceCount--; // If balanceCount is now zero, all opens have been balanced by closes, and we have found our matching delimiter if (balanceCount == 0) { [self showFindIndicatorForRange:NSMakeRange(scanPosition, 1)]; return; } } // Advance, forward or backward, to the next character scanPosition += (forward ? 1 : -1); if ((scanPosition == stringLength) || (scanPosition == -1)) { //NSBeep(); return; } // Make sure we're in the right token while ((scan_token->token_UTF16_start_ > scanPosition) || (scan_token->token_UTF16_end_ < scanPosition)) { scan_token_index += (forward ? 1 : -1); if ((scan_token_index < 0) || (scan_token_index == token_count)) { NSLog(@"ran out of tokens!"); //NSBeep(); return; } scan_token = &tokens[scan_token_index]; } } } } } // handle autoindent by matching the whitespace beginning the current line - (void)insertNewline:(id)sender { NSString *textString = [self string]; NSUInteger selectionStart = [self selectedRange].location; NSCharacterSet *newlineChars = [NSCharacterSet newlineCharacterSet]; NSCharacterSet *whitespaceChars = [NSCharacterSet whitespaceCharacterSet]; // start at the start of the selection and move backwards to the beginning of the line NSUInteger lineStart = selectionStart; while (lineStart > 0) { unichar ch = [textString characterAtIndex:lineStart - 1]; if ([newlineChars characterIsMember:ch]) break; --lineStart; } // now we're either at the beginning of the content, or the beginning of the line; now find the end of the whitespace there, up to where we started NSUInteger whitespaceEnd = lineStart; while (whitespaceEnd < selectionStart) { unichar ch = [textString characterAtIndex:whitespaceEnd]; if (![whitespaceChars characterIsMember:ch]) break; ++whitespaceEnd; } // now we have the range of the leading whitespace; copy that, call super to insert the newline, and then paste in the whitespace NSRange whitespaceRange = NSMakeRange(lineStart, whitespaceEnd - lineStart); NSString *whitespaceString = [textString substringWithRange:whitespaceRange]; // We use the insert... methods, which handle change notifications and undo correctly [super insertNewline:sender]; [self insertText:whitespaceString replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } // handle the home and end keys; rather bizarre that Apple doesn't implement these for us, but whatever... // OK, Apple *does* implement these, but apparently sometimes their implementation does nothing. I haven't figured out a reproducible case to make that // happen, but it does happen, and when it does, this fixes it. Overriding these methods seems quite harmless, so I'll leave this code in here. - (void)scrollToBeginningOfDocument:(id)sender { [self scrollRangeToVisible:NSMakeRange(0, 0)]; } - (void)scrollToEndOfDocument:(id)sender { [self scrollRangeToVisible:NSMakeRange([[self string] length], 0)]; } // NSTextView copies only plain text for us, because it is set to have rich text turned off. That setting only means it is turned off for the user; the // user can't change the font, size, etc. But we still can, and do, programatically do our syntax formatting. We want that style information to get // copied to the pasteboard. This has gotten even trickier now that our syntax coloring is done with temporary attributes; we need to translate those // into real attributes here. - (NSMutableAttributedString *)eidosAttrStringForSelectedRange { NSAttributedString *attrString = [self textStorage]; NSRange selectedRange = [self selectedRange]; NSMutableAttributedString *attrStringInRange = [[[attrString attributedSubstringFromRange:selectedRange] mutableCopy] autorelease]; // Loop over the temporary color attributes and set them on the attributed string as real attributes NSLayoutManager *lm = [self layoutManager]; NSRange effectiveRange = NSMakeRange(0, 0); NSUInteger charIndex = selectedRange.location; while (charIndex < selectedRange.location + selectedRange.length) { NSColor *color = [lm temporaryAttribute:NSForegroundColorAttributeName atCharacterIndex:charIndex longestEffectiveRange:&effectiveRange inRange:selectedRange]; if (color) [attrStringInRange addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(effectiveRange.location - selectedRange.location, effectiveRange.length)]; charIndex = effectiveRange.location + effectiveRange.length; } return attrStringInRange; } - (IBAction)copy:(id)sender { NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; NSAttributedString *attrStringInRange = [self eidosAttrStringForSelectedRange]; // The documentation sucks, but as far as I can tell, this puts both a plain-text and a rich-text representation on the pasteboard [pasteboard clearContents]; [pasteboard writeObjects:@[attrStringInRange]]; } - (IBAction)copyAsParagraph:(id)sender { NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; NSMutableAttributedString *attrStringInRange = [self eidosAttrStringForSelectedRange]; NSString *originalString = [attrStringInRange string]; unichar lineBreakChar = NSLineSeparatorCharacter; NSString *lineSeparator = [NSString stringWithCharacters:&lineBreakChar length:1]; // Replace new paragraphs (\n) with linebreaks (NSLineSeparatorCharacter) except at the very end (thus -2) // We do this by very dumb brute force, for now; doesn't matter for (int i = (int)[attrStringInRange length] - 2; i >= 0; --i) { unichar ch = [originalString characterAtIndex:i]; if (ch == '\n') [attrStringInRange replaceCharactersInRange:NSMakeRange(i, 1) withString:lineSeparator]; } // The documentation sucks, but as far as I can tell, this puts both a plain-text and a rich-text representation on the pasteboard [pasteboard clearContents]; [pasteboard writeObjects:@[attrStringInRange]]; } - (IBAction)paste:(id)sender { NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; NSString *pbString = nil; if ([pasteboard canReadObjectForClasses:@[[NSString class]] options:nil]) pbString = [[pasteboard readObjectsForClasses:@[[NSString class]] options:nil] objectAtIndex:0]; else if ([pasteboard canReadObjectForClasses:@[[NSAttributedString class]] options:nil]) pbString = [(NSAttributedString *)[[pasteboard readObjectsForClasses:@[[NSAttributedString class]] options:nil] objectAtIndex:0] string]; if (pbString) { // This is the point of this override: to standardize the line endings present pbString = [pbString stringByReplacingOccurrencesOfString:@"\n\r" withString:@"\n"]; pbString = [pbString stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"]; pbString = [pbString stringByReplacingOccurrencesOfString:@"\r" withString:@"\n"]; pbString = [pbString stringByReplacingOccurrencesOfString:@"\U00002028" withString:@"\n"]; // NSLineSeparatorCharacter pbString = [pbString stringByReplacingOccurrencesOfString:@"\U00002029" withString:@"\n"]; // NSParagraphSeparatorCharacter pbString = [pbString stringByReplacingOccurrencesOfString:@"\U000000A0" withString:@" "]; // Unicode "no-break space" pbString = [pbString stringByReplacingOccurrencesOfString:@"\U0000202F" withString:@" "]; // Unicode "narrow no-break space" pbString = [pbString stringByReplacingOccurrencesOfString:@"\U00002009" withString:@" "]; // Unicode "thin space" pbString = [pbString stringByReplacingOccurrencesOfString:@"\U00002007" withString:@" "]; // Unicode "figure space" [self insertText:pbString replacementRange:[self selectedRange]]; } else [super paste:sender]; } - (IBAction)pasteAsRichText:(id)sender { [self paste:sender]; } - (IBAction)pasteAsPlainText:(id)sender { [self paste:sender]; } - (void)undoRedoSelectionRange:(NSRange)range { [[[self undoManager] prepareWithInvocationTarget:self] undoRedoSelectionRange:range]; [self setSelectedRange: range]; } - (IBAction)shiftSelectionLeft:(id)sender { if ([self isEditable]) { NSTextStorage *ts = [self textStorage]; NSMutableString *scriptString = [[self string] mutableCopy]; NSUInteger scriptLength = [scriptString length]; NSRange selectedRange = [self selectedRange]; NSCharacterSet *newlineChars = [NSCharacterSet newlineCharacterSet]; NSUInteger scanPosition; // start at the start of the selection and scan backwards over non-newline text until we hit a newline or the start of the file scanPosition = selectedRange.location; while (scanPosition > 0) { if ([newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition - 1]]) break; --scanPosition; } // we want to recolor only once, at the end of the whole operation [self setShouldRecolorAfterChanges:NO]; // save the current selection so undo restores it [[[self undoManager] prepareWithInvocationTarget:self] undoRedoSelectionRange:selectedRange]; // ok, we're at the start of the line that the selection starts on; start removing tabs [ts beginEditing]; while ((scanPosition == selectedRange.location) || (scanPosition < selectedRange.location + selectedRange.length)) { // if we are at the very end of the script string, then we have hit the end and we're done if (scanPosition == scriptLength) break; // insert a tab at the start of this line and adjust our selection if ([scriptString characterAtIndex:scanPosition] == '\t') { NSRange changeRange = NSMakeRange(scanPosition, 1); if ([self shouldChangeTextInRange:changeRange replacementString:@""]) { [ts replaceCharactersInRange:changeRange withString:@""]; [self didChangeText]; [scriptString replaceCharactersInRange:changeRange withString:@""]; scriptLength--; if (scanPosition < selectedRange.location) selectedRange.location--; else if (selectedRange.length > 0) selectedRange.length--; } } // now scan forward to the end of this line while (scanPosition < scriptLength) { if ([newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } // and then scan forward to the beginning of the next line while (scanPosition < scriptLength) { if (![newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } } [scriptString release]; [ts endEditing]; // set the new selection in a way that will work with redo [[[self undoManager] prepareWithInvocationTarget:self] undoRedoSelectionRange:selectedRange]; [self setSelectedRange:selectedRange]; // recolor, coalesced [self setShouldRecolorAfterChanges:YES]; [self recolorAfterChanges]; } else { NSBeep(); } } - (IBAction)shiftSelectionRight:(id)sender { if ([self isEditable]) { NSTextStorage *ts = [self textStorage]; NSMutableString *scriptString = [[self string] mutableCopy]; NSUInteger scriptLength = [scriptString length]; NSRange selectedRange = [self selectedRange]; NSCharacterSet *newlineChars = [NSCharacterSet newlineCharacterSet]; NSUInteger scanPosition; // start at the start of the selection and scan backwards over non-newline text until we hit a newline or the start of the file scanPosition = selectedRange.location; while (scanPosition > 0) { if ([newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition - 1]]) break; --scanPosition; } // we want to recolor only once, at the end of the whole operation [self setShouldRecolorAfterChanges:NO]; // save the current selection so undo restores it [[[self undoManager] prepareWithInvocationTarget:self] undoRedoSelectionRange:selectedRange]; // ok, we're at the start of the line that the selection starts on; start inserting tabs [ts beginEditing]; while ((scanPosition == selectedRange.location) || (scanPosition < selectedRange.location + selectedRange.length)) { // insert a tab at the start of this line and adjust our selection NSRange changeRange = NSMakeRange(scanPosition, 0); if ([self shouldChangeTextInRange:changeRange replacementString:@"\t"]) { [ts replaceCharactersInRange:changeRange withString:@"\t"]; [self didChangeText]; [scriptString replaceCharactersInRange:changeRange withString:@"\t"]; scriptLength++; if ((scanPosition < selectedRange.location) || (selectedRange.length == 0)) selectedRange.location++; else selectedRange.length++; } // now scan forward to the end of this line while (scanPosition < scriptLength) { if ([newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } // and then scan forward to the beginning of the next line while (scanPosition < scriptLength) { if (![newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } // if we are at the very end of the script string, then we have hit the end and we're done if (scanPosition == scriptLength) break; } [scriptString release]; [ts endEditing]; // set the new selection in a way that will work with redo [[[self undoManager] prepareWithInvocationTarget:self] undoRedoSelectionRange:selectedRange]; [self setSelectedRange:selectedRange]; // recolor, coalesced [self setShouldRecolorAfterChanges:YES]; [self recolorAfterChanges]; } else { NSBeep(); } } - (IBAction)commentUncommentSelection:(id)sender { if ([self isEditable]) { NSTextStorage *ts = [self textStorage]; NSMutableString *scriptString = [[self string] mutableCopy]; NSUInteger scriptLength = [scriptString length]; NSRange selectedRange = [self selectedRange]; NSCharacterSet *newlineChars = [NSCharacterSet newlineCharacterSet]; NSUInteger scanPosition; // start at the start of the selection and scan backwards over non-newline text until we hit a newline or the start of the file scanPosition = selectedRange.location; while (scanPosition > 0) { if ([newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition - 1]]) break; --scanPosition; } // decide whether we are commenting or uncommenting; we are only uncommenting if every line spanned by the selection starts with "//" BOOL uncommenting = YES; NSUInteger scanPositionSave = scanPosition; while ((scanPosition == selectedRange.location) || (scanPosition < selectedRange.location + selectedRange.length)) { // comment/uncomment at the start of this line and adjust our selection if ((scanPosition + 1 >= scriptLength) || ([scriptString characterAtIndex:scanPosition] != '/') || ([scriptString characterAtIndex:scanPosition + 1] != '/')) { uncommenting = NO; break; } // now scan forward to the end of this line while (scanPosition < scriptLength) { if ([newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } // and then scan forward to the beginning of the next line while (scanPosition < scriptLength) { if (![newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } // if we are at the very end of the script string, then we have hit the end and we're done if (scanPosition == scriptLength) break; } scanPosition = scanPositionSave; // we want to recolor only once, at the end of the whole operation [self setShouldRecolorAfterChanges:NO]; // save the current selection so undo restores it [[[self undoManager] prepareWithInvocationTarget:self] undoRedoSelectionRange:selectedRange]; // ok, we're at the start of the line that the selection starts on; start commenting / uncommenting [ts beginEditing]; while ((scanPosition == selectedRange.location) || (scanPosition < selectedRange.location + selectedRange.length)) { // if we are at the very end of the script string, then we have hit the end and we're done if (uncommenting && (scanPosition == scriptLength)) break; // comment/uncomment at the start of this line and adjust our selection if (uncommenting) { NSRange changeRange = NSMakeRange(scanPosition, 2); if ([self shouldChangeTextInRange:changeRange replacementString:@""]) { [ts replaceCharactersInRange:changeRange withString:@""]; [self didChangeText]; [scriptString replaceCharactersInRange:changeRange withString:@""]; scriptLength -= 2; if (scanPosition < selectedRange.location) { if (scanPosition == selectedRange.location - 1) { selectedRange.location--; if (selectedRange.length > 0) selectedRange.length--; } else selectedRange.location -= 2; } else { if (selectedRange.length > 2) selectedRange.length -= 2; else selectedRange.length = 0; } } } else { NSRange changeRange = NSMakeRange(scanPosition, 0); if ([self shouldChangeTextInRange:changeRange replacementString:@"//"]) { [ts replaceCharactersInRange:changeRange withString:@"//"]; //[ts setAttributes:[NSDictionary eidosTextAttributesWithColor:[NSColor blackColor]] range:NSMakeRange(scanPosition, 2)]; [self didChangeText]; [scriptString replaceCharactersInRange:changeRange withString:@"//"]; scriptLength += 2; if ((scanPosition < selectedRange.location) || (selectedRange.length == 0)) selectedRange.location += 2; else selectedRange.length += 2; } } // now scan forward to the end of this line while (scanPosition < scriptLength) { if ([newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } // and then scan forward to the beginning of the next line while (scanPosition < scriptLength) { if (![newlineChars characterIsMember:[scriptString characterAtIndex:scanPosition]]) break; ++scanPosition; } // if we are at the very end of the script string, then we have hit the end and we're done if (!uncommenting && (scanPosition == scriptLength)) break; } [scriptString release]; [ts endEditing]; // set the new selection in a way that will work with redo [[[self undoManager] prepareWithInvocationTarget:self] undoRedoSelectionRange:selectedRange]; [self setSelectedRange:selectedRange]; // recolor, coalesced [self setShouldRecolorAfterChanges:YES]; [self recolorAfterChanges]; } else { NSBeep(); } } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; // Mimic the way copy: enables, for consistency; enabled only when the selection is not zero-length if (sel == @selector(copyAsParagraph:)) return ([self selectedRange].length > 0); return [super validateMenuItem:menuItem]; } - (void)selectErrorRange { // If there is error-tracking information set, and the error is attributed to the user script, // then we can highlight the error range if ((!gEidosErrorContext.currentScript || (gEidosErrorContext.currentScript->UserScriptUTF16Offset() == 0)) && (gEidosErrorContext.errorPosition.characterStartOfErrorUTF16 >= 0) && (gEidosErrorContext.errorPosition.characterEndOfErrorUTF16 >= gEidosErrorContext.errorPosition.characterStartOfErrorUTF16)) { NSRange charRange = NSMakeRange(gEidosErrorContext.errorPosition.characterStartOfErrorUTF16, gEidosErrorContext.errorPosition.characterEndOfErrorUTF16 - gEidosErrorContext.errorPosition.characterStartOfErrorUTF16 + 1); [self setSelectedRange:charRange]; [self scrollRangeToVisible:charRange]; // Set the selection color to red for maximal visibility; this gets set back in setSelectedRanges:affinity:stillSelecting: [self setSelectedTextAttributes:@{NSBackgroundColorAttributeName:[NSColor redColor], NSForegroundColorAttributeName:[NSColor whiteColor]}]; } // In any case, since we are the ultimate consumer of the error information, we should clear out // the error state to avoid misattribution of future errors ClearErrorContext(); } - (void)setSelectedRanges:(NSArray *)ranges affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag { // The text selection color may have been changed by -selectErrorRange; here we just make sure it is set back again [self setSelectedTextAttributes:@{NSBackgroundColorAttributeName:[NSColor selectedTextBackgroundColor]}]; [super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag]; } - (void)mouseDown:(NSEvent *)theEvent { NSUInteger modifiers = [theEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; // BCH 4/7/2016: NSEventModifierFlags not defined in 10.9 // If the control key is down, the click will be interpreted by super as a context-menu click even with option down, so we can let that through. // Otherwise, option-clicks produce discontiguous selections that we want to prevent since we are not prepared to deal with them. Instead, we // use option-clicks to indicate that the word clicked on should be looked up in EidosHelpController. if ((modifiers & NSEventModifierFlagOption) && !(modifiers & NSEventModifierFlagControl)) { NSPoint windowPoint = [theEvent locationInWindow]; NSRect windowRect = NSMakeRect(windowPoint.x, windowPoint.y, 0, 0); NSRect screenRect = [[self window] convertRectToScreen:windowRect]; // why is convertBaseToScreen: deprecated?? NSPoint screenPoint = screenRect.origin; NSUInteger charIndex = [self characterIndexForPoint:screenPoint]; // takes a point in screen coordinates, for some weird reason if (charIndex < [[self textStorage] length]) { NSRange wordRange = [self selectionRangeForProposedRange:NSMakeRange(charIndex, 1) granularity:NSSelectByWord]; NSString *word = [[self string] substringWithRange:wordRange]; NSString *trimmedWord = [word stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; // A few substitutions to improve the search if ([trimmedWord isEqualToString:@":"]) trimmedWord = @"operator :"; else if ([trimmedWord isEqualToString:@"("]) trimmedWord = @"operator ()"; else if ([trimmedWord isEqualToString:@")"]) trimmedWord = @"operator ()"; else if ([trimmedWord isEqualToString:@","]) trimmedWord = @"calls: operator ()"; else if ([trimmedWord isEqualToString:@"["]) trimmedWord = @"operator []"; else if ([trimmedWord isEqualToString:@"]"]) trimmedWord = @"operator []"; else if ([trimmedWord isEqualToString:@"{"]) trimmedWord = @"compound statements"; else if ([trimmedWord isEqualToString:@"}"]) trimmedWord = @"compound statements"; else if ([trimmedWord isEqualToString:@"."]) trimmedWord = @"operator ."; else if ([trimmedWord isEqualToString:@"="]) trimmedWord = @"operator ="; else if ([trimmedWord isEqualToString:@"+"]) trimmedWord = @"Arithmetic operators"; else if ([trimmedWord isEqualToString:@"-"]) trimmedWord = @"Arithmetic operators"; else if ([trimmedWord isEqualToString:@"*"]) trimmedWord = @"Arithmetic operators"; else if ([trimmedWord isEqualToString:@"/"]) trimmedWord = @"Arithmetic operators"; else if ([trimmedWord isEqualToString:@"%"]) trimmedWord = @"Arithmetic operators"; else if ([trimmedWord isEqualToString:@"^"]) trimmedWord = @"Arithmetic operators"; else if ([trimmedWord isEqualToString:@"|"]) trimmedWord = @"Logical operators"; else if ([trimmedWord isEqualToString:@"&"]) trimmedWord = @"Logical operators"; else if ([trimmedWord isEqualToString:@"!"]) trimmedWord = @"Logical operators"; else if ([trimmedWord isEqualToString:@"=="]) trimmedWord = @"Comparative operators"; else if ([trimmedWord isEqualToString:@"!="]) trimmedWord = @"Comparative operators"; else if ([trimmedWord isEqualToString:@"<="]) trimmedWord = @"Comparative operators"; else if ([trimmedWord isEqualToString:@">="]) trimmedWord = @"Comparative operators"; else if ([trimmedWord isEqualToString:@"<"]) trimmedWord = @"Comparative operators"; else if ([trimmedWord isEqualToString:@">"]) trimmedWord = @"Comparative operators"; else if ([trimmedWord isEqualToString:@"'"]) trimmedWord = @"type string"; else if ([trimmedWord isEqualToString:@"\""]) trimmedWord = @"type string"; else if ([trimmedWord isEqualToString:@";"]) trimmedWord = @"null statements"; else if ([trimmedWord isEqualToString:@"//"]) trimmedWord = @"comments"; else if ([trimmedWord isEqualToString:@"if"]) trimmedWord = @"if and if–else statements"; else if ([trimmedWord isEqualToString:@"else"]) trimmedWord = @"if and if–else statements"; else if ([trimmedWord isEqualToString:@"for"]) trimmedWord = @"for statements"; else if ([trimmedWord isEqualToString:@"in"]) trimmedWord = @"for statements"; else if ([trimmedWord isEqualToString:@"function"]) trimmedWord = @"user-defined functions"; else { // Give the delegate a chance to supply additional help-text substitutions id delegate = [self delegate]; if ([delegate respondsToSelector:@selector(eidosTextView:helpTextForClickedText:)]) { NSString *delegateResult = [delegate eidosTextView:self helpTextForClickedText:trimmedWord]; // The delegate may return nil or @"" to indicate no substitution desired if ([delegateResult length]) trimmedWord = delegateResult; } } // And then look up the hit using EidosHelpController if ([trimmedWord length] != 0) { [self setSelectedRange:wordRange]; EidosHelpController *helpController = [EidosHelpController sharedController]; [helpController enterSearchForString:trimmedWord titlesOnly:YES]; [[helpController window] makeKeyAndOrderFront:self]; } } // We need this to keep the help controller window in front after an option-click, otherwise AppKit forces us back on top again [NSApp preventWindowOrdering]; return; } // Start out willing to work with a double-click for purposes of delimiter-balancing; // see selectionRangeForProposedRange:proposedCharRange granularity: below inEligibleDoubleClick = YES; [super mouseDown:theEvent]; } - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent { NSUInteger modifiers = [theEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; // BCH 4/7/2016: NSEventModifierFlags not defined in 10.9 // We need this to keep the help controller window in front after an option-click, otherwise AppKit forces us back on top again if ((modifiers & NSEventModifierFlagOption) && !(modifiers & NSEventModifierFlagControl)) return YES; return NO; } - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)theEvent { NSUInteger modifiers = [theEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; // BCH 4/7/2016: NSEventModifierFlags not defined in 10.9 // We need this to keep the help controller window in front after an option-click, otherwise AppKit forces us back on top again if ((modifiers & NSEventModifierFlagOption) && !(modifiers & NSEventModifierFlagControl)) return YES; return NO; } - (NSRange)selectionRangeForProposedRange:(NSRange)proposedCharRange granularity:(NSSelectionGranularity)granularity { if ((granularity == NSSelectByWord) && inEligibleDoubleClick) { // The proposed range has to be zero-length, otherwise this click sequence is ineligible if (proposedCharRange.length == 0) { NSEvent *event = [NSApp currentEvent]; NSEventType eventType = [event type]; NSTimeInterval eventTime = [event timestamp]; if (eventType == NSEventTypeLeftMouseDown) { // This is the mouseDown of the double-click; we do not want to modify the selection here, just log the time doubleDownTime = eventTime; } else if (eventType == NSEventTypeLeftMouseUp) { // After the double-click interval since the second mouseDown, the mouseUp is no longer eligible if (eventTime - doubleDownTime <= [NSEvent doubleClickInterval]) { // MAINTENANCE NOTE: This also exists, in slightly altered form, in insertText:replacementRange:, so please maintain the two in parallel! NSString *scriptString = [[self textStorage] string]; int stringLength = (int)[scriptString length]; int proposedCharacterIndex = (int)proposedCharRange.location; unichar uch = [scriptString characterAtIndex:proposedCharacterIndex]; BOOL forward = false; unichar incrementChar = ' '; unichar decrementChar = ' '; EidosTokenType token1 = EidosTokenType::kTokenNone; EidosTokenType token2 = EidosTokenType::kTokenNone; // Check for any of the delimiter types we support, and set up to do a scan if (uch == '{') { forward = true; incrementChar = '{'; decrementChar = '}'; token1 = EidosTokenType::kTokenLBrace; token2 = EidosTokenType::kTokenRBrace; } if (uch == '}') { forward = false; incrementChar = '}'; decrementChar = '{'; token1 = EidosTokenType::kTokenLBrace; token2 = EidosTokenType::kTokenRBrace; } if (uch == '(') { forward = true; incrementChar = '('; decrementChar = ')'; token1 = EidosTokenType::kTokenLParen; token2 = EidosTokenType::kTokenRParen; } if (uch == ')') { forward = false; incrementChar = ')'; decrementChar = '('; token1 = EidosTokenType::kTokenLParen; token2 = EidosTokenType::kTokenRParen; } if (uch == '[') { forward = true; incrementChar = '['; decrementChar = ']'; token1 = EidosTokenType::kTokenLBracket; token2 = EidosTokenType::kTokenRBracket; } if (uch == ']') { forward = false; incrementChar = ']'; decrementChar = '['; token1 = EidosTokenType::kTokenLBracket; token2 = EidosTokenType::kTokenRBracket; } if (token1 != EidosTokenType::kTokenNone) { // We've got a double-click on a character that we want to balance-select. This is complicated mostly // because of strings, which are a pain in the butt. To simplify that issue, we tokenize and search // in the token stream parallel to searching in the text. std::string script_string([scriptString UTF8String]); EidosScript script(script_string); // Tokenize script.Tokenize(true, true); // make bad tokens as needed, keep nonsignificant tokens // Find the token containing the double-click point const std::vector &tokens = script.Tokens(); int token_count = (int)tokens.size(); const EidosToken *click_token = nullptr; int click_token_index; for (click_token_index = 0; click_token_index < token_count; ++click_token_index) { click_token = &tokens[click_token_index]; if ((click_token->token_UTF16_start_ <= proposedCharacterIndex) && (click_token->token_UTF16_end_ >= proposedCharacterIndex)) break; } if (click_token) { // OK, this token contains the character that was double-clicked. We could have the actual token // (incrementToken), we could be in a string, or we could be in a comment. We stay in the domain // that we start in; if in a string, for example, only delimiters within strings affect our scan. EidosTokenType click_token_type = click_token->token_type_; const EidosToken *scan_token = click_token; int scan_token_index = click_token_index; NSInteger scanPosition = proposedCharacterIndex; int balanceCount = 0; while (YES) { EidosTokenType scan_token_type = scan_token->token_type_; BOOL isCandidate; if (click_token_type == EidosTokenType::kTokenComment) isCandidate = (scan_token_type == EidosTokenType::kTokenComment); else if (click_token_type == EidosTokenType::kTokenCommentLong) isCandidate = (scan_token_type == EidosTokenType::kTokenCommentLong); else if (click_token_type == EidosTokenType::kTokenString) isCandidate = (scan_token_type == EidosTokenType::kTokenString); else isCandidate = ((scan_token_type == token1) || (scan_token_type == token2)); if (isCandidate) { uch = [scriptString characterAtIndex:scanPosition]; if (uch == incrementChar) balanceCount++; else if (uch == decrementChar) balanceCount--; // If balanceCount is now zero, all opens have been balanced by closes, and we have found our matching delimiter if (balanceCount == 0) { inEligibleDoubleClick = false; // clear this to avoid potential confusion if we get called again later if (forward) return NSMakeRange(proposedCharacterIndex, scanPosition - proposedCharacterIndex + 1); else return NSMakeRange(scanPosition, proposedCharacterIndex - scanPosition + 1); } } // Advance, forward or backward, to the next character; if we reached the end without balancing, beep scanPosition += (forward ? 1 : -1); if ((scanPosition == stringLength) || (scanPosition == -1)) { NSBeep(); inEligibleDoubleClick = false; return NSMakeRange(proposedCharacterIndex, 1); } // Make sure we're in the right token while ((scan_token->token_UTF16_start_ > scanPosition) || (scan_token->token_UTF16_end_ < scanPosition)) { scan_token_index += (forward ? 1 : -1); if ((scan_token_index < 0) || (scan_token_index == token_count)) { NSLog(@"ran out of tokens!"); NSBeep(); inEligibleDoubleClick = false; return NSMakeRange(proposedCharacterIndex, 1); } scan_token = &tokens[scan_token_index]; } } } else { inEligibleDoubleClick = false; } } else { inEligibleDoubleClick = false; } } else { inEligibleDoubleClick = false; } } else { inEligibleDoubleClick = false; } } else { inEligibleDoubleClick = false; } } return [super selectionRangeForProposedRange:proposedCharRange granularity:granularity]; } // // Syntax coloring // #pragma mark - #pragma mark Syntax coloring - (void)syntaxColorForEidos { id delegate = [self delegate]; // Construct a Script object from the current script string NSString *scriptString = [self string]; std::string script_string([scriptString UTF8String]); EidosScript script(script_string); // Tokenize script.Tokenize(true, true); // make bad tokens as needed, keep nonsignificant tokens // Set up our shared colors static NSColor *numberLiteralColor = nil; static NSColor *stringLiteralColor = nil; static NSColor *commentColor = nil; static NSColor *identifierColor = nil; static NSColor *keywordColor = nil; static NSColor *contextKeywordColor = nil; if (!numberLiteralColor) { numberLiteralColor = [[NSColor colorWithCalibratedRed:28/255.0 green:0/255.0 blue:207/255.0 alpha:1.0] retain]; stringLiteralColor = [[NSColor colorWithCalibratedRed:196/255.0 green:26/255.0 blue:22/255.0 alpha:1.0] retain]; commentColor = [[NSColor colorWithCalibratedRed:0/255.0 green:116/255.0 blue:0/255.0 alpha:1.0] retain]; identifierColor = [[NSColor colorWithCalibratedRed:63/255.0 green:110/255.0 blue:116/255.0 alpha:1.0] retain]; keywordColor = [[NSColor colorWithCalibratedRed:170/255.0 green:13/255.0 blue:145/255.0 alpha:1.0] retain]; contextKeywordColor = [[NSColor colorWithCalibratedRed:80/255.0 green:13/255.0 blue:145/255.0 alpha:1.0] retain]; } // Syntax color! NSRange fullRange = NSMakeRange(0, [[self textStorage] length]); NSLayoutManager *lm = [self layoutManager]; [lm ensureGlyphsForCharacterRange:fullRange]; [lm removeTemporaryAttribute:NSForegroundColorAttributeName forCharacterRange:fullRange]; for (const EidosToken &token : script.Tokens()) { NSRange tokenRange = NSMakeRange(token.token_UTF16_start_, token.token_UTF16_end_ - token.token_UTF16_start_ + 1); if (token.token_type_ == EidosTokenType::kTokenNumber) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:numberLiteralColor forCharacterRange:tokenRange]; if (token.token_type_ == EidosTokenType::kTokenString) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:stringLiteralColor forCharacterRange:tokenRange]; if ((token.token_type_ == EidosTokenType::kTokenComment) || (token.token_type_ == EidosTokenType::kTokenCommentLong)) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:commentColor forCharacterRange:tokenRange]; if (token.token_type_ > EidosTokenType::kFirstIdentifierLikeToken) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:keywordColor forCharacterRange:tokenRange]; if (token.token_type_ == EidosTokenType::kTokenIdentifier) { // most identifiers are left as black; only special ones get colored const std::string &token_string = token.token_string_; if ((token_string.compare("T") == 0) || (token_string.compare("F") == 0) || (token_string.compare("E") == 0) || (token_string.compare("PI") == 0) || (token_string.compare("INF") == 0) || (token_string.compare("NAN") == 0) || (token_string.compare("NULL") == 0)) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:identifierColor forCharacterRange:tokenRange]; else { // we also let the Context specify special identifiers that we will syntax color if ([delegate respondsToSelector:@selector(eidosTextView:tokenStringIsSpecialIdentifier:)]) { EidosSyntaxHighlightType highlightType = [delegate eidosTextView:self tokenStringIsSpecialIdentifier:token_string]; if (highlightType == EidosSyntaxHighlightType::kHighlightAsIdentifier) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:identifierColor forCharacterRange:tokenRange]; else if (highlightType == EidosSyntaxHighlightType::kHighlightAsKeyword) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:keywordColor forCharacterRange:tokenRange]; else if (highlightType == EidosSyntaxHighlightType::kHighlightAsContextKeyword) [lm addTemporaryAttribute:NSForegroundColorAttributeName value:contextKeywordColor forCharacterRange:tokenRange]; } } } } } - (void)syntaxColorForOutput { NSRange fullRange = NSMakeRange(0, [[self textStorage] length]); NSLayoutManager *lm = [self layoutManager]; NSString *string = [self string]; NSArray *lines = [string componentsSeparatedByString:@"\n"]; int lineCount = (int)[lines count]; int stringPosition = 0; // Set up our shared attributes static NSDictionary *poundDirectiveAttrs = nil; static NSDictionary *commentAttrs = nil; static NSDictionary *subpopAttrs = nil; static NSDictionary *genomicElementAttrs = nil; static NSDictionary *mutationTypeAttrs = nil; if (!poundDirectiveAttrs) { poundDirectiveAttrs = [@{NSForegroundColorAttributeName : [NSColor colorWithCalibratedRed:196/255.0 green:26/255.0 blue:22/255.0 alpha:1.0]} retain]; commentAttrs = [@{NSForegroundColorAttributeName : [NSColor colorWithCalibratedRed:0/255.0 green:116/255.0 blue:0/255.0 alpha:1.0]} retain]; subpopAttrs = [@{NSForegroundColorAttributeName : [NSColor colorWithCalibratedRed:28/255.0 green:0/255.0 blue:207/255.0 alpha:1.0]} retain]; genomicElementAttrs = [@{NSForegroundColorAttributeName : [NSColor colorWithCalibratedRed:63/255.0 green:110/255.0 blue:116/255.0 alpha:1.0]} retain]; mutationTypeAttrs = [@{NSForegroundColorAttributeName : [NSColor colorWithCalibratedRed:170/255.0 green:13/255.0 blue:145/255.0 alpha:1.0]} retain]; } // And then tokenize and color [lm removeTemporaryAttribute:NSForegroundColorAttributeName forCharacterRange:fullRange]; for (int lineIndex = 0; lineIndex < lineCount; ++lineIndex) { NSString *line = [lines objectAtIndex:lineIndex]; NSRange lineRange = NSMakeRange(stringPosition, (int)[line length]); int nextStringPosition = (int)(stringPosition + lineRange.length + 1); // +1 for the newline if (lineRange.length) { //NSLog(@"lineIndex %d, lineRange == %@", lineIndex, NSStringFromRange(lineRange)); // find comments and color and remove them NSRange commentRange = [line rangeOfString:@"//"]; if ((commentRange.location != NSNotFound) && (commentRange.length == 2)) { int commentLength = (int)(lineRange.length - commentRange.location); [lm addTemporaryAttributes:commentAttrs forCharacterRange:NSMakeRange(lineRange.location + commentRange.location, commentLength)]; lineRange.length -= commentLength; line = [line substringToIndex:commentRange.location]; } // if anything is left... if (lineRange.length) { // remove leading whitespace do { NSRange leadingWhitespaceRange = [line rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] options:NSAnchoredSearch]; if (leadingWhitespaceRange.location == NSNotFound || leadingWhitespaceRange.length == 0) break; lineRange.location += leadingWhitespaceRange.length; lineRange.length -= leadingWhitespaceRange.length; line = [line substringFromIndex:leadingWhitespaceRange.length]; } while (YES); // remove trailing whitespace do { NSRange trailingWhitespaceRange = [line rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] options:NSAnchoredSearch | NSBackwardsSearch]; if (trailingWhitespaceRange.location == NSNotFound || trailingWhitespaceRange.length == 0) break; lineRange.length -= trailingWhitespaceRange.length; line = [line substringToIndex:trailingWhitespaceRange.location]; } while (YES); // if anything is left... if (lineRange.length) { // find pound directives and color them if ([line characterAtIndex:0] == '#') [lm addTemporaryAttributes:poundDirectiveAttrs forCharacterRange:lineRange]; else { NSRange scanRange = NSMakeRange(0, lineRange.length); do { NSRange tokenRange = [line rangeOfString:@"\\b[pgm][0-9]+\\b" options:NSRegularExpressionSearch range:scanRange]; if (tokenRange.location == NSNotFound || tokenRange.length == 0) break; NSString *substring = [line substringWithRange:tokenRange]; NSDictionary *syntaxAttrs = nil; unichar firstChar = [substring characterAtIndex:0]; if (firstChar == 'p') syntaxAttrs = subpopAttrs; else if (firstChar == 'g') syntaxAttrs = genomicElementAttrs; else if (firstChar == 'm') syntaxAttrs = mutationTypeAttrs; // we don't presently color sX or iX in the output if (syntaxAttrs) [lm addTemporaryAttributes:syntaxAttrs forCharacterRange:NSMakeRange(tokenRange.location + lineRange.location, tokenRange.length)]; scanRange.length = (scanRange.location + scanRange.length) - (tokenRange.location + tokenRange.length); scanRange.location = (tokenRange.location + tokenRange.length); if (scanRange.length < 2) break; } while (YES); } } } } stringPosition = nextStringPosition; } } - (void)clearSyntaxColoring { NSRange fullRange = NSMakeRange(0, [[self textStorage] length]); NSLayoutManager *lm = [self layoutManager]; [lm ensureGlyphsForCharacterRange:fullRange]; [lm removeTemporaryAttribute:NSForegroundColorAttributeName forCharacterRange:fullRange]; } - (void)updateSyntaxColoring { // This method will clear all syntax coloring if EidosSyntaxColoringOption::NoSyntaxColoring is set, so // it should not be called on every text change, to allow the console textview to maintain its own coloring. // This does not trigger textDidChange (that is done manually), so there is no need for re-entrancy protection. switch (_syntaxColoring) { case kEidosSyntaxColoringNone: [self clearSyntaxColoring]; break; case kEidosSyntaxColoringEidos: [self syntaxColorForEidos]; break; case kEidosSyntaxColoringOutput: [self syntaxColorForOutput]; break; } } - (void)setSyntaxColoring:(EidosSyntaxColoringOption)syntaxColoring { if (_syntaxColoring != syntaxColoring) { _syntaxColoring = syntaxColoring; [self updateSyntaxColoring]; } } - (void)setDisplayFontSize:(int)fontSize { // This is used by SLiMgui; there is no UI exposing it in EidosScribe if (_displayFontSize != fontSize) { _displayFontSize = fontSize; NSFont *newFont = [NSFont fontWithName:@"Menlo" size:fontSize]; NSTextStorage *ts = [self textStorage]; // go through all attribute runs for NSFontAttribute and change them [ts beginEditing]; [ts enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, ts.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (value) { NSFont *oldFont = (NSFont *)value; CGFloat pointSize = [oldFont pointSize]; if ((pointSize >= 6.0) && (pointSize <= 100.0)) // avoid resizing the padding lines in the console... { [ts removeAttribute:NSFontAttributeName range:range]; [ts addAttribute:NSFontAttributeName value:newFont range:range]; } } }]; [ts endEditing]; // set the typing attributes; this code just makes an assumption about the correct typing attributes // based on the class, probably I ought to add a new method on EidosTextView to get a typing attr dict... NSDictionary *textAttributes; if ([self isKindOfClass:[EidosConsoleTextView class]]) textAttributes = [NSDictionary eidosInputAttrsWithSize:[self displayFontSize]]; else textAttributes = [NSDictionary eidosTextAttributesWithColor:nil size:_displayFontSize]; [self setTypingAttributes:textAttributes]; // fix the tab stops NSParagraphStyle *pstyle = [textAttributes objectForKey:NSParagraphStyleAttributeName]; [ts beginEditing]; [ts enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, ts.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (value) { [ts removeAttribute:NSParagraphStyleAttributeName range:range]; [ts addAttribute:NSParagraphStyleAttributeName value:pstyle range:range]; } }]; [ts endEditing]; } } - (void)recolorAfterChanges { // We fold in syntax coloring as part of every change set. If _syntaxColoring==NoSyntaxColoring, we don't do // anything on text changes; we only clear attributes when NoSyntaxColoring is initially set on the textview. if (_syntaxColoring != kEidosSyntaxColoringNone) [self updateSyntaxColoring]; } - (void)didChangeText { // When we used regular attributes on the text storage for syntax coloring, this was done in // textStorageDidProcessEditing: since that is the right time to change attributes in response // to other changes. Now that we use temporary attributes, though, we need to do it in this // method, so that the layout manager is completely synchronized with the new, changed text. [super didChangeText]; if (_shouldRecolorAfterChanges) [self recolorAfterChanges]; } - (void)clearHighlightMatches { NSRange fullRange = NSMakeRange(0, [[self textStorage] length]); NSLayoutManager *lm = [self layoutManager]; [lm ensureGlyphsForCharacterRange:fullRange]; [lm removeTemporaryAttribute:NSBackgroundColorAttributeName forCharacterRange:fullRange]; } - (void)highlightMatchesForString:(NSString *)matchString { NSColor *highlightColor; // Get the find highlight color if available, otherwise the standard selected-text background color if (@available(macOS 10.13, *)) { highlightColor = [NSColor findHighlightColor]; } else { highlightColor = [NSColor selectedControlColor]; } // Highlight using the background color on all matches NSRange fullRange = NSMakeRange(0, [[self textStorage] length]); NSString *string = [[self textStorage] string]; NSLayoutManager *lm = [self layoutManager]; [lm ensureGlyphsForCharacterRange:fullRange]; // thanks to https://stackoverflow.com/a/7033787/2752221 NSRange searchRange = fullRange, foundRange; while (searchRange.location < string.length) { searchRange.length = string.length - searchRange.location; foundRange = [string rangeOfString:matchString options:0 range:searchRange]; if (foundRange.location != NSNotFound) { [lm addTemporaryAttribute:NSBackgroundColorAttributeName value:highlightColor forCharacterRange:foundRange]; searchRange.location = foundRange.location + 1; } else { break; } } } // // Signature display // #pragma mark - #pragma mark Signature display - (EidosFunctionMap *)functionMapForScriptString:(NSString *)scriptString includingOptionalFunctions:(BOOL)includingOptionalFunctions { // This returns a function map (owned by the caller) that reflects the best guess we can make, incorporating // any functions known to our delegate, as well as all functions we can scrape from the script string. std::string script_string([scriptString UTF8String]); EidosScript script(script_string); // Tokenize script.Tokenize(true, false); // make bad tokens as needed, don't keep nonsignificant tokens return [self functionMapForTokenizedScript:script includingOptionalFunctions:includingOptionalFunctions]; } - (EidosFunctionMap *)functionMapForTokenizedScript:(EidosScript &)script includingOptionalFunctions:(BOOL)includingOptionalFunctions { // This lower-level function takes a tokenized script object and works from there, allowing reuse of work // in the case of attributedSignatureForScriptString:... id delegate = [self delegate]; EidosFunctionMap *functionMapPtr = nullptr; if ([delegate respondsToSelector:@selector(functionMapForEidosTextView:)]) functionMapPtr = [delegate functionMapForEidosTextView:self]; if (functionMapPtr) functionMapPtr = new EidosFunctionMap(*functionMapPtr); else functionMapPtr = new EidosFunctionMap(*EidosInterpreter::BuiltInFunctionMap()); // functionMapForEidosTextView: returns the function map for the current interpreter state, and the type-interpreter // stuff we do below gives the delegate no chance to intervene (note that SLiMTypeInterpreter does not get in here, // unlike in the code completion machinery!). But sometimes we want SLiM's zero-gen functions to be added to the map // in all cases; it would be even better to be smart the way code completion is, but that's more work than it's worth. if (includingOptionalFunctions) if ([delegate respondsToSelector:@selector(eidosTextView:addOptionalFunctionsToMap:)]) [delegate eidosTextView:self addOptionalFunctionsToMap:functionMapPtr]; // OK, now we have a starting point. We now want to use the type-interpreter to add any functions that are declared // in the full script, so that such declarations are known to us even before they have actually been executed. EidosTypeTable typeTable; EidosCallTypeTable callTypeTable; EidosSymbolTable *symbols = gEidosConstantsSymbolTable; if ([delegate respondsToSelector:@selector(eidosTextView:symbolsFromBaseSymbols:)]) symbols = [delegate eidosTextView:self symbolsFromBaseSymbols:symbols]; if (symbols) symbols->AddSymbolsToTypeTable(&typeTable); script.ParseInterpreterBlockToAST(true, true); // make bad nodes as needed (i.e. never raise, and produce a correct tree) EidosTypeInterpreter typeInterpreter(script, typeTable, *functionMapPtr, callTypeTable); typeInterpreter.TypeEvaluateInterpreterBlock(); // result not used return functionMapPtr; } - (NSAttributedString *)attributedSignatureForScriptString:(NSString *)scriptString selection:(NSRange)selection { if ([scriptString length]) { std::string script_string([scriptString UTF8String]); EidosScript script(script_string); // Tokenize script.Tokenize(true, false); // make bad tokens as needed, don't keep nonsignificant tokens const std::vector &tokens = script.Tokens(); int tokenCount = (int)tokens.size(); //NSLog(@"script string \"%@\" contains %d tokens", scriptString, tokenCount); // Search forward to find the token position of the start of the selection int selectionStart = (int)selection.location; int tokenIndex; for (tokenIndex = 0; tokenIndex < tokenCount; ++tokenIndex) if (tokens[tokenIndex].token_UTF16_start_ >= selectionStart) break; //NSLog(@"token %d follows the selection (selectionStart == %d)", tokenIndex, selectionStart); //if (tokenIndex == tokenCount) // NSLog(@" (end of script)"); //else // NSLog(@" token string: %s", tokens[tokenIndex]->token_string_.c_str()); // tokenIndex now has the index of the first token *after* the selection start; it can be equal to tokenCount // Now we want to scan backward from there, balancing parentheses and looking for the pattern "identifier(" int backscanIndex = tokenIndex - 1; int parenCount = 0, lowestParenCountSeen = 0; while (backscanIndex > 0) // last examined position is 1, since we can't look for an identifier at 0 - 1 == -1 { const EidosToken &token = tokens[backscanIndex]; EidosTokenType tokenType = token.token_type_; if (tokenType == EidosTokenType::kTokenLParen) { --parenCount; if (parenCount < lowestParenCountSeen) { const EidosToken &previousToken = tokens[backscanIndex - 1]; EidosTokenType previousTokenType = previousToken.token_type_; if (previousTokenType == EidosTokenType::kTokenIdentifier) { // OK, we found the pattern "identifier("; extract the name of the function/method // We also figure out here whether it is a method call (tokens like ".identifier(") or not NSString *callName = [NSString stringWithUTF8String:previousToken.token_string_.c_str()]; NSAttributedString *callAttrString = nil; if ((backscanIndex > 1) && (tokens[backscanIndex - 2].token_type_ == EidosTokenType::kTokenDot)) { // This is a method call, so look up its signature that way callAttrString = [self attributedSignatureForMethodName:callName]; } else { // If this is a function declaration like "function(...)identifier(" then show no signature; it's not a function call // Determining this requires a fairly complex backscan, because we also have things like "if (...) identifier(" which // are function calls. This is the price we pay for working at the token level rather than the AST level for this; // so it goes. Note that this backscan is separate from the one done outside this block. BCH 1 March 2018. if ((backscanIndex > 1) && (tokens[backscanIndex - 2].token_type_ == EidosTokenType::kTokenRParen)) { // Start a new backscan starting at the right paren preceding the identifier; we need to scan back to the balancing // left paren, and then see if the next thing before that is "function" or not. int funcCheckIndex = backscanIndex - 2; int funcCheckParens = 0; while (funcCheckIndex >= 0) { const EidosToken &backscanToken = tokens[funcCheckIndex]; EidosTokenType backscanTokenType = backscanToken.token_type_; if (backscanTokenType == EidosTokenType::kTokenRParen) funcCheckParens++; else if (backscanTokenType == EidosTokenType::kTokenLParen) funcCheckParens--; --funcCheckIndex; if (funcCheckParens == 0) break; } if ((funcCheckParens == 0) && (funcCheckIndex >= 0) && (tokens[funcCheckIndex].token_type_ == EidosTokenType::kTokenFunction)) break; } // This is a function call, so look up its signature that way, using our best-guess function map EidosFunctionMap *functionMapPtr = [self functionMapForTokenizedScript:script includingOptionalFunctions:YES]; callAttrString = [self attributedSignatureForFunctionName:callName functionMap:functionMapPtr]; delete functionMapPtr; } if (!callAttrString) { // Assemble an attributed string for our failed lookup message NSMutableAttributedString *attrStr = [[[NSMutableAttributedString alloc] init] autorelease]; NSDictionary *plainAttrs = [NSDictionary eidosOutputAttrsWithSize:_displayFontSize]; NSDictionary *functionAttrs = [NSDictionary eidosParseAttrsWithSize:_displayFontSize]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:callName attributes:functionAttrs] autorelease]]; [attrStr appendAttributedString:[[[NSAttributedString alloc] initWithString:@"() – unrecognized call" attributes:plainAttrs] autorelease]]; [attrStr addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [attrStr length])]; callAttrString = attrStr; } return callAttrString; } lowestParenCountSeen = parenCount; } } else if (tokenType == EidosTokenType::kTokenRParen) { ++parenCount; } --backscanIndex; } } return [[[NSAttributedString alloc] init] autorelease]; } - (NSAttributedString *)attributedSignatureForFunctionName:(NSString *)callName functionMap:(EidosFunctionMap *)functionMapPtr { std::string call_name([callName UTF8String]); // Look for a matching function signature for the call name. for (const auto& function_iter : *functionMapPtr) { const EidosFunctionSignature *sig = function_iter.second.get(); const std::string &sig_call_name = sig->call_name_; if (sig_call_name.compare(call_name) == 0) return [NSAttributedString eidosAttributedStringForCallSignature:sig size:[self displayFontSize]]; } return nil; } - (NSAttributedString *)attributedSignatureForMethodName:(NSString *)callName { std::string call_name([callName UTF8String]); // Look for a matching method signature for the call name. for (const EidosMethodSignature_CSP &sig : EidosClass::RegisteredClassMethods(true, true)) { const std::string &sig_call_name = sig->call_name_; if (sig_call_name.compare(call_name) == 0) return [NSAttributedString eidosAttributedStringForCallSignature:sig.get() size:[self displayFontSize]]; } return nil; } // // Auto-completion // #pragma mark - #pragma mark Auto-completion - (void)keyDown:(NSEvent *)event { // Sometimes esc and command-. do not seem to be bound to complete:. I'm not sure if that is a change on 10.12, // or a matter of individual key bindings. In any case, we make sure, in this override, that they always work. NSString *chars = [event charactersIgnoringModifiers]; NSUInteger flags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; // BCH 4/7/2016: NSEventModifierFlags not defined in 10.9 if ([chars length] == 1) { unichar keyChar = [chars characterAtIndex:0]; if ((keyChar == 0x1B) && (flags == 0)) { // escape key pressed [self doCommandBySelector:@selector(complete:)]; return; } if ((keyChar == '.') && (flags == NSEventModifierFlagCommand)) { // command-. pressed [self doCommandBySelector:@selector(complete:)]; return; } } [super keyDown:event]; } - (void)cancel:(id)sender { // Apple is now sending this for esc and/or command-. in some OS X versions; we want it to continue to trigger complete: [self complete:sender]; } - (void)cancelOperation:(id)sender { // Apple is now sending this for esc and/or command-. in some OS X versions; we want it to continue to trigger complete: [self complete:sender]; } - (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index { NSArray *completions = nil; #if EIDOS_DEBUG_COMPLETION std::cout << "<<<<<<<<<<<<<< completionsForPartialWordRange: START" << std::endl; #endif [self _completionHandlerWithRangeForCompletion:NULL completions:&completions]; #if EIDOS_DEBUG_COMPLETION std::cout << "<<<<<<<<<<<<<< completionsForPartialWordRange: END" << std::endl; #endif return completions; } - (NSRange)rangeForUserCompletion { NSRange baseRange = NSMakeRange(NSNotFound, 0); #if EIDOS_DEBUG_COMPLETION std::cout << "<<<<<<<<<<<<<< rangeForUserCompletion: START" << std::endl; #endif [self _completionHandlerWithRangeForCompletion:&baseRange completions:NULL]; #if EIDOS_DEBUG_COMPLETION std::cout << "<<<<<<<<<<<<<< rangeForUserCompletion: END" << std::endl; #endif return baseRange; } - (NSMutableArray *)globalCompletionsWithTypes:(EidosTypeTable *)typeTable functions:(EidosFunctionMap *)functionMap keywords:(NSArray *)keywords argumentNames:(NSArray *)argumentNames { NSMutableArray *globals = [NSMutableArray array]; // First add entries for symbols in our type table (from Eidos constants, defined symbols, or our delegate) if (typeTable) { std::vector typedSymbols = typeTable->AllSymbols(); for (std::string &symbol_name : typedSymbols) [globals addObject:[NSString stringWithUTF8String:symbol_name.c_str()]]; } // Sort the symbols, who knows what order they come from EidosTypeTable in... [globals sortUsingSelector:@selector(compare:)]; // Next, if we have argument names that are completion matches, we want them at the top if (argumentNames && [argumentNames count]) { NSArray *oldGlobals = globals; globals = [[argumentNames mutableCopy] autorelease]; [globals addObjectsFromArray:oldGlobals]; } // Next, a sorted list of functions, with () appended if (functionMap) { for (const auto& function_iter : *functionMap) { const EidosFunctionSignature *sig = function_iter.second.get(); NSString *functionName = [NSString stringWithUTF8String:sig->call_name_.c_str()]; // Exclude internal functions such as _Test() if (![functionName hasPrefix:@"_"]) [globals addObject:[functionName stringByAppendingString:@"()"]]; } } // Finally, provide language keywords as an option if requested if (keywords) [globals addObjectsFromArray:keywords]; return globals; } - (NSMutableArray *)completionsForKeyPathEndingInTokenIndex:(int)lastDotTokenIndex ofTokenStream:(const std::vector &)tokens withTypes:(EidosTypeTable *)typeTable functions:(EidosFunctionMap *)functionMap callTypes:(EidosCallTypeTable *)callTypeTable keywords:(NSArray *)keywords { const EidosToken *token = &tokens[lastDotTokenIndex]; EidosTokenType token_type = token->token_type_; if (token_type != EidosTokenType::kTokenDot) { NSLog(@"***** completionsForKeyPathEndingInTokenIndex... called for non-kTokenDot token!"); return nil; } // OK, we've got a key path ending in a dot, and we want to return a list of completions that would work for that key path. // We'll trace backward, adding identifiers to a vector to build up the chain of references. If we hit a bracket, we'll // skip back over everything inside it, since subsetting does not change the type; we just need to balance brackets. If we // hit a parenthesis, we do similarly. If we hit other things – a semicolon, a comma, a brace – that terminates the key path chain. std::vector identifiers; std::vector identifiers_are_calls; std::vector identifier_positions; int bracketCount = 0, parenCount = 0; BOOL lastTokenWasDot = YES, justFinishedParenBlock = NO; for (int tokenIndex = lastDotTokenIndex - 1; tokenIndex >= 0; --tokenIndex) { token = &tokens[tokenIndex]; token_type = token->token_type_; // skip backward over whitespace and comments; they make no difference to us if ((token_type == EidosTokenType::kTokenWhitespace) || (token_type == EidosTokenType::kTokenComment) || (token_type == EidosTokenType::kTokenCommentLong)) continue; if (bracketCount) { // If we're inside a bracketed stretch, all we do is balance brackets and run backward. We don't even clear lastTokenWasDot, // because a []. sequence puts us in the same situation as having just seen a dot – we're still waiting for an identifier. if (token_type == EidosTokenType::kTokenRBracket) { bracketCount++; continue; } if (token_type == EidosTokenType::kTokenLBracket) { bracketCount--; continue; } // Check for tokens that simply make no sense, and bail if ((token_type == EidosTokenType::kTokenLBrace) || (token_type == EidosTokenType::kTokenRBrace) || (token_type == EidosTokenType::kTokenSemicolon) || (token_type >= EidosTokenType::kFirstIdentifierLikeToken)) return nil; continue; } else if (parenCount) { // If we're inside a paren stretch – which could be a parenthesized expression or a function call – we do similarly // to the brackets case, just balancing parens and running backward. We don't clear lastTokenWasDot, because a // (). sequence puts us in the same situation (almost) as having just seen a dot – waiting for an identifier. if (token_type == EidosTokenType::kTokenRParen) { parenCount++; continue; } if (token_type == EidosTokenType::kTokenLParen) { parenCount--; if (parenCount == 0) justFinishedParenBlock = YES; continue; } // Check for tokens that simply make no sense, and bail if ((token_type == EidosTokenType::kTokenLBrace) || (token_type == EidosTokenType::kTokenRBrace) || (token_type == EidosTokenType::kTokenSemicolon) || (token_type >= EidosTokenType::kFirstIdentifierLikeToken)) return nil; continue; } if (!lastTokenWasDot) { // We just saw an identifier, so the only thing that can continue the key path is a dot if (token_type == EidosTokenType::kTokenDot) { lastTokenWasDot = YES; justFinishedParenBlock = NO; continue; } // the key path has terminated at some non-key-path token, so we're done tracing it break; } // OK, the last token was a dot (or a subset preceding a dot). We're looking for an identifier, but we're willing // to get distracted by a subset sequence, since that does not change the type. Anything else does not make sense. if (token_type == EidosTokenType::kTokenIdentifier) { identifiers.emplace_back(token->token_string_); identifiers_are_calls.push_back(justFinishedParenBlock); identifier_positions.emplace_back(token->token_start_); // set up to continue searching the key path backwards lastTokenWasDot = NO; justFinishedParenBlock = NO; continue; } else if (token_type == EidosTokenType::kTokenRBracket) { bracketCount++; continue; } else if (token_type == EidosTokenType::kTokenRParen) { parenCount++; continue; } // This makes no sense, so bail return nil; } // If we were in the middle of tracing the key path when the loop ended, then something is wrong, bail. if (lastTokenWasDot || bracketCount || parenCount) return nil; // OK, we've got an identifier chain in identifiers, in reverse order. We want to start at // the beginning of the key path, and figure out what the class of the key path root is int key_path_index = (int)identifiers.size() - 1; std::string &identifier_name = identifiers[key_path_index]; EidosGlobalStringID identifier_ID = EidosStringRegistry::GlobalStringIDForString(identifier_name); bool identifier_is_call = identifiers_are_calls[key_path_index]; const EidosClass *key_path_class = nullptr; if (identifier_is_call) { // The root identifier is a call, so it should be a function call; try to look it up for (const auto& function_iter : *functionMap) { const EidosFunctionSignature *sig = function_iter.second.get(); if (sig->call_name_.compare(identifier_name) == 0) { key_path_class = sig->return_class_; // In some cases, the function signature does not have the information we need, because the class of the return value // of the function depends upon its parameters. This is the case for functions like sample(), rep(), and so forth. // For this case, we have a special mechanism set up, whereby the EidosTypeInterpreter has logged the class of the // return value of function calls that it has evaluated. We can look up the correct class in that log. This is kind // of a gross solution, but short of rewriting all the completion code, it seems to be the easiest fix. (Rewriting // to fix this more properly would involve doing code completion using a type-annotated tree, without any of the // token-stream handling that we have now; that would be a better design, but I'm going to save that rewrite for later.) if (!key_path_class) { auto callTypeIter = callTypeTable->find(identifier_positions[key_path_index]); if (callTypeIter != callTypeTable->end()) key_path_class = callTypeIter->second; } break; } } } else if (typeTable) { // The root identifier is not a call, so it should be a global symbol; try to look it up EidosTypeSpecifier type_specifier = typeTable->GetTypeForSymbol(identifier_ID); if (!!(type_specifier.type_mask & kEidosValueMaskObject)) key_path_class = type_specifier.object_class; } if (!key_path_class) return nil; // unknown symbol at the root // Now we've got a class for the root of the key path; follow forward through the key path to arrive at the final type. while (--key_path_index >= 0) { identifier_name = identifiers[key_path_index]; identifier_is_call = identifiers_are_calls[key_path_index]; EidosGlobalStringID identifier_id = EidosStringRegistry::GlobalStringIDForString(identifier_name); if (identifier_id == gEidosID_none) return nil; // unrecognized identifier in the key path, so there is probably a typo and we can't complete off of it if (identifier_is_call) { // We have a method call; look up its signature and get the class const EidosCallSignature *call_signature = key_path_class->SignatureForMethod(identifier_id); if (!call_signature) return nil; // no signature, so the class does not support the method given key_path_class = call_signature->return_class_; } else { // We have a property; look up its signature and get the class const EidosPropertySignature *property_signature = key_path_class->SignatureForProperty(identifier_id); if (!property_signature) return nil; // no signature, so the class does not support the property given key_path_class = property_signature->value_class_; } if (!key_path_class) return nil; // unknown symbol at the root; the property yields a non-object type } // OK, we've now got a EidosValue object that represents the end of the line; the final dot is off of this object. // So we want to extract all of its properties and methods, and return them all as candidates. NSMutableArray *candidates = [NSMutableArray array]; const EidosClass *terminus = key_path_class; // First, a sorted list of globals for (auto symbol_sig : *terminus->Properties()) { if (!symbol_sig->deprecated_) [candidates addObject:[NSString stringWithUTF8String:symbol_sig->property_name_.c_str()]]; } [candidates sortUsingSelector:@selector(compare:)]; // Next, a sorted list of methods, with () appended for (auto method_sig : *terminus->Methods()) { if (!method_sig->deprecated_) { NSString *methodName = [NSString stringWithUTF8String:method_sig->call_name_.c_str()]; [candidates addObject:[methodName stringByAppendingString:@"()"]]; } } return candidates; } - (NSArray *)completionsFromArray:(NSArray *)candidates matchingBase:(NSString *)base { NSMutableArray *completions = [NSMutableArray array]; NSInteger candidateCount = [candidates count]; #if 0 // This is simple prefix-based completion; if a candidates begins with base, then it is used for (int candidateIndex = 0; candidateIndex < candidateCount; ++candidateIndex) { NSString *candidate = [candidates objectAtIndex:candidateIndex]; if ([candidate hasPrefix:base]) [completions addObject:candidate]; } #else // This is part-based completion, where iTr will complete to initializeTreeSeq() and iGTy // will complete to initializeGenomicElementType(). To do this, we use a special comparator // that returns a score for the quality of the match, and then we sort all matches by score. std::vector scores; NSMutableArray *unsortedCompletions = [NSMutableArray array]; for (int candidateIndex = 0; candidateIndex < candidateCount; ++candidateIndex) { NSString *candidate = [candidates objectAtIndex:candidateIndex]; int64_t score = [candidate eidosScoreAsCompletionOfString:base]; if (score != INT64_MIN) { [unsortedCompletions addObject:candidate]; scores.emplace_back(score); } } if (scores.size()) { std::vector order = EidosSortIndexes(scores.data(), scores.size(), false); for (int64_t index : order) [completions addObject:[unsortedCompletions objectAtIndex:index]]; } #endif return completions; } - (NSArray *)completionsForTokenStream:(const std::vector &)tokens index:(int)lastTokenIndex canExtend:(BOOL)canExtend withTypes:(EidosTypeTable *)typeTable functions:(EidosFunctionMap *)functionMap callTypes:(EidosCallTypeTable *)callTypeTable keywords:(NSArray *)keywords argumentNames:(NSArray *)argumentNames { // What completions we offer depends on the token stream const EidosToken &token = tokens[lastTokenIndex]; EidosTokenType token_type = token.token_type_; switch (token_type) { case EidosTokenType::kTokenNone: case EidosTokenType::kTokenEOF: case EidosTokenType::kTokenWhitespace: case EidosTokenType::kTokenComment: case EidosTokenType::kTokenCommentLong: case EidosTokenType::kTokenInterpreterBlock: case EidosTokenType::kTokenContextFile: case EidosTokenType::kTokenContextEidosBlock: case EidosTokenType::kFirstIdentifierLikeToken: // These should never be hit return nil; case EidosTokenType::kTokenIdentifier: case EidosTokenType::kTokenIf: case EidosTokenType::kTokenWhile: case EidosTokenType::kTokenFor: case EidosTokenType::kTokenNext: case EidosTokenType::kTokenBreak: case EidosTokenType::kTokenFunction: case EidosTokenType::kTokenReturn: case EidosTokenType::kTokenElse: case EidosTokenType::kTokenDo: case EidosTokenType::kTokenIn: if (canExtend) { NSMutableArray *completions = nil; // This is the tricky case, because the identifier we're extending could be the end of a key path like foo.bar[5:8].ba... // We need to move backwards from the current token until we find or fail to find a dot token; if we see a dot we're in // a key path, otherwise we're in the global context and should filter from those candidates for (int previousTokenIndex = lastTokenIndex - 1; previousTokenIndex >= 0; --previousTokenIndex) { const EidosToken &previous_token = tokens[previousTokenIndex]; EidosTokenType previous_token_type = previous_token.token_type_; // if the token we're on is skippable, continue backwards if ((previous_token_type == EidosTokenType::kTokenWhitespace) || (previous_token_type == EidosTokenType::kTokenComment) || (previous_token_type == EidosTokenType::kTokenCommentLong)) continue; // if the token we're on is a dot, we are indeed at the end of a key path, and can fetch the completions for it if (previous_token_type == EidosTokenType::kTokenDot) { completions = [self completionsForKeyPathEndingInTokenIndex:previousTokenIndex ofTokenStream:tokens withTypes:typeTable functions:functionMap callTypes:callTypeTable keywords:keywords]; break; } // if we see a semicolon or brace, we are in a completely global context if ((previous_token_type == EidosTokenType::kTokenSemicolon) || (previous_token_type == EidosTokenType::kTokenLBrace) || (previous_token_type == EidosTokenType::kTokenRBrace)) { completions = [self globalCompletionsWithTypes:typeTable functions:functionMap keywords:keywords argumentNames:nil]; break; } // if we see any other token, we are not in a key path; let's assume we're following an operator completions = [self globalCompletionsWithTypes:typeTable functions:functionMap keywords:nil argumentNames:argumentNames]; break; } // If we ran out of tokens, we're at the beginning of the file and so in the global context if (!completions) completions = [self globalCompletionsWithTypes:typeTable functions:functionMap keywords:keywords argumentNames:nil]; // Now we have an array of possible completions; we just need to remove those that don't complete the base string, // according to a heuristic algorithm, and sort those that do match by a score of their closeness of match. return [self completionsFromArray:completions matchingBase:[NSString stringWithUTF8String:token.token_string_.c_str()]]; } else if ((token_type == EidosTokenType::kTokenReturn) || (token_type == EidosTokenType::kTokenElse) || (token_type == EidosTokenType::kTokenDo) || (token_type == EidosTokenType::kTokenIn)) { // If you can't extend and you're following an identifier, you presumably need an operator or a keyword or something; // you can't have two identifiers in a row. The same is true of keywords that do not take an expression after them. // But return, else, do, and in can be followed immediately by an expression, so here we handle that case. Identifiers // and other keywords will drop through to return nil below, expressing that we cannot complete in that case. // We used to put return, else, do, and in down the the operators at the bottom, but when canExtend is YES that // prevents them from completing to other things ("in" to "inSLiMgui", for example); moving them up to this case // allows that completion to work, but necessitates the addition of this block to get the correct functionality when // canExtend is NO. BCH 1/22/2019 return [self globalCompletionsWithTypes:typeTable functions:functionMap keywords:nil argumentNames:argumentNames]; } // If the previous token was an identifier and we can't extend it, the next thing probably needs to be an operator or something return nil; case EidosTokenType::kTokenBad: case EidosTokenType::kTokenNumber: case EidosTokenType::kTokenString: case EidosTokenType::kTokenRParen: case EidosTokenType::kTokenRBracket: case EidosTokenType::kTokenSingleton: // We don't have anything to suggest after such tokens; the next thing will need to be an operator, semicolon, etc. return nil; case EidosTokenType::kTokenDot: // This is the other tricky case, because we're being asked to extend a key path like foo.bar[5:8]. return [self completionsForKeyPathEndingInTokenIndex:lastTokenIndex ofTokenStream:tokens withTypes:typeTable functions:functionMap callTypes:callTypeTable keywords:keywords]; case EidosTokenType::kTokenSemicolon: case EidosTokenType::kTokenLBrace: case EidosTokenType::kTokenRBrace: // We are in the global context and anything goes, including a new statement return [self globalCompletionsWithTypes:typeTable functions:functionMap keywords:keywords argumentNames:nil]; case EidosTokenType::kTokenColon: case EidosTokenType::kTokenComma: case EidosTokenType::kTokenLParen: case EidosTokenType::kTokenLBracket: case EidosTokenType::kTokenPlus: case EidosTokenType::kTokenMinus: case EidosTokenType::kTokenMod: case EidosTokenType::kTokenMult: case EidosTokenType::kTokenExp: case EidosTokenType::kTokenAnd: case EidosTokenType::kTokenOr: case EidosTokenType::kTokenDiv: case EidosTokenType::kTokenConditional: case EidosTokenType::kTokenAssign: case EidosTokenType::kTokenAssign_R: case EidosTokenType::kTokenEq: case EidosTokenType::kTokenLt: case EidosTokenType::kTokenLtEq: case EidosTokenType::kTokenGt: case EidosTokenType::kTokenGtEq: case EidosTokenType::kTokenNot: case EidosTokenType::kTokenNotEq: // We are following an operator or similar, so globals are OK but new statements are not return [self globalCompletionsWithTypes:typeTable functions:functionMap keywords:nil argumentNames:argumentNames]; } return nil; } - (NSUInteger)rangeOffsetForCompletionRange { // This is for EidosConsoleTextView to be able to remove the prompt string from the string being completed return 0; } - (NSArray *)uniquedArgumentNameCompletions:(std::vector *)argumentCompletions { // put argument-name completions, if any, at the top of the list; we unique them (preserving order) and add "=" if (argumentCompletions && argumentCompletions->size()) { NSMutableArray *completionsWithArgs = [NSMutableArray array]; for (std::string &arg_completion : *argumentCompletions) { NSString *argNameString = [NSString stringWithUTF8String:arg_completion.c_str()]; NSString *argNameWithEquals = [argNameString stringByAppendingString:@"="]; if (![completionsWithArgs containsObject:argNameWithEquals]) [completionsWithArgs addObject:argNameWithEquals]; } return completionsWithArgs; } return nil; } // one funnel for all completion work, since we use the same pattern to answer both questions... - (void)_completionHandlerWithRangeForCompletion:(NSRange *)baseRange completions:(NSArray **)completions { NSString *scriptString = [self string]; NSRange selection = [self selectedRange]; // ignore charRange and work from the selection NSUInteger rangeOffset = [self rangeOffsetForCompletionRange]; // correct the script string to have only what is entered after the prompt, if we are a EidosConsoleTextView if (rangeOffset) { scriptString = [scriptString substringFromIndex:rangeOffset]; selection.location -= rangeOffset; selection.length -= rangeOffset; } NSUInteger selStart = selection.location; if (selStart != NSNotFound) { // Get the substring up to the start of the selection; that is the range relevant for completion NSString *scriptSubstring = [scriptString substringToIndex:selStart]; std::string script_string([scriptSubstring UTF8String]); // Do shared completion processing that can be intercepted by our delegate: getting a type table for defined variables, // as well as a function map and any added language keywords, all of which depend upon the point of completion id delegate = [self delegate]; EidosTypeTable typeTable; EidosTypeTable *typeTablePtr = &typeTable; EidosFunctionMap functionMap(*EidosInterpreter::BuiltInFunctionMap()); EidosFunctionMap *functionMapPtr = &functionMap; EidosCallTypeTable callTypeTable; EidosCallTypeTable *callTypeTablePtr = &callTypeTable; NSMutableArray *keywords = [NSMutableArray arrayWithObjects:@"break", @"do", @"else", @"for", @"if", @"in", @"next", @"return", @"while", @"function", nil]; std::vector argumentCompletions; BOOL delegateHandled = NO; if ([delegate respondsToSelector:@selector(eidosTextView:completionContextWithScriptString:selection:typeTable:functionMap:callTypeTable:keywords:argumentNameCompletions:)]) { delegateHandled = [delegate eidosTextView:self completionContextWithScriptString:scriptSubstring selection:selection typeTable:&typeTablePtr functionMap:&functionMapPtr callTypeTable:&callTypeTablePtr keywords:keywords argumentNameCompletions:&argumentCompletions]; } // set up automatic disposal of a substitute type table or function map provided by delegate std::unique_ptr raii_typeTablePtr((typeTablePtr != &typeTable) ? typeTablePtr : nullptr); std::unique_ptr raii_functionMapPtr((functionMapPtr != &functionMap) ? functionMapPtr : nullptr); std::unique_ptr raii_callTypeTablePtr((callTypeTablePtr != &callTypeTable) ? callTypeTablePtr : nullptr); if (!delegateHandled) { // First, set up a base type table using the symbol table EidosSymbolTable *symbols = gEidosConstantsSymbolTable; if ([delegate respondsToSelector:@selector(eidosTextView:symbolsFromBaseSymbols:)]) symbols = [delegate eidosTextView:self symbolsFromBaseSymbols:symbols]; if (symbols) symbols->AddSymbolsToTypeTable(typeTablePtr); // Next, a definitive function map that covers all functions defined in the entire script string (not just the script above // the completion point); this seems best, for mutually recursive functions etc.. Duplicate it back into functionMap and // delete the original, so we don't get confused. EidosFunctionMap *definitive_function_map = [self functionMapForScriptString:scriptString includingOptionalFunctions:NO]; functionMap = *definitive_function_map; delete definitive_function_map; // Next, add type table entries based on parsing and analysis of the user's code EidosScript script(script_string); #if EIDOS_DEBUG_COMPLETION std::cout << "Eidos script:\n" << script_string << std::endl << std::endl; #endif script.Tokenize(true, false); // make bad tokens as needed, do not keep nonsignificant tokens script.ParseInterpreterBlockToAST(true, true); // make bad nodes as needed (i.e. never raise, and produce a correct tree) #if EIDOS_DEBUG_COMPLETION std::ostringstream parse_stream; script.PrintAST(parse_stream); std::cout << "Eidos AST:\n" << parse_stream.str() << std::endl << std::endl; #endif EidosTypeInterpreter typeInterpreter(script, *typeTablePtr, *functionMapPtr, *callTypeTablePtr); typeInterpreter.TypeEvaluateInterpreterBlock_AddArgumentCompletions(&argumentCompletions, script_string.length()); // result not used } #if EIDOS_DEBUG_COMPLETION std::cout << "Type table:\n" << *typeTablePtr << std::endl; #endif // Tokenize; we can't use the tokenization done above, as we want whitespace tokens here... EidosScript script(script_string); script.Tokenize(true, true); // make bad tokens as needed, keep nonsignificant tokens #if EIDOS_DEBUG_COMPLETION std::cout << "Eidos token stream:" << std::endl; script.PrintTokens(std::cout); #endif const std::vector &tokens = script.Tokens(); int lastTokenIndex = (int)tokens.size() - 1; BOOL endedCleanly = NO, lastTokenInterrupted = NO; // if we ended with an EOF, that means we did not have a raise and there should be no untokenizable range at the end if ((lastTokenIndex >= 0) && (tokens[lastTokenIndex].token_type_ == EidosTokenType::kTokenEOF)) { --lastTokenIndex; endedCleanly = YES; } // if we are at the end of a comment, without whitespace following it, then we are actually in the comment, and cannot complete // BCH 5 August 2017: Note that EidosTokenType::kTokenCommentLong is deliberately omitted here; this rule does not apply to it if ((lastTokenIndex >= 0) && (tokens[lastTokenIndex].token_type_ == EidosTokenType::kTokenComment)) { if (baseRange) *baseRange = NSMakeRange(NSNotFound, 0); if (completions) *completions = nil; return; } // if we ended with whitespace or a comment, the previous token cannot be extended while (lastTokenIndex >= 0) { const EidosToken &token = tokens[lastTokenIndex]; if ((token.token_type_ != EidosTokenType::kTokenWhitespace) && (token.token_type_ != EidosTokenType::kTokenComment) && (token.token_type_ != EidosTokenType::kTokenCommentLong)) break; --lastTokenIndex; lastTokenInterrupted = YES; } // now diagnose what range we want to use as a basis for completion if (!endedCleanly) { // the selection is at the end of an untokenizable range; we might be in the middle of a string or a comment, // or there might be a tokenization error upstream of us. let's not try to guess what the situation is. if (baseRange) *baseRange = NSMakeRange(NSNotFound, 0); if (completions) *completions = nil; return; } else { if (lastTokenIndex < 0) { // We're at the end of nothing but initial whitespace and comments; or if (!lastTokenInterrupted), // we're at the very beginning of the file. Either way, offer insertion-point completions. if (baseRange) *baseRange = NSMakeRange(selection.location + rangeOffset, 0); if (completions) *completions = [self globalCompletionsWithTypes:typeTablePtr functions:functionMapPtr keywords:keywords argumentNames:nil]; return; } const EidosToken &token = tokens[lastTokenIndex]; EidosTokenType token_type = token.token_type_; // BCH 31 May 2016: If the previous token is a right-paren, that is a tricky case because we could be following // for(), an if(), or while (), in which case we should allow an identifier to follow the right paren, or we could // be following parentheses for grouping, i.e. (a+b), or parentheses for a function call, foo(), in which case we // should not allow an identifier to follow the right paren. This annoyance is basically because the right paren // serves a lot of different functions in the language and so just knowing that we are after one is not sufficient. // So we will walk backwards, balancing our parenthesis count, to try to figure out which case we are in. Note // that even this code is not quite right; it mischaracterizes the do...while() case as allowing an identifier to // follow, because it sees the "while". This is harder to fix, and do...while() is not a common construct, and // the mistake is pretty harmless, so whatever. if (token_type == EidosTokenType::kTokenRParen) { int parenCount = 1; int walkbackIndex = lastTokenIndex; // First walk back until our paren count balances while (--walkbackIndex >= 0) { const EidosToken &walkback_token = tokens[walkbackIndex]; EidosTokenType walkback_token_type = walkback_token.token_type_; if (walkback_token_type == EidosTokenType::kTokenRParen) parenCount++; else if (walkback_token_type == EidosTokenType::kTokenLParen) parenCount--; if (parenCount == 0) break; } // Then walk back over whitespace, and if the first non-white thing we see is right, allow completion while (--walkbackIndex >= 0) { const EidosToken &walkback_token = tokens[walkbackIndex]; EidosTokenType walkback_token_type = walkback_token.token_type_; if ((walkback_token_type != EidosTokenType::kTokenWhitespace) && (walkback_token_type != EidosTokenType::kTokenComment) && (walkback_token_type != EidosTokenType::kTokenCommentLong)) { if ((walkback_token_type == EidosTokenType::kTokenFor) || (walkback_token_type == EidosTokenType::kTokenWhile) || (walkback_token_type == EidosTokenType::kTokenIf)) { // We are at the end of for(), if(), or while(), so we allow global completions as if we were after a semicolon if (baseRange) *baseRange = NSMakeRange(selection.location + rangeOffset, 0); if (completions) *completions = [self globalCompletionsWithTypes:typeTablePtr functions:functionMapPtr keywords:keywords argumentNames:nil]; return; } break; // we didn't hit one of the favored cases, so the code below will reject completion } } } if (lastTokenInterrupted) { // the last token cannot be extended, so if the last token is something an identifier can follow, like an // operator, then we can offer completions at the insertion point based on that, otherwise punt. if ((token_type == EidosTokenType::kTokenNumber) || (token_type == EidosTokenType::kTokenString) || (token_type == EidosTokenType::kTokenRParen) || (token_type == EidosTokenType::kTokenRBracket) || (token_type == EidosTokenType::kTokenIdentifier) || (token_type == EidosTokenType::kTokenIf) || (token_type == EidosTokenType::kTokenWhile) || (token_type == EidosTokenType::kTokenFor) || (token_type == EidosTokenType::kTokenNext) || (token_type == EidosTokenType::kTokenBreak) || (token_type == EidosTokenType::kTokenFunction)) { if (baseRange) *baseRange = NSMakeRange(NSNotFound, 0); if (completions) *completions = nil; return; } if (baseRange) *baseRange = NSMakeRange(selection.location + rangeOffset, 0); if (completions) { NSArray *argumentCompletionsArray = [self uniquedArgumentNameCompletions:&argumentCompletions]; *completions = [self completionsForTokenStream:tokens index:lastTokenIndex canExtend:NO withTypes:typeTablePtr functions:functionMapPtr callTypes:callTypeTablePtr keywords:keywords argumentNames:argumentCompletionsArray]; } return; } else { // the last token was not interrupted, so we can offer completions of it if we want to. NSRange tokenRange = NSMakeRange(token.token_UTF16_start_, token.token_UTF16_end_ - token.token_UTF16_start_ + 1); if (token_type >= EidosTokenType::kTokenIdentifier) { if (baseRange) *baseRange = NSMakeRange(tokenRange.location + rangeOffset, tokenRange.length); if (completions) { NSArray *argumentCompletionsArray = [self uniquedArgumentNameCompletions:&argumentCompletions]; *completions = [self completionsForTokenStream:tokens index:lastTokenIndex canExtend:YES withTypes:typeTablePtr functions:functionMapPtr callTypes:callTypeTablePtr keywords:keywords argumentNames:argumentCompletionsArray]; } return; } if ((token_type == EidosTokenType::kTokenNumber) || (token_type == EidosTokenType::kTokenString) || (token_type == EidosTokenType::kTokenRParen) || (token_type == EidosTokenType::kTokenRBracket)) { if (baseRange) *baseRange = NSMakeRange(NSNotFound, 0); if (completions) *completions = nil; return; } if (baseRange) *baseRange = NSMakeRange(selection.location + rangeOffset, 0); if (completions) { NSArray *argumentCompletionsArray = [self uniquedArgumentNameCompletions:&argumentCompletions]; *completions = [self completionsForTokenStream:tokens index:lastTokenIndex canExtend:NO withTypes:typeTablePtr functions:functionMapPtr callTypes:callTypeTablePtr keywords:keywords argumentNames:argumentCompletionsArray]; } return; } } } } @end // // EidosTextStorage // #pragma mark - #pragma mark EidosTextStorage @interface EidosTextStorage () { NSMutableAttributedString *contents; } @end @implementation EidosTextStorage - (id)initWithAttributedString:(NSAttributedString *)attrStr { if (self = [super init]) { contents = attrStr ? [attrStr mutableCopy] : [[NSMutableAttributedString alloc] init]; } return self; } - init { return [self initWithAttributedString:nil]; } - (void)dealloc { [contents release]; [super dealloc]; } // The next set of methods are the primitives for attributed and mutable attributed string... - (NSString *)string { return [contents string]; } - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRange *)range { return [contents attributesAtIndex:location effectiveRange:range]; } - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str { NSUInteger origLen = [self length]; [contents replaceCharactersInRange:range withString:str]; [self edited:NSTextStorageEditedCharacters range:range changeInLength:[self length] - origLen]; } - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range { [contents setAttributes:attrs range:range]; [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; } // And now the actual reason for this subclass: to provide code-aware word selection behavior - (NSRange)doubleClickAtIndex:(NSUInteger)location { // Start by calling super to get a proposed range. This is documented to raise if location >= [self length] // or location < 0, so in the code below we can assume that location indicates a valid character position. NSRange superRange = [super doubleClickAtIndex:location]; NSString *string = [self string]; NSUInteger stringLength = [string length]; unichar uch = [string characterAtIndex:location]; // If the user has actually double-clicked a period, we want to just return the range of the period. if (uch == '.') return NSMakeRange(location, 1); // Two-character tokens should be considered words, so look for those and fix as needed if (location < stringLength - 1) { // we have another character following us, so let's get it and check for cases unichar uch_after = [string characterAtIndex:location + 1]; if (((uch == '/') && (uch_after == '/')) || ((uch == '=') && (uch_after == '=')) || ((uch == '<') && (uch_after == '=')) || ((uch == '>') && (uch_after == '=')) || ((uch == '!') && (uch_after == '='))) return NSMakeRange(location, 2); } if (location > 0) { // we have another character preceding us, so let's get it and check for cases unichar uch_before = [string characterAtIndex:location - 1]; if (((uch_before == '/') && (uch == '/')) || ((uch_before == '=') && (uch == '=')) || ((uch_before == '<') && (uch == '=')) || ((uch_before == '>') && (uch == '=')) || ((uch_before == '!') && (uch == '='))) return NSMakeRange(location - 1, 2); } // Another case where super's behavior is wrong involves the dot operator; x.y should not be considered a word. // So we check for a period before or after the anchor position, and trim away the periods and everything // past them on both sides. This will correctly handle longer sequences like foo.bar.baz.is.a.test. NSRange candidateRangeBeforeLocation = NSMakeRange(superRange.location, location - superRange.location); NSRange candidateRangeAfterLocation = NSMakeRange(location + 1, NSMaxRange(superRange) - (location + 1)); NSRange periodBeforeRange = [string rangeOfString:@"." options:NSBackwardsSearch range:candidateRangeBeforeLocation]; NSRange periodAfterRange = [string rangeOfString:@"." options:(NSStringCompareOptions)0 range:candidateRangeAfterLocation]; if (periodBeforeRange.location != NSNotFound) { // Change superRange to start after the preceding period; fix its length so its end remains unchanged. superRange.length -= (periodBeforeRange.location + 1 - superRange.location); superRange.location = periodBeforeRange.location + 1; } if (periodAfterRange.location != NSNotFound) { // Change superRange to end before the following period superRange.length -= (NSMaxRange(superRange) - periodAfterRange.location); } return superRange; } @end ================================================ FILE: EidosScribe/EidosTextViewDelegate.h ================================================ // // EidosTextViewDelegate.h // EidosScribe // // Created by Ben Haller on 9/10/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #include #include #include "eidos_interpreter.h" #include "eidos_type_table.h" #include "eidos_type_interpreter.h" @class EidosTextView; /* This is an Objective-C++ header, and so can only be included by Objective-C++ compilations (.mm files instead of .m files). You should not need to include this header in your .h files, since you can declare protocol conformance in a class-continuation category in your .m file, so only classes that conform to this protocol should need to be Objective-C++. EidosTextViewDelegate is a protocol of optional methods that EidosTextView's delegate can implement, to allow the Context to customize code completion in an EidosTextView. Note that if EidosConsoleWindowController is used, these methods get forwarded on by its delegate as well, so that EidosConsoleWindowController also gets Context-defined behavior. */ enum class EidosSyntaxHighlightType { kNoSyntaxHighlight, kHighlightAsIdentifier, kHighlightAsKeyword, kHighlightAsContextKeyword }; @interface EidosTextView(ObjCppExtensions) // Getting a "definitive" function map using type-interpreting to add to the delegate's function map - (EidosFunctionMap *)functionMapForScriptString:(NSString *)scriptString includingOptionalFunctions:(BOOL)includingOptionalFunctions; - (EidosFunctionMap *)functionMapForTokenizedScript:(EidosScript &)script includingOptionalFunctions:(BOOL)includingOptionalFunctions; @end @protocol EidosTextViewDelegate @optional // This allows the Context to define its own symbols beyond those in Eidos itself. // The returned symbol table is not freed by the caller, since it is assumed to be // an existing object with a lifetime managed by the callee. - (EidosSymbolTable *)eidosTextView:(EidosTextView *)eidosTextView symbolsFromBaseSymbols:(EidosSymbolTable *)baseSymbols; // This allows the Context to define its own functions beyond those in Eidos itself. // The returned symbol table is not freed by the caller, since it is assumed to be // an existing object with a lifetime managed by the callee. - (EidosFunctionMap *)functionMapForEidosTextView:(EidosTextView *)eidosTextView; // The functionMapForEidosTextView: delegate method returns the current function map from // the state of the delegate. That may not include some optional functions, such as SLiM's // zero-generation functions, that EidosTextView wants to know about in some situations. // This delegate method requests those optional functions to be added. - (void)eidosTextView:(EidosTextView *)eidosTextView addOptionalFunctionsToMap:(EidosFunctionMap *)functionMap; // This allows the Context to define some special identifier tokens that should // receive different syntax coloring from standard identifiers because they are // in some way built in or provided by the Context automatically - (EidosSyntaxHighlightType)eidosTextView:(EidosTextView *)eidosTextView tokenStringIsSpecialIdentifier:(const std::string &)token_string; // This allows the Context to define substitutions for help searches when the user // option-clicks a token, to provide more targeted help results. If no substitution // is desired, returning nil is recommended. - (NSString *)eidosTextView:(EidosTextView *)eidosTextView helpTextForClickedText:(NSString *)clickedText; // This allows the Context to customize the behavior of code completion, depending upon // the context in which the completion occurs (as determined by the script string, which // extends up to the end of the selection, and the selection range). The delegate should // add types to the type table (which is empty), add functions to the function map (which // has the built-in Eidos functions already), and add applicable language keywords to the // keywords array. If this delegate method is not implemented, EidosTextView will do its // standard behavior. In particular, types will be found with ParseInterpreterBlockToAST() // and TypeEvaluateInterpreterBlock() in addition to symbolsFromBaseSymbols:, functions // will be found with functionMapForEidosTextView:, and no keywords will be added to the base // set. This standard behavior is fine for Contexts that do not define context-dependent // language constructs in the way that SLiM does. // // Note that unlike symbolsFromBaseSymbols: and functionMapForEidosTextView:, here the delegate // is expected to modify the objects passed to it. This difference is motivated by the idea // that the other delegate methods are providing a standard symbol table and function map // kept by the Context, whereas this delegate method is expected to create context-dependent // information that differs from the current state of the Context. The delegate may even // replace the pointers passed to the type table and/or function map, in order to substitute // a new object (perhaps a subclass object) for those objects; in that case, the substituted // object will be freed by the caller (not the delegate), so don't give your private objects. // // Also note that the delegate does not need to worry about uniquing or sorting type entries. // // Return NO if you want Eidos to do its default behavior, YES if you have taken care of it. - (BOOL)eidosTextView:(EidosTextView *)eidosTextView completionContextWithScriptString:(NSString *)scriptString selection:(NSRange)selection typeTable:(EidosTypeTable **)typeTable functionMap:(EidosFunctionMap **)functionMap callTypeTable:(EidosCallTypeTable **)callTypeTable keywords:(NSMutableArray *)keywords argumentNameCompletions:(std::vector *)argNameCompletions; @end ================================================ FILE: EidosScribe/EidosValueWrapper.h ================================================ // // EidosValueWrapper.h // EidosScribe // // Created by Ben Haller on 5/31/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import #include "eidos_value.h" // BCH 4/7/2016: So we can build against the OS X 10.9 SDK #ifndef NS_DESIGNATED_INITIALIZER #define NS_DESIGNATED_INITIALIZER #endif /* This is an Objective-C++ header, and so can only be included by Objective-C++ compilations (.mm files instead of .m files). You should not need to include this header in your .h files, since you can declare protocol conformance in a class-continuation category in your .m file, so only classes that conform to this protocol should need to be Objective-C++. However, EidosValueWrapper is used by EidosVariableBrowserController; it should never be used directly by client code. EidosValueWrapper is a rather tricky little beast. The basic point is to provide the variable browser's NSOutlineView with Obj-C objects to represent the items that it displays. Those items are "really" EidosValues of one sort or another – root EidosValues from the current symbol table, or sub-values that represent individual elements, properties, etc. from those root values. Effectively, these values are related in a similar way to key paths, but with individual-element subscripting as well; foo.bar[5].baz.foobar[2] is a line that might be displayed in the variable browser, with a corresponding EidosValue. The first bit of complication comes from the fact that it isn't really kosher to keep EidosValues around unless you own them, so we participate in the smart pointer scheme used with EidosValue in eidos. Whenever the state of the Eidos interpreter changes, we throw out all of our old wrappers, thereby getting rid of the EidosValue pointers they contain; we have a retain on them, but they are no longer part of the interpreter state and so should not be used. The second bit of complication, however, is that we want NSOutlineView to keep its expansion state identical across such reloads, even though it is displaying a whole new batch of EidosValueWrapper objects. We therefore need EidosValueWrapper to implement -hash and -isEqual:, but those implementations cannot refer to the wrapped EidosValues at all, because they might already be invalid. Hashing and equality therefor need to be based solely on the non-Eidos state of the wrappers. This means that a given wrapper needs to "know" its full "key path", not just its own name; hash and equality are determined by the full key path, in this scheme. This is achieved by giving EidosValueWrapper a pointer to its parent, and making -hash and -isEqual: be recursive methods that follow the key path upward to the root and integrate information from the full path. */ @interface EidosValueWrapper : NSObject { } + (instancetype)wrapperForName:(NSString *)aName parent:(EidosValueWrapper *)parent value:(EidosValue_SP)aValue; + (instancetype)wrapperForName:(NSString *)aName parent:(EidosValueWrapper *)parent value:(EidosValue_SP)aValue index:(int)anIndex of:(int)siblingCount; - (instancetype)init __attribute__((unavailable("This method is not available"))); // superclass designated initializer is not valid - (instancetype)initWithWrappedName:(NSString *)aName parent:(EidosValueWrapper *)parent value:(EidosValue_SP)aValue index:(int)anIndex of:(int)siblingCount NS_DESIGNATED_INITIALIZER; - (void)invalidateWrappedValues; - (void)releaseChildWrappers; - (NSArray *)childWrappers; - (BOOL)isExpandable; - (id)displaySymbol; - (id)displayType; - (id)displaySize; - (id)displayValue; @end ================================================ FILE: EidosScribe/EidosValueWrapper.mm ================================================ // // EidosValueWrapper.m // EidosScribe // // Created by Ben Haller on 5/31/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosValueWrapper.h" #include "eidos_value.h" #include "eidos_property_signature.h" @interface EidosValueWrapper () { @private EidosValueWrapper *parentWrapper; NSString *wrappedName; // the displayed name int wrappedIndex; // the index of wrappedValue upon which the row is based; -1 if the row represents the whole value int wrappedSiblingCount; // the number of siblings of this item; used for -hash and -isEqual: EidosValue_SP wrappedValue; // the value upon which the row is based BOOL isExpandable; // a cached value; YES if wrappedValue is of type object, NO otherwise BOOL isConstant; // is this value a built-in Eidos constant? NSMutableArray *childWrappers; } @end @implementation EidosValueWrapper + (instancetype)wrapperForName:(NSString *)aName parent:(EidosValueWrapper *)parent value:(EidosValue_SP)aValue { return [[[self alloc] initWithWrappedName:aName parent:parent value:std::move(aValue) index:-1 of:0] autorelease]; } + (instancetype)wrapperForName:(NSString *)aName parent:(EidosValueWrapper *)parent value:(EidosValue_SP)aValue index:(int)anIndex of:(int)siblingCount { return [[[self alloc] initWithWrappedName:aName parent:parent value:std::move(aValue) index:anIndex of:siblingCount] autorelease]; } - (instancetype)init { // The superclass designated initializer is not valid for us NSAssert(false, @"-init is not defined for this class"); return [self initWithWrappedName:nil parent:nil value:EidosValue_SP(nullptr) index:0 of:0]; } - (instancetype)initWithWrappedName:(NSString *)aName parent:(EidosValueWrapper *)parent value:(EidosValue_SP)aValue index:(int)anIndex of:(int)siblingCount { if (self = [super init]) { parentWrapper = [parent retain]; wrappedName = [aName retain]; wrappedIndex = anIndex; wrappedSiblingCount = siblingCount; wrappedValue = std::move(aValue); // We cache this so that we know whether we are expandable without needing to dereference wrappedValue; // we therefore know whether or not we are expandable even after wrappedValue is invalidated if (wrappedValue) isExpandable = (wrappedValue->Type() == EidosValueType::kValueObject); else isExpandable = false; // We want to display Eidos constants in gray text, to de-emphasize them. For now, we just hard-code them // as a hack, because we *don't* want SLiM constants (sim, g1, p1, etc.) to display dimmed isConstant = NO; if (!parentWrapper && ([wrappedName isEqualToString:@"T"] || [wrappedName isEqualToString:@"F"] || [wrappedName isEqualToString:@"E"] || [wrappedName isEqualToString:@"PI"] || [wrappedName isEqualToString:@"INF"] || [wrappedName isEqualToString:@"NAN"] || [wrappedName isEqualToString:@"NULL"])) isConstant = YES; childWrappers = nil; } return self; } - (void)dealloc { wrappedValue.reset(); [wrappedName release]; wrappedName = nil; [parentWrapper release]; parentWrapper = nil; [childWrappers release]; childWrappers = nil; [super dealloc]; } - (void)invalidateWrappedValues { wrappedValue.reset(); [childWrappers makeObjectsPerformSelector:@selector(invalidateWrappedValues)]; } - (void)releaseChildWrappers { [childWrappers makeObjectsPerformSelector:@selector(releaseChildWrappers)]; [childWrappers release]; childWrappers = nil; } - (NSArray *)childWrappers { if (!childWrappers) { // If we don't have our cache of child wrappers, set it up on demand childWrappers = [NSMutableArray new]; // If we have no wrapped value, we have no children if (!wrappedValue) return childWrappers; int elementCount = wrappedValue->Count(); // values which are of object type and contain more than one element get displayed as a list of elements if (elementCount > 1) { for (int index = 0; index < elementCount;++ index) { NSString *childName = [NSString stringWithFormat:@"%@[%ld]", wrappedName, (long)index]; EidosValue_SP childValue = wrappedValue->GetValueAtIndex(index, nullptr); EidosValueWrapper *childWrapper = [EidosValueWrapper wrapperForName:childName parent:self value:std::move(childValue) index:index of:elementCount]; [childWrappers addObject:childWrapper]; } } else if (wrappedValue->Type() == EidosValueType::kValueObject) { EidosValue_Object *wrapped_object = ((EidosValue_Object *)wrappedValue.get()); const EidosClass *object_class = wrapped_object->Class(); const std::vector *properties = object_class->Properties(); int propertyCount = (int)properties->size(); bool oldSuppressWarnings = gEidosSuppressWarnings, inaccessibleCaught = false; gEidosSuppressWarnings = true; // prevent warnings from questionable property accesses from producing warnings in the user's output pane for (int index = 0; index < propertyCount; ++index) { const EidosPropertySignature_CSP &propertySig = (*properties)[index]; const std::string &symbolName = propertySig->property_name_; EidosGlobalStringID symbolID = propertySig->property_id_; NSString *symbolObjcName = [NSString stringWithUTF8String:symbolName.c_str()]; EidosValue_SP symbolValue; // protect against raises in property accesses due to inaccessible properties try { symbolValue = wrapped_object->GetPropertyOfElements(symbolID); } catch (...) { //std::cout << "caught inaccessible property " << symbolName << std::endl; inaccessibleCaught = true; } EidosValueWrapper *childWrapper = [EidosValueWrapper wrapperForName:symbolObjcName parent:self value:std::move(symbolValue)]; [childWrappers addObject:childWrapper]; } gEidosSuppressWarnings = oldSuppressWarnings; if (inaccessibleCaught) { // throw away the raise message(s) so they don't confuse us gEidosTermination.clear(); gEidosTermination.str(gEidosStr_empty_string); } } } return childWrappers; } - (BOOL)isExpandable { return isExpandable; } + (NSDictionary *)italicAttrs { static NSDictionary *italicAttrs = nil; if (!italicAttrs) { NSFont *baseFont = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeSmall]]; NSFont *italicFont = [[NSFontManager sharedFontManager] convertFont:baseFont toHaveTrait:NSItalicFontMask]; italicAttrs = [@{NSFontAttributeName : italicFont} retain]; } return italicAttrs; } + (NSDictionary *)dimmedAttrs { static NSDictionary *dimmedAttrs = nil; if (!dimmedAttrs) { NSFont *baseFont = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeSmall]]; dimmedAttrs = [@{NSFontAttributeName : baseFont, NSForegroundColorAttributeName : [NSColor colorWithCalibratedWhite:0.5 alpha:1.0]} retain]; } return dimmedAttrs; } + (NSDictionary *)centeredDimmedAttrs { static NSDictionary *centeredDimmedAttrs = nil; if (!centeredDimmedAttrs) { NSFont *baseFont = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeSmall]]; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; [paragraphStyle setAlignment:NSTextAlignmentCenter]; centeredDimmedAttrs = [@{NSFontAttributeName : baseFont, NSForegroundColorAttributeName : [NSColor colorWithCalibratedWhite:0.5 alpha:1.0], NSParagraphStyleAttributeName : paragraphStyle} retain]; [paragraphStyle release]; } return centeredDimmedAttrs; } - (id)displaySymbol { // If this row is a marker for an element within an object we treat it specially if (wrappedIndex != -1) return [[[NSAttributedString alloc] initWithString:wrappedName attributes:[EidosValueWrapper italicAttrs]] autorelease]; if (isConstant) return [[[NSAttributedString alloc] initWithString:wrappedName attributes:[EidosValueWrapper dimmedAttrs]] autorelease]; return wrappedName; } - (id)displayType { // If this row is a marker for an element within an object we treat it specially if (wrappedIndex != -1) return @""; // If the value is inaccessible display nothing if (!wrappedValue) return @""; EidosValueType type = wrappedValue->Type(); std::string type_string = StringForEidosValueType(type); const char *type_cstr = type_string.c_str(); NSString *typeString = [NSString stringWithUTF8String:type_cstr]; if (type == EidosValueType::kValueObject) { EidosValue_Object *object_value = (EidosValue_Object *)wrappedValue.get(); const std::string &element_string = object_value->ElementType(); const char *element_cstr = element_string.c_str(); NSString *elementString = [NSString stringWithUTF8String:element_cstr]; typeString = [NSString stringWithFormat:@"%@<%@>", typeString, elementString]; } if (isConstant) return [[[NSAttributedString alloc] initWithString:typeString attributes:[EidosValueWrapper dimmedAttrs]] autorelease]; return typeString; } - (id)displaySize { // If this row is a marker for an element within an object we treat it specially if (wrappedIndex != -1) return @""; // If the value is inaccessible display nothing if (!wrappedValue) return @""; NSString *sizeString = [NSString stringWithFormat:@"%d", wrappedValue->Count()]; if (isConstant) return [[[NSAttributedString alloc] initWithString:sizeString attributes:[EidosValueWrapper centeredDimmedAttrs]] autorelease]; return sizeString; } - (id)displayValue { // If this row is a marker for an element within an object we treat it specially if (wrappedIndex != -1) return @""; // If the value is inaccessible display "" if (!wrappedValue) return @""; int value_count = wrappedValue->Count(); std::ostringstream outstream; // print values as a comma-separated list with strings quoted; halfway between print() and cat() for (int value_index = 0; value_index < value_count; ++value_index) { EidosValue_SP element_value = wrappedValue->GetValueAtIndex(value_index, nullptr); if (value_index > 0) { outstream << ", "; // terminate the list at some reasonable point, otherwise we generate massively long strings for large vectors... if (value_index > 50) { outstream << ", ..."; break; } } outstream << *element_value; } std::string &&out_string = outstream.str(); NSString *outString = [NSString stringWithUTF8String:out_string.c_str()]; if (isConstant) return [[[NSAttributedString alloc] initWithString:outString attributes:[EidosValueWrapper dimmedAttrs]] autorelease]; return outString; } // // NSObject subclass overrides, to redefine equality // - (BOOL)isEqual:(id)anObject { if (self == anObject) return YES; if (![anObject isMemberOfClass:[EidosValueWrapper class]]) return NO; EidosValueWrapper *otherWrapper = (EidosValueWrapper *)anObject; if (wrappedIndex != otherWrapper->wrappedIndex) return NO; if (wrappedSiblingCount != otherWrapper->wrappedSiblingCount) return NO; if (![wrappedName isEqualToString:otherWrapper->wrappedName]) return NO; // We call through to isEqualToWrapper: only if the parent wrapper is defined for both objects; // this is partly for speed, and partly because [nil isEqualToWrapper:nil] would give NO. EidosValueWrapper *otherWrapperParent = otherWrapper->parentWrapper; if (parentWrapper == otherWrapperParent) return YES; if ((parentWrapper && !otherWrapperParent) || (!parentWrapper && otherWrapperParent)) return NO; if (![parentWrapper isEqualToWrapper:otherWrapperParent]) return NO; return YES; } - (BOOL)isEqualToWrapper:(EidosValueWrapper *)otherWrapper { // Note that this method is missing the self==object test at the beginning! This is because it // was already done by the caller; this method is not designed to be called externally! // Similarly, it does not check for object==nil, so it will crash if called with nil! if (wrappedIndex != otherWrapper->wrappedIndex) return NO; if (wrappedSiblingCount != otherWrapper->wrappedSiblingCount) return NO; if (![wrappedName isEqualToString:otherWrapper->wrappedName]) return NO; // We call through to isEqualToWrapper: only if the parent wrapper is defined for both objects; // this is partly for speed, and partly because [nil isEqualToWrapper:nil] would give NO. EidosValueWrapper *otherWrapperParent = otherWrapper->parentWrapper; if (parentWrapper == otherWrapperParent) return YES; if ((parentWrapper && !otherWrapperParent) || (!parentWrapper && otherWrapperParent)) return NO; if (![parentWrapper isEqualToWrapper:otherWrapperParent]) return NO; return YES; } - (NSUInteger)hash { NSUInteger hash = [wrappedName hash]; hash ^= wrappedIndex; hash ^= (wrappedSiblingCount << 16); if (parentWrapper) hash ^= ([parentWrapper hash] << 1); //NSLog(@"hash for item named %@ == %lu", wrappedName, (unsigned long)hash); return hash; } @end ================================================ FILE: EidosScribe/EidosVariableBrowserController.h ================================================ // // EidosVariableBrowserController.h // EidosScribe // // Created by Ben Haller on 6/13/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import // BCH 4/7/2016: So we can build against the OS X 10.9 SDK #ifndef NS_DESIGNATED_INITIALIZER #define NS_DESIGNATED_INITIALIZER #endif @protocol EidosVariableBrowserControllerDelegate; /* EidosVariableBrowserController provides a prefab variable browser for Eidos. It is integrated into EidosConsoleWindowController, so if you use that class, you get the variable browser for free. If you build your own Eidos UI, you can use EidosVariableBrowserController directly. In that case, you will need to extract it from EidosConsoleWindow.xib. */ // Notifications sent to allow other objects that care about the visibility of the browser, such as toggle buttons, to update extern NSString *EidosVariableBrowserWillHideNotification; extern NSString *EidosVariableBrowserWillShowNotification; @interface EidosVariableBrowserController : NSObject { } // The delegate is often EidosConsoleWindowController, but can be your own delegate object @property (nonatomic, assign) IBOutlet NSObject *delegate; // These properties are used by the nib, and are not likely to be used by clients @property (nonatomic, retain) IBOutlet NSWindow *browserWindow; @property (nonatomic, assign) IBOutlet NSOutlineView *browserOutline; @property (nonatomic, assign) IBOutlet NSTableColumn *symbolColumn; @property (nonatomic, assign) IBOutlet NSTableColumn *typeColumn; @property (nonatomic, assign) IBOutlet NSTableColumn *sizeColumn; @property (nonatomic, assign) IBOutlet NSTableColumn *valueColumn; // Normally EidosConsoleWindowController is instantiated in the EidosConsoleWindow.xib nib - (instancetype)init NS_DESIGNATED_INITIALIZER; // Show/hide the browser window - (void)showWindow; - (void)hideWindow; // Tell the controller that the browser window should be disposed of, not just closed; breaks retain loops - (void)cleanup; // Trigger a reload of the variable browser when symbols have changed - (void)reloadBrowser; // Make the browser show or hide; will send the appropriate notifications - (IBAction)toggleBrowserVisibility:(id)sender; @end ================================================ FILE: EidosScribe/EidosVariableBrowserController.mm ================================================ // // EidosVariableBrowserController.m // EidosScribe // // Created by Ben Haller on 6/13/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import "EidosVariableBrowserController.h" #import "EidosVariableBrowserControllerDelegate.h" #import "EidosValueWrapper.h" #include "eidos_property_signature.h" #include "eidos_symbol_table.h" #include // Notifications sent to allow other objects that care about the visibility of the browser, such as toggle buttons, to update NSString *EidosVariableBrowserWillHideNotification = @"EidosVariableBrowserWillHide"; NSString *EidosVariableBrowserWillShowNotification = @"EidosVariableBrowserWillShow"; @interface EidosVariableBrowserController () { @private // Wrappers for the currently displayed objects NSMutableArray *rootBrowserWrappers; // A set used to remember expanded items; see -reloadBrowser NSMutableSet *expandedSet; } @end @implementation EidosVariableBrowserController - (instancetype)init { if (self = [super init]) { // We permanently keep a set of all elements that should be expanded; this persists across browser reloads, etc. expandedSet = [[NSMutableSet alloc] init]; } return self; } - (void)dealloc { //NSLog(@"EidosVariableBrowserController dealloc"); [self invalidateRootWrappers]; [expandedSet release]; expandedSet = nil; [self cleanup]; [super dealloc]; } - (void)setDelegate:(NSObject *)newDelegate { if (newDelegate && ![newDelegate conformsToProtocol:@protocol(EidosVariableBrowserControllerDelegate)]) NSLog(@"Delegate %@ assigned to EidosVariableBrowserController %p does not conform to the EidosVariableBrowserControllerDelegate protocol!", newDelegate, self); _delegate = newDelegate; // nonatomic, assign // Since our delegate defines what appears in our outline view, we need to reload when the delegate changes [self reloadBrowser]; } - (void)showWindow { if (![_browserWindow isVisible]) { [[NSNotificationCenter defaultCenter] postNotificationName:EidosVariableBrowserWillShowNotification object:self]; [_browserWindow makeKeyAndOrderFront:nil]; } } - (void)hideWindow { if ([_browserWindow isVisible]) { [[NSNotificationCenter defaultCenter] postNotificationName:EidosVariableBrowserWillHideNotification object:self]; [_browserWindow performClose:nil]; } } - (void)cleanup { [[self browserWindow] setDelegate:nil]; [self setBrowserWindow:nil]; [self setDelegate:nil]; } - (void)expandItemsInSet:(NSSet *)set belowItem:(id)parentItem { // BCH 4/7/2016: The -numberOfChildrenOfItem: and child:ofItem: methods of NSOutlineView do not exist in 10.9, // so we have to get the datasource of the outline view and get the information ourselves. BOOL respondsToNumberOfChildrenOfItem = [_browserOutline respondsToSelector:@selector(numberOfChildrenOfItem:)]; BOOL respondsToChildOfItem = [_browserOutline respondsToSelector:@selector(child:ofItem:)]; id datasource = ((respondsToNumberOfChildrenOfItem && respondsToChildOfItem) ? nil : [_browserOutline dataSource]); NSUInteger childCount = (respondsToNumberOfChildrenOfItem ? [_browserOutline numberOfChildrenOfItem:parentItem] : [datasource outlineView:_browserOutline numberOfChildrenOfItem:parentItem]); //NSLog(@"after reload, outline has %lu children (%ld rows), expandedSet has %lu items", (unsigned long)childCount, (long)[_browserOutline numberOfRows], (unsigned long)[expandedSet count]); for (unsigned int i = 0; i < childCount; ++i) { id childItem = (respondsToChildOfItem ? [_browserOutline child:i ofItem:parentItem] : [datasource outlineView:_browserOutline child:i ofItem:parentItem]); if ([expandedSet containsObject:childItem]) // uses -hash and -isEqual:, not pointer equality! { if ([_browserOutline isExpandable:childItem]) { [_browserOutline expandItem:childItem]; //NSLog(@" requested expansion for item with name %@ (%p, hash %lu)", ((EidosValueWrapper *)childItem)->wrappedName, childItem, (unsigned long)[childItem hash]); // having expanded the item, we forget it from our set; this way if the user collapses it we won't re-expand it // note that if it stays expanded, it will be re-added to our set in reloadBrowser [expandedSet removeObject:childItem]; [self expandItemsInSet:set belowItem:childItem]; } } } } - (void)reloadBrowser { // Reload symbols in our outline view. This is complicated because we generally do a reload to a state that has zero items (while // script is executing) and then do another reload to a state that has items again (because the script is done executing), and we // want the expansion state to be preserved across that rather jumpy process. The basic scheme is: (1) When we are about to // reload, we save all expanded items by going through all the rows of the outline view, getting the item for each row, finding // out if it is expanded, and if it is, keeping it in an NSSet – but we do this only if the outline view has items in it. Then, // (2) After reloading, we go through each new item, see if it exists in our saved set (which uses -hash and -isEqual:, which // EidosValueWrapper implements for this purpose), and if so, we expand it and recurse down to check its children as well. // First, invalidate all of our old wrapped objects, to make sure that any invalid dereferences cause a clean crash [rootBrowserWrappers makeObjectsPerformSelector:@selector(invalidateWrappedValues)]; // Then go through the outline view and save all items that are expanded NSUInteger rowCount = [_browserOutline numberOfRows]; // BCH 26 Jan. 2020: On 10.15 NSOutlineView apparently caches the number of rows, so even though we would now return 0 from // outlineView:numberOfChildrenOfItem:, it gives us a non-zero count and then logs errors when we call itemAtRow:. So now // we explicitly check that we actually have a symbol table, and if not, correct the number of rows to zero. if (![_delegate symbolTableForEidosVariableBrowserController:self]) rowCount = 0; if (rowCount > 0) { // The outline has items in it, so we will start a new set to save the expanded items for (unsigned int i = 0; i < rowCount; ++i) { id rowItem = [_browserOutline itemAtRow:i]; // Because we are in transition, it is possible for rowItem to be nil. Specifically, the outline view may cache the // number of rows, and thus rowCount may be non-zero; and yet when a given row is asked for, the outline view may // then attempt to obtain it from the delegate (because NSOutlineView loads rows lazily), and the row may no longer // be there because the symbol table has changed (or been tossed entirely). This is OK, actually. Any row that // comes back as nil here is not expanded; a row doesn't get expanded if it hasn't even been loaded. So rows that // come back as nil here can simply be ignored, since our only goal is to log a set of the expanded rows. This // situation does, however, point out the trickiness inherent in asking NSOutlineView about its own state; it will // not necessarily answer in a self-consistent fashion! if (rowItem) { if ([_browserOutline isItemExpanded:rowItem]) { [expandedSet addObject:rowItem]; //NSLog(@"remembering item with name %@ (%p, hash %lu) as expanded", ((EidosValueWrapper *)rowItem)->wrappedName, rowItem, (unsigned long)[rowItem hash]); } } } } // Clear out our wrapper arrays; the ones that are expanded are now retained by expandedSet [self invalidateRootWrappers]; // Reload the outline view from scratch [_browserOutline reloadData]; // Go through and expand items as needed [self expandItemsInSet:expandedSet belowItem:nil]; } - (void)invalidateRootWrappers { [rootBrowserWrappers makeObjectsPerformSelector:@selector(releaseChildWrappers)]; [rootBrowserWrappers release]; rootBrowserWrappers = nil; // set up to be recached } - (NSArray *)rootWrappers { if (!rootBrowserWrappers) { // If we don't have our root wrappers, set up the cache now rootBrowserWrappers = [NSMutableArray new]; EidosSymbolTable *symbols = [_delegate symbolTableForEidosVariableBrowserController:self]; { std::vector readOnlySymbols = symbols->ReadOnlySymbols(); int readOnlySymbolCount = (int)readOnlySymbols.size(); for (int index = 0; index < readOnlySymbolCount;++ index) { const std::string &symbolName = readOnlySymbols[index]; EidosValue_SP symbolValue = symbols->GetValueOrRaiseForSymbol(EidosStringRegistry::GlobalStringIDForString(symbolName)); NSString *symbolObjcName = [NSString stringWithUTF8String:symbolName.c_str()]; EidosValueWrapper *wrapper = [EidosValueWrapper wrapperForName:symbolObjcName parent:nil value:symbolValue]; [rootBrowserWrappers addObject:wrapper]; } } { std::vector readWriteSymbols = symbols->ReadWriteSymbols(); int readWriteSymbolCount = (int)readWriteSymbols.size(); for (int index = 0; index < readWriteSymbolCount;++ index) { const std::string &symbolName = readWriteSymbols[index]; EidosValue_SP symbolValue = symbols->GetValueOrRaiseForSymbol(EidosStringRegistry::GlobalStringIDForString(symbolName)); NSString *symbolObjcName = [NSString stringWithUTF8String:symbolName.c_str()]; EidosValueWrapper *wrapper = [EidosValueWrapper wrapperForName:symbolObjcName parent:nil value:symbolValue]; [rootBrowserWrappers addObject:wrapper]; } } } return rootBrowserWrappers; } - (IBAction)toggleBrowserVisibility:(id)sender { if ([_browserWindow isVisible]) [self hideWindow]; else [self showWindow]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; if (sel == @selector(toggleBrowserVisibility:)) [menuItem setTitle:([_browserWindow isVisible] ? @"Hide Variable Browser" : @"Show Variable Browser")]; return YES; } // // NSWindow delegate methods // #pragma mark - #pragma mark NSWindow delegate - (void)windowWillClose:(NSNotification *)notification { NSWindow *closingWindow = [notification object]; if (closingWindow == _browserWindow) [[NSNotificationCenter defaultCenter] postNotificationName:EidosVariableBrowserWillHideNotification object:self]; } // // NSOutlineView datasource / delegate methods // #pragma mark - #pragma mark NSOutlineView delegate - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { if ([_delegate symbolTableForEidosVariableBrowserController:self]) { NSArray *wrapperArray = (item ? [(EidosValueWrapper *)item childWrappers] : [self rootWrappers]); NSInteger count = [wrapperArray count]; //NSLog(@"count == %ld", (long)count); return count; } //NSLog(@"count == 0"); return 0; } - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { if ([_delegate symbolTableForEidosVariableBrowserController:self]) { NSArray *wrapperArray = (item ? [(EidosValueWrapper *)item childWrappers] : [self rootWrappers]); id child = [wrapperArray objectAtIndex:index]; //NSLog(@"child at index %ld == %@", (long)index, child); return child; } //NSLog(@"child at index %ld == NULL", (long)index); return (id _Nonnull)nil; // get rid of the static analyzer warning } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { if (item) return [(EidosValueWrapper *)item isExpandable]; return NO; } - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { if (item) { EidosValueWrapper *wrapper = (EidosValueWrapper *)item; if (tableColumn == _symbolColumn) return [wrapper displaySymbol]; if (tableColumn == _typeColumn) return [wrapper displayType]; if (tableColumn == _sizeColumn) return [wrapper displaySize]; if (tableColumn == _valueColumn) return [wrapper displayValue]; } return @""; } @end ================================================ FILE: EidosScribe/EidosVariableBrowserControllerDelegate.h ================================================ // // EidosVariableBrowserControllerDelegate.h // SLiM // // Created by Ben Haller on 9/11/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of Eidos. // // Eidos is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // Eidos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with Eidos. If not, see . #import @class EidosVariableBrowserController; class EidosSymbolTable; /* This is an Objective-C++ header, and so can only be included by Objective-C++ compilations (.mm files instead of .m files). You should not need to include this header in your .h files, since you can declare protocol conformance in a class-continuation category in your .m file, so only classes that conform to this protocol should need to be Objective-C++. EidosVariableBrowserControllerDelegate is a protocol for the delegate of an EidosVariableBrowserController. The variable browser controller gets the symbols to display from its delegate; if you are using EidosConsoleWindowController it typically acts as the delegate for the variable browser, but if you are building your own Eidos user interface you will need to provide this delegate method. */ @protocol EidosVariableBrowserControllerDelegate @required - (EidosSymbolTable *)symbolTableForEidosVariableBrowserController:(EidosVariableBrowserController *)browserController; @end ================================================ FILE: EidosScribe/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "AppIcon_16x16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "AppIcon_16x16@2x.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "AppIcon_32x32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "AppIcon_32x32@2x.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "AppIcon_128x128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "AppIcon_128x128@2x.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "AppIcon_256x256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "AppIcon_256x256@2x.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "AppIcon_512x512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "AppIcon_512x512@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: EidosScribe/main.m ================================================ // // main.m // EidosScribe // // Created by Ben Haller on 4/7/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // #import int main(int argc, const char * argv[]) { return NSApplicationMain(argc, argv); } ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: PARALLEL ================================================ This file contains unreleased/unannounced changes for parallelization, in addition to those listed in VERSIONS. It will be merged into VERSIONS once parallel SLiM is released publicly. PARALLEL changes (now in the master branch, but disabled): TO DO: the openmp branch had changes to travis.yml to test the openmp branch, which should be replicated somehow for GitHub Actions: using macos xcode11.6/clang and linux bionic/gcc on osx, "brew install libomp;" "ls -l /usr/local/opt/libomp/include/omp.h;" "ls -l /usr/local/include;" note the paths for the headers/lib are likely different, so that will require a bit of legwork updated SLiM.xcodeproj for OpenMP configuration: duplicate the entire SLiM directory to SLiM_temp, open the SLiM.xcodeproj file in the copy, rename it in the Project Navigator to "SLiM_OpenMP", copy it back into the SLiM folder and delete the rest of the copy use grep to find "SLiM_temp" inside files in the new SLiM_OpenMP.xcodeproj and fix them to refer to the correct directory (might be just one, or none) add system header search path for targets eidos_multi and SLiM_multi, in Build Settings: /usr/local/include/ (non-recursive) add library search path for targets eidos_multi and SLiM_multi, in Build Settings: /usr/local/lib/ (non-recursive) add link to binary library, under "Build Phases" (Link Binary With Libraries) on targets eidos_multi and SLiM_multi, by dragging in /usr/local/lib/libomp.dylib add to Other C Flags and Other C++ Flags for targets eidos_multi and SLiM_multi, in Build Settings: -Xclang and -fopenmp disable library validation for targets eidos_multi and SLiM_multi, in Signing & Capabilities: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation set Enable Index-While-Building Functionality to NO in Build Settings on targets eidos_multi and SLiM_multi disable Link-Time Optimization, which appears to be incompatible with OpenMP switched "Build Active Architecture Only" in Build Settings to YES for Release builds, for targets eidos_multi and SLiM_multi, because the older libomp.dylib builds do not have arm64 in them see http://antonmenshov.com/2017/09/09/clang-openmp-setup-in-xcode/ added -Rpass="loop|vect" -Rpass-missed="loop|vect" -Rpass-analysis="loop|vect" to Other C++ Flags for eidos_multi, to aid in optimization work added "-fno-math-errno" to the project-level Other C++ Flags, to match the main project added $(OTHER_CPLUSPLUSFLAGS) to that build setting for slim_multi and eidos_multi, to inherit from the project's settings require 10.13 or later for eidos_multi and slim_multi, since that's the macOS version that the R libomp packages target added SLiMguiLegacy_multi and EidosScribe_multi targets/schemes to the project for debugging/testing purposes; not intended for end users! modify CMakeLists.txt to add a "PARALLEL" flag that can be set to "ON" to enable OpenMP with the -fopenmp flag modify CMakeLists.txt to add /usr/local/include to all targets (through GSL_INCLUDES, which should maybe get cleaned up somehow) modify CMakeLists.txt to build executables eidos_multi and slim_multi when PARALLEL is ON, and to link in libomp disable parallel builds of EidosScribe, SLiMgui, and SLiMguiLegacy with #error directives; parallelization is for the command line only (active threads peg the CPU, passive threads are slower than single-threaded) add a command-line option, -maxThreads , to control the maximum number of threads used in OpenMP add a header eidos_openmp.h that should be used instead of #include "omp.h", providing various useful definitions etc. add an initialization function for OpenMP, Eidos_WarmUpOpenMP(), that must be called when warming up; sets various options, logs diagnostic info set default values for OMP_WAIT_POLICY (active) and OMP_PROC_BIND (false) and OMP_DYNAMIC (false) for slim and eidos add THREAD_SAFETY_CHECK() convention for marking areas that are known not to be thread-safe, such as due to use of statics or globals add gEidosMaxThreads global that has the max number of threads for the session (for pre-allocating per-thread data structures) create per-thread SparseVector pools to allow high-scalability parallelization of spatial interactions add parallel sorting code - quicksort and mergesort, rather experimental at this point, just early work create per-thread random number generators to allow parallelization of code that involves RNG usage parallelize dnorm(), rbinom(), rdunif(), rexp(), rnorm(), rpois(), runif() parallelize tallying mutation refcounts, for the optimized fast case parallelize fitness evaluation for mutations, when there are no mutationEffect() or fitnessEffect() callbacks parallelize non-mutation-based fitness evaluation (i.e., only fitnessScaling values) parallelize some minor tick cycle bookkeeping: incrementing individual ages, clearing the migrant flag parallelize the viability/survival tick cycle stage's survival evaluation (without callbacks) parallelize localPopulationDensity(), clippedIntegral(), and neighborCount() add gEidosNumThreads global that has the currently requested number of threads (usually equal to gEidosMaxThreads) add functions to Eidos to support parallel execution: parallelGetNumThreads(), parallelGetMaxThreads(), and parallelSetNumThreads() add unit tests for parallelized functions in Eidos, comparing against the same functions non-parallel add nowait clause on parallel for loops that would otherwise have a double barrier shift to allocating per-thread RNGs in parallelized allocations, so "first touch" places each RNG in memory close to that thread's core switch to dynamic scheduling for rbinom() and rdunif(), which seem to need it for unclear reasons use a minimum chunk size of 16 for all dynamic schedules; problems that parallelize should always be at least that big, I imagine the exception is countOfMutationsOfType() / sumOfMutationsOfType() and similar, because they do so much work per iteration that they can be worth going parallel even for two iterations note that we never use schedule(guided) because its implementation in GCC is brain-dead; see https://stackoverflow.com/a/43047074/2752221 switch to OMP_PROC_BIND == true by default; keep threads with the data they're working on, recommended practice parallelize more Eidos math functions: abs(float), ceil(), exp(float), floor(), log(float), log10(float), log2(float), round(), sqrt(float), trunc() parallelize Eidos min/max functions: max(int), max(float), min(int), min(float), pmax(int), pmax(float), pmin(int), pmin(float) parallelize match(), tabulate(), sample (int/float/object, replace=T, weights=NULL), mean(l/i/f) parallelize Individual -relatedness(), -countOfMutationsOfType(), -setSpatialPosition(); Species -individualsWithPedigreeIDs() parallelize Subpopulation -pointInBounds(), -pointReflected(), -pointStopped(), pointPeriodic(), pointUniform() parallelize Haplosome -containsMarkerMutation() change how RNG seeding works, for single-threaded and multi-threaded runs: use /dev/random to generate initial seeds, and for multi-threaded runs, use /dev/random for seeds for all threads beyond thread 0 parallelize Haplosome -countOfMutationsOfType(), Subpopulation -spatialMapValue() and -sampleIndividuals() add unit tests for various SLiM methods that have been parallelized parallelize InteractionType nearestNeighbors(), nearestInteractingNeighbors(), drawByStrength() by adding a returnDict parameter that lets the user get a Dictionary as a result, with one entry per receiver their "receiver" parameter is no longer required to be singleton (when returnDict == T), and their return value is now just "object" since it can be Individual or Dictionary overhaul the internal MutationRun design to allow it to be used multithreaded - get rid of refcounting, switch to usage tallying parallelize offspring generation in WF models, when no callbacks (mateChoice(), modifyChild(), recombination(), mutation()) are active; no tree-seq, no cloning parallelize offspring generation in WF model: add support for cloning parallelize tallying of mutation runs, clearing of parental haplosomes at the end of WF model ticks, and mutation run uniquing add support for parallel reproduction when a non-default stacking policy is in use add support for parallel reproduction with tree-sequence recording add parallel reproduction in nonWF models, with no callbacks (recombination(), mutation()) active - modifyChild() callbacks are legal but cannot access child haplosomes since they are deferred this is achieved by passing defer=T to addCrossed(), addCloned(), addSelfed(), addMultiRecombinant(), or addRecombinant() thread-safety work - break in backward reproducibility for scripts that use a type 's' DFE, because the code path for that shifted algorithm change for nearestNeighbors() and nearestNeighborsOfPoint(), when count is >1 and . #include "QtSLiMAbout.h" #include "ui_QtSLiMAbout.h" #include "QtSLiMAppDelegate.h" #include "slim_globals.h" // Get our Git commit SHA-1, as C string "g_GIT_SHA1" #include "../cmake/GitSHA1.h" QtSLiMAbout::QtSLiMAbout(QWidget *p_parent) : QDialog(p_parent), ui(new Ui::QtSLiMAbout) { ui->setupUi(this); // change the app icon to our multi-size app icon for best results ui->appIconButton->setIcon(qtSLiMAppDelegate->applicationIcon()); // prevent this window from keeping the app running when all main windows are closed setAttribute(Qt::WA_QuitOnClose, false); // disable resizing layout()->setSizeConstraint(QLayout::SetFixedSize); setSizeGripEnabled(false); // fix version number; we incorporate the Git commit number here too, if we can QString versionString("version " + QString(SLIM_VERSION_STRING) + " (Qt " + QT_VERSION_STR + ", Git SHA-1 "); QString gitSHA(g_GIT_SHA1); if (gitSHA.startsWith("unknown")) versionString.append("unknown"); else if (gitSHA == "GITDIR-NOTFOUND") versionString.append("not available"); else versionString.append(gitSHA.left(7)); // standard truncation is the last seven hex digits of the SHA-1 versionString.append(")"); ui->versionLabel->setText(versionString); // make window actions for all global menu items qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); } QtSLiMAbout::~QtSLiMAbout() { delete ui; } ================================================ FILE: QtSLiM/QtSLiMAbout.h ================================================ // // QtSLiMAbout.h // SLiM // // Created by Ben Haller on 8/2/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMABOUT_H #define QTSLIMABOUT_H #include namespace Ui { class QtSLiMAbout; } class QtSLiMAbout : public QDialog { Q_OBJECT public: explicit QtSLiMAbout(QWidget *p_parent = nullptr); ~QtSLiMAbout(); private: Ui::QtSLiMAbout *ui; }; #endif // QTSLIMABOUT_H ================================================ FILE: QtSLiM/QtSLiMAbout.ui ================================================ QtSLiMAbout 0 0 620 554 0 0 20 20 0 0 128 128 128 128 :/icons/AppIcon128.png:/icons/AppIcon128.png 128 128 true Qt::Vertical 20 10 3 true 0 0 75 true <html><head/><body><p><span style=" font-size:15pt;">SLiMgui : A graphical user interface for SLiM</span></p></body></html> Qt::Vertical QSizePolicy::Fixed 20 8 <html><head/><body><p>Messer Lab, Cornell University, <a href="http://messerlab.org/slim/"><span style=" text-decoration: underline; color:#0000ff;">http://messerlab.org/slim/</span></a></p></body></html> true Copyright © 2016–2025 Benjamin C. Haller. All rights reserved. Qt::Vertical QSizePolicy::Fixed 20 8 <html><head/><body><p>By Benjamin C. Haller and Philipp W. Messer.</p></body></html> true Qt::Vertical 20 10 Qt::Horizontal QSizePolicy::Fixed 20 20 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'.SF NS Text'; font-size:12pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:10px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.</p> <p style=" margin-top:10px; margin-bottom:10px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p> <p style=" margin-top:10px; margin-bottom:10px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You should have received a copy of the GNU General Public License along with SLiM. If not, see <a href="http://www.gnu.org/licenses/"><span style=" text-decoration: underline; color:#0000ff;">http://www.gnu.org/licenses/</span></a>.</p> <p style=" margin-top:10px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Please see the SLiM manual for credits and license information for code that has been incorporated into SLiM from other authors.</p></body></html> true true Qt::LinksAccessibleByMouse Qt::Horizontal QSizePolicy::Fixed 20 20 0 0 10 3.3 (build 1) Qt::Horizontal QDialogButtonBox::Close QtSLiMIconView QPushButton
QtSLiMExtras.h
buttonBox accepted() QtSLiMAbout accept() 248 254 157 274 buttonBox rejected() QtSLiMAbout reject() 316 260 286 274
================================================ FILE: QtSLiM/QtSLiMAppDelegate.cpp ================================================ // // QtSLiMAppDelegate.cpp // SLiM // // Created by Ben Haller on 7/13/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMAppDelegate.h" #include "QtSLiMWindow.h" #include "QtSLiMFindRecipe.h" #include "QtSLiM_SLiMgui.h" #include "QtSLiM_Plot.h" #include "QtSLiMScriptTextEdit.h" #include "QtSLiMAbout.h" #include "QtSLiMPreferences.h" #include "QtSLiMHelpWindow.h" #include "QtSLiMFindPanel.h" #include "QtSLiMEidosConsole.h" #include "QtSLiMVariableBrowser.h" #include "QtSLiMTablesDrawer.h" #include "QtSLiMDebugOutputWindow.h" #include "QtSLiMConsoleTextEdit.h" #include "QtSLiMOpenGL.h" #include "QtSLiMExtras.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "eidos_globals.h" #include "eidos_beep.h" #include "slim_globals.h" #include "slim_globals.h" // Check the Qt version and display an error if it is unacceptable. This can be turned off with // -D NO_QT_VERSION_ERROR; at present, that is used to allow GitHub Actions to test version // combinations that are not officially supported. I do not recommend that end users use this flag. #ifdef NO_QT_VERSION_ERROR #warning "Qt version check for SLiMgui disabled by -D NO_QT_VERSION_ERROR" #else #ifdef __APPLE__ // On macOS we enforce Qt 5.15.2 as a hard limit; macOS does not have Qt preinstalled, and there is // not much reason for anybody to use a version prior to 5.15.2 for a build. 5.15.2 is the only // Qt5 LTS version with support for macOS 11, dark mode, and various other things we want. However, // if you need to build against an earlier Qt version (because you're using a macOS version earlier // than 10.13, perhaps), you can disable this check using the above flag and your build will probably // work; just note that that configuration is unsupported. #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 2)) #error "SLiMgui on macOS requires Qt version 5.15.2 or later. Please uninstall Qt and then install a more recent version (5.15.2 or later recommended)." #endif #else // On Linux we enforce Qt 5.9.5 as a hard limit, since it is what Ubuntu 18.04 LTS has preinstalled. // We don't rely on any specific post 5.9.5 features on Linux, and the best Qt version on Linux is // probably the version that a given distro has chosen to preinstall. #if (QT_VERSION < QT_VERSION_CHECK(5, 9, 5)) #error "SLiMgui on Linux requires Qt version 5.9.5 or later. Please uninstall Qt and then install a more recent version (5.15 LTS recommended)." #endif #endif // We now support Qt 6, but do not require it. Note that SLiM 4.2.2 was the last version that supported only Qt 5. #if (QT_VERSION >= QT_VERSION_CHECK(7, 0, 0)) #error "SLiMgui requires Qt 5 or Qt 6. Qt 7 and later are not supported at this time." #endif #if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0)) #error "SLiMgui requires Qt 5 or Qt 6. Qt 4 and earlier are not supported." #endif #endif static std::string Eidos_Beep_QT(const std::string &p_sound_name); QtSLiMAppDelegate *qtSLiMAppDelegate = nullptr; // A custom message handler that we can use, optionally, for debugging. This is useful if Qt is emitting a warning and you don't know // where it's coming from; turn on the use of the message handler, and then set a breakpoint inside it, perhaps conditional on msg. void QtSLiM_MessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { #pragma unused (type, context) // Test for a specific message so you can break on it when it occurs //if (msg.contains("Using QCharRef with an index")) // qDebug() << "HIT WATCH MESSAGE; SET A BREAKPOINT HERE!"; // Filter known benign Windows QPA spam when displays change #ifdef _WIN32 { QByteArray category = context.category ? QByteArray(context.category) : QByteArray(); if (category == "qwindows") { if (msg.startsWith("Unable to get device information for")) return; } } #endif // useful behavior, from https://doc.qt.io/qt-5/qtglobal.html#qInstallMessageHandler { QByteArray localMsg = msg.toLocal8Bit(); #if 1 time_t rawtime; struct tm *timeinfo; time(&rawtime); timeinfo = localtime(&rawtime); fprintf(stderr, "%02d:%02d:%02d : ", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec); #endif #if 0 // Log file/line number and function/method name const char *file = context.file ? context.file : ""; const char *function = context.function ? context.function : ""; fprintf(stderr, "%s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); #else // Just log the message itself fprintf(stderr, "%s\n", localMsg.constData()); #endif #if 0 // Log a backtrace after each message Eidos_PrintStacktrace(stderr, 20); #endif fflush(stderr); } } QtSLiMAppDelegate::QtSLiMAppDelegate(QObject *p_parent) : QObject(p_parent) { // Install a message handler; this seems useful for debugging qInstallMessageHandler(QtSLiM_MessageHandler); // Determine whether we were launched from a shell or from something else (Finder, Xcode, etc.) launchedFromShell_ = (isatty(fileno(stdin)) == 1); // && !SLiM_AmIBeingDebugged(); // Install our custom beep handler Eidos_Beep = &Eidos_Beep_QT; // Let Qt know who we are, for QSettings configuration QCoreApplication::setOrganizationName("MesserLab"); QCoreApplication::setOrganizationDomain("messerlab.org"); // Qt expects the domain in the standard order, and reverses it to form "org.messerlab.SLiMgui.plist" as per Apple's usage QCoreApplication::setApplicationName("SLiMgui"); // This governs the location of our prefs, which we keep under org.messerlab.SLiMgui QCoreApplication::setApplicationVersion(SLIM_VERSION_STRING); // Warm up our back ends before anything else happens, including our own class objects SLiM_ConfigureContext(); Eidos_WarmUp(); SLiM_WarmUp(); gSLiM_Plot_Class = new Plot_Class(gStr_Plot, gEidosDictionaryUnretained_Class); gSLiM_Plot_Class->CacheDispatchTables(); gSLiM_SLiMgui_Class = new SLiMgui_Class(gStr_SLiMgui, gEidosDictionaryUnretained_Class); gSLiM_SLiMgui_Class->CacheDispatchTables(); // Free the Eidos RNG; we want it to be uninitialized whenever we are not executing a // simulation. We keep our own RNG state, per-window. Note that SLiMguiLegacy does // not contain code corresponding to this in its delegate, because the order of its // initialization is different; the Eidos RNG gets freed by -startNewSimulationFromScript // before it causes any trouble. if (gEidos_RNG_Initialized) { _Eidos_FreeOneRNG(gEidos_RNG_SINGLE); gEidos_RNG_Initialized = false; } // Remember our current working directory, to return to whenever we are not inside SLiM/Eidos app_cwd_ = Eidos_CurrentDirectory(); // Initialize our OpenGL wrapper state QtSLiM_AllocateGLBuffers(); // Set up the format for OpenGL buffers globally, so that it applies to all windows and contexts // This defaults to OpenGL 2.0, which is what we want, so right now we don't customize QSurfaceFormat format; //format.setDepthBufferSize(24); //format.setStencilBufferSize(8); //format.setVersion(3, 2); //format.setProfile(QSurfaceFormat::CompatibilityProfile); QSurfaceFormat::setDefaultFormat(format); // Connect to the app to find out when we're terminating QApplication *app = qApp; connect(app, &QApplication::lastWindowClosed, this, &QtSLiMAppDelegate::lastWindowClosed); connect(app, &QApplication::aboutToQuit, this, &QtSLiMAppDelegate::aboutToQuit); // Install our event filter to detect modifier key changes app->installEventFilter(this); // Track the last focused main window, for activeQtSLiMWindow() connect(app, &QApplication::focusChanged, this, &QtSLiMAppDelegate::focusChanged); // We assume we are the global instance; FIXME singleton pattern would be good qtSLiMAppDelegate = this; // Ensure windows stay on-screen when displays change (added, removed, or modified) #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) // This lambda (1) ensures that visible top-level windows remain on screen; // (2) raises the active window to the front on the appropriate screen, if appropriate. auto ensureOnScreen = [this]() { const auto topLevels = QApplication::topLevelWidgets(); QWidget *active = qApp->activeWindow(); for (QWidget *w : topLevels) { if (!w || !w->isWindow()) continue; // Do not affect hidden or minimized windows; only adjust currently visible ones if (!w->isVisible()) continue; if (w->windowState() & Qt::WindowMinimized) continue; // If the window is largely off-screen after a display change, move it without forcing it to the front if (QtSLiMIsMostlyOnScreen(w)) continue; if (w == active) { // For the active window, fully expose it (bring to front) QtSLiMMakeWindowVisibleAndExposed(w); } else { // For other visible windows, adjust quietly without raising QtSLiMRelocateQuietly(w); } } }; // Connect to the screenAdded, screenRemoved, and geometryChanged signals // to ensure that all top-level windows are visible and exposed connect(app, &QGuiApplication::screenAdded, this, [ensureOnScreen](QScreen *) { ensureOnScreen(); }); connect(app, &QGuiApplication::screenRemoved, this, [ensureOnScreen](QScreen *) { ensureOnScreen(); }); for (QScreen *screen : QGuiApplication::screens()) connect(screen, &QScreen::geometryChanged, this, [ensureOnScreen](const QRect &) { ensureOnScreen(); }); #endif // Cache app-wide icons slimDocumentIcon_.addFile(":/icons/DocIcon16.png"); slimDocumentIcon_.addFile(":/icons/DocIcon32.png"); slimDocumentIcon_.addFile(":/icons/DocIcon48.png"); slimDocumentIcon_.addFile(":/icons/DocIcon64.png"); slimDocumentIcon_.addFile(":/icons/DocIcon128.png"); slimDocumentIcon_.addFile(":/icons/DocIcon256.png"); slimDocumentIcon_.addFile(":/icons/DocIcon512.png"); genericDocumentIcon_.addFile(":/icons/GenericDocIcon16.png"); genericDocumentIcon_.addFile(":/icons/GenericDocIcon32.png"); appIcon_.addFile(":/icons/AppIcon16.png"); appIcon_.addFile(":/icons/AppIcon32.png"); appIcon_.addFile(":/icons/AppIcon48.png"); appIcon_.addFile(":/icons/AppIcon64.png"); appIcon_.addFile(":/icons/AppIcon128.png"); appIcon_.addFile(":/icons/AppIcon256.png"); appIcon_.addFile(":/icons/AppIcon512.png"); appIcon_.addFile(":/icons/AppIcon1024.png"); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(16, 16))); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(32, 32))); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(48, 48))); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(64, 64))); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(128, 128))); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(256, 256))); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(512, 512))); appIconHighlighted_.addPixmap(QtSLiMDarkenPixmap(appIcon_.pixmap(1024, 1024))); // Set the application icon; this fixes the app icon in the dock/toolbar/whatever, // even if the right icon is not attached for display in the desktop environment app->setWindowIcon(appIcon_); } QtSLiMAppDelegate::~QtSLiMAppDelegate(void) { //qDebug() << "QtSLiMAppDelegate::~QtSLiMAppDelegate"; #if SLIM_LEAK_CHECKING QtSLiM_FreeGLBuffers(); #endif qtSLiMAppDelegate = nullptr; // kill the global shared instance, for safety } // // Document opening // QtSLiMWindow *QtSLiMAppDelegate::findMainWindow(const QString &fileName) const { QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath(); const QList topLevelWidgets = QApplication::topLevelWidgets(); for (QWidget *widget : topLevelWidgets) { QtSLiMWindow *mainWin = qobject_cast(widget); if (mainWin && (mainWin->isZombieWindow_ == false) && (mainWin->currentFile == canonicalFilePath)) return mainWin; } return nullptr; } void QtSLiMAppDelegate::newFile_WF(bool includeComments) { QtSLiMWindow *currentActiveWindow = activeQtSLiMWindow(); QtSLiMWindow *window = new QtSLiMWindow(QtSLiMWindow::ModelType::WF, includeComments); window->tile(currentActiveWindow); window->show(); } void QtSLiMAppDelegate::newFile_nonWF(bool includeComments) { QtSLiMWindow *currentActiveWindow = activeQtSLiMWindow(); QtSLiMWindow *window = new QtSLiMWindow(QtSLiMWindow::ModelType::nonWF, includeComments); window->tile(currentActiveWindow); window->show(); } QtSLiMWindow *QtSLiMAppDelegate::open(QtSLiMWindow *requester) { QSettings settings; QString directory = settings.value("QtSLiMDefaultOpenDirectory", QVariant(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation))).toString(); QString fileName; if (!requester) requester = activeQtSLiMWindow(); if (requester) fileName = QFileDialog::getOpenFileName(requester, QString(), directory, "Files (*.slim *.txt *.png *.jpg *.jpeg *.bmp *.gif)"); else fileName = QFileDialog::getOpenFileName(nullptr, QString(), directory, "Files (*.slim *.txt)"); if (!fileName.isEmpty()) { settings.setValue("QtSLiMDefaultOpenDirectory", QVariant(QFileInfo(fileName).path())); return openFile(fileName, requester); } return nullptr; } QtSLiMWindow *QtSLiMAppDelegate::openFile(const QString &fileName, QtSLiMWindow *requester) { if (!requester) requester = activeQtSLiMWindow(); if (fileName.endsWith(".png", Qt::CaseInsensitive) || fileName.endsWith(".jpg", Qt::CaseInsensitive) || fileName.endsWith(".jpeg", Qt::CaseInsensitive) || fileName.endsWith(".bmp", Qt::CaseInsensitive) || fileName.endsWith(".gif", Qt::CaseInsensitive)) { if (requester) { // An image; this opens as a child of the current SLiM window QWidget *imageWindow = requester->imageWindowWithPath(fileName); if (imageWindow) QtSLiMMakeWindowVisibleAndExposed(imageWindow); return requester; } else { qApp->beep(); return nullptr; } } else { // Should be a .slim or .txt file; look for an existing window for the file, otherwise open a new window (or reuse a reuseable window) QtSLiMWindow *existing = findMainWindow(fileName); if (existing) { QtSLiMMakeWindowVisibleAndExposed(existing); return existing; } if (requester && requester->windowIsReuseable()) { requester->loadFile(fileName); return requester; } QtSLiMWindow *window = new QtSLiMWindow(fileName); if (window->isUntitled) { delete window; return nullptr; } window->tile(requester); window->show(); return window; } } void QtSLiMAppDelegate::openRecipeWithName(const QString &recipeName, const QString &recipeScript, QtSLiMWindow *requester) { if (recipeName.endsWith(".py")) { // Python recipes live in GitHub and get opened in the user's browser. This seems like the best solution; // we don't want SLiMgui to become a Python IDE, so we don't want to open them in-app, but there is no // good cross-platform solution for opening them in an app that makes sense, or even for revealing them // on the desktop. See https://github.com/MesserLab/SLiM/issues/137 for discussion. QString urlString = "https://raw.githubusercontent.com/MesserLab/SLiM/master/QtSLiM/recipes/"; urlString.append(recipeName); QDesktopServices::openUrl(QUrl(urlString, QUrl::TolerantMode)); } else { // SLiM recipes get opened in a new window (or reusing the current window, if applicable) if (!requester) requester = activeQtSLiMWindow(); if (requester && requester->windowIsReuseable()) { requester->loadRecipe(recipeName, recipeScript); return; } QtSLiMWindow *window = new QtSLiMWindow(recipeName, recipeScript); if (!window->isRecipe) { delete window; return; } window->tile(requester); window->show(); } } // // Recipes menu handling // void QtSLiMAppDelegate::setUpRecipesMenu(QMenu *openRecipesMenu, QAction *findRecipeAction) { // This is called by QtSLiMWindow::initializeUI(). We are to take control of the // recipes submenu, filling it in and implementing the Find Recipe panel. connect(findRecipeAction, &QAction::triggered, this, &QtSLiMAppDelegate::findRecipe); // Find recipes in our resources and sort by numeric order QDir recipesDir(":/recipes/", "Recipe *.*", QDir::NoSort, QDir::Files | QDir::NoSymLinks); QStringList entryList = recipesDir.entryList(QStringList("Recipe *.*")); // the previous name filter seems to be ignored std::sort(entryList.begin(), entryList.end(), EidosNaturalSort); //qDebug() << entryList; // Append an action for each, organized into submenus QString previousItemChapter; QMenu *chapterSubmenu = nullptr; for (QString &entryName : entryList) { QString recipeName; // Determine the menu item text for the recipe, removing "Recipe " and ".txt", and adding pythons if (entryName.endsWith(".txt")) recipeName = entryName.mid(7, entryName.length() - 11); else if (entryName.endsWith(".py")) recipeName = entryName.mid(7, entryName.length() - 7) + QString(" 🐍"); int firstDotIndex = recipeName.indexOf('.'); if (firstDotIndex != -1) { QString recipeChapter = recipeName.left(firstDotIndex); // Create a submenu for each chapter if (recipeChapter != previousItemChapter) { int recipeChapterValue = recipeChapter.toInt(); QString chapterName; switch (recipeChapterValue) { case 4: chapterName = "Getting started: Neutral evolution in a panmictic population"; break; case 5: chapterName = "Demography and population structure"; break; case 6: chapterName = "Mutation types, genomic elements, and chromosome structure"; break; case 7: chapterName = "SLiMgui visualizations for polymorphism patterns"; break; case 8: chapterName = "Reproduction, meiosis, and multiple chromosomes"; break; case 9: chapterName = "Selective sweeps"; break; case 10:chapterName = "Context-dependent selection using mutationEffect() callbacks"; break; case 11:chapterName = "Complex mating schemes using mateChoice() callbacks"; break; case 12:chapterName = "Direct child modifications using modifyChild() callbacks"; break; case 13:chapterName = "Phenotypes, fitness functions, quantitative traits, and QTLs"; break; case 14:chapterName = "Advanced WF models"; break; case 15:chapterName = "Going beyond Wright-Fisher models: nonWF model recipes"; break; case 16:chapterName = "Advanced nonWF techniques for managing reproduction"; break; case 17:chapterName = "Continuous-space models, interactions, and spatial maps"; break; case 18:chapterName = "Tree-sequence recording: tracking population history"; break; case 19:chapterName = "Modeling explicit nucleotides"; break; case 20:chapterName = "Multispecies modeling"; break; case 23:chapterName = "Parallel SLiM: Running SLiM multithreaded"; break; default: break; } if (chapterName.length()) { QString fullChapterName = QString("%1 – %2").arg(QString::number(recipeChapterValue), chapterName); QAction *mainItem = openRecipesMenu->addAction(fullChapterName); chapterSubmenu = new QMenu(fullChapterName, openRecipesMenu); mainItem->setMenu(chapterSubmenu); } else { qDebug() << "unrecognized chapter value " << recipeChapterValue; qApp->beep(); break; } } // Move on to the current chapter previousItemChapter = recipeChapter; // And now add the menu item for the recipe QAction *menuItem = chapterSubmenu->addAction(recipeName); connect(menuItem, &QAction::triggered, this, &QtSLiMAppDelegate::openRecipe); menuItem->setData(QVariant(entryName)); } } } // // Recent document menu handling // static const int MaxRecentFiles = 10; static inline QString recentFilesKey() { return QStringLiteral("QtSLiMRecentFilesList"); } static inline QString fileKey() { return QStringLiteral("file"); } static QStringList readRecentFiles(QSettings &settings) { QStringList result; const int count = settings.beginReadArray(recentFilesKey()); for (int i = 0; i < count; ++i) { settings.setArrayIndex(i); result.append(settings.value(fileKey()).toString()); } settings.endArray(); return result; } static void writeRecentFiles(const QStringList &files, QSettings &settings) { const int count = files.size(); settings.beginWriteArray(recentFilesKey()); for (int i = 0; i < count; ++i) { settings.setArrayIndex(i); settings.setValue(fileKey(), files.at(i)); } settings.endArray(); } void QtSLiMAppDelegate::updateRecentFileActions() { const QMenu *menu = qobject_cast(sender()); QSettings settings; const QStringList recentFiles = readRecentFiles(settings); const int count = qMin(int(MaxRecentFiles), recentFiles.size()); QList actions = menu->actions(); for (int i = 0 ; i < MaxRecentFiles; ++i) { QAction *recentAction = actions[i]; if (i < count) { const QString fileName = QFileInfo(recentFiles.at(i)).fileName(); recentAction->setText(fileName); recentAction->setData(recentFiles.at(i)); recentAction->setVisible(true); } else { recentAction->setVisible(false); } } } void QtSLiMAppDelegate::openRecentFile() { const QAction *action = qobject_cast(sender()); if (action) openFile(action->data().toString(), nullptr); } void QtSLiMAppDelegate::clearRecentFiles() { QSettings settings; QStringList emptyRecentFiles; writeRecentFiles(emptyRecentFiles, settings); } void QtSLiMAppDelegate::setUpRecentsMenu(QMenu *openRecentSubmenu) { connect(openRecentSubmenu, &QMenu::aboutToShow, this, &QtSLiMAppDelegate::updateRecentFileActions); for (int i = 0; i < MaxRecentFiles; ++i) openRecentSubmenu->addAction(QString(), this, &QtSLiMAppDelegate::openRecentFile)->setVisible(false); openRecentSubmenu->addSeparator(); openRecentSubmenu->addAction("Clear Menu", this, &QtSLiMAppDelegate::clearRecentFiles); } void QtSLiMAppDelegate::prependToRecentFiles(const QString &fileName) { QSettings settings; const QStringList oldRecentFiles = readRecentFiles(settings); QStringList recentFiles = oldRecentFiles; recentFiles.removeAll(fileName); recentFiles.prepend(fileName); if (oldRecentFiles != recentFiles) writeRecentFiles(recentFiles, settings); } // // Event filtering // bool QtSLiMAppDelegate::eventFilter(QObject *p_obj, QEvent *p_event) { QEvent::Type type = p_event->type(); if ((type == QEvent::KeyPress) || (type == QEvent::KeyRelease)) { // emit modifier changed signals for use by the app QKeyEvent *keyEvent = dynamic_cast(p_event); if (keyEvent) { Qt::Key key = static_cast(keyEvent->key()); // why does this return int? if ((key == Qt::Key_Shift) || (key == Qt::Key_Control) || (key == Qt::Key_Meta) || (key == Qt::Key_Alt) || (key == Qt::Key_AltGr) || (key == Qt::Key_CapsLock) || (key == Qt::Key_NumLock) || (key == Qt::Key_ScrollLock)) emit modifiersChanged(keyEvent->modifiers()); } } else if ((type == QEvent::WindowActivate) || (type == QEvent::WindowDeactivate) || (type == QEvent::WindowStateChange) || (type == QEvent::WindowBlocked) || (type == QEvent::WindowUnblocked) || (type == QEvent::HideToParent) || (type == QEvent::ShowToParent) || (type == QEvent::Close)) { // track the active window; we use a timer here because the active window is not yet accurate in all cases // we also want to coalesce the work involved, so we use a flag to avoid scheduling more than one update if (!queuedActiveWindowUpdate) { queuedActiveWindowUpdate = true; //qDebug() << "SCHEDULED... event type ==" << type; QTimer::singleShot(0, this, &QtSLiMAppDelegate::updateActiveWindowList); } else { //qDebug() << "REDUNDANT... event type ==" << type; } } else if (type == QEvent::FileOpen) { // the user has requested that a file be opened QFileOpenEvent *openEvent = static_cast(p_event); QString filePath = openEvent->file(); openFile(filePath, nullptr); return true; // filter this event, i.e., prevent any further Qt handling of it } else if (type == QEvent::ApplicationPaletteChange) { if (inDarkMode_ != QtSLiMInDarkMode()) { inDarkMode_ = QtSLiMInDarkMode(); //qDebug() << "inDarkMode_ == " << inDarkMode_; emit applicationPaletteChanged(); } } else if (type == QEvent::StatusTip) { // These are events that Qt sends to itself, to display "status tips" in the status bar for widgets the // mouse is over, like buttons and menu bar items. This is not a feature I presently want to use, and // on Linux these events get sent even when the tip is empty, and cause the status bar to be cleared // for no apparent reason. So I'm going to just disable these events for now. return true; // filter this event, i.e., prevent any further Qt handling of it } // standard event processing return QObject::eventFilter(p_obj, p_event); } // // public slots // void QtSLiMAppDelegate::appDidFinishLaunching(QtSLiMWindow *initialWindow) { // Display a startup message in the initial window's status bar if (initialWindow) initialWindow->displayStartupMessage(); // Cache our dark mode flag so we can respond to palette changes later inDarkMode_ = QtSLiMInDarkMode(); } void QtSLiMAppDelegate::lastWindowClosed(void) { } void QtSLiMAppDelegate::aboutToQuit(void) { //qDebug() << "QtSLiMAppDelegate::aboutToQuit"; } void QtSLiMAppDelegate::findRecipe(void) { QtSLiMFindRecipe findRecipePanel(nullptr); int result = findRecipePanel.exec(); if (result == QDialog::Accepted) { const QStringList resourceNames = findRecipePanel.selectedRecipeFilenames(); for (const QString &resourceName : resourceNames) { //qDebug() << "recipe name:" << resourceName; QString resourcePath = ":/recipes/" + resourceName; QFile recipeFile(resourcePath); if (recipeFile.open(QFile::ReadOnly | QFile::Text)) { QTextStream recipeTextStream(&recipeFile); QString recipeScript = recipeTextStream.readAll(); QString trimmedName = resourceName; if (trimmedName.endsWith(".txt")) trimmedName.chop(4); openRecipeWithName(trimmedName, recipeScript, nullptr); } } } } void QtSLiMAppDelegate::openRecipe(void) { QAction *action = qobject_cast(sender()); if (action) { const QVariant &data = action->data(); QString resourceName = data.toString(); if (resourceName.length()) { //qDebug() << "recipe name:" << resourceName; QString resourcePath = ":/recipes/" + resourceName; QFile recipeFile(resourcePath); if (recipeFile.open(QFile::ReadOnly | QFile::Text)) { QTextStream recipeTextStream(&recipeFile); QString recipeScript = recipeTextStream.readAll(); QString trimmedName = resourceName; if (trimmedName.endsWith(".txt")) trimmedName.chop(4); openRecipeWithName(trimmedName, recipeScript, nullptr); } } } } void QtSLiMAppDelegate::playStateChanged(void) { bool anyPlaying = false; const QWidgetList topLevelWidgets = qApp->topLevelWidgets(); for (QWidget *widget : topLevelWidgets) { QtSLiMWindow *mainWin = qobject_cast(widget); if (mainWin && mainWin->isPlaying()) anyPlaying = true; } qApp->setWindowIcon(anyPlaying ? appIconHighlighted_ : appIcon_); } // // "First responder" type dispatch for actions shared across the app // // Here is the problem driving this design. We want various actions, like "Close" and "New nonWF" and so forth, // to work (with their shortcut) regardless of what window is frontmost; they should be global actions that // are, at least conceptually, handled by the app, not by any specific widget or window. Originally I tried // defining these as "application" shortcuts, with Qt::ApplicationShortcut; that worked well on macOS, but on // Linux they were deemed "ambiguous" by Qt when more than one main window was open – Qt didn't know which // main window to dispatch to, for some reason. Designating them as "window" shortcuts, with Qt::WindowShortcut, // therefore seems to be a forced move. However, they then work only if a main window is the active window, on // Linux, unless we add them as window actions to every window in the app. So that's what we do – add them to // every window. This helper method takes a window and adds all the global actions to it. I feel like I'm // maybe missing something with this design; there must be a more elegant way to do this in Qt. But I've gone // around and around on the menu item dispatch mechanism, and this is the best solution I've found. // Note that it remains the case that the menu bar is owned by QtSLiMWindow, and that each QtSLiMWindow has its // own menu bar; on Linux that is visible to the user, on macOS it is not. QtSLiMWindow is therefore responsible // for menu item enabling and validation, even for the global actions defined here. The validation logic needs // to be parallel to the dispatch logic here. Arguably, we could have QtSLiMWindow delegate the validation to // QtSLiMAppDelegate so that all the global action logic is together in this class, but since QtSLiMWindow owns // the menu items, and they are private, I left the validation code there. // Some actions added here have no shortcut. Since they are not associated with any menu item or toolbar item, // they will never actually be called. Conceptually, they are global actions, though, and if a shortcut is // added for them later, they will become callable here. So they are declared here as placeholders. #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) // On Qt 5 we seem to need to combine flags with +, I have no idea why exactly static int flagsAndKey(int modifiers, int keys) { return modifiers + keys; } #else // On Qt 6 we seem to need to combine flags with |, I have no idea why exactly static int flagsAndKey(int modifiers, int keys) { return modifiers | keys; } #endif void QtSLiMAppDelegate::addActionsForGlobalMenuItems(QWidget *window) { { QAction *actionPreferences = new QAction("Preferences", this); actionPreferences->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionPreferences, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_preferences); window->addAction(actionPreferences); } { QAction *actionAbout = new QAction("About", this); //actionAbout->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionAbout, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_about); window->addAction(actionAbout); } { QAction *actionShowCycle_WF = new QAction("Show WF Tick Cycle", this); //actionShowCycle_WF->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionShowCycle_WF, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_WF); window->addAction(actionShowCycle_WF); } { QAction *actionShowCycle_nonWF = new QAction("Show nonWF Tick Cycle", this); //actionShowCycle_nonWF->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionShowCycle_nonWF, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_nonWF); window->addAction(actionShowCycle_nonWF); } { QAction *actionShowCycle_WF_MS = new QAction("Show WF Tick Cycle (Multispecies)", this); //actionShowCycle_WF_MS->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionShowCycle_WF_MS, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_WF_MS); window->addAction(actionShowCycle_WF_MS); } { QAction *actionShowCycle_nonWF_MS = new QAction("Show nonWF Tick Cycle (Multispecies)", this); //actionShowCycle_nonWF_MS->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionShowCycle_nonWF_MS, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_nonWF_MS); window->addAction(actionShowCycle_nonWF_MS); } { QAction *actionShowColorChart = new QAction("Show Color Chart", this); //actionShowColorChart->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionShowColorChart, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showColorChart); window->addAction(actionShowColorChart); } { QAction *actionShowPlotSymbols = new QAction("Show Plot Symbols", this); //actionShowPlotSymbols->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionShowPlotSymbols, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showPlotSymbols); window->addAction(actionShowPlotSymbols); } { QAction *actionShowColorScales = new QAction("Show SLiMgui Color Scales", this); //actionShowColorScales->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionShowColorScales, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showColorScales); window->addAction(actionShowColorScales); } { QAction *actionHelp = new QAction("Help", this); //actionHelp->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Comma)); connect(actionHelp, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_help); window->addAction(actionHelp); } { QAction *actionQuit = new QAction("Quit", this); actionQuit->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Q)); connect(actionQuit, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_quit); window->addAction(actionQuit); } { QAction *actionNewWF = new QAction("New WF", this); actionNewWF->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_N)); connect(actionNewWF, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newWF); window->addAction(actionNewWF); } { QAction *actionNewWF_commentless = new QAction("New WF (Commentless)", this); actionNewWF_commentless->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::AltModifier, Qt::Key_N)); connect(actionNewWF_commentless, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newWF_commentless); window->addAction(actionNewWF_commentless); } { QAction *actionNewNonWF = new QAction("New nonWF", this); actionNewNonWF->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier, Qt::Key_N)); connect(actionNewNonWF, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newNonWF); window->addAction(actionNewNonWF); } { QAction *actionNewNonWF_commentless = new QAction("New nonWF (Commentless)", this); actionNewNonWF_commentless->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier, Qt::Key_N)); connect(actionNewNonWF_commentless, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newNonWF_commentless); window->addAction(actionNewNonWF_commentless); } { QAction *actionOpen = new QAction("Open", this); actionOpen->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_O)); connect(actionOpen, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_open); window->addAction(actionOpen); } { QAction *actionClose = new QAction("Close", this); actionClose->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_W)); connect(actionClose, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_close); window->addAction(actionClose); } { QAction *actionFocusOnScript = new QAction("Focus on Script", this); actionFocusOnScript->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_1)); connect(actionFocusOnScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_focusOnScript); window->addAction(actionFocusOnScript); } { QAction *actionFocusOnConsole = new QAction("Focus on Console", this); actionFocusOnConsole->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_2)); connect(actionFocusOnConsole, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_focusOnConsole); window->addAction(actionFocusOnConsole); } { QAction *actionCheckScript = new QAction("Check Script", this); actionCheckScript->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Equal)); connect(actionCheckScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_checkScript); window->addAction(actionCheckScript); } { QAction *actionPrettyprintScript = new QAction("Prettyprint Script", this); //actionPrettyprintScript->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier, Qt::Key_Equal)); // this now goes to actionBiggerFont connect(actionPrettyprintScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_prettyprintScript); window->addAction(actionPrettyprintScript); } { QAction *actionReformatScript = new QAction("Reformat Script", this); // removed since the shortcut for actionPrettyprintScript was removed //actionReformatScript->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier, Qt::Key_Equal)); connect(actionReformatScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_reformatScript); window->addAction(actionReformatScript); } { QAction *actionShowScriptHelp = new QAction("Show Script Help", this); //actionShowScriptHelp->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_K)); connect(actionShowScriptHelp, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_help); window->addAction(actionShowScriptHelp); } { QAction *actionBiggerFont = new QAction("Bigger Font", this); actionBiggerFont->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Plus)); connect(actionBiggerFont, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_biggerFont); window->addAction(actionBiggerFont); } { QAction *actionSmallerFont = new QAction("Smaller Font", this); actionSmallerFont->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Minus)); connect(actionSmallerFont, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_smallerFont); window->addAction(actionSmallerFont); } { QAction *actionShowEidosConsole = new QAction("Show Eidos Console", this); actionShowEidosConsole->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier, Qt::Key_E)); connect(actionShowEidosConsole, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showEidosConsole); window->addAction(actionShowEidosConsole); } { QAction *actionShowVariableBrowser = new QAction("Show Variable Browser", this); actionShowVariableBrowser->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_B)); connect(actionShowVariableBrowser, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showVariableBrowser); window->addAction(actionShowVariableBrowser); } { QAction *actionClearOutput = new QAction("Clear Output", this); actionClearOutput->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_K)); connect(actionClearOutput, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_clearOutput); window->addAction(actionClearOutput); } { QAction *actionClearDebug = new QAction("Clear Debug Points", this); actionClearDebug->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::AltModifier, Qt::Key_K)); connect(actionClearDebug, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_clearDebugPoints); window->addAction(actionClearDebug); } { QAction *actionShowDebuggingOutput = new QAction("Show Debugging Output", this); actionShowDebuggingOutput->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier, Qt::Key_D)); connect(actionShowDebuggingOutput, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showDebuggingOutput); window->addAction(actionShowDebuggingOutput); } { QAction *actionExecuteSelection = new QAction("Execute Selection", this); actionExecuteSelection->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Return)); connect(actionExecuteSelection, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_executeSelection); window->addAction(actionExecuteSelection); } { QAction *actionExecuteAll = new QAction("Execute All", this); actionExecuteAll->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier, Qt::Key_Return)); connect(actionExecuteAll, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_executeAll); window->addAction(actionExecuteAll); } { QAction *actionCopyAsHTML = new QAction("Copy as HTML", this); actionCopyAsHTML->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::AltModifier, Qt::Key_C)); connect(actionCopyAsHTML, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_copyAsHTML); window->addAction(actionCopyAsHTML); } { QAction *actionShiftLeft = new QAction("Shift Left", this); actionShiftLeft->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_BracketLeft)); connect(actionShiftLeft, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_shiftLeft); window->addAction(actionShiftLeft); } { QAction *actionShiftRight = new QAction("Shift Right", this); actionShiftRight->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_BracketRight)); connect(actionShiftRight, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_shiftRight); window->addAction(actionShiftRight); } { QAction *actionCommentUncomment = new QAction("CommentUncomment", this); actionCommentUncomment->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Slash)); connect(actionCommentUncomment, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_commentUncomment); window->addAction(actionCommentUncomment); } { QAction *actionUndo = new QAction("Undo", this); actionUndo->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_Z)); connect(actionUndo, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_undo); window->addAction(actionUndo); } { QAction *actionRedo = new QAction("Redo", this); actionRedo->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier, Qt::Key_Z)); connect(actionRedo, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_redo); window->addAction(actionRedo); } { QAction *actionCut = new QAction("Cut", this); actionCut->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_X)); connect(actionCut, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_cut); window->addAction(actionCut); } { QAction *actionCopy = new QAction("Copy", this); actionCopy->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_C)); connect(actionCopy, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_copy); window->addAction(actionCopy); } { QAction *actionPaste = new QAction("Paste", this); actionPaste->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_V)); connect(actionPaste, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_paste); window->addAction(actionPaste); } { QAction *actionDuplicate = new QAction("Duplicate", this); actionDuplicate->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_D)); connect(actionDuplicate, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_duplicate); window->addAction(actionDuplicate); } { QAction *actionDelete = new QAction("Delete", this); //actionDelete->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_V)); connect(actionDelete, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_delete); window->addAction(actionDelete); } { QAction *actionSelectAll = new QAction("Select All", this); actionSelectAll->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_A)); connect(actionSelectAll, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_selectAll); window->addAction(actionSelectAll); } { QAction *actionFindShow = new QAction("Find...", this); actionFindShow->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_F)); connect(actionFindShow, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_findShow); window->addAction(actionFindShow); } { QAction *actionFindNext = new QAction("Find Next", this); actionFindNext->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_G)); connect(actionFindNext, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_findNext); window->addAction(actionFindNext); } { QAction *actionFindPrevious = new QAction("Find Previous", this); actionFindPrevious->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::ShiftModifier, Qt::Key_G)); connect(actionFindPrevious, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_findPrevious); window->addAction(actionFindPrevious); } { QAction *actionReplaceAndFind = new QAction("Replace and Find", this); actionReplaceAndFind->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::AltModifier, Qt::Key_G)); connect(actionReplaceAndFind, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_replaceAndFind); window->addAction(actionReplaceAndFind); } { QAction *actionUseSelectionForFind = new QAction("Use Selection for Find", this); actionUseSelectionForFind->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_E)); connect(actionUseSelectionForFind, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_useSelectionForFind); window->addAction(actionUseSelectionForFind); } { QAction *actionUseSelectionForReplace = new QAction("Use Selection for Replace", this); actionUseSelectionForReplace->setShortcut(flagsAndKey(Qt::ControlModifier | Qt::AltModifier, Qt::Key_E)); connect(actionUseSelectionForReplace, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_useSelectionForReplace); window->addAction(actionUseSelectionForReplace); } { QAction *actionJumpToSelection = new QAction("Jump to Selection", this); actionJumpToSelection->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_J)); connect(actionJumpToSelection, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_jumpToSelection); window->addAction(actionJumpToSelection); } { QAction *actionJumpToLine = new QAction("Jump to Line", this); actionJumpToLine->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_L)); connect(actionJumpToLine, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_jumpToLine); window->addAction(actionJumpToLine); } { QAction *actionMinimize = new QAction("Minimize", this); actionMinimize->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_M)); connect(actionMinimize, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_minimize); window->addAction(actionMinimize); } { QAction *actionZoom = new QAction("Zoom", this); //actionZoom->setShortcut(flagsAndKey(Qt::ControlModifier, Qt::Key_M)); connect(actionZoom, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_zoom); window->addAction(actionZoom); } } QtSLiMWindow *QtSLiMAppDelegate::dispatchQtSLiMWindowFromSecondaries(void) { // The QtSLiMWindow associated with the focused widget; this should be used for // dispatching actions that we want to work inside secondary windows that are // associated with a particular QtSLiMWindow. QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : nullptr); // If we have no focused widget, just work from the active window if (!focusWindow) focusWindow = activeWindow(); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); QtSLiMVariableBrowser *varBrowser = dynamic_cast(focusWindow); QtSLiMTablesDrawer *tablesDrawer = dynamic_cast(focusWindow); QtSLiMDebugOutputWindow *debugOutputWindow = dynamic_cast(focusWindow); if (eidosConsole) slimWindow = eidosConsole->parentSLiMWindow; else if (varBrowser) slimWindow = varBrowser->parentEidosConsole->parentSLiMWindow; else if (tablesDrawer) slimWindow = tablesDrawer->parentSLiMWindow; else if (debugOutputWindow) slimWindow = debugOutputWindow->parentSLiMWindow; // If we still can't find it, try using the parent of the active window // This makes graph windows work for dispatch; they are just QWidgets but their parent is correct if (focusWindow && !slimWindow) slimWindow = dynamic_cast(focusWindow->parent()); return slimWindow; } void QtSLiMAppDelegate::dispatch_preferences(void) { QtSLiMPreferences &prefsWindow = QtSLiMPreferences::instance(); QtSLiMMakeWindowVisibleAndExposed(&prefsWindow); } void QtSLiMAppDelegate::dispatch_about(void) { QtSLiMAbout *aboutWindow = new QtSLiMAbout(nullptr); aboutWindow->setAttribute(Qt::WA_DeleteOnClose); QtSLiMMakeWindowVisibleAndExposed(aboutWindow); } QWidget *QtSLiMAppDelegate::globalImageWindowWithPath(const QString &path, const QString &title, double scaleFactor) { // This is based on QtSLiMWindow::imageWindowWithPath(), but makes a simple global window // without context menus and other fluff, for simple display of help-related images QImage image(path); if (image.isNull()) { qApp->beep(); return nullptr; } int window_width = round(image.width() * scaleFactor); int window_height = round(image.height() * scaleFactor); QWidget *image_window = new QWidget(nullptr, Qt::Window | Qt::Tool); // a parentless standalone window image_window->setWindowTitle(title); image_window->setFixedSize(window_width, window_height); // Make the image view QLabel *imageView = new QLabel(); imageView->setStyleSheet("QLabel { background-color : white; }"); imageView->setBackgroundRole(QPalette::Base); imageView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); imageView->setScaledContents(true); imageView->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); imageView->setPixmap(QPixmap::fromImage(image)); // Install imageView in the window QVBoxLayout *topLayout = new QVBoxLayout; image_window->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(0); topLayout->addWidget(imageView); // Position the window nicely //positionNewSubsidiaryWindow(image_window); // make window actions for all global menu items // this does not seem to be necessary on macOS, but maybe it is on Linux; will need testing FIXME //qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); image_window->setAttribute(Qt::WA_DeleteOnClose, true); return image_window; } void QtSLiMAppDelegate::dispatch_showCycle_WF(void) { QWidget *imageWindow = globalImageWindowWithPath(":/help/TickCycle_WF.png", "WF Cycle", 0.32); if (imageWindow) QtSLiMMakeWindowVisibleAndExposed(imageWindow); } void QtSLiMAppDelegate::dispatch_showCycle_nonWF(void) { QWidget *imageWindow = globalImageWindowWithPath(":/help/TickCycle_nonWF.png", "nonWF Cycle", 0.32); if (imageWindow) QtSLiMMakeWindowVisibleAndExposed(imageWindow); } void QtSLiMAppDelegate::dispatch_showCycle_WF_MS(void) { QWidget *imageWindow = globalImageWindowWithPath(":/help/TickCycle_WF_MS.png", "WF Cycle (Multispecies)", 0.32); if (imageWindow) QtSLiMMakeWindowVisibleAndExposed(imageWindow); } void QtSLiMAppDelegate::dispatch_showCycle_nonWF_MS(void) { QWidget *imageWindow = globalImageWindowWithPath(":/help/TickCycle_nonWF_MS.png", "nonWF Cycle (Multispecies)", 0.32); if (imageWindow) QtSLiMMakeWindowVisibleAndExposed(imageWindow); } void QtSLiMAppDelegate::dispatch_showColorChart(void) { QWidget *imageWindow = globalImageWindowWithPath(":/help/ColorChart.png", "Eidos Color Chart", 0.5); if (imageWindow) QtSLiMMakeWindowVisibleAndExposed(imageWindow); } void QtSLiMAppDelegate::dispatch_showPlotSymbols(void) { QWidget *imageWindow = globalImageWindowWithPath(":/help/PlotSymbols.png", "Plot Symbols", 0.32); if (imageWindow) QtSLiMMakeWindowVisibleAndExposed(imageWindow); } void QtSLiMAppDelegate::dispatch_showColorScales(void) { // This shows a global window that displays SLiMgui's color scales. Unlike the above global image // windows shown by globalImageWindowWithPath(), this window is drawn dynamically by a custom widget. // This code is based on the code in globalImageWindowWithPath(), with that custom widget. int window_width = round(301); int window_height = round(197); QWidget *image_window = new QWidget(nullptr, Qt::Window | Qt::Tool); // a parentless standalone window image_window->setWindowTitle("SLiMgui Color Scales"); image_window->setFixedSize(window_width, window_height); // Make the custom widget QtSLiMColorScaleWidget *colorScaleView = new QtSLiMColorScaleWidget(image_window); // Install imageView in the window QVBoxLayout *topLayout = new QVBoxLayout; image_window->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(0); topLayout->addWidget(colorScaleView); // Position the window nicely //positionNewSubsidiaryWindow(image_window); // make window actions for all global menu items // this does not seem to be necessary on macOS, but maybe it is on Linux; will need testing FIXME //qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); image_window->setAttribute(Qt::WA_DeleteOnClose, true); if (image_window) QtSLiMMakeWindowVisibleAndExposed(image_window); } void QtSLiMAppDelegate::dispatch_help(void) { QtSLiMHelpWindow &helpWindow = QtSLiMHelpWindow::instance(); QtSLiMMakeWindowVisibleAndExposed(&helpWindow); } void QtSLiMAppDelegate::dispatch_biggerFont(void) { QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); prefs.displayFontBigger(); } void QtSLiMAppDelegate::dispatch_smallerFont(void) { QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); prefs.displayFontSmaller(); } void QtSLiMAppDelegate::dispatch_quit(void) { if (qApp) { closeRejected_ = false; qApp->closeAllWindows(); if (!closeRejected_) { // On macOS, explicitly quit since last window close doesn't auto-quit, for Qt 5.15.2. // Builds against older Qt versions will just quit on the last window close, because // QTBUG-86874 and QTBUG-86875 prevent this from working. #ifdef __APPLE__ #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 2)) qApp->quit(); #endif #endif } } } void QtSLiMAppDelegate::dispatch_newWF(void) { newFile_WF(true); } void QtSLiMAppDelegate::dispatch_newWF_commentless(void) { newFile_WF(false); } void QtSLiMAppDelegate::dispatch_newNonWF(void) { newFile_nonWF(true); } void QtSLiMAppDelegate::dispatch_newNonWF_commentless(void) { newFile_nonWF(false); } void QtSLiMAppDelegate::dispatch_open(void) { open(nullptr); } void QtSLiMAppDelegate::dispatch_close(void) { // We close the "active" window, which is a bit different from the front window // It can be nullptr; in that case it's hard to know what to do QWidget *currentActiveWindow = QApplication::activeWindow(); if (currentActiveWindow) currentActiveWindow->close(); else qApp->beep(); } void QtSLiMAppDelegate::dispatch_copyAsHTML(void) { QWidget *focusWidget = QApplication::focusWidget(); QtSLiMScriptTextEdit *scriptEdit = dynamic_cast(focusWidget); if (scriptEdit && scriptEdit->isEnabled()) scriptEdit->copyAsHTML(); } void QtSLiMAppDelegate::dispatch_shiftLeft(void) { QWidget *focusWidget = QApplication::focusWidget(); QtSLiMScriptTextEdit *scriptEdit = dynamic_cast(focusWidget); if (scriptEdit && scriptEdit->isEnabled() && !scriptEdit->isReadOnly()) scriptEdit->shiftSelectionLeft(); } void QtSLiMAppDelegate::dispatch_shiftRight(void) { QWidget *focusWidget = QApplication::focusWidget(); QtSLiMScriptTextEdit *scriptEdit = dynamic_cast(focusWidget); if (scriptEdit && scriptEdit->isEnabled() && !scriptEdit->isReadOnly()) scriptEdit->shiftSelectionRight(); } void QtSLiMAppDelegate::dispatch_commentUncomment(void) { QWidget *focusWidget = QApplication::focusWidget(); QtSLiMScriptTextEdit *scriptEdit = dynamic_cast(focusWidget); if (scriptEdit && scriptEdit->isEnabled() && !scriptEdit->isReadOnly()) scriptEdit->commentUncommentSelection(); } void QtSLiMAppDelegate::dispatch_undo(void) { QWidget *focusWidget = QApplication::focusWidget(); QLineEdit *lineEdit = dynamic_cast(focusWidget); QTextEdit *textEdit = dynamic_cast(focusWidget); QPlainTextEdit *plainTextEdit = dynamic_cast(focusWidget); if (lineEdit && lineEdit->isEnabled() && !lineEdit->isReadOnly()) lineEdit->undo(); else if (textEdit && textEdit->isEnabled() && !textEdit->isReadOnly()) textEdit->undo(); else if (plainTextEdit && plainTextEdit->isEnabled() && !plainTextEdit->isReadOnly()) plainTextEdit->undo(); } void QtSLiMAppDelegate::dispatch_redo(void) { QWidget *focusWidget = QApplication::focusWidget(); QLineEdit *lineEdit = dynamic_cast(focusWidget); QTextEdit *textEdit = dynamic_cast(focusWidget); QPlainTextEdit *plainTextEdit = dynamic_cast(focusWidget); if (lineEdit && lineEdit->isEnabled() && !lineEdit->isReadOnly()) lineEdit->redo(); else if (textEdit && textEdit->isEnabled() && !textEdit->isReadOnly()) textEdit->redo(); else if (plainTextEdit && plainTextEdit->isEnabled() && !plainTextEdit->isReadOnly()) plainTextEdit->redo(); } void QtSLiMAppDelegate::dispatch_cut(void) { QWidget *focusWidget = QApplication::focusWidget(); QLineEdit *lineEdit = dynamic_cast(focusWidget); QTextEdit *textEdit = dynamic_cast(focusWidget); QPlainTextEdit *plainTextEdit = dynamic_cast(focusWidget); if (lineEdit && lineEdit->isEnabled() && !lineEdit->isReadOnly()) lineEdit->cut(); else if (textEdit && textEdit->isEnabled() && !textEdit->isReadOnly()) textEdit->cut(); else if (plainTextEdit && plainTextEdit->isEnabled() && !plainTextEdit->isReadOnly()) plainTextEdit->cut(); } void QtSLiMAppDelegate::dispatch_copy(void) { QWidget *focusWidget = QApplication::focusWidget(); QLineEdit *lineEdit = dynamic_cast(focusWidget); QTextEdit *textEdit = dynamic_cast(focusWidget); QPlainTextEdit *plainTextEdit = dynamic_cast(focusWidget); if (lineEdit && lineEdit->isEnabled()) lineEdit->copy(); else if (textEdit && textEdit->isEnabled()) textEdit->copy(); else if (plainTextEdit && plainTextEdit->isEnabled()) plainTextEdit->copy(); } void QtSLiMAppDelegate::dispatch_paste(void) { QWidget *focusWidget = QApplication::focusWidget(); QLineEdit *lineEdit = dynamic_cast(focusWidget); QTextEdit *textEdit = dynamic_cast(focusWidget); QPlainTextEdit *plainTextEdit = dynamic_cast(focusWidget); if (lineEdit && lineEdit->isEnabled() && !lineEdit->isReadOnly()) lineEdit->paste(); else if (textEdit && textEdit->isEnabled() && !textEdit->isReadOnly()) textEdit->paste(); else if (plainTextEdit && plainTextEdit->isEnabled() && !plainTextEdit->isReadOnly()) plainTextEdit->paste(); } void QtSLiMAppDelegate::dispatch_duplicate(void) { QWidget *focusWidget = QApplication::focusWidget(); QtSLiMScriptTextEdit *scriptEdit = dynamic_cast(focusWidget); if (scriptEdit && scriptEdit->isEnabled() && !scriptEdit->isReadOnly()) scriptEdit->duplicateSelection(); } void QtSLiMAppDelegate::dispatch_delete(void) { QWidget *focusWidget = QApplication::focusWidget(); QLineEdit *lineEdit = dynamic_cast(focusWidget); QTextEdit *textEdit = dynamic_cast(focusWidget); QPlainTextEdit *plainTextEdit = dynamic_cast(focusWidget); if (lineEdit && lineEdit->isEnabled() && !lineEdit->isReadOnly()) lineEdit->insert(""); else if (textEdit && textEdit->isEnabled() && !textEdit->isReadOnly()) textEdit->insertPlainText(""); else if (plainTextEdit && plainTextEdit->isEnabled() && !plainTextEdit->isReadOnly()) plainTextEdit->insertPlainText(""); } void QtSLiMAppDelegate::dispatch_selectAll(void) { QWidget *focusWidget = QApplication::focusWidget(); QLineEdit *lineEdit = dynamic_cast(focusWidget); QTextEdit *textEdit = dynamic_cast(focusWidget); QPlainTextEdit *plainTextEdit = dynamic_cast(focusWidget); if (lineEdit && lineEdit->isEnabled()) lineEdit->selectAll(); else if (textEdit && textEdit->isEnabled()) textEdit->selectAll(); else if (plainTextEdit && plainTextEdit->isEnabled()) plainTextEdit->selectAll(); } void QtSLiMAppDelegate::dispatch_findShow(void) { QtSLiMFindPanel::instance().showFindPanel(); } void QtSLiMAppDelegate::dispatch_findNext(void) { QtSLiMFindPanel::instance().findNext(); } void QtSLiMAppDelegate::dispatch_findPrevious(void) { QtSLiMFindPanel::instance().findPrevious(); } void QtSLiMAppDelegate::dispatch_replaceAndFind(void) { QtSLiMFindPanel::instance().replaceAndFind(); } void QtSLiMAppDelegate::dispatch_useSelectionForFind(void) { QtSLiMFindPanel::instance().useSelectionForFind(); } void QtSLiMAppDelegate::dispatch_useSelectionForReplace(void) { QtSLiMFindPanel::instance().useSelectionForReplace(); } void QtSLiMAppDelegate::dispatch_jumpToSelection(void) { QtSLiMFindPanel::instance().jumpToSelection(); } void QtSLiMAppDelegate::dispatch_jumpToLine(void) { QtSLiMFindPanel::instance().jumpToLine(); } void QtSLiMAppDelegate::dispatch_focusOnScript(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (slimWindow) slimWindow->scriptTextEdit()->setFocus(); else if (eidosConsole) eidosConsole->scriptTextEdit()->setFocus(); } void QtSLiMAppDelegate::dispatch_focusOnConsole(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (slimWindow) slimWindow->outputTextEdit()->setFocus(); else if (eidosConsole) eidosConsole->consoleTextEdit()->setFocus(); } void QtSLiMAppDelegate::dispatch_checkScript(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (slimWindow) slimWindow->scriptTextEdit()->checkScript(); else if (eidosConsole) eidosConsole->scriptTextEdit()->checkScript(); } void QtSLiMAppDelegate::dispatch_prettyprintScript(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (slimWindow) slimWindow->scriptTextEdit()->prettyprint(); else if (eidosConsole) eidosConsole->scriptTextEdit()->prettyprint(); } void QtSLiMAppDelegate::dispatch_reformatScript(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (slimWindow) slimWindow->scriptTextEdit()->reformat(); else if (eidosConsole) eidosConsole->scriptTextEdit()->reformat(); } void QtSLiMAppDelegate::dispatch_showEidosConsole(void) { QtSLiMWindow *slimWindow = dispatchQtSLiMWindowFromSecondaries(); if (slimWindow) slimWindow->showConsoleClicked(); } void QtSLiMAppDelegate::dispatch_showVariableBrowser(void) { QtSLiMWindow *slimWindow = dispatchQtSLiMWindowFromSecondaries(); if (slimWindow) slimWindow->showBrowserClicked(); } void QtSLiMAppDelegate::dispatch_showDebuggingOutput(void) { QtSLiMWindow *slimWindow = dispatchQtSLiMWindowFromSecondaries(); if (slimWindow) slimWindow->debugOutputClicked(); } void QtSLiMAppDelegate::dispatch_clearOutput(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (slimWindow) slimWindow->clearOutputClicked(); else if (eidosConsole) eidosConsole->consoleTextEdit()->clearToPrompt(); } void QtSLiMAppDelegate::dispatch_clearDebugPoints(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMWindow *slimWindow = dynamic_cast(focusWindow); if (slimWindow) slimWindow->clearDebugPointsClicked(); } void QtSLiMAppDelegate::dispatch_executeSelection(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (eidosConsole) eidosConsole->executeSelectionClicked(); } void QtSLiMAppDelegate::dispatch_executeAll(void) { QWidget *focusWidget = QApplication::focusWidget(); QWidget *focusWindow = (focusWidget ? focusWidget->window() : activeWindow()); QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow); if (eidosConsole) eidosConsole->executeAllClicked(); } void QtSLiMAppDelegate::dispatch_minimize(void) { // We minimize the "active" window, which is a bit different from the front window // It can be nullptr; in that case it's hard to know what to do QWidget *currentActiveWindow = QApplication::activeWindow(); if (currentActiveWindow) { bool isMinimized = currentActiveWindow->windowState() & Qt::WindowMinimized; if (isMinimized) currentActiveWindow->setWindowState((currentActiveWindow->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); else currentActiveWindow->setWindowState(currentActiveWindow->windowState() | Qt::WindowMinimized); // Qt refuses to minimize Qt::Tool windows; see https://bugreports.qt.io/browse/QTBUG-86520 } else qApp->beep(); } void QtSLiMAppDelegate::dispatch_zoom(void) { // We zoom the "active" window, which is a bit different from the front window // It can be nullptr; in that case it's hard to know what to do QWidget *currentActiveWindow = QApplication::activeWindow(); if (currentActiveWindow) { bool isZoomed = currentActiveWindow->windowState() & Qt::WindowMaximized; if (isZoomed) currentActiveWindow->setWindowState((currentActiveWindow->windowState() & ~Qt::WindowMaximized) | Qt::WindowActive); else currentActiveWindow->setWindowState((currentActiveWindow->windowState() | Qt::WindowMaximized) | Qt::WindowActive); } else qApp->beep(); } void QtSLiMAppDelegate::dispatch_helpWorkshops(void) { QDesktopServices::openUrl(QUrl("http://benhaller.com/workshops/workshops.html", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpFeedback(void) { QDesktopServices::openUrl(QUrl("mailto:bhaller@mac.com?subject=SLiM%20Feedback", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpSLiMDiscuss(void) { QDesktopServices::openUrl(QUrl("https://groups.google.com/d/forum/slim-discuss", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpSLiMAnnounce(void) { QDesktopServices::openUrl(QUrl("https://groups.google.com/d/forum/slim-announce", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpSLiMHome(void) { QDesktopServices::openUrl(QUrl("http://messerlab.org/slim/", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpSLiMExtras(void) { QDesktopServices::openUrl(QUrl("https://github.com/MesserLab/SLiM-Extras", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpMesserLab(void) { QDesktopServices::openUrl(QUrl("http://messerlab.org/", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpBenHaller(void) { QDesktopServices::openUrl(QUrl("http://www.benhaller.com/", QUrl::TolerantMode)); } void QtSLiMAppDelegate::dispatch_helpStickSoftware(void) { QDesktopServices::openUrl(QUrl("http://www.sticksoftware.com/", QUrl::TolerantMode)); } // // Active QtSLiMWindow tracking // // For the Find window and similar modeless interactions, we need to be able to find the active // main window, which Qt does not provide (the "active window" is not necessarily a main window). // To do this, we have to track focus changes, to maintain a list of windows that is sorted from // front to back. // void QtSLiMAppDelegate::updateActiveWindowList(void) { QWidget *window = qApp->activeWindow(); if (window) { // window is now the front window, so move it to the front of focusedQtSLiMWindowList focusedWindowList.removeOne(window); focusedWindowList.push_front(window); } // keep the window list trim and accurate pruneWindowList(); // emit our signal emit activeWindowListChanged(); // we're done updating, we can now update again if something new happens queuedActiveWindowUpdate = false; // debug output // qDebug() << "New window list:"; // // for (QPointer &window_ptr : focusedWindowList) // if (window_ptr) // qDebug() << " " << window_ptr->windowTitle(); } void QtSLiMAppDelegate::focusChanged(QWidget * /* old */, QWidget * /* now */) { // track the active window; we use a timer here because the active window is not yet accurate in all cases // we also want to coalesce the work involved, so we use a flag to avoid scheduling more than one update if (!queuedActiveWindowUpdate) { queuedActiveWindowUpdate = true; QTimer::singleShot(0, this, &QtSLiMAppDelegate::updateActiveWindowList); } } void QtSLiMAppDelegate::pruneWindowList(void) { int windowListCount = focusedWindowList.size(); for (int listIndex = 0; listIndex < windowListCount; listIndex++) { QPointer &focused_window_ptr = focusedWindowList[listIndex]; if (focused_window_ptr && focused_window_ptr->isVisible() && !focused_window_ptr->isHidden()) { // prune zombie windows bool isZombie = false; QtSLiMWindow *qtSLiMWindow = qobject_cast(focused_window_ptr); if (qtSLiMWindow) isZombie = qtSLiMWindow->isZombieWindow_; if (!isZombie) continue; } // prune focusedWindowList.removeAt(listIndex); windowListCount--; listIndex--; } } QtSLiMWindow *QtSLiMAppDelegate::activeQtSLiMWindow(void) { // First try qApp's active window; if the SLiM window is key, this suffices // This allows Qt to define the active main window in some platform-specific way, // perhaps based upon which window the cursor is in, for example; for the // activeWindowExcluding() method we want our window list to be the sole authority, // but for this method I don't think we do...? // We use this for dispatching commands that could go to any QtSLiMWindow, but that // open a new document window that gets tiled; it's best to tile from the user's // frontmost document window, I suppose. We do not use it for other dispatch; see // dispatchQtSLiMWindowFromSecondaries() for that. QWidget *currentActiveWindow = qApp->activeWindow(); QtSLiMWindow *currentActiveQtSLiMWindow = qobject_cast(currentActiveWindow); if (currentActiveQtSLiMWindow && !currentActiveQtSLiMWindow->isZombieWindow_) return currentActiveQtSLiMWindow; // If that fails, use the last focused main window, as tracked by focusChanged() pruneWindowList(); for (QPointer &focused_window_ptr : focusedWindowList) { if (focused_window_ptr) { QWidget *focused_window = focused_window_ptr.data(); QtSLiMWindow *focusedQtSLiMWindow = qobject_cast(focused_window); if (focusedQtSLiMWindow && !focusedQtSLiMWindow->isZombieWindow_) return focusedQtSLiMWindow; } } return nullptr; } QWidget *QtSLiMAppDelegate::activeWindow(void) { // QApplication can handle this one return qApp->activeWindow(); } QWidget *QtSLiMAppDelegate::activeWindowExcluding(QWidget *excluded) { // Use the last focused window, as tracked by focusChanged() pruneWindowList(); for (QPointer &focused_window_ptr : focusedWindowList) { if (focused_window_ptr) { QWidget *focused_window = focused_window_ptr.data(); if (focused_window != excluded) return focused_window; } } return nullptr; } // This is declared in eidos_beep.h, but in QtSLiM it is actually defined here, // so that we can produce the beep sound with Qt std::string Eidos_Beep_QT(const std::string &__attribute__((__unused__)) p_sound_name) { std::string return_string; qApp->beep(); return return_string; } ================================================ FILE: QtSLiM/QtSLiMAppDelegate.h ================================================ // // QtSLiMAppDelegate.h // SLiM // // Created by Ben Haller on 7/13/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMAPPDELEGATE_H #define QTSLIMAPPDELEGATE_H #include #include #include #include #include #include class QMenuBar; class QMenu; class QAction; class QtSLiMWindow; class QtSLiMAppDelegate; extern QtSLiMAppDelegate *qtSLiMAppDelegate; // global instance class QtSLiMAppDelegate : public QObject { Q_OBJECT std::string app_cwd_; // the app's current working directory bool launchedFromShell_; // true if launched from shell, false if launched from Finder/other bool closeRejected_ = false; // true if the user cancels the close of a window, to prevent quit bool inDarkMode_ = false; QIcon appIcon_; QIcon appIconHighlighted_; QIcon slimDocumentIcon_; QIcon genericDocumentIcon_; public: explicit QtSLiMAppDelegate(QObject *parent); virtual ~QtSLiMAppDelegate(void) override; // Whether we were launched from a shell (true) or Finder/other (false) bool launchedFromShell(void) { return launchedFromShell_; } // The current working directory for the app std::string &QtSLiMCurrentWorkingDirectory(void) { return app_cwd_; } // Tracking the current active main window QtSLiMWindow *activeQtSLiMWindow(void); // the frontmost window that is a QtSLiMWindow QWidget *activeWindow(void); // the frontmost window QWidget *activeWindowExcluding(QWidget *excluded); // the frontmost window that is not excluded // Finding targets for action dispatch QtSLiMWindow *dispatchQtSLiMWindowFromSecondaries(void); // the QtSLiMWindow associated with the focused widget or active window // Document opening QtSLiMWindow *findMainWindow(const QString &fileName) const; void newFile_WF(bool includeComments); void newFile_nonWF(bool includeComments); QtSLiMWindow *open(QtSLiMWindow *requester); QtSLiMWindow *openFile(const QString &fileName, QtSLiMWindow *requester); void openRecipeWithName(const QString &recipeName, const QString &recipeScript, QtSLiMWindow *requester); // Recipes and Recents menus void setUpRecipesMenu(QMenu *openRecipesSubmenu, QAction *findRecipeAction); void setUpRecentsMenu(QMenu *openRecentSubmenu); void prependToRecentFiles(const QString &fileName); // App-wide shared icons QIcon applicationIcon(void) { return appIcon_; } QIcon slimDocumentIcon(void) { return slimDocumentIcon_; } QIcon genericDocumentIcon(void) { return genericDocumentIcon_; } // Global actions: designated as "window" actions to avoid ambiguity, but defined on every window and handled by us void addActionsForGlobalMenuItems(QWidget *window); public slots: void appDidFinishLaunching(QtSLiMWindow *initialWindow); void closeRejected(void) { closeRejected_ = true; } void findRecipe(void); void openRecipe(void); void playStateChanged(void); // These are slots for menu bar actions that get dispatched to the focal widget/window by us. // Every window should use addActionsForGlobalMenuItems() to connect to these slots (except // QtSLiMWindow, which connects the main menu bar actions to these slots instead). This gives // us a little bit of a "first responder" type functionality, where menu items work globally // and get dispatched to the right target according to focus. void dispatch_preferences(void); void dispatch_about(void); void dispatch_showCycle_WF(void); void dispatch_showCycle_nonWF(void); void dispatch_showCycle_WF_MS(void); void dispatch_showCycle_nonWF_MS(void); void dispatch_showColorChart(void); void dispatch_showPlotSymbols(void); void dispatch_showColorScales(void); void dispatch_help(void); void dispatch_quit(void); void dispatch_biggerFont(void); void dispatch_smallerFont(void); void dispatch_newWF(void); void dispatch_newWF_commentless(void); void dispatch_newNonWF(void); void dispatch_newNonWF_commentless(void); void dispatch_open(void); void dispatch_close(void); void dispatch_copyAsHTML(void); void dispatch_shiftLeft(void); void dispatch_shiftRight(void); void dispatch_commentUncomment(void); void dispatch_undo(void); void dispatch_redo(void); void dispatch_cut(void); void dispatch_copy(void); void dispatch_paste(void); void dispatch_duplicate(void); void dispatch_delete(void); void dispatch_selectAll(void); void dispatch_findShow(void); void dispatch_findNext(void); void dispatch_findPrevious(void); void dispatch_replaceAndFind(void); void dispatch_useSelectionForFind(void); void dispatch_useSelectionForReplace(void); void dispatch_jumpToSelection(void); void dispatch_jumpToLine(void); void dispatch_focusOnScript(void); void dispatch_focusOnConsole(void); void dispatch_checkScript(void); void dispatch_prettyprintScript(void); void dispatch_reformatScript(void); void dispatch_showEidosConsole(void); void dispatch_showVariableBrowser(void); void dispatch_clearOutput(void); void dispatch_clearDebugPoints(void); void dispatch_showDebuggingOutput(void); void dispatch_executeSelection(void); void dispatch_executeAll(void); void dispatch_minimize(void); void dispatch_zoom(void); void dispatch_helpWorkshops(void); void dispatch_helpFeedback(void); void dispatch_helpSLiMDiscuss(void); void dispatch_helpSLiMAnnounce(void); void dispatch_helpSLiMHome(void); void dispatch_helpSLiMExtras(void); void dispatch_helpMesserLab(void); void dispatch_helpBenHaller(void); void dispatch_helpStickSoftware(void); signals: void modifiersChanged(Qt::KeyboardModifiers newModifiers); void activeWindowListChanged(void); void applicationPaletteChanged(void); private: virtual bool eventFilter(QObject *p_obj, QEvent *p_event) override; QVector> focusedWindowList; // a list of all windows, from front to back void pruneWindowList(void); // remove all windows that are closed or hidden bool queuedActiveWindowUpdate = false; QWidget *globalImageWindowWithPath(const QString &path, const QString &title, double scaleFactor); void updateRecentFileActions(void); void openRecentFile(void); void clearRecentFiles(void); bool hasRecentFiles(void); private slots: void lastWindowClosed(void); void aboutToQuit(void); void focusChanged(QWidget *old, QWidget *now); void updateActiveWindowList(void); }; #endif // QTSLIMAPPDELEGATE_H ================================================ FILE: QtSLiM/QtSLiMChromosomeWidget.cpp ================================================ // // QtSLiMChromosomeWidget.cpp // SLiM // // Created by Ben Haller on 7/28/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMChromosomeWidget.h" #include "QtSLiMWindow.h" #include "QtSLiMExtras.h" #include "QtSLiMHaplotypeManager.h" #include "QtSLiMPreferences.h" #include #include #include #include #include #include #include #include #include #include #include static const int numberOfTicksPlusOne = 4; static const int tickLength = 5; static const int heightForTicks = 16; static const int selectionKnobSizeExtension = 2; // a 5-pixel-width knob is 2: 2 + 1 + 2, an extension on each side plus the one pixel of the bar in the middle static const int selectionKnobSize = selectionKnobSizeExtension + selectionKnobSizeExtension + 1; static const int spaceBetweenChromosomes = 5; QtSLiMChromosomeWidgetController::QtSLiMChromosomeWidgetController(QtSLiMWindow *slimWindow, QWidget *displayWindow, Species *focalSpecies, std::string chromosomeSymbol) : QObject(displayWindow ? displayWindow : slimWindow), slimWindow_(slimWindow), displayWindow_(displayWindow), chromosomeSymbol_(chromosomeSymbol) { connect(slimWindow_, &QtSLiMWindow::controllerPartialUpdateAfterTick, this, &QtSLiMChromosomeWidgetController::updateFromController); // focalSpecies is used only in the displayWindow case, for showing the species badge in multispecies models // otherwise, slimWindow will control the focal display species for our chromosome view as it requires if (displayWindow) { if (!focalSpecies) { qDebug() << "no focal species for creating a chromosome display!"; return; } focalSpeciesName_ = focalSpecies->name_; // focalSpeciesAvatar_ is set up in buildChromosomeDisplay(); } } void QtSLiMChromosomeWidgetController::updateFromController(void) { if (displayWindow_) { Species *displaySpecies = focalDisplaySpecies(); if (displaySpecies) { Community *community = slimWindow_->community; if (needsRebuild_ && !invalidSimulation() && community->simulation_valid_ && (community->tick_ >= 1)) { // It's hard to tell, in general, whether we need a rebuild: if the number of // chromosomes has changed, or the length of any chromosome, or the symbol of // any chromosome, etc. There's no harm, so we just always rebuild at the // first valid moment after recycling. buildChromosomeDisplay(/* resetWindowSize */ false); needsRebuild_ = false; } } else { // we've just recycled or become invalid; our next update should rebuild the display needsRebuild_ = true; } } emit needsRedisplay(); } Species *QtSLiMChromosomeWidgetController::focalDisplaySpecies(void) { if (displayWindow_) { // with a chromosome display, we are not based on the current focal species of slimWindow_, so we // need to look up the focal display species dynamically based on its name (which could fail) if (focalSpeciesName_.length() == 0) return nullptr; if (slimWindow_ && slimWindow_->community && (slimWindow_->community->Tick() >= 1)) return slimWindow_->community->SpeciesWithName(focalSpeciesName_); return nullptr; } // otherwise, our focal display species comes directly from slimWindow_ return slimWindow_->focalDisplaySpecies(); } void QtSLiMChromosomeWidgetController::buildChromosomeDisplay(bool resetWindowSize) { // Remove any existing content from our display window and build new content if (!displayWindow_) return; // Assess the chromosomes to be displayed Species *focalSpecies = focalDisplaySpecies(); std::vector chromosomes; bool singleChromosomeDisplay = (chromosomeSymbol_.length() != 0); // a single-chromosome display has an associated symbol if (singleChromosomeDisplay) { // displaying a specific chromosome; check that it still exists Chromosome *symbol_chrom = focalSpecies->ChromosomeFromSymbol(chromosomeSymbol_); if (symbol_chrom) chromosomes.push_back(symbol_chrom); } else { // displaying all chromosomes chromosomes = focalSpecies->Chromosomes(); // copies it, whatever } int chromosomeCount = (int)chromosomes.size(); slim_position_t chromosomeMaxLength = 0; for (Chromosome *chromosome : chromosomes) { slim_position_t length = chromosome->last_position_ + 1; chromosomeMaxLength = std::max(chromosomeMaxLength, length); } // Deal with window sizing; when displaying a single chromosome, the height is 16 more to make room for ticks const int margin = 5; const int spacing = 5; const int buttonRowHeight = margin + margin + 20; const int minChromosomeHeight = 20 + (singleChromosomeDisplay ? 35 : 0); const int maxChromosomeHeight = 200 + (singleChromosomeDisplay ? 35 : 0); const int defaultChromosomeHeight = 30 + (singleChromosomeDisplay ? 35 : 0); const int rowCount = std::max(chromosomeCount, 1); // space for a message row, if there are no chromosomes displayWindow_->setMinimumSize(500, margin + minChromosomeHeight * rowCount + spacing * (rowCount - 1) + buttonRowHeight); displayWindow_->setMaximumSize(4096, margin + maxChromosomeHeight * rowCount + spacing * (rowCount - 1) + buttonRowHeight); if (resetWindowSize) displayWindow_->resize(800, margin + defaultChromosomeHeight * rowCount + spacing * (rowCount - 1) + buttonRowHeight); // Find the top-level layout and remove all of its current children QVBoxLayout *topLayout = qobject_cast(displayWindow_->layout()); QtSLiMClearLayout(topLayout, /* deleteWidgets */ true); if (chromosomeCount > 0) { // Add a chromosome view for each chromosome in the model, with a spacer next to it to give it the right length std::vector labels; bool firstRow = true; for (Chromosome *chromosome : chromosomes) { QHBoxLayout *rowLayout = new QHBoxLayout; rowLayout->setContentsMargins(margin, firstRow ? margin : spacing, margin, 0); rowLayout->setSpacing(0); topLayout->addLayout(rowLayout); QtSLiMChromosomeWidget *chromosomeWidget = new QtSLiMChromosomeWidget(nullptr); chromosomeWidget->setController(this); chromosomeWidget->setFocalChromosome(chromosome); chromosomeWidget->setDisplayedRange(QtSLiMRange(0, 0)); // display entirety // multi-chromosome displays do not show ticks, because it would be too crowded; single-chromosome displays do if (!singleChromosomeDisplay) chromosomeWidget->setShowsTicks(false); slim_position_t length = chromosome->last_position_ + 1; double fractionOfMax = length / (double)chromosomeMaxLength; int chromosomeStretch = (int)(round(fractionOfMax * 255)); // Qt requires a max value of 255 QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Expanding); sizePolicy1.setHorizontalStretch(useScaledWidths_ ? chromosomeStretch : 0); sizePolicy1.setVerticalStretch(0); chromosomeWidget->setSizePolicy(sizePolicy1); QLabel *chromosomeLabel = new QLabel(); chromosomeLabel->setText(QString::fromStdString(chromosome->symbol_)); chromosomeLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); QSizePolicy sizePolicy2(QSizePolicy::Fixed, QSizePolicy::Expanding); chromosomeLabel->setSizePolicy(sizePolicy2); rowLayout->addWidget(chromosomeLabel); rowLayout->addSpacing(margin); rowLayout->addWidget(chromosomeWidget); if (useScaledWidths_) rowLayout->addStretch(255 - chromosomeStretch); // the remaining width after chromosomeStretch labels.push_back(chromosomeLabel); firstRow = false; } // adjust all the labels to have the same width int maxWidth = 0; for (QLabel *label : labels) maxWidth = std::max(maxWidth, label->sizeHint().width()); for (QLabel *label : labels) label->setMinimumWidth(maxWidth); } else { // no chromosomes; make a single row with a message label and nothing else QHBoxLayout *rowLayout = new QHBoxLayout; rowLayout->setContentsMargins(margin, margin, margin, 0); rowLayout->setSpacing(0); topLayout->addLayout(rowLayout); QLabel *chromosomeLabel = new QLabel(); if (singleChromosomeDisplay) chromosomeLabel->setText(QString("no chromosome with symbol '%1' found").arg(QString::fromStdString(chromosomeSymbol_))); else chromosomeLabel->setText("no chromosomes found for display"); chromosomeLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); chromosomeLabel->setEnabled(false); QFont labelFont(chromosomeLabel->font()); labelFont.setBold(true); chromosomeLabel->setFont(labelFont); QSizePolicy sizePolicy2(QSizePolicy::Expanding, QSizePolicy::Expanding); chromosomeLabel->setSizePolicy(sizePolicy2); rowLayout->addWidget(chromosomeLabel); } // Add a horizontal layout at the bottom, for the action button QHBoxLayout *buttonLayout = nullptr; { buttonLayout = new QHBoxLayout; buttonLayout->setContentsMargins(margin, margin, margin, margin); buttonLayout->setSpacing(5); topLayout->addLayout(buttonLayout); // set up the species badge; note that unlike QtSLiMGraphView, we set it up here immediately, // since we are guaranteed to already have a valid species object, and then we don't update it focalSpeciesAvatar_ = focalSpecies->avatar_; if (focalSpeciesAvatar_.length() && (focalSpecies->community_.all_species_.size() > 1)) { QLabel *speciesLabel = new QLabel(); speciesLabel->setText(QString::fromStdString(focalSpeciesAvatar_)); buttonLayout->addWidget(speciesLabel); } QSpacerItem *rightSpacer = new QSpacerItem(16, 5, QSizePolicy::Expanding, QSizePolicy::Minimum); buttonLayout->addItem(rightSpacer); // this code is based on the creation of executeScriptButton in ui_QtSLiMEidosConsole.h QtSLiMPushButton *actionButton = new QtSLiMPushButton(displayWindow_); actionButton->setObjectName(QString::fromUtf8("actionButton")); actionButton->setMinimumSize(QSize(20, 20)); actionButton->setMaximumSize(QSize(20, 20)); actionButton->setFocusPolicy(Qt::NoFocus); QIcon icon4; icon4.addFile(QtSLiMImagePath("action", false), QSize(), QIcon::Normal, QIcon::Off); icon4.addFile(QtSLiMImagePath("action", true), QSize(), QIcon::Normal, QIcon::On); actionButton->setIcon(icon4); actionButton->setIconSize(QSize(20, 20)); actionButton->qtslimSetBaseName("action"); actionButton->setCheckable(true); actionButton->setFlat(true); #if QT_CONFIG(tooltip) actionButton->setToolTip("

configure chromosome display

"); #endif // QT_CONFIG(tooltip) buttonLayout->addWidget(actionButton); connect(actionButton, &QPushButton::pressed, this, [actionButton, this]() { actionButton->qtslimSetHighlight(true); actionButtonRunMenu(actionButton); }); connect(actionButton, &QPushButton::released, this, [actionButton]() { actionButton->qtslimSetHighlight(false); }); // note that this action button has no enable/disable code anywhere, since it is happy to respond at all times } } void QtSLiMChromosomeWidgetController::runChromosomeContextMenuAtPoint(QPoint p_globalPoint) { if (!slimWindow_) return; Community *community = slimWindow_->community; if (!invalidSimulation() && community && community->simulation_valid_) { QMenu contextMenu("chromosome_menu", slimWindow_); // slimWindow_ is the parent in the sense that the menu is freed if slimWindow_ is freed QAction *scaledWidths = nullptr; QAction *unscaledWidths = nullptr; Species *focalSpecies = focalDisplaySpecies(); if (displayWindow_ && focalSpecies && (focalSpecies->Chromosomes().size() > 1)) { // only in multichromosome models, offer to scale the widths of the displayed chromosomes // according to their length or not, as the user prefers scaledWidths = contextMenu.addAction("Use Scaled Widths"); scaledWidths->setCheckable(true); scaledWidths->setChecked(useScaledWidths_); unscaledWidths = contextMenu.addAction("Use Full Widths"); unscaledWidths->setCheckable(true); unscaledWidths->setChecked(!useScaledWidths_); contextMenu.addSeparator(); } QAction *displayMutations = contextMenu.addAction("Display Mutations"); displayMutations->setCheckable(true); displayMutations->setChecked(shouldDrawMutations_); QAction *displaySubstitutions = contextMenu.addAction("Display Substitutions"); displaySubstitutions->setCheckable(true); displaySubstitutions->setChecked(shouldDrawFixedSubstitutions_); QAction *displayGenomicElements = contextMenu.addAction("Display Genomic Elements"); displayGenomicElements->setCheckable(true); displayGenomicElements->setChecked(shouldDrawGenomicElements_); QAction *displayRateMaps = contextMenu.addAction("Display Rate Maps"); displayRateMaps->setCheckable(true); displayRateMaps->setChecked(shouldDrawRateMaps_); contextMenu.addSeparator(); QAction *displayFrequencies = contextMenu.addAction("Display Frequencies"); displayFrequencies->setCheckable(true); displayFrequencies->setChecked(!displayHaplotypes_); QAction *displayHaplotypes = contextMenu.addAction("Display Haplotypes"); displayHaplotypes->setCheckable(true); displayHaplotypes->setChecked(displayHaplotypes_); QActionGroup *displayGroup = new QActionGroup(this); // On Linux this provides a radio-button-group appearance displayGroup->addAction(displayFrequencies); displayGroup->addAction(displayHaplotypes); QAction *displayAllMutations = nullptr; QAction *selectNonneutralMutations = nullptr; // mutation type checkmark items { const std::map &muttypes = community->AllMutationTypes(); if (muttypes.size() > 0) { contextMenu.addSeparator(); displayAllMutations = contextMenu.addAction("Display All Mutations"); displayAllMutations->setCheckable(true); displayAllMutations->setChecked(displayMuttypes_.size() == 0); // Make a sorted list of all mutation types we know – those that exist, and those that used to exist that we are displaying std::vector all_muttypes; for (auto muttype_iter : muttypes) { MutationType *muttype = muttype_iter.second; slim_objectid_t muttype_id = muttype->mutation_type_id_; all_muttypes.emplace_back(muttype_id); } all_muttypes.insert(all_muttypes.end(), displayMuttypes_.begin(), displayMuttypes_.end()); // Avoid building a huge menu, which will hang the app if (all_muttypes.size() <= 500) { std::sort(all_muttypes.begin(), all_muttypes.end()); all_muttypes.resize(static_cast(std::distance(all_muttypes.begin(), std::unique(all_muttypes.begin(), all_muttypes.end())))); // Then add menu items for each of those muttypes for (slim_objectid_t muttype_id : all_muttypes) { QString menuItemTitle = QString("Display m%1").arg(muttype_id); MutationType *muttype = community->MutationTypeWithID(muttype_id); // try to look up the mutation type; can fail if it doesn't exists now if (muttype && (community->all_species_.size() > 1)) menuItemTitle.append(" ").append(QString::fromStdString(muttype->species_.avatar_)); QAction *mutationAction = contextMenu.addAction(menuItemTitle); mutationAction->setData(muttype_id); mutationAction->setCheckable(true); if (std::find(displayMuttypes_.begin(), displayMuttypes_.end(), muttype_id) != displayMuttypes_.end()) mutationAction->setChecked(true); } } contextMenu.addSeparator(); selectNonneutralMutations = contextMenu.addAction("Select Non-Neutral MutationTypes"); } } // Run the context menu synchronously QAction *action = contextMenu.exec(p_globalPoint); // Act upon the chosen action; we just do it right here instead of dealing with slots if (action) { if (action == scaledWidths) { if (!useScaledWidths_) { useScaledWidths_ = true; buildChromosomeDisplay(/* resetWindowSize */ false); } } else if (action == unscaledWidths) { if (useScaledWidths_) { useScaledWidths_ = false; buildChromosomeDisplay(/* resetWindowSize */ false); } } else if (action == displayMutations) shouldDrawMutations_ = !shouldDrawMutations_; else if (action == displaySubstitutions) shouldDrawFixedSubstitutions_ = !shouldDrawFixedSubstitutions_; else if (action == displayGenomicElements) shouldDrawGenomicElements_ = !shouldDrawGenomicElements_; else if (action == displayRateMaps) shouldDrawRateMaps_ = !shouldDrawRateMaps_; else if (action == displayFrequencies) displayHaplotypes_ = false; else if (action == displayHaplotypes) displayHaplotypes_ = true; else { const std::map &muttypes = community->AllMutationTypes(); if (action == displayAllMutations) displayMuttypes_.clear(); else if (action == selectNonneutralMutations) { // - (IBAction)filterNonNeutral:(id)sender displayMuttypes_.clear(); for (auto muttype_iter : muttypes) { MutationType *muttype = muttype_iter.second; slim_objectid_t muttype_id = muttype->mutation_type_id_; if ((muttype->dfe_type_ != DFEType::kFixed) || (muttype->dfe_parameters_[0] != 0.0)) displayMuttypes_.emplace_back(muttype_id); } } else { // - (IBAction)filterMutations:(id)sender slim_objectid_t muttype_id = action->data().toInt(); auto present_iter = std::find(displayMuttypes_.begin(), displayMuttypes_.end(), muttype_id); if (present_iter == displayMuttypes_.end()) { // this mut-type is not being displayed, so add it to our display list displayMuttypes_.emplace_back(muttype_id); } else { // this mut-type is being displayed, so remove it from our display list displayMuttypes_.erase(present_iter); } } } emit needsRedisplay(); } } } void QtSLiMChromosomeWidgetController::actionButtonRunMenu(QtSLiMPushButton *p_actionButton) { QPoint mousePos = QCursor::pos(); runChromosomeContextMenuAtPoint(mousePos); // This is not called by Qt, for some reason (nested tracking loops?), so we call it explicitly p_actionButton->qtslimSetHighlight(false); } QtSLiMChromosomeWidget::QtSLiMChromosomeWidget(QWidget *p_parent, QtSLiMChromosomeWidgetController *controller, Species *displaySpecies, Qt::WindowFlags f) #ifndef SLIM_NO_OPENGL : QOpenGLWidget(p_parent, f) #else : QWidget(p_parent, f) #endif { controller_ = controller; setFocalDisplaySpecies(displaySpecies); // We support both OpenGL and non-OpenGL display, because some platforms seem // to have problems with OpenGL (https://github.com/MesserLab/SLiM/issues/462) QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::useOpenGLPrefChanged, this, [this]() { update(); }); } QtSLiMChromosomeWidget::~QtSLiMChromosomeWidget() { setDependentChromosomeView(nullptr); controller_ = nullptr; } void QtSLiMChromosomeWidget::setController(QtSLiMChromosomeWidgetController *controller) { if (controller != controller_) { if (controller_) disconnect(controller_, &QtSLiMChromosomeWidgetController::needsRedisplay, this, nullptr); controller_ = controller; connect(controller, &QtSLiMChromosomeWidgetController::needsRedisplay, this, &QtSLiMChromosomeWidget::updateAfterTick); } } void QtSLiMChromosomeWidget::setFocalDisplaySpecies(Species *displaySpecies) { // We can have no focal species (when coming out of the nib, in particular); in that case we display empty state if (displaySpecies && (displaySpecies->name_ != focalSpeciesName_)) { // we've switched species, so we should remember the new one focalSpeciesName_ = displaySpecies->name_; // ... and reset to showing an overview of all the chromosomes focalChromosomeSymbol_ = ""; update(); updateDependentView(); } else { // if displaySpecies is nullptr or unchanged, we just stick with our last remembered species } } Species *QtSLiMChromosomeWidget::focalDisplaySpecies(void) { // We look up our focal species object by name every time, since keeping a pointer to it would be unsafe // Before initialize() is done species have not been created, so we return nullptr in that case if (focalSpeciesName_.length() == 0) return nullptr; if (controller_ && controller_->community() && (controller_->community()->Tick() >= 1)) return controller_->community()->SpeciesWithName(focalSpeciesName_); return nullptr; } void QtSLiMChromosomeWidget::setFocalChromosome(Chromosome *chromosome) { if (chromosome) { if (chromosome->Symbol() != focalChromosomeSymbol_) { // we've switched chromosomes, so remember the new one focalChromosomeSymbol_ = chromosome->Symbol(); // ... and reset to the default selection setSelectedRange(QtSLiMRange(0, 0)); // ... and if our new chromosome belongs to a different species, remember that if (chromosome->species_.name_ != focalSpeciesName_) focalSpeciesName_ = chromosome->species_.name_; update(); updateDependentView(); } } else { if (focalChromosomeSymbol_.length()) { // we had a focal chromosome symbol, so reset to the overall view focalChromosomeSymbol_ = ""; update(); updateDependentView(); } } } Chromosome *QtSLiMChromosomeWidget::focalChromosome(void) { Species *focalSpecies = focalDisplaySpecies(); if (focalSpecies) { if (focalChromosomeSymbol_.length()) { Chromosome *chromosome = focalSpecies->ChromosomeFromSymbol(focalChromosomeSymbol_); if (isOverview_ && !chromosome) { // The focal chromosome apparently no longer exists, but we want to keep // trying to focus on it if it comes back (e.g., after a recycle), so we // do not reset or forget the focal chromosome symbol here; we only reset // the symbol to "" in setFocalDisplaySpecies() and setFocalChromosome(). // However, if the focal species has chromosomes (and they don't match), // then apparently we're waiting for something that won't happen; give up // and switch. if (focalSpecies->Chromosomes().size() > 1) { focalChromosomeSymbol_ = ""; return nullptr; } else if (focalSpecies->Chromosomes().size() == 1) { chromosome = focalSpecies->Chromosomes()[0]; focalChromosomeSymbol_ = chromosome->Symbol(); } } return chromosome; } else if (focalSpecies->Chromosomes().size() == 1) { // The species has just one chromosome, so there is no visual difference // between that chromosome being selected vs. not selected. However, we // want to return that chromosome to the caller, so if it is not selected, // we fix that here and return it. Chromosome *chromosome = focalSpecies->Chromosomes()[0]; focalChromosomeSymbol_ = chromosome->Symbol(); return chromosome; } } return nullptr; } void QtSLiMChromosomeWidget::setDependentChromosomeView(QtSLiMChromosomeWidget *p_dependent_widget) { if (dependentChromosomeView_ != p_dependent_widget) { dependentChromosomeView_ = p_dependent_widget; isOverview_ = (dependentChromosomeView_ ? true : false); showsTicks_ = !isOverview_; updateDependentView(); } } void QtSLiMChromosomeWidget::updateDependentView(void) { if (dependentChromosomeView_) { Chromosome *chromosome = focalChromosome(); dependentChromosomeView_->setFocalChromosome(chromosome); if (chromosome) dependentChromosomeView_->setDisplayedRange(getSelectedRange(chromosome)); else dependentChromosomeView_->setDisplayedRange(QtSLiMRange(0, 0)); // display entirely dependentChromosomeView_->stateChanged(); } } void QtSLiMChromosomeWidget::stateChanged(void) { update(); } void QtSLiMChromosomeWidget::updateAfterTick(void) { // overview chromosomes don't need to update all the time, since their display doesn't change if (!isOverview_) stateChanged(); } #ifndef SLIM_NO_OPENGL void QtSLiMChromosomeWidget::initializeGL() { initializeOpenGLFunctions(); glClearColor(1.0f, 0.0f, 0.0f, 1.0f); } void QtSLiMChromosomeWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); // Update the projection glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } #endif QRect QtSLiMChromosomeWidget::rectEncompassingBaseToBase(slim_position_t startBase, slim_position_t endBase, QRect interiorRect, QtSLiMRange displayedRange) { double startFraction = (startBase - static_cast(displayedRange.location)) / static_cast(displayedRange.length); double leftEdgeDouble = interiorRect.left() + startFraction * interiorRect.width(); double endFraction = (endBase + 1 - static_cast(displayedRange.location)) / static_cast(displayedRange.length); double rightEdgeDouble = interiorRect.left() + endFraction * interiorRect.width(); int leftEdge, rightEdge; if (rightEdgeDouble - leftEdgeDouble > 1.0) { // If the range spans a width of more than one pixel, then use the maximal pixel range leftEdge = static_cast(floor(leftEdgeDouble)); rightEdge = static_cast(ceil(rightEdgeDouble)); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the left-right positions span a pixel boundary leftEdge = static_cast(floor(leftEdgeDouble)); rightEdge = leftEdge + 1; } return QRect(leftEdge, interiorRect.top(), rightEdge - leftEdge, interiorRect.height()); } slim_position_t QtSLiMChromosomeWidget::baseForPosition(double position, QRect interiorRect, QtSLiMRange displayedRange) { double fraction = (position - interiorRect.left()) / interiorRect.width(); slim_position_t base = static_cast(floor(fraction * (displayedRange.length + 1) + displayedRange.location)); return base; } QRect QtSLiMChromosomeWidget::getContentRect(void) { QRect bounds = rect(); // The height gets adjusted because our "content rect" does not include the space for selection knobs below // (for the overview) or for tick marks and labels (for the zoomed view). Note that SLiMguiLegacy has a two- // pixel margin on the left and right of the chromosome view, to avoid clipping the selection knobs, but that // is a bit harder to do in Qt since the UI layout is trickier, so we just let the knobs clip; it's fine. int bottomMargin = (isOverview_ ? (selectionKnobSize+1) : heightForTicks); if (!isOverview_ && !showsTicks_) bottomMargin = 0; return QRect(bounds.left(), bounds.top(), bounds.width(), bounds.height() - bottomMargin); } QtSLiMRange QtSLiMChromosomeWidget::getSelectedRange(Chromosome *chromosome) { if (hasSelection_ && chromosome && (chromosome == focalChromosome())) { return QtSLiMRange(selectionFirstBase_, selectionLastBase_ - selectionFirstBase_ + 1); // number of bases encompassed; a selection from x to x encompasses 1 base } else if (chromosome) { slim_position_t chromosomeLastPosition = chromosome->last_position_; return QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed } else { return QtSLiMRange(0, 0); } } void QtSLiMChromosomeWidget::setSelectedRange(QtSLiMRange p_selectionRange) { if (isOverview_ && (p_selectionRange.length >= 1)) { selectionFirstBase_ = static_cast(p_selectionRange.location); selectionLastBase_ = static_cast(p_selectionRange.location + p_selectionRange.length) - 1; hasSelection_ = true; // Save the selection for restoring across recycles, etc. savedSelectionFirstBase_ = selectionFirstBase_; savedSelectionLastBase_ = selectionLastBase_; savedHasSelection_ = hasSelection_; } else if (hasSelection_) { hasSelection_ = false; // Save the selection for restoring across recycles, etc. savedHasSelection_ = hasSelection_; } else { // Save the selection for restoring across recycles, etc. savedHasSelection_ = false; return; } // Our selection changed, so update and post a change notification update(); if (isOverview_ && dependentChromosomeView_) updateDependentView(); } void QtSLiMChromosomeWidget::restoreLastSelection(void) { if (isOverview_ && savedHasSelection_) { selectionFirstBase_ = savedSelectionFirstBase_; selectionLastBase_ = savedSelectionLastBase_; hasSelection_ = savedHasSelection_; } else if (hasSelection_) { hasSelection_ = false; } // Our selection changed, so update and post a change notification update(); // We want to always post the notification, to make sure updating happens correctly; // this ensures that correct ticks marks get drawn after a recycle, etc. if (isOverview_ && dependentChromosomeView_) updateDependentView(); } QtSLiMRange QtSLiMChromosomeWidget::getDisplayedRange(Chromosome *chromosome) { if (isOverview_) { // the overview always displays the whole length slim_position_t chromosomeLastPosition = chromosome->last_position_; return QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed } else if (!chromosome || (displayedRange_.length == 0)) { // the detail view displays the entire length unless a specific displayed range is set slim_position_t chromosomeLastPosition = chromosome->last_position_; return QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed } else { return displayedRange_; } } void QtSLiMChromosomeWidget::setDisplayedRange(QtSLiMRange p_displayedRange) { displayedRange_ = p_displayedRange; update(); } void QtSLiMChromosomeWidget::setShowsTicks(bool p_showTicks) { if (p_showTicks != showsTicks_) { showsTicks_ = p_showTicks; update(); } } #ifndef SLIM_NO_OPENGL void QtSLiMChromosomeWidget::paintGL() #else void QtSLiMChromosomeWidget::paintEvent(QPaintEvent * /* p_paint_event */) #endif { QPainter painter(this); bool inDarkMode = QtSLiMInDarkMode(); painter.eraseRect(rect()); // erase to background color, which is not guaranteed //painter.fillRect(rect(), Qt::red); painter.setPen(Qt::black); // make sure we have our default color of black, since Qt apparently does not guarantee that Species *displaySpecies = focalDisplaySpecies(); bool ready = (isEnabled() && controller_ && !controller_->invalidSimulation() && (displaySpecies != nullptr)); QRect contentRect = getContentRect(); QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); // if the simulation is at tick 0, it is not ready if (ready) if (controller_->community()->Tick() == 0) ready = false; if (ready) { if (isOverview_) { drawOverview(displaySpecies, painter); } else { Chromosome *chromosome = focalChromosome(); if (!chromosome) { // display all chromosomes simultaneously drawFullGenome(displaySpecies, painter); } else { // display one chromosome in the regular way QtSLiMRange displayedRange = getDisplayedRange(chromosome); // draw ticks at bottom of content rect if (showsTicks_) drawTicksInContentRect(contentRect, displaySpecies, displayedRange, painter); // do the core drawing, with or without OpenGL according to user preference #ifndef SLIM_NO_OPENGL if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) { painter.beginNativePainting(); glDrawRect(contentRect, displaySpecies, chromosome); painter.endNativePainting(); } else #endif { qtDrawRect(contentRect, displaySpecies, chromosome, painter); } // frame near the end, so that any roundoff errors that caused overdrawing by a pixel get cleaned up QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter); } } } else { // erase the content area itself painter.fillRect(interiorRect, QtSLiMColorWithWhite(inDarkMode ? 0.118 : 0.9, 1.0)); // frame QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.77, 1.0), painter); } } void QtSLiMChromosomeWidget::drawOverview(Species *displaySpecies, QPainter &painter) { // the overview draws all of the chromosomes showing genomic elements; always with Qt, not GL Chromosome *focalChrom = focalChromosome(); QRect contentRect = getContentRect(); bool inDarkMode = QtSLiMInDarkMode(); if (!displaySpecies->HasGenetics()) { QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); painter.fillRect(interiorRect, QtSLiMColorWithWhite(inDarkMode ? 0.118 : 0.9, 1.0)); QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.77, 1.0), painter); return; } const std::vector &chromosomes = displaySpecies->Chromosomes(); int chromosomeCount = (int)chromosomes.size(); int64_t availableWidth = contentRect.width() - (chromosomeCount * 2) - ((chromosomeCount - 1) * spaceBetweenChromosomes); int64_t totalLength = 0; // after a delay, we show chromosome numbers unless we're tracking or have a single-chromosome model bool showChromosomeNumbers = (showChromosomeNumbers_ && !isTracking_ && (chromosomeCount > 1)); for (Chromosome *chrom : chromosomes) { slim_position_t chromLength = (chrom->last_position_ + 1); totalLength += chromLength; } if (showChromosomeNumbers) { painter.save(); static QFont *tickFont = nullptr; if (!tickFont) { tickFont = new QFont(); #ifdef __linux__ tickFont->setPointSize(8); #else tickFont->setPointSize(10); #endif } painter.setFont(*tickFont); } int64_t remainingLength = totalLength; int leftPosition = contentRect.left(); for (Chromosome *chrom : chromosomes) { double scale = (double)availableWidth / remainingLength; slim_position_t chromLength = (chrom->last_position_ + 1); int width = (int)round(chromLength * scale); int paddedWidth = 2 + width; QRect chromContentRect(leftPosition, contentRect.top(), paddedWidth, contentRect.height()); QRect chromInteriorRect = chromContentRect.marginsRemoved(QMargins(1, 1, 1, 1)); QtSLiMRange displayedRange = getDisplayedRange(chrom); if (showChromosomeNumbers) { painter.fillRect(chromInteriorRect, Qt::white); const std::string &symbol = chrom->Symbol(); QString symbolLabel = QString::fromStdString(symbol); QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, symbolLabel); double labelWidth = labelBoundingRect.width(); // display the chromosome symbol only if there is space for it if (labelWidth < chromInteriorRect.width()) { int symbolLabelX = static_cast(round(chromContentRect.center().x())) + 1; int symbolLabelY = static_cast(round(chromContentRect.center().y())) + 7; int textFlags = (Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignBottom | Qt::AlignHCenter); painter.drawText(QRect(symbolLabelX, symbolLabelY, 0, 0), textFlags, symbolLabel); } } else { painter.fillRect(chromInteriorRect, Qt::black); qtDrawGenomicElements(chromInteriorRect, chrom, displayedRange, painter); } if (chrom == focalChrom) { if (hasSelection_) { // overlay the selection last, since it bridges over the frame; when showing chromosome numbers // in light mode, drawing the interior shadow looks better because of the white background QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter); overlaySelection(chromInteriorRect, displayedRange, /* drawInteriorShadow */ showChromosomeNumbers && !inDarkMode, painter); } else if (chromosomes.size() > 1) { // the selected chromosome gets a heavier frame, if we have more than one chromosome QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.0 : 0.4, 1.0), painter); } else { QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter); } } else { if ((chromosomes.size() > 1) && focalChrom) { // with more than one chromosome, chromosomes other than the focal chromosome get washed out painter.fillRect(chromInteriorRect, QtSLiMColorWithWhite(inDarkMode ? 0.0 : 1.0, inDarkMode ? 0.50 : 0.60)); QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.1 : 0.8, 1.0), painter); } else { QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter); } } leftPosition += (paddedWidth + spaceBetweenChromosomes); availableWidth -= width; remainingLength -= chromLength; } if (showChromosomeNumbers) { painter.restore(); } } void QtSLiMChromosomeWidget::drawFullGenome(Species *displaySpecies, QPainter &painter) { // this is similar to drawOverview(), but shows the detail view and can use GL // we end up here in no-genetics models, because there is no selected chromosome (no chromosomes at all) QRect contentRect = getContentRect(); bool inDarkMode = QtSLiMInDarkMode(); const std::vector &chromosomes = displaySpecies->Chromosomes(); int chromosomeCount = (int)chromosomes.size(); int64_t availableWidth = contentRect.width() - (chromosomeCount * 2) - ((chromosomeCount - 1) * spaceBetweenChromosomes); int64_t totalLength = 0; if (!displaySpecies->HasGenetics()) { QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); painter.fillRect(interiorRect, QtSLiMColorWithWhite(inDarkMode ? 0.118 : 0.9, 1.0)); QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.77, 1.0), painter); // draw the "no genetics" label at the bottom; we now do this instead of drawTicksInContentRect() static QFont *tickFont = nullptr; if (!tickFont) { tickFont = new QFont(); #ifdef __linux__ tickFont->setPointSize(7); #else tickFont->setPointSize(9); #endif } painter.setFont(*tickFont); QString tickLabel("no genetics"); int tickLabelX = static_cast(floor(contentRect.left() + contentRect.width() / 2.0)); int tickLabelY = contentRect.bottom() + (2 + 13); int textFlags = (Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignBottom | Qt::AlignHCenter); painter.drawText(QRect(tickLabelX, tickLabelY, 0, 0), textFlags, tickLabel); return; } for (Chromosome *chrom : chromosomes) { slim_position_t chromLength = (chrom->last_position_ + 1); totalLength += chromLength; } int64_t remainingLength = totalLength; int leftPosition = contentRect.left(); for (Chromosome *chrom : chromosomes) { // display one chromosome in the regular way double scale = (double)availableWidth / remainingLength; slim_position_t chromLength = (chrom->last_position_ + 1); int width = (int)round(chromLength * scale); int paddedWidth = 2 + width; QRect chromContentRect(leftPosition, contentRect.top(), paddedWidth, contentRect.height()); QRect chromInteriorRect = chromContentRect.marginsRemoved(QMargins(1, 1, 1, 1)); slim_position_t chromosomeLastPosition = chrom->last_position_; QtSLiMRange displayedRange = QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed painter.fillRect(chromInteriorRect, Qt::black); // draw ticks at bottom of content rect if (showsTicks_) drawTicksInContentRect(chromContentRect, displaySpecies, displayedRange, painter); // do the core drawing, with or without OpenGL according to user preference #ifndef SLIM_NO_OPENGL if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) { painter.beginNativePainting(); glDrawRect(chromContentRect, displaySpecies, chrom); painter.endNativePainting(); } else #endif { qtDrawRect(chromContentRect, displaySpecies, chrom, painter); } // frame near the end, so that any roundoff errors that caused overdrawing by a pixel get cleaned up QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter); leftPosition += (paddedWidth + spaceBetweenChromosomes); availableWidth -= width; remainingLength -= chromLength; } } void QtSLiMChromosomeWidget::drawTicksInContentRect(QRect contentRect, __attribute__((__unused__)) Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter) { bool inDarkMode = QtSLiMInDarkMode(); QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); int64_t lastTickIndex = numberOfTicksPlusOne; painter.save(); painter.setPen(inDarkMode ? Qt::white : Qt::black); // Display fewer ticks when we are displaying a very small number of positions lastTickIndex = std::min(lastTickIndex, (displayedRange.length + 1) / 3); double tickIndexDivisor = ((lastTickIndex == 0) ? 1.0 : static_cast(lastTickIndex)); // avoid a divide by zero when we are displaying a single site static QFont *tickFont = nullptr; if (!tickFont) { tickFont = new QFont(); #ifdef __linux__ tickFont->setPointSize(7); #else tickFont->setPointSize(9); #endif } painter.setFont(*tickFont); QFontMetricsF fontMetrics(*tickFont); if (displayedRange.length == 0) { // The "no genetics" case is now handled in drawFullGenome() painter.restore(); return; } // We use scientific notation in two situations: (1) for numbers larger than 1e8, for readability, with four digits // after the decimal point, and (2) when the largest tick label and 0 won't fit together, for space-efficiency, // with two digits after the decimal point bool useScientificNotation = false; // the rightmost tick determines this since it has the largest tickBase int scientificNotationDigits = 4; slim_position_t largestTickBase = static_cast(displayedRange.location) + static_cast(ceil((displayedRange.length - 1) * (lastTickIndex / tickIndexDivisor))); if (largestTickBase >= 1e7) { useScientificNotation = true; } { QString largestTickLabel; if (useScientificNotation) // assume X.XXXXeX and check whether space forces us down to X.XXeX format { largestTickLabel = QString::asprintf("0 %.4e", static_cast(largestTickBase)); // is there enough room for a zero and the max tick label? largestTickLabel.replace(".0000e", ".0e"); largestTickLabel.replace(".000e", ".0e"); largestTickLabel.replace("000e", "e"); // didn't get replaced by the previous line, so it must follow a non-zero digit largestTickLabel.replace(".00e", ".0e"); largestTickLabel.replace("00e", "e"); // didn't get replaced by the previous line, so it must follow a non-zero digit largestTickLabel.replace("e+0", "e"); largestTickLabel.replace("e+", "e"); } else QTextStream(&largestTickLabel) << "0 " << static_cast(largestTickBase); // is there enough room for a zero and the max tick label? #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) double tickLabelWidth = fontMetrics.width(largestTickLabel); // deprecated in 5.11 #else double tickLabelWidth = fontMetrics.horizontalAdvance(largestTickLabel); // added in Qt 5.11 #endif if (tickLabelWidth > interiorRect.width()) { useScientificNotation = true; scientificNotationDigits = 2; // space is tight, so just assume we need to display fewer digits; trying to decide how many we can fit gets very complex } } // Draw tick marks and tick labels; we go from the right backwards because we want to at least fit the rightmost tick label if we can int leftmostNotDrawn = interiorRect.left(); // used to avoid overlapping labels int rightmostNotDrawn = interiorRect.right(); // used to avoid overlapping labels for (int simpleTickIndex = 0; simpleTickIndex <= lastTickIndex; ++simpleTickIndex) { // first figure out the tick position int tickIndex = ((simpleTickIndex == 0) ? lastTickIndex : (((simpleTickIndex == 1) ? 0 : (lastTickIndex - simpleTickIndex + 1)))); // lastTickIndex first, 0 second, then the rest in backwards order slim_position_t tickBase = static_cast(displayedRange.location) + static_cast(ceil((displayedRange.length - 1) * (tickIndex / tickIndexDivisor))); // -1 because we are choosing an in-between-base position that falls, at most, to the left of the last base QRect tickRect = rectEncompassingBaseToBase(tickBase, tickBase, interiorRect, displayedRange); tickRect.setHeight(tickLength); tickRect.moveBottom(contentRect.bottom() + tickLength); // figure out the label's alignment relative to the tick bool forceCenteredLabel = (tickRect.width() > 50); // with wide ticks, just center all labels; there's room for lots of digits here Qt::AlignmentFlag labelAlignment; int tickLabelX; if ((tickIndex == lastTickIndex) && !forceCenteredLabel) { labelAlignment = Qt::AlignRight; tickLabelX = tickRect.right() + 2; } else if ((tickIndex == 0) && !forceCenteredLabel) { labelAlignment = Qt::AlignLeft; tickLabelX = tickRect.left() - 1; } else { labelAlignment = Qt::AlignHCenter; tickLabelX = static_cast(floor(tickRect.left() + tickRect.width() / 2.0)) + 1; } // make the label QString tickLabel; if (useScientificNotation && (tickBase != 0)) { // we remove cruft around the exponential; we want "1.5000e+09" -> "1.5e9", "1.0000e+09" -> "1.0e9", etc. if (scientificNotationDigits == 4) { tickLabel = QString::asprintf("%.4e", static_cast(tickBase)); tickLabel.replace(".0000e", ".0e"); tickLabel.replace(".000e", ".0e"); tickLabel.replace("000e", "e"); // didn't get replaced by the previous line, so it must follow a non-zero digit tickLabel.replace(".00e", ".0e"); tickLabel.replace("00e", "e"); // didn't get replaced by the previous line, so it must follow a non-zero digit } else { tickLabel = QString::asprintf("%.2e", static_cast(tickBase)); tickLabel.replace(".00e", ".0e"); tickLabel.replace("00e", "e"); // didn't get replaced by the previous line, so it must follow a non-zero digit } tickLabel.replace("e+0", "e"); tickLabel.replace("e+", "e"); } else QTextStream(&tickLabel) << static_cast(tickBase); // measure it #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) double tickLabelWidth = fontMetrics.width(tickLabel); // deprecated in 5.11 #else double tickLabelWidth = fontMetrics.horizontalAdvance(tickLabel); // added in Qt 5.11 #endif int labelLeftEdge, labelRightEdge; if (labelAlignment == Qt::AlignRight) { labelLeftEdge = tickLabelX - tickLabelWidth; labelRightEdge = tickLabelX; } else if (labelAlignment == Qt::AlignLeft) { labelLeftEdge = tickLabelX; labelRightEdge = tickLabelX + tickLabelWidth; } else // Qt::AlignHCenter { labelLeftEdge = tickLabelX - (int)std::ceil(tickLabelWidth / 2.0); labelRightEdge = tickLabelX + (int)std::ceil(tickLabelWidth / 2.0); } // decide whether the label fits bool drawLabel = true; if ((tickIndex == lastTickIndex) && (labelLeftEdge < leftmostNotDrawn)) drawLabel = false; else if ((tickIndex > 0) && (tickIndex != lastTickIndex) && (labelLeftEdge - 5 < leftmostNotDrawn)) drawLabel = false; else if ((tickIndex < lastTickIndex) && (labelRightEdge + 5 > rightmostNotDrawn)) drawLabel = false; if (!drawLabel && (tickIndex == lastTickIndex)) // if the rightmost label doesn't fit, skip all ticks and labels break; if (!drawLabel && (tickIndex != 0) && (tickIndex != lastTickIndex)) // skip interior tick marks if we skip their label continue; // draw a tick for it; if we are displaying a single site or two sites, make a tick mark one pixel wide; a very wide one looks weird if (displayedRange.length <= 2) { tickRect.setLeft(static_cast(floor(tickRect.left() + tickRect.width() / 2.0 - 0.5))); tickRect.setWidth(1); } painter.fillRect(tickRect, inDarkMode ? QColor(10, 10, 10, 255) : QColor(127, 127, 127, 255)); // in dark mode, 17 matches the frame, but is too light // if we decided to draw the tick even though the label doesn't fit, now skip the label if (!drawLabel) continue; // and then draw the tick label int tickLabelY = contentRect.bottom() + (tickLength + 13); int textFlags = (Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignBottom | labelAlignment); painter.drawText(QRect(tickLabelX, tickLabelY, 0, 0), textFlags, tickLabel); // keep track of where we have drawn text, to avoid overlap; this is a bit tricky because we draw the ticks out of order if (simpleTickIndex == 1) leftmostNotDrawn = std::max(leftmostNotDrawn, labelRightEdge); // changes only when we draw the left-edge tick if (simpleTickIndex != 1) rightmostNotDrawn = std::min(rightmostNotDrawn, labelLeftEdge); // does not change when we draw the left-edge tick } painter.restore(); } void QtSLiMChromosomeWidget::updateDisplayedMutationTypes(Species *displaySpecies) { // We use a flag in MutationType to indicate whether we're drawing that type or not; we update those flags here, // before every drawing of mutations, from the vector of mutation type IDs that we keep internally if (controller_) { if (displaySpecies) { std::map &muttypes = displaySpecies->mutation_types_; std::vector &displayTypes = displayMuttypes(); for (auto muttype_iter : muttypes) { MutationType *muttype = muttype_iter.second; if (displayTypes.size()) { slim_objectid_t muttype_id = muttype->mutation_type_id_; muttype->mutation_type_displayed_ = (std::find(displayTypes.begin(), displayTypes.end(), muttype_id) != displayTypes.end()); } else { muttype->mutation_type_displayed_ = true; } } } } } void QtSLiMChromosomeWidget::overlaySelection(QRect interiorRect, QtSLiMRange displayedRange, bool drawInteriorShadow, QPainter &painter) { if (hasSelection_) { // wash out the exterior of the selection bool inDarkMode = QtSLiMInDarkMode(); if (selectionFirstBase_ > 0) { QRect leftOfSelectionRect = rectEncompassingBaseToBase(0, selectionFirstBase_ - 1, interiorRect, displayedRange); painter.fillRect(leftOfSelectionRect, QtSLiMColorWithWhite(inDarkMode ? 0.0 : 1.0, inDarkMode ? 0.50 : 0.60)); } if (selectionLastBase_ < displayedRange.length - 1) { QRect rightOfSelectionRect = rectEncompassingBaseToBase(selectionLastBase_ + 1, displayedRange.length - 1, interiorRect, displayedRange); painter.fillRect(rightOfSelectionRect, QtSLiMColorWithWhite(inDarkMode ? 0.0 : 1.0, inDarkMode ? 0.50 : 0.60)); } // draw a bar at the start and end of the selection QRect selectionRect = rectEncompassingBaseToBase(selectionFirstBase_, selectionLastBase_, interiorRect, displayedRange); QRect selectionStartBar1 = QRect(selectionRect.left() - 1, interiorRect.top(), 1, interiorRect.height()); QRect selectionStartBar2 = QRect(selectionRect.left(), interiorRect.top(), 1, interiorRect.height() + 5); QRect selectionStartBar3 = QRect(selectionRect.left() + 1, interiorRect.top(), 1, interiorRect.height()); QRect selectionEndBar1 = QRect(selectionRect.left() + selectionRect.width() - 2, interiorRect.top(), 1, interiorRect.height()); QRect selectionEndBar2 = QRect(selectionRect.left() + selectionRect.width() - 1, interiorRect.top(), 1, interiorRect.height() + 5); QRect selectionEndBar3 = QRect(selectionRect.left() + selectionRect.width(), interiorRect.top(), 1, interiorRect.height()); painter.fillRect(selectionStartBar1, QtSLiMColorWithWhite(1.0, 0.15)); if (drawInteriorShadow) painter.fillRect(selectionEndBar1, QtSLiMColorWithWhite(1.0, 0.15)); painter.fillRect(selectionStartBar2, inDarkMode ? QtSLiMColorWithWhite(0.8, 1.0) : Qt::black); painter.fillRect(selectionEndBar2, inDarkMode ? QtSLiMColorWithWhite(0.8, 1.0) : Qt::black); if (drawInteriorShadow) painter.fillRect(selectionStartBar3, QtSLiMColorWithWhite(0.0, 0.30)); painter.fillRect(selectionEndBar3, QtSLiMColorWithWhite(0.0, 0.30)); // draw a ball at the end of each bar // FIXME this doesn't look quite as nice as SLiMgui, because QPainter doesn't antialias // also we can get clipped by one pixel at the edge of the view; subtle but imperfect QRect selectionStartBall = QRect(selectionRect.left() - selectionKnobSizeExtension, interiorRect.bottom() + (selectionKnobSize - 2), selectionKnobSize, selectionKnobSize); QRect selectionEndBall = QRect(selectionRect.left() + selectionRect.width() - (selectionKnobSizeExtension + 1), interiorRect.bottom() + (selectionKnobSize - 2), selectionKnobSize, selectionKnobSize); painter.save(); painter.setPen(Qt::NoPen); painter.setBrush(inDarkMode ? QtSLiMColorWithWhite(0.65, 1.0) : Qt::black); // outline painter.drawEllipse(selectionStartBall); painter.drawEllipse(selectionEndBall); painter.setBrush(QtSLiMColorWithWhite(0.3, 1.0)); // interior painter.drawEllipse(selectionStartBall.adjusted(1, 1, -1, -1)); painter.drawEllipse(selectionEndBall.adjusted(1, 1, -1, -1)); painter.setBrush(QtSLiMColorWithWhite(1.0, 0.5)); // highlight painter.drawEllipse(selectionStartBall.adjusted(1, 1, -2, -2)); painter.drawEllipse(selectionEndBall.adjusted(1, 1, -2, -2)); painter.restore(); } } Chromosome *QtSLiMChromosomeWidget::_findFocalChromosomeForTracking(QMouseEvent *p_event) { // this hit-tracks the same layout that drawOverview() displays QPoint curPoint = p_event->pos(); QRect overallRect = rect(); QRect contentRect = getContentRect(); Species *displaySpecies = focalDisplaySpecies(); const std::vector &chromosomes = displaySpecies->Chromosomes(); int chromosomeCount = (int)chromosomes.size(); int64_t availableWidth = contentRect.width() - (chromosomeCount * 2) - ((chromosomeCount - 1) * spaceBetweenChromosomes); int64_t totalLength = 0; for (Chromosome *chrom : chromosomes) { slim_position_t chromLength = (chrom->last_position_ + 1); totalLength += chromLength; } int64_t remainingLength = totalLength; int leftPosition = contentRect.left(); // note that we hit-test against the overall frames of the chromosomes (including the margin // at the bottom for selection knobs), but set contentRectForTrackedChromosome_ based on the // content rect for the chromosome (excluding that margin); see mousePressEvent() for why. for (Chromosome *chrom : chromosomes) { double scale = (double)availableWidth / remainingLength; slim_position_t chromLength = (chrom->last_position_ + 1); int width = (int)round(chromLength * scale); int paddedWidth = 2 + width; QRect chromOverallFrame(leftPosition, overallRect.top(), paddedWidth, overallRect.height()); if (chromOverallFrame.contains(curPoint)) { QRect chromContentRect(leftPosition, contentRect.top(), paddedWidth, contentRect.height()); contentRectForTrackedChromosome_ = chromContentRect; return chrom; } leftPosition += (paddedWidth + spaceBetweenChromosomes); availableWidth -= width; remainingLength -= chromLength; } return nullptr; } void QtSLiMChromosomeWidget::mousePressEvent(QMouseEvent *p_event) { Species *displaySpecies = focalDisplaySpecies(); bool ready = (isOverview_ && isEnabled() && !controller_->invalidSimulation() && (displaySpecies != nullptr)); // if the simulation is at tick 0, it is not ready if (ready) if (controller_->community()->Tick() == 0) ready = false; if (ready) { // find which chromosome was clicked in; this sets contentRectForTrackedChromosome_ to the content rect of that chromosome // note that it hit-tests aginst the overall chromosome view, including the selection knob margin, though Chromosome *hitChromosome = _findFocalChromosomeForTracking(p_event); simpleClickInFocalChromosome_ = false; // only true in the one case set below // if the click was not in a chromosome (like in the gap between them), just return with no effect if (!hitChromosome) return; QRect contentRect = contentRectForTrackedChromosome_; QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); QtSLiMRange displayedRange = getDisplayedRange(hitChromosome); QPoint curPoint = p_event->pos(); // check for a hit in one of our selection handles if (hasSelection_ && (hitChromosome == focalChromosome())) { QRect selectionRect = rectEncompassingBaseToBase(selectionFirstBase_, selectionLastBase_, interiorRect, displayedRange); int leftEdge = selectionRect.left(); int rightEdge = selectionRect.left() + selectionRect.width() - 1; // -1 to be on the left edge of the right-edge pixel strip QRect leftSelectionBar = QRect(leftEdge - 2, selectionRect.top() - 1, 5, selectionRect.height() + 2); QRect leftSelectionKnob = QRect(leftEdge - (selectionKnobSizeExtension + 1), selectionRect.bottom() + (selectionKnobSize - 3), (selectionKnobSizeExtension + 1) * 2 + 1, selectionKnobSize + 2); QRect rightSelectionBar = QRect(rightEdge - 2, selectionRect.top() - 1, 5, selectionRect.height() + 2); QRect rightSelectionKnob = QRect(rightEdge - (selectionKnobSizeExtension + 1), selectionRect.bottom() + (selectionKnobSize - 3), (selectionKnobSizeExtension + 1) * 2 + 1, selectionKnobSize + 2); if (leftSelectionBar.contains(curPoint) || leftSelectionKnob.contains(curPoint)) { isTracking_ = true; movedSufficiently_ = true; // a hit in a selection bar is unambiguously a drag trackingXAdjust_ = (curPoint.x() - leftEdge) - 1; // I'm not sure why the -1 is needed, but it is... trackingStartBase_ = selectionLastBase_; // we're dragging the left knob, so the right knob is the tracking anchor trackingLastBase_ = baseForPosition(curPoint.x() - trackingXAdjust_, interiorRect, displayedRange); // instead of selectionFirstBase, so the selection does not change at all if the mouse does not move mouseMoveEvent(p_event); // the click may not be aligned exactly on the center of the bar, so clicking might shift it a bit; do that now return; } else if (rightSelectionBar.contains(curPoint) || rightSelectionKnob.contains(curPoint)) { isTracking_ = true; movedSufficiently_ = true; // a hit in a selection bar is unambiguously a drag trackingXAdjust_ = (curPoint.x() - rightEdge); trackingStartBase_ = selectionFirstBase_; // we're dragging the right knob, so the left knob is the tracking anchor trackingLastBase_ = baseForPosition(curPoint.x() - trackingXAdjust_, interiorRect, displayedRange); // instead of selectionLastBase, so the selection does not change at all if the mouse does not move mouseMoveEvent(p_event); // the click may not be aligned exactly on the center of the bar, so clicking might shift it a bit; do that now return; } } // _findFocalChromosomeForTracking() will return a hit anywhere in the overall chromosome view, so that we can test for hits // in the selection knobs above; but from this point forward, we only want to handle hits that are actually in the content area if (!contentRect.contains(curPoint)) return; // our behavior depends on whether the hit chromosome was already the focal chromosome if ((hitChromosome == focalChromosome()) && !hasSelection_) { // if the click was in the currently selected chromosome, and there is presently no selection, remember // that fact; if we get a mouse-up without a selection being dragged out, we will deselect completely // (if we presently have a selection, the click removes the selection, but does not deselect completely) simpleClickInFocalChromosome_ = true; update(); } else if (hitChromosome != focalChromosome()) { // given that it wasn't a hit in a selection handle, we now switch to the chromosome that was clicked in; // other kinds of clicks change the focal chromosome to the one hit by the click setFocalChromosome(hitChromosome); update(); } // option-clicks just set the selection to the clicked genomic element, no questions asked // tracking does not continue beyond this step, since we don't set isTracking_ = true if (p_event->modifiers() & Qt::AltModifier) { slim_position_t clickedBase = baseForPosition(curPoint.x(), interiorRect, displayedRange); QtSLiMRange selectionRange = QtSLiMRange(0, 0); GenomicElement *genomicElement = hitChromosome->ElementForPosition(clickedBase); if (genomicElement) { slim_position_t startPosition = genomicElement->start_position_; slim_position_t endPosition = genomicElement->end_position_; selectionRange = QtSLiMRange(startPosition, endPosition - startPosition + 1); } mouseInsideCounter_++; // prevent a flip to displaying chromosome numbers simpleClickInFocalChromosome_ = false; setSelectedRange(selectionRange); return; } // otherwise we have an ordinary click, selecting a chromosome and perhaps dragging out a selection { isTracking_ = true; movedSufficiently_ = false; // require a movement threshold before beginning to drag initialMouseX = curPoint.x(); trackingStartBase_ = baseForPosition(curPoint.x(), interiorRect, displayedRange); trackingLastBase_ = trackingStartBase_; trackingXAdjust_ = 0; // We start off with no selection, and wait for the user to drag out a selection if (hasSelection_) { hasSelection_ = false; // Save the selection for restoring across recycles, etc. savedHasSelection_ = hasSelection_; update(); updateDependentView(); } } } } void QtSLiMChromosomeWidget::_mouseTrackEvent(QMouseEvent *p_event) { QRect contentRect = contentRectForTrackedChromosome_; QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); QtSLiMRange displayedRange = getDisplayedRange(focalChromosome()); QPoint curPoint = p_event->pos(); QPoint correctedPoint = QPoint(curPoint.x() - trackingXAdjust_, curPoint.y()); slim_position_t trackingNewBase = baseForPosition(correctedPoint.x(), interiorRect, displayedRange); bool selectionChanged = false; if (trackingNewBase != trackingLastBase_) { trackingLastBase_ = trackingNewBase; slim_position_t trackingLeftBase = trackingStartBase_, trackingRightBase = trackingLastBase_; if (trackingLeftBase > trackingRightBase) { trackingLeftBase = trackingLastBase_; trackingRightBase = trackingStartBase_; } if (trackingLeftBase <= static_cast(displayedRange.location)) trackingLeftBase = static_cast(displayedRange.location); if (trackingRightBase > static_cast((displayedRange.location + displayedRange.length) - 1)) trackingRightBase = static_cast((displayedRange.location + displayedRange.length) - 1); if (trackingRightBase <= trackingLeftBase + 3) // minimum selection length is 5; below that, reset to no selection { if (hasSelection_) selectionChanged = true; hasSelection_ = false; // Save the selection for restoring across recycles, etc. savedHasSelection_ = hasSelection_; } else if (movedSufficiently_ || (abs(initialMouseX - curPoint.x()) > 2)) // movement threshold before drag-selection begins { selectionChanged = true; hasSelection_ = true; movedSufficiently_ = true; selectionFirstBase_ = trackingLeftBase; selectionLastBase_ = trackingRightBase; simpleClickInFocalChromosome_ = false; // no resetting to overview // Save the selection for restoring across recycles, etc. savedSelectionFirstBase_ = selectionFirstBase_; savedSelectionLastBase_ = selectionLastBase_; savedHasSelection_ = hasSelection_; } if (selectionChanged) { update(); updateDependentView(); } } } void QtSLiMChromosomeWidget::mouseMoveEvent(QMouseEvent *p_event) { if (isOverview_ && isTracking_) _mouseTrackEvent(p_event); } void QtSLiMChromosomeWidget::mouseReleaseEvent(QMouseEvent *p_event) { if (isOverview_ && isTracking_) { _mouseTrackEvent(p_event); // prevent a flip to showing chromosome numbers after user tracking mouseInsideCounter_++; showChromosomeNumbers_ = false; // if we had a simple click and mouse-up in the focal chromosome, and there // was no existing selection, then reset to showing all chromosomes if (simpleClickInFocalChromosome_) { setFocalChromosome(nullptr); update(); updateDependentView(); } } isTracking_ = false; } void QtSLiMChromosomeWidget::contextMenuEvent(QContextMenuEvent * /* p_event */) { // BCH 5/9/2022: I think now that we can have multiple chromosome views it might be best to make // people use the action button; a context menu running on a particular view looks view-specific, // but the multiple chromosome views share all their configuration state, so that would be odd. //if (!isOverview_) // controller_->runChromosomeContextMenuAtPoint(p_event->globalPos()); } void QtSLiMChromosomeWidget::enterEvent(QTSLIM_ENTER_EVENT * /* event */) { if (isOverview_) { // When the mouse enters, we want to switch to showing chromosome numbers, but we want it to // happen with a bit of a delay so it doesn't flip visually when the user is just moving the // mouse around. We want the display change not to happen again if the mouse exits before // the delay is up, *even* if it re-enters again within the delay period. To achieve that, // we use a unique identifier for each entry, in the form of a counter, mouseInsideCounter_. // We use a one-second delay to give the user time to start dragging a selection if they // want to; that would often depend upon the genomic elements, so we don't want to hide them. mouseInside_ = true; mouseInsideCounter_++; int thisMouseInsideCounter = mouseInsideCounter_; QTimer::singleShot(1000, this, [this, thisMouseInsideCounter]() { if (mouseInside_ && (mouseInsideCounter_ == thisMouseInsideCounter)) { showChromosomeNumbers_ = true; update(); } }); } } void QtSLiMChromosomeWidget::leaveEvent(QEvent * /* event */) { if (isOverview_) { mouseInside_ = false; mouseInsideCounter_++; if (showChromosomeNumbers_) { // When the mouse exists, we want to switch away from showing chromosome numbers, but we // again want it to happen with a bit of delay, so that the user can mouse over to the // chromosome number they want without having it flip back due to a mouse track that // passes outside the overview strip. So we want the display change not to happen if // the mouse enters again within that delay. We can use the same mechanism as above. int thisMouseInsideCounter = mouseInsideCounter_; QTimer::singleShot(500, this, [this, thisMouseInsideCounter]() { if (!mouseInside_ && (mouseInsideCounter_ == thisMouseInsideCounter)) { showChromosomeNumbers_ = false; update(); } }); } } } ================================================ FILE: QtSLiM/QtSLiMChromosomeWidget.h ================================================ // // QtSLiMChromosomeWidget.h // SLiM // // Created by Ben Haller on 7/28/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMCHROMOSOMEWIDGET_H #define QTSLIMCHROMOSOMEWIDGET_H // Silence deprecated OpenGL warnings on macOS #define GL_SILENCE_DEPRECATION #include #include #ifndef SLIM_NO_OPENGL #include #include #endif #include "slim_globals.h" #include "QtSLiMWindow.h" class QtSLiMWindow; class QtSLiMHaplotypeManager; class QPainter; class QContextMenuEvent; class QButton; struct QtSLiMRange { int64_t location, length; explicit QtSLiMRange() : location(0), length(0) {} explicit QtSLiMRange(int64_t p_location, int64_t p_length) : location(p_location), length(p_length) {} }; // This is a little controller class that governs a chromosome view or views, and an associated action button // It is used by QtSLiMWindow for the main chromosome views (overview and zoomed), and by the chromosome display class QtSLiMChromosomeWidgetController : public QObject { Q_OBJECT QtSLiMWindow *slimWindow_ = nullptr; // state used only in the chromosome display case QPointer displayWindow_ = nullptr; std::string focalSpeciesName_; // we keep the name of our focal species, since a pointer would be unsafe std::string focalSpeciesAvatar_; // cached so we can display it even when the simulation is invalid std::string chromosomeSymbol_; // a chromosome symbol, or "" for "all chromosomes" bool needsRebuild_ = false; // true immediately after recycling public: bool useScaledWidths_ = true; // used only by the chromosome display bool shouldDrawMutations_ = true; bool shouldDrawFixedSubstitutions_ = false; bool shouldDrawGenomicElements_ = false; bool shouldDrawRateMaps_ = false; bool displayHaplotypes_ = false; // if false, displaying frequencies; if true, displaying haplotypes std::vector displayMuttypes_; // if empty, display all mutation types; otherwise, display only the muttypes chosen QtSLiMChromosomeWidgetController(QtSLiMWindow *slimWindow, QWidget *displayWindow, Species *focalSpecies, std::string chromosomeSymbol); void buildChromosomeDisplay(bool resetWindowSize); void updateFromController(void); void runChromosomeContextMenuAtPoint(QPoint p_globalPoint); void actionButtonRunMenu(QtSLiMPushButton *p_actionButton); // forwards from slimWindow_; this is everything QtSLiMChromosomeWidget needs from the outside world bool invalidSimulation(void) { return slimWindow_->invalidSimulation(); } Community *community(void) { return slimWindow_->community; } Species *focalDisplaySpecies(void); void colorForGenomicElementType(GenomicElementType *elementType, slim_objectid_t elementTypeID, float *p_red, float *p_green, float *p_blue, float *p_alpha) { slimWindow_->colorForGenomicElementType(elementType, elementTypeID, p_red, p_green, p_blue, p_alpha); } QtSLiMWindow *slimWindow(void) { return slimWindow_; } signals: void needsRedisplay(void); }; // This is a fast macro for when all we need is the offset of a base from the left edge of interiorRect; interiorRect.origin.x is not added here! // This is based on the same math as rectEncompassingBase:toBase:interiorRect:displayedRange:, and must be kept in synch with that method. #define LEFT_OFFSET_OF_BASE(startBase, interiorRect, displayedRange) (static_cast(floor(((startBase - static_cast(displayedRange.location)) / static_cast(displayedRange.length)) * interiorRect.width()))) #ifndef SLIM_NO_OPENGL class QtSLiMChromosomeWidget : public QOpenGLWidget, protected QOpenGLFunctions #else class QtSLiMChromosomeWidget : public QWidget #endif { Q_OBJECT QtSLiMChromosomeWidgetController *controller_ = nullptr; std::string focalSpeciesName_; // we keep the name of our focal species, since a pointer would be unsafe std::string focalChromosomeSymbol_; // we keep the symbol of our focal chromosome, since a pointer would be unsafe bool isOverview_ = false; QtSLiMChromosomeWidget *dependentChromosomeView_ = nullptr; bool showsTicks_ = true; // Displayed range (only in a regular chromosome view) QtSLiMRange displayedRange_; // Selection (only in the overview) bool hasSelection_ = false; slim_position_t selectionFirstBase_ = 0, selectionLastBase_ = 0; // Selection memory – saved and restored across events like recycles bool savedHasSelection_ = false; slim_position_t savedSelectionFirstBase_ = 0, savedSelectionLastBase_ = 0; // Mouse-over display change (only in the overview) bool mouseInside_ = false; int mouseInsideCounter_ = 0; bool showChromosomeNumbers_ = false; // set true after a delay when the mouse is inside // Tracking (only in the overview) bool isTracking_ = false; bool movedSufficiently_ = false; // a movement threshold is required before dragging begins int initialMouseX = 0; // the initial x for the movement threshold QRect contentRectForTrackedChromosome_; slim_position_t trackingStartBase_ = 0, trackingLastBase_ = 0; int trackingXAdjust_ = 0; // to keep the cursor stuck on a knob that is click-dragged bool simpleClickInFocalChromosome_ = false; // used to keep track of whether we could deselect on mouse-up //SLiMSelectionMarker *startMarker, *endMarker; public: explicit QtSLiMChromosomeWidget(QWidget *p_parent = nullptr, QtSLiMChromosomeWidgetController *controller = nullptr, Species *displaySpecies = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); virtual ~QtSLiMChromosomeWidget() override; void setController(QtSLiMChromosomeWidgetController *controller); void setFocalDisplaySpecies(Species *displaySpecies); Species *focalDisplaySpecies(void); void setFocalChromosome(Chromosome *chromosome); Chromosome *focalChromosome(void); void setDependentChromosomeView(QtSLiMChromosomeWidget *p_dependent_widget); bool hasSelection(void) { return hasSelection_; } QtSLiMRange getSelectedRange(Chromosome *chromosome); void setSelectedRange(QtSLiMRange p_selectionRange); void restoreLastSelection(void); void updateDependentView(void); QtSLiMRange getDisplayedRange(Chromosome *chromosome); void setDisplayedRange(QtSLiMRange p_displayedRange); bool showsTicks(void) { return showsTicks_; } void setShowsTicks(bool p_showTicks); void stateChanged(void); // update when the SLiM model state changes; tosses any cached display info void updateAfterTick(void); protected: #ifndef SLIM_NO_OPENGL virtual void initializeGL() override; virtual void resizeGL(int w, int h) override; virtual void paintGL() override; #else virtual void paintEvent(QPaintEvent *event) override; #endif QRect rectEncompassingBaseToBase(slim_position_t startBase, slim_position_t endBase, QRect interiorRect, QtSLiMRange displayedRange); slim_position_t baseForPosition(double position, QRect interiorRect, QtSLiMRange displayedRange); QRect getContentRect(void); void drawOverview(Species *displaySpecies, QPainter &painter); void drawFullGenome(Species *displaySpecies, QPainter &painter); void drawTicksInContentRect(QRect contentRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); void overlaySelection(QRect interiorRect, QtSLiMRange displayedRange, bool drawInteriorShadow, QPainter &painter); void updateDisplayedMutationTypes(Species *displaySpecies); // OpenGL drawing; this is the primary drawing code #ifndef SLIM_NO_OPENGL void glDrawRect(QRect contentRect, Species *displaySpecies, Chromosome *chromosome); void glDrawGenomicElements(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange); void glDrawFixedSubstitutions(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange); void glDrawMutations(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange); void _glDrawRateMapIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue); void glDrawRecombinationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange); void glDrawMutationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange); void glDrawRateMaps(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange); #endif // Qt-based drawing, provided as a backup if OpenGL has problems on a given platform void qtDrawRect(QRect contentRect, Species *displaySpecies, Chromosome *chromosome, QPainter &painter); void qtDrawGenomicElements(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter); void qtDrawFixedSubstitutions(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter); void qtDrawMutations(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter); void _qtDrawRateMapIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue, QPainter &painter); void qtDrawRecombinationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter); void qtDrawMutationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter); void qtDrawRateMaps(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter); Chromosome *_findFocalChromosomeForTracking(QMouseEvent *p_event); virtual void mousePressEvent(QMouseEvent *p_event) override; void _mouseTrackEvent(QMouseEvent *p_event); virtual void mouseMoveEvent(QMouseEvent *p_event) override; virtual void mouseReleaseEvent(QMouseEvent *p_event) override; virtual void contextMenuEvent(QContextMenuEvent *p_event) override; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #define QTSLIM_ENTER_EVENT QEvent #else #define QTSLIM_ENTER_EVENT QEnterEvent #endif virtual void enterEvent(QTSLIM_ENTER_EVENT *event) override; virtual void leaveEvent(QEvent *event) override; // Our configuration is kept by the controller, since it is shared by all chromosome views for multispecies models // However, "overview" chromosome views are always configured the same, hard-coded here inline bool shouldDrawMutations(void) const { return isOverview_ ? false : controller_->shouldDrawMutations_; } inline bool shouldDrawFixedSubstitutions(void) const { return isOverview_ ? false : controller_->shouldDrawFixedSubstitutions_; } inline bool shouldDrawGenomicElements(void) const { return isOverview_ ? true : controller_->shouldDrawGenomicElements_; } inline bool shouldDrawRateMaps(void) const { return isOverview_ ? false : controller_->shouldDrawRateMaps_; } inline bool displayHaplotypes(void) const { return isOverview_ ? false : controller_->displayHaplotypes_; } inline std::vector &displayMuttypes(void) const { return controller_->displayMuttypes_; } }; #endif // QTSLIMCHROMOSOMEWIDGET_H ================================================ FILE: QtSLiM/QtSLiMChromosomeWidget_GL.cpp ================================================ // // QtSLiMChromosomeWidget_GL.cpp // SLiM // // Created by Ben Haller on 8/25/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef SLIM_NO_OPENGL #include "QtSLiMChromosomeWidget.h" #include "QtSLiMHaplotypeManager.h" #include "QtSLiMOpenGL.h" #include #include #include #include // // OpenGL-based drawing; maintain this in parallel with the Qt-based drawing! // void QtSLiMChromosomeWidget::glDrawRect(QRect contentRect, Species *displaySpecies, Chromosome *chromosome) { bool ready = isEnabled() && !controller_->invalidSimulation(); QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); // if the simulation is at tick 0, it is not ready if (ready) if (controller_->community()->Tick() == 0) ready = false; if (ready) { // erase the content area itself glColor3f(0.0f, 0.0f, 0.0f); glRecti(interiorRect.left(), interiorRect.top(), interiorRect.left() + interiorRect.width(), interiorRect.top() + interiorRect.height()); QtSLiMRange displayedRange = getDisplayedRange(chromosome); bool splitHeight = (shouldDrawRateMaps() && shouldDrawGenomicElements()); QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); // draw recombination intervals in interior if (shouldDrawRateMaps()) glDrawRateMaps(splitHeight ? topInteriorRect : interiorRect, chromosome, displayedRange); // draw genomic elements in interior if (shouldDrawGenomicElements()) glDrawGenomicElements(splitHeight ? bottomInteriorRect : interiorRect, chromosome, displayedRange); // figure out which mutation types we're displaying if (shouldDrawFixedSubstitutions() || shouldDrawMutations()) updateDisplayedMutationTypes(displaySpecies); // draw fixed substitutions in interior if (shouldDrawFixedSubstitutions()) glDrawFixedSubstitutions(interiorRect, chromosome, displayedRange); // draw mutations in interior if (shouldDrawMutations()) { if (displayHaplotypes()) { // display mutations as a haplotype plot, courtesy of QtSLiMHaplotypeManager; we use ClusterNearestNeighbor and // ClusterNoOptimization because they're fast, and NN might also provide a bit more run-to-run continuity size_t interiorHeight = static_cast(interiorRect.height()); // one sample per available pixel line, for simplicity and speed; 47, in the current UI layout QtSLiMHaplotypeManager *haplotype_mgr = new QtSLiMHaplotypeManager(nullptr, QtSLiMHaplotypeManager::ClusterNearestNeighbor, QtSLiMHaplotypeManager::ClusterNoOptimization, controller_, displaySpecies, chromosome, displayedRange, interiorHeight, false, 0, 0); if (haplotype_mgr) haplotype_mgr->glDrawHaplotypes(interiorRect, false, false, false); delete haplotype_mgr; } else { // display mutations as a frequency plot; this is the standard display mode glDrawMutations(interiorRect, chromosome, displayedRange); } } } else { // erase the content area itself glColor3f(0.88f, 0.88f, 0.88f); glRecti(0, 0, interiorRect.width(), interiorRect.height()); } } void QtSLiMChromosomeWidget::glDrawGenomicElements(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange) { int previousIntervalLeftEdge = -10000; SLIM_GL_PREPARE(); for (GenomicElement *genomicElement : chromosome->GenomicElements()) { slim_position_t startPosition = genomicElement->start_position_; slim_position_t endPosition = genomicElement->end_position_; QRect elementRect = rectEncompassingBaseToBase(startPosition, endPosition, interiorRect, displayedRange); bool widthOne = (elementRect.width() == 1); // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. if (!widthOne && (elementRect.left() == previousIntervalLeftEdge)) elementRect.adjust(1, 0, 0, 0); // draw only the visible part, if any elementRect = elementRect.intersected(interiorRect); if (!elementRect.isEmpty()) { GenomicElementType *geType = genomicElement->genomic_element_type_ptr_; float colorRed, colorGreen, colorBlue, colorAlpha; if (!geType->color_.empty()) { colorRed = geType->color_red_; colorGreen = geType->color_green_; colorBlue = geType->color_blue_; colorAlpha = 1.0; } else { slim_objectid_t elementTypeID = geType->genomic_element_type_id_; controller_->colorForGenomicElementType(geType, elementTypeID, &colorRed, &colorGreen, &colorBlue, &colorAlpha); } SLIM_GL_DEFCOORDS(elementRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location if (widthOne) previousIntervalLeftEdge = elementRect.left(); else previousIntervalLeftEdge = -10000; } } SLIM_GL_FINISH(); } void QtSLiMChromosomeWidget::glDrawMutations(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange) { double scalingFactor = 0.8; // used to be controller->selectionColorScale; Species *displaySpecies = &chromosome->species_; Population &pop = displaySpecies->population_; double totalHaplosomeCount = chromosome->gui_total_haplosome_count_; // this includes only haplosomes in the selected subpopulations // Prefetch the mutations we actually want to display static std::vector mutations; mutations.resize(0); { int registry_size; const MutationIndex *registry = pop.MutationRegistry(®istry_size); Mutation *mut_block_ptr = gSLiM_Mutation_Block; slim_chromosome_index_t chromosome_index = chromosome->Index(); for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; if (mutation->chromosome_index_ == chromosome_index) { const MutationType *mutType = mutation->mutation_type_ptr_; if (mutType->mutation_type_displayed_) mutations.emplace_back(mutation); } } } // Set up to draw rects float colorRed = 0.0f, colorGreen = 0.0f, colorBlue = 0.0f, colorAlpha = 1.0; SLIM_GL_PREPARE(); if ((mutations.size() < 1000) || (displayedRange.length < interiorRect.width())) { // This is the simple version of the display code, avoiding the memory allocations and such for (const Mutation *mutation : mutations) { const MutationType *mutType = mutation->mutation_type_ptr_; slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); if (!mutType->color_.empty()) { colorRed = mutType->color_red_; colorGreen = mutType->color_green_; colorBlue = mutType->color_blue_; } else { RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); } int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); mutationTickRect.setTop(mutationTickRect.top() + height_adjust); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } else { // We have a lot of mutations, so let's try to be smarter. It's hard to be smarter. The overhead from allocating the NSColors and such // is pretty negligible; practially all the time is spent in NSRectFill(). Unfortunately, NSRectFillListWithColors() provides basically // no speedup; Apple doesn't appear to have optimized it. So, here's what I came up with. For each mutation type that uses a fixed DFE, // and thus a fixed color, we can do a radix sort of mutations into bins corresponding to each pixel in our displayed image. Then we // can draw each bin just once, making one bar for the highest bar in that bin. Mutations from non-fixed DFEs, and mutations which have // had their selection coefficient changed, will be drawn at the end in the usual (slow) way. int displayPixelWidth = interiorRect.width(); int16_t *heightBuffer = static_cast(malloc(static_cast(displayPixelWidth) * sizeof(int16_t))); bool *mutationsPlotted = static_cast(calloc(mutations.size(), sizeof(bool))); // faster than using gui_scratch_reference_count_ because of cache locality int64_t remainingMutations = mutations.size(); // First zero out the scratch refcount, which we use to track which mutations we have drawn already //for (int mutIndex = 0; mutIndex < mutationCount; ++mutIndex) // mutations[mutIndex]->gui_scratch_reference_count_ = 0; // Then loop through the declared mutation types std::map &mut_types = displaySpecies->mutation_types_; bool draw_muttypes_sequentially = (mut_types.size() <= 20); // with a lot of mutation types, the algorithm below becomes very inefficient for (auto mutationTypeIter : mut_types) { MutationType *mut_type = mutationTypeIter.second; if (mut_type->mutation_type_displayed_) { if (draw_muttypes_sequentially) { bool mut_type_fixed_color = !mut_type->color_.empty(); // We optimize fixed-DFE mutation types only, and those using a fixed color set by the user if ((mut_type->dfe_type_ == DFEType::kFixed) || mut_type_fixed_color) { slim_selcoeff_t mut_type_selcoeff = (mut_type_fixed_color ? 0.0 : static_cast(mut_type->dfe_parameters_[0])); EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); // Scan through the mutation list for mutations of this type with the right selcoeff for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { const Mutation *mutation = mutations[mutation_index]; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wfloat-equal" // We do want to do an exact floating-point equality compare here; we want to see whether the mutation's selcoeff is unmodified from the fixed DFE if ((mutation->mutation_type_ptr_ == mut_type) && (mut_type_fixed_color || (mutation->selection_coeff_ == mut_type_selcoeff))) #pragma clang diagnostic pop #pragma GCC diagnostic pop { slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // includes only refs from the selected subpopulations slim_position_t mutationPosition = mutation->position_; //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); int16_t barHeight = static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); if ((xPos >= 0) && (xPos < displayPixelWidth)) if (barHeight > heightBuffer[xPos]) heightBuffer[xPos] = barHeight; // tally this mutation as handled //mutation->gui_scratch_reference_count_ = 1; mutationsPlotted[mutation_index] = true; --remainingMutations; } } // Now draw all of the mutations we found, by looping through our radix bins if (mut_type_fixed_color) { colorRed = mut_type->color_red_; colorGreen = mut_type->color_green_; colorBlue = mut_type->color_blue_; } else { RGBForSelectionCoeff(static_cast(mut_type_selcoeff), &colorRed, &colorGreen, &colorBlue, scalingFactor); } for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { int barHeight = heightBuffer[binIndex]; if (barHeight) { QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } } } else { // We're not displaying this mutation type, so we need to mark off all the mutations belonging to it as handled for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { const Mutation *mutation = mutations[mutation_index]; if (mutation->mutation_type_ptr_ == mut_type) { // tally this mutation as handled //mutation->gui_scratch_reference_count_ = 1; mutationsPlotted[mutation_index] = true; --remainingMutations; } } } } // Draw any undrawn mutations on top; these are guaranteed not to use a fixed color set by the user, since those are all handled above if (remainingMutations) { if (remainingMutations < 1000) { // Plot the remainder by brute force, since there are not that many for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { //if (mutation->gui_scratch_reference_count_ == 0) if (!mutationsPlotted[mutation_index]) { const Mutation *mutation = mutations[mutation_index]; slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); mutationTickRect.setTop(mutationTickRect.top() + height_adjust); RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else { // OK, we have a lot of mutations left to draw. Here we will again use the radix sort trick, to keep track of only the tallest bar in each column const Mutation **mutationBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(Mutation *))); EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); // Find the tallest bar in each column for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { //if (mutation->gui_scratch_reference_count_ == 0) if (!mutationsPlotted[mutation_index]) { const Mutation *mutation = mutations[mutation_index]; slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); int16_t barHeight = static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); if ((xPos >= 0) && (xPos < displayPixelWidth)) { if (barHeight > heightBuffer[xPos]) { heightBuffer[xPos] = barHeight; mutationBuffer[xPos] = mutation; } } } } // Now plot the bars for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { int barHeight = heightBuffer[binIndex]; if (barHeight) { QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); const Mutation *mutation = mutationBuffer[binIndex]; RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } free(mutationBuffer); } } free(heightBuffer); free(mutationsPlotted); } SLIM_GL_FINISH(); mutations.resize(0); } void QtSLiMChromosomeWidget::glDrawFixedSubstitutions(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange) { double scalingFactor = 0.8; // used to be controller->selectionColorScale; Species *displaySpecies = &chromosome->species_; Population &pop = displaySpecies->population_; bool chromosomeHasDefaultColor = !chromosome->color_sub_.empty(); std::vector &substitutions = pop.substitutions_; slim_chromosome_index_t chromosome_index = chromosome->Index(); // Set up to draw rects float colorRed = 0.2f, colorGreen = 0.2f, colorBlue = 1.0f, colorAlpha = 1.0; if (chromosomeHasDefaultColor) { colorRed = chromosome->color_sub_red_; colorGreen = chromosome->color_sub_green_; colorBlue = chromosome->color_sub_blue_; } SLIM_GL_PREPARE(); if ((substitutions.size() < 1000) || (displayedRange.length < interiorRect.width())) { // This is the simple version of the display code, avoiding the memory allocations and such for (const Substitution *substitution : substitutions) { if ((substitution->chromosome_index_ == chromosome_index) && (substitution->mutation_type_ptr_->mutation_type_displayed_)) { slim_position_t substitutionPosition = substitution->position_; QRect substitutionTickRect = rectEncompassingBaseToBase(substitutionPosition, substitutionPosition, interiorRect, displayedRange); if (!shouldDrawMutations() || !chromosomeHasDefaultColor) { // If we're drawing mutations as well, then substitutions just get colored blue (set above), to contrast // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations const MutationType *mutType = substitution->mutation_type_ptr_; if (!mutType->color_sub_.empty()) { colorRed = mutType->color_sub_red_; colorGreen = mutType->color_sub_green_; colorBlue = mutType->color_sub_blue_; } else { RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); } } SLIM_GL_DEFCOORDS(substitutionTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else { // We have a lot of substitutions, so do a radix sort, as we do in drawMutationsInInteriorRect: below. int displayPixelWidth = interiorRect.width(); const Substitution **subBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(Substitution *))); for (const Substitution *substitution : substitutions) { if ((substitution->chromosome_index_ == chromosome_index) && (substitution->mutation_type_ptr_->mutation_type_displayed_)) { slim_position_t substitutionPosition = substitution->position_; double startFraction = (substitutionPosition - static_cast(displayedRange.location)) / static_cast(displayedRange.length); int xPos = static_cast(floor(startFraction * interiorRect.width())); if ((xPos >= 0) && (xPos < displayPixelWidth)) subBuffer[xPos] = substitution; } } if (shouldDrawMutations() && chromosomeHasDefaultColor) { // If we're drawing mutations as well, then substitutions just get colored blue, to contrast QRect mutationTickRect = interiorRect; for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { const Substitution *substitution = subBuffer[binIndex]; if (substitution) { mutationTickRect.setX(interiorRect.x() + binIndex); mutationTickRect.setWidth(1); // consolidate adjacent lines together, since they are all the same color while ((binIndex + 1 < displayPixelWidth) && subBuffer[binIndex + 1]) { mutationTickRect.setWidth(mutationTickRect.width() + 1); binIndex++; } SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else { // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations QRect mutationTickRect = interiorRect; for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { const Substitution *substitution = subBuffer[binIndex]; if (substitution) { const MutationType *mutType = substitution->mutation_type_ptr_; if (!mutType->color_sub_.empty()) { colorRed = mutType->color_sub_red_; colorGreen = mutType->color_sub_green_; colorBlue = mutType->color_sub_blue_; } else { RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); } mutationTickRect.setX(interiorRect.x() + binIndex); mutationTickRect.setWidth(1); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } free(subBuffer); } SLIM_GL_FINISH(); } void QtSLiMChromosomeWidget::_glDrawRateMapIntervals(QRect &interiorRect, __attribute__((__unused__)) Chromosome *chromosome, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue) { size_t recombinationIntervalCount = ends.size(); slim_position_t intervalStartPosition = 0; int previousIntervalLeftEdge = -10000; SLIM_GL_PREPARE(); for (size_t interval = 0; interval < recombinationIntervalCount; ++interval) { slim_position_t intervalEndPosition = ends[interval]; double intervalRate = rates[interval]; QRect intervalRect = rectEncompassingBaseToBase(intervalStartPosition, intervalEndPosition, interiorRect, displayedRange); bool widthOne = (intervalRect.width() == 1); // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. if (!widthOne && (intervalRect.left() == previousIntervalLeftEdge)) intervalRect.adjust(1, 0, 0, 0); // draw only the visible part, if any intervalRect = intervalRect.intersected(interiorRect); if (!intervalRect.isEmpty()) { // color according to how "hot" the region is float colorRed, colorGreen, colorBlue, colorAlpha; if (intervalRate == 0.0) { // a recombination or mutation rate of 0.0 comes out as black, whereas the lowest brightness below is 0.5; we want to distinguish this colorRed = colorGreen = colorBlue = 0.0; colorAlpha = 1.0; } else { // the formula below scales 1e-6 to 1.0 and 1e-9 to 0.0; values outside that range clip, but we want there to be // reasonable contrast within the range of values commonly used, so we don't want to make the range too wide double lightness, brightness = 1.0, saturation = 1.0; lightness = (log10(intervalRate) + 9.0) / 3.0; lightness = std::max(lightness, 0.0); lightness = std::min(lightness, 1.0); if (lightness >= 0.5) saturation = 1.0 - ((lightness - 0.5) * 2.0); // goes from 1.0 at lightness 0.5 to 0.0 at lightness 1.0 else brightness = 0.5 + lightness; // goes from 1.0 at lightness 0.5 to 0.5 at lightness 0.0 QColor intervalColor = QtSLiMColorWithHSV(hue, saturation, brightness, 1.0); #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) // In Qt5, getRgbF() expects pointers to qreal, which is double double r, g, b, a; intervalColor.getRgbF(&r, &g, &b, &a); colorRed = static_cast(r); colorGreen = static_cast(g); colorBlue = static_cast(b); colorAlpha = static_cast(a); #else // In Qt6, getRgbF() expects pointers to float intervalColor.getRgbF(&colorRed, &colorGreen, &colorBlue, &colorAlpha); #endif } SLIM_GL_DEFCOORDS(intervalRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location if (widthOne) previousIntervalLeftEdge = intervalRect.left(); else previousIntervalLeftEdge = -10000; } // the next interval starts at the next base after this one ended intervalStartPosition = intervalEndPosition + 1; } SLIM_GL_FINISH(); } void QtSLiMChromosomeWidget::glDrawRecombinationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange) { if (chromosome->single_recombination_map_) { _glDrawRateMapIntervals(interiorRect, chromosome, displayedRange, chromosome->recombination_end_positions_H_, chromosome->recombination_rates_H_, 0.65); } else { QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); _glDrawRateMapIntervals(topInteriorRect, chromosome, displayedRange, chromosome->recombination_end_positions_M_, chromosome->recombination_rates_M_, 0.65); _glDrawRateMapIntervals(bottomInteriorRect, chromosome, displayedRange, chromosome->recombination_end_positions_F_, chromosome->recombination_rates_F_, 0.65); } } void QtSLiMChromosomeWidget::glDrawMutationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange) { if (chromosome->single_mutation_map_) { _glDrawRateMapIntervals(interiorRect, chromosome, displayedRange, chromosome->mutation_end_positions_H_, chromosome->mutation_rates_H_, 0.75); } else { QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); _glDrawRateMapIntervals(topInteriorRect, chromosome, displayedRange, chromosome->mutation_end_positions_M_, chromosome->mutation_rates_M_, 0.75); _glDrawRateMapIntervals(bottomInteriorRect, chromosome, displayedRange, chromosome->mutation_end_positions_F_, chromosome->mutation_rates_F_, 0.75); } } void QtSLiMChromosomeWidget::glDrawRateMaps(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange) { bool recombinationWorthShowing = false; bool mutationWorthShowing = false; if (chromosome->single_mutation_map_) mutationWorthShowing = (chromosome->mutation_end_positions_H_.size() > 1); else mutationWorthShowing = ((chromosome->mutation_end_positions_M_.size() > 1) || (chromosome->mutation_end_positions_F_.size() > 1)); if (chromosome->single_recombination_map_) recombinationWorthShowing = (chromosome->recombination_end_positions_H_.size() > 1); else recombinationWorthShowing = ((chromosome->recombination_end_positions_M_.size() > 1) || (chromosome->recombination_end_positions_F_.size() > 1)); // If neither map is worth showing, we show just the recombination map, to mirror the behavior of 2.4 and earlier if ((!mutationWorthShowing && !recombinationWorthShowing) || (!mutationWorthShowing && recombinationWorthShowing)) { glDrawRecombinationIntervals(interiorRect, chromosome, displayedRange); } else if (mutationWorthShowing && !recombinationWorthShowing) { glDrawMutationIntervals(interiorRect, chromosome, displayedRange); } else // mutationWorthShowing && recombinationWorthShowing { QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); glDrawRecombinationIntervals(topInteriorRect, chromosome, displayedRange); glDrawMutationIntervals(bottomInteriorRect, chromosome, displayedRange); } } #endif ================================================ FILE: QtSLiM/QtSLiMChromosomeWidget_QT.cpp ================================================ // // QtSLiMChromosomeWidget_QT.cpp // SLiM // // Created by Ben Haller on 8/25/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMChromosomeWidget.h" #include "QtSLiMHaplotypeManager.h" #include "QtSLiMOpenGL_Emulation.h" #include #include #include #include // // OpenGL-based drawing; maintain this in parallel with the Qt-based drawing! // void QtSLiMChromosomeWidget::qtDrawRect(QRect contentRect, Species *displaySpecies, Chromosome *chromosome, QPainter &painter) { bool ready = isEnabled() && !controller_->invalidSimulation(); QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1)); // if the simulation is at tick 0, it is not ready if (ready) if (controller_->community()->Tick() == 0) ready = false; if (ready) { // erase the content area itself painter.fillRect(interiorRect, Qt::black); QtSLiMRange displayedRange = getDisplayedRange(chromosome); bool splitHeight = (shouldDrawRateMaps() && shouldDrawGenomicElements()); QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); // draw recombination intervals in interior if (shouldDrawRateMaps()) qtDrawRateMaps(splitHeight ? topInteriorRect : interiorRect, chromosome, displayedRange, painter); // draw genomic elements in interior if (shouldDrawGenomicElements()) qtDrawGenomicElements(splitHeight ? bottomInteriorRect : interiorRect, chromosome, displayedRange, painter); // figure out which mutation types we're displaying if (shouldDrawFixedSubstitutions() || shouldDrawMutations()) updateDisplayedMutationTypes(displaySpecies); // draw fixed substitutions in interior if (shouldDrawFixedSubstitutions()) qtDrawFixedSubstitutions(interiorRect, chromosome, displayedRange, painter); // draw mutations in interior if (shouldDrawMutations()) { if (displayHaplotypes()) { // display mutations as a haplotype plot, courtesy of QtSLiMHaplotypeManager; we use ClusterNearestNeighbor and // ClusterNoOptimization because they're fast, and NN might also provide a bit more run-to-run continuity size_t interiorHeight = static_cast(interiorRect.height()); // one sample per available pixel line, for simplicity and speed; 47, in the current UI layout QtSLiMHaplotypeManager *haplotype_mgr = new QtSLiMHaplotypeManager(nullptr, QtSLiMHaplotypeManager::ClusterNearestNeighbor, QtSLiMHaplotypeManager::ClusterNoOptimization, controller_, displaySpecies, chromosome, displayedRange, interiorHeight, false, 0, 0); if (haplotype_mgr) haplotype_mgr->qtDrawHaplotypes(interiorRect, false, false, false, painter); delete haplotype_mgr; } else { // display mutations as a frequency plot; this is the standard display mode qtDrawMutations(interiorRect, chromosome, displayedRange, painter); } } } else { // erase the content area itself painter.fillRect(QRect(0, 0, interiorRect.width(), interiorRect.height()), QtSLiMColorWithWhite(0.88, 1.0)); } } void QtSLiMChromosomeWidget::qtDrawGenomicElements(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter) { int previousIntervalLeftEdge = -10000; SLIM_GL_PREPARE(); for (GenomicElement *genomicElement : chromosome->GenomicElements()) { slim_position_t startPosition = genomicElement->start_position_; slim_position_t endPosition = genomicElement->end_position_; QRect elementRect = rectEncompassingBaseToBase(startPosition, endPosition, interiorRect, displayedRange); bool widthOne = (elementRect.width() == 1); // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. if (!widthOne && (elementRect.left() == previousIntervalLeftEdge)) elementRect.adjust(1, 0, 0, 0); // draw only the visible part, if any elementRect = elementRect.intersected(interiorRect); if (!elementRect.isEmpty()) { GenomicElementType *geType = genomicElement->genomic_element_type_ptr_; float colorRed, colorGreen, colorBlue, colorAlpha; if (!geType->color_.empty()) { colorRed = geType->color_red_; colorGreen = geType->color_green_; colorBlue = geType->color_blue_; colorAlpha = 1.0; } else { slim_objectid_t elementTypeID = geType->genomic_element_type_id_; controller_->colorForGenomicElementType(geType, elementTypeID, &colorRed, &colorGreen, &colorBlue, &colorAlpha); } SLIM_GL_DEFCOORDS(elementRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location if (widthOne) previousIntervalLeftEdge = elementRect.left(); else previousIntervalLeftEdge = -10000; } } SLIM_GL_FINISH(); } void QtSLiMChromosomeWidget::qtDrawMutations(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter) { double scalingFactor = 0.8; // used to be controller->selectionColorScale; Species *displaySpecies = &chromosome->species_; Population &pop = displaySpecies->population_; double totalHaplosomeCount = chromosome->gui_total_haplosome_count_; // this includes only haplosomes in the selected subpopulations // Prefetch the mutations we actually want to display static std::vector mutations; mutations.resize(0); { int registry_size; const MutationIndex *registry = pop.MutationRegistry(®istry_size); Mutation *mut_block_ptr = gSLiM_Mutation_Block; slim_chromosome_index_t chromosome_index = chromosome->Index(); for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; if (mutation->chromosome_index_ == chromosome_index) { const MutationType *mutType = mutation->mutation_type_ptr_; if (mutType->mutation_type_displayed_) mutations.emplace_back(mutation); } } } // Set up to draw rects float colorRed = 0.0f, colorGreen = 0.0f, colorBlue = 0.0f, colorAlpha = 1.0; SLIM_GL_PREPARE(); if ((mutations.size() < 1000) || (displayedRange.length < interiorRect.width())) { // This is the simple version of the display code, avoiding the memory allocations and such for (const Mutation *mutation : mutations) { const MutationType *mutType = mutation->mutation_type_ptr_; slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); if (!mutType->color_.empty()) { colorRed = mutType->color_red_; colorGreen = mutType->color_green_; colorBlue = mutType->color_blue_; } else { RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); } int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); mutationTickRect.setTop(mutationTickRect.top() + height_adjust); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } else { // We have a lot of mutations, so let's try to be smarter. It's hard to be smarter. The overhead from allocating the NSColors and such // is pretty negligible; practially all the time is spent in NSRectFill(). Unfortunately, NSRectFillListWithColors() provides basically // no speedup; Apple doesn't appear to have optimized it. So, here's what I came up with. For each mutation type that uses a fixed DFE, // and thus a fixed color, we can do a radix sort of mutations into bins corresponding to each pixel in our displayed image. Then we // can draw each bin just once, making one bar for the highest bar in that bin. Mutations from non-fixed DFEs, and mutations which have // had their selection coefficient changed, will be drawn at the end in the usual (slow) way. int displayPixelWidth = interiorRect.width(); int16_t *heightBuffer = static_cast(malloc(static_cast(displayPixelWidth) * sizeof(int16_t))); bool *mutationsPlotted = static_cast(calloc(mutations.size(), sizeof(bool))); // faster than using gui_scratch_reference_count_ because of cache locality int64_t remainingMutations = mutations.size(); // First zero out the scratch refcount, which we use to track which mutations we have drawn already //for (int mutIndex = 0; mutIndex < mutationCount; ++mutIndex) // mutations[mutIndex]->gui_scratch_reference_count_ = 0; // Then loop through the declared mutation types std::map &mut_types = displaySpecies->mutation_types_; bool draw_muttypes_sequentially = (mut_types.size() <= 20); // with a lot of mutation types, the algorithm below becomes very inefficient for (auto mutationTypeIter : mut_types) { MutationType *mut_type = mutationTypeIter.second; if (mut_type->mutation_type_displayed_) { if (draw_muttypes_sequentially) { bool mut_type_fixed_color = !mut_type->color_.empty(); // We optimize fixed-DFE mutation types only, and those using a fixed color set by the user if ((mut_type->dfe_type_ == DFEType::kFixed) || mut_type_fixed_color) { slim_selcoeff_t mut_type_selcoeff = (mut_type_fixed_color ? 0.0 : static_cast(mut_type->dfe_parameters_[0])); EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); // Scan through the mutation list for mutations of this type with the right selcoeff for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { const Mutation *mutation = mutations[mutation_index]; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wfloat-equal" // We do want to do an exact floating-point equality compare here; we want to see whether the mutation's selcoeff is unmodified from the fixed DFE if ((mutation->mutation_type_ptr_ == mut_type) && (mut_type_fixed_color || (mutation->selection_coeff_ == mut_type_selcoeff))) #pragma clang diagnostic pop #pragma GCC diagnostic pop { slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // includes only refs from the selected subpopulations slim_position_t mutationPosition = mutation->position_; //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); int16_t barHeight = static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); if ((xPos >= 0) && (xPos < displayPixelWidth)) if (barHeight > heightBuffer[xPos]) heightBuffer[xPos] = barHeight; // tally this mutation as handled //mutation->gui_scratch_reference_count_ = 1; mutationsPlotted[mutation_index] = true; --remainingMutations; } } // Now draw all of the mutations we found, by looping through our radix bins if (mut_type_fixed_color) { colorRed = mut_type->color_red_; colorGreen = mut_type->color_green_; colorBlue = mut_type->color_blue_; } else { RGBForSelectionCoeff(static_cast(mut_type_selcoeff), &colorRed, &colorGreen, &colorBlue, scalingFactor); } for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { int barHeight = heightBuffer[binIndex]; if (barHeight) { QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } } } else { // We're not displaying this mutation type, so we need to mark off all the mutations belonging to it as handled for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { const Mutation *mutation = mutations[mutation_index]; if (mutation->mutation_type_ptr_ == mut_type) { // tally this mutation as handled //mutation->gui_scratch_reference_count_ = 1; mutationsPlotted[mutation_index] = true; --remainingMutations; } } } } // Draw any undrawn mutations on top; these are guaranteed not to use a fixed color set by the user, since those are all handled above if (remainingMutations) { if (remainingMutations < 1000) { // Plot the remainder by brute force, since there are not that many for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { //if (mutation->gui_scratch_reference_count_ == 0) if (!mutationsPlotted[mutation_index]) { const Mutation *mutation = mutations[mutation_index]; slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); mutationTickRect.setTop(mutationTickRect.top() + height_adjust); RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else { // OK, we have a lot of mutations left to draw. Here we will again use the radix sort trick, to keep track of only the tallest bar in each column const Mutation **mutationBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(Mutation *))); EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); // Find the tallest bar in each column for (int mutation_index = 0; mutation_index < (int)mutations.size(); ++mutation_index) { //if (mutation->gui_scratch_reference_count_ == 0) if (!mutationsPlotted[mutation_index]) { const Mutation *mutation = mutations[mutation_index]; slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); int16_t barHeight = static_cast(ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.height())); if ((xPos >= 0) && (xPos < displayPixelWidth)) { if (barHeight > heightBuffer[xPos]) { heightBuffer[xPos] = barHeight; mutationBuffer[xPos] = mutation; } } } } // Now plot the bars for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { int barHeight = heightBuffer[binIndex]; if (barHeight) { QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); const Mutation *mutation = mutationBuffer[binIndex]; RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } free(mutationBuffer); } } free(heightBuffer); free(mutationsPlotted); } SLIM_GL_FINISH(); mutations.resize(0); } void QtSLiMChromosomeWidget::qtDrawFixedSubstitutions(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter) { double scalingFactor = 0.8; // used to be controller->selectionColorScale; Species *displaySpecies = &chromosome->species_; Population &pop = displaySpecies->population_; bool chromosomeHasDefaultColor = !chromosome->color_sub_.empty(); std::vector &substitutions = pop.substitutions_; slim_chromosome_index_t chromosome_index = chromosome->Index(); // Set up to draw rects float colorRed = 0.2f, colorGreen = 0.2f, colorBlue = 1.0f, colorAlpha = 1.0; if (chromosomeHasDefaultColor) { colorRed = chromosome->color_sub_red_; colorGreen = chromosome->color_sub_green_; colorBlue = chromosome->color_sub_blue_; } SLIM_GL_PREPARE(); if ((substitutions.size() < 1000) || (displayedRange.length < interiorRect.width())) { // This is the simple version of the display code, avoiding the memory allocations and such for (const Substitution *substitution : substitutions) { if ((substitution->chromosome_index_ == chromosome_index) && (substitution->mutation_type_ptr_->mutation_type_displayed_)) { slim_position_t substitutionPosition = substitution->position_; QRect substitutionTickRect = rectEncompassingBaseToBase(substitutionPosition, substitutionPosition, interiorRect, displayedRange); if (!shouldDrawMutations() || !chromosomeHasDefaultColor) { // If we're drawing mutations as well, then substitutions just get colored blue (set above), to contrast // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations const MutationType *mutType = substitution->mutation_type_ptr_; if (!mutType->color_sub_.empty()) { colorRed = mutType->color_sub_red_; colorGreen = mutType->color_sub_green_; colorBlue = mutType->color_sub_blue_; } else { RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); } } SLIM_GL_DEFCOORDS(substitutionTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else { // We have a lot of substitutions, so do a radix sort, as we do in drawMutationsInInteriorRect: below. int displayPixelWidth = interiorRect.width(); const Substitution **subBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(Substitution *))); for (const Substitution *substitution : substitutions) { if ((substitution->chromosome_index_ == chromosome_index) && (substitution->mutation_type_ptr_->mutation_type_displayed_)) { slim_position_t substitutionPosition = substitution->position_; double startFraction = (substitutionPosition - static_cast(displayedRange.location)) / static_cast(displayedRange.length); int xPos = static_cast(floor(startFraction * interiorRect.width())); if ((xPos >= 0) && (xPos < displayPixelWidth)) subBuffer[xPos] = substitution; } } if (shouldDrawMutations() && chromosomeHasDefaultColor) { // If we're drawing mutations as well, then substitutions just get colored blue, to contrast QRect mutationTickRect = interiorRect; for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { const Substitution *substitution = subBuffer[binIndex]; if (substitution) { mutationTickRect.setX(interiorRect.x() + binIndex); mutationTickRect.setWidth(1); // consolidate adjacent lines together, since they are all the same color while ((binIndex + 1 < displayPixelWidth) && subBuffer[binIndex + 1]) { mutationTickRect.setWidth(mutationTickRect.width() + 1); binIndex++; } SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else { // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations QRect mutationTickRect = interiorRect; for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { const Substitution *substitution = subBuffer[binIndex]; if (substitution) { const MutationType *mutType = substitution->mutation_type_ptr_; if (!mutType->color_sub_.empty()) { colorRed = mutType->color_sub_red_; colorGreen = mutType->color_sub_green_; colorBlue = mutType->color_sub_blue_; } else { RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); } mutationTickRect.setX(interiorRect.x() + binIndex); mutationTickRect.setWidth(1); SLIM_GL_DEFCOORDS(mutationTickRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } free(subBuffer); } SLIM_GL_FINISH(); } void QtSLiMChromosomeWidget::_qtDrawRateMapIntervals(QRect &interiorRect, __attribute__((__unused__)) Chromosome *chromosome, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue, QPainter &painter) { size_t recombinationIntervalCount = ends.size(); slim_position_t intervalStartPosition = 0; int previousIntervalLeftEdge = -10000; SLIM_GL_PREPARE(); for (size_t interval = 0; interval < recombinationIntervalCount; ++interval) { slim_position_t intervalEndPosition = ends[interval]; double intervalRate = rates[interval]; QRect intervalRect = rectEncompassingBaseToBase(intervalStartPosition, intervalEndPosition, interiorRect, displayedRange); bool widthOne = (intervalRect.width() == 1); // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. if (!widthOne && (intervalRect.left() == previousIntervalLeftEdge)) intervalRect.adjust(1, 0, 0, 0); // draw only the visible part, if any intervalRect = intervalRect.intersected(interiorRect); if (!intervalRect.isEmpty()) { // color according to how "hot" the region is float colorRed, colorGreen, colorBlue, colorAlpha; if (intervalRate == 0.0) { // a recombination or mutation rate of 0.0 comes out as black, whereas the lowest brightness below is 0.5; we want to distinguish this colorRed = colorGreen = colorBlue = 0.0; colorAlpha = 1.0; } else { // the formula below scales 1e-6 to 1.0 and 1e-9 to 0.0; values outside that range clip, but we want there to be // reasonable contrast within the range of values commonly used, so we don't want to make the range too wide double lightness, brightness = 1.0, saturation = 1.0; lightness = (log10(intervalRate) + 9.0) / 3.0; lightness = std::max(lightness, 0.0); lightness = std::min(lightness, 1.0); if (lightness >= 0.5) saturation = 1.0 - ((lightness - 0.5) * 2.0); // goes from 1.0 at lightness 0.5 to 0.0 at lightness 1.0 else brightness = 0.5 + lightness; // goes from 1.0 at lightness 0.5 to 0.5 at lightness 0.0 QColor intervalColor = QtSLiMColorWithHSV(hue, saturation, brightness, 1.0); #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) // In Qt5, getRgbF() expects pointers to qreal, which is double double r, g, b, a; intervalColor.getRgbF(&r, &g, &b, &a); colorRed = static_cast(r); colorGreen = static_cast(g); colorBlue = static_cast(b); colorAlpha = static_cast(a); #else // In Qt6, getRgbF() expects pointers to float intervalColor.getRgbF(&colorRed, &colorGreen, &colorBlue, &colorAlpha); #endif } SLIM_GL_DEFCOORDS(intervalRect); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location if (widthOne) previousIntervalLeftEdge = intervalRect.left(); else previousIntervalLeftEdge = -10000; } // the next interval starts at the next base after this one ended intervalStartPosition = intervalEndPosition + 1; } SLIM_GL_FINISH(); } void QtSLiMChromosomeWidget::qtDrawRecombinationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter) { if (chromosome->single_recombination_map_) { _qtDrawRateMapIntervals(interiorRect, chromosome, displayedRange, chromosome->recombination_end_positions_H_, chromosome->recombination_rates_H_, 0.65, painter); } else { QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); _qtDrawRateMapIntervals(topInteriorRect, chromosome, displayedRange, chromosome->recombination_end_positions_M_, chromosome->recombination_rates_M_, 0.65, painter); _qtDrawRateMapIntervals(bottomInteriorRect, chromosome, displayedRange, chromosome->recombination_end_positions_F_, chromosome->recombination_rates_F_, 0.65, painter); } } void QtSLiMChromosomeWidget::qtDrawMutationIntervals(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter) { if (chromosome->single_mutation_map_) { _qtDrawRateMapIntervals(interiorRect, chromosome, displayedRange, chromosome->mutation_end_positions_H_, chromosome->mutation_rates_H_, 0.75, painter); } else { QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); _qtDrawRateMapIntervals(topInteriorRect, chromosome, displayedRange, chromosome->mutation_end_positions_M_, chromosome->mutation_rates_M_, 0.75, painter); _qtDrawRateMapIntervals(bottomInteriorRect, chromosome, displayedRange, chromosome->mutation_end_positions_F_, chromosome->mutation_rates_F_, 0.75, painter); } } void QtSLiMChromosomeWidget::qtDrawRateMaps(QRect &interiorRect, Chromosome *chromosome, QtSLiMRange displayedRange, QPainter &painter) { bool recombinationWorthShowing = false; bool mutationWorthShowing = false; if (chromosome->single_mutation_map_) mutationWorthShowing = (chromosome->mutation_end_positions_H_.size() > 1); else mutationWorthShowing = ((chromosome->mutation_end_positions_M_.size() > 1) || (chromosome->mutation_end_positions_F_.size() > 1)); if (chromosome->single_recombination_map_) recombinationWorthShowing = (chromosome->recombination_end_positions_H_.size() > 1); else recombinationWorthShowing = ((chromosome->recombination_end_positions_M_.size() > 1) || (chromosome->recombination_end_positions_F_.size() > 1)); // If neither map is worth showing, we show just the recombination map, to mirror the behavior of 2.4 and earlier if ((!mutationWorthShowing && !recombinationWorthShowing) || (!mutationWorthShowing && recombinationWorthShowing)) { qtDrawRecombinationIntervals(interiorRect, chromosome, displayedRange, painter); } else if (mutationWorthShowing && !recombinationWorthShowing) { qtDrawMutationIntervals(interiorRect, chromosome, displayedRange, painter); } else // mutationWorthShowing && recombinationWorthShowing { QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); int remainingHeight = interiorRect.height() - halfHeight; topInteriorRect.setHeight(halfHeight); bottomInteriorRect.setHeight(remainingHeight); bottomInteriorRect.translate(0, halfHeight); qtDrawRecombinationIntervals(topInteriorRect, chromosome, displayedRange, painter); qtDrawMutationIntervals(bottomInteriorRect, chromosome, displayedRange, painter); } } ================================================ FILE: QtSLiM/QtSLiMConsoleTextEdit.cpp ================================================ // // QtSLiMConsoleTextEdit.cpp // SLiM // // Created by Ben Haller on 12/6/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMConsoleTextEdit.h" #include #include #include #include #include #include #include #include #include "QtSLiMExtras.h" #include "eidos_globals.h" #include "slim_globals.h" // It's tempting to use QChar::LineSeparator here instead of \n, and in some ways it // produces better behavior (copy/paste to TextEdit produces better results, for example), // but it might cause the user problems because it's Unicode-specific; and we want command // lines to be a separate block with margins above and below, also. static const QString NEWLINE("\n"); // This is margin in pixels above and below command lines, to set them off nicely static const double BLOCK_MARGIN = 3.0; QtSLiMConsoleTextEdit::QtSLiMConsoleTextEdit(const QString &text, QWidget *p_parent) : QtSLiMTextEdit(text, p_parent) { selfInit(); } QtSLiMConsoleTextEdit::QtSLiMConsoleTextEdit(QWidget *p_parent) : QtSLiMTextEdit(p_parent) { selfInit(); } void QtSLiMConsoleTextEdit::selfInit(void) { // set up to react to selection changes; see these methods for comments connect(this, &QPlainTextEdit::selectionChanged, this, &QtSLiMConsoleTextEdit::handleSelectionChanged); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &QtSLiMConsoleTextEdit::handleSelectionChanged); connect(this, &QtSLiMConsoleTextEdit::selectionWasChangedDuringLastEvent, this, &QtSLiMConsoleTextEdit::adjustSelectionAndReadOnly, Qt::QueuedConnection); } QtSLiMConsoleTextEdit::~QtSLiMConsoleTextEdit() { } QTextCharFormat QtSLiMConsoleTextEdit::textFormatForColor(QColor color) { QTextCharFormat attrs; attrs.setForeground(QBrush(color)); return attrs; } void QtSLiMConsoleTextEdit::scriptStringAndSelection(QString &scriptString, int &position, int &length, int &offset) { // here we provide a subclass definition of what we consider "script": just what follows the prompt QTextCursor commandCursor(lastPromptCursor); commandCursor.setPosition(commandCursor.position(), QTextCursor::MoveAnchor); commandCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); scriptString = commandCursor.selectedText(); QTextCursor selection = textCursor(); position = selection.anchor(); length = selection.position() - position; position -= commandCursor.anchor(); // compensate for snipping off everything from the prompt back offset = commandCursor.anchor(); // tell the caller how much we snipped off } void QtSLiMConsoleTextEdit::showWelcome(void) { bool inDarkMode = QtSLiMInDarkMode(); setCurrentCharFormat(textFormatForColor(inDarkMode ? Qt::white : Qt::black)); QString welcomeMessage; welcomeMessage = welcomeMessage + "Eidos version " + EIDOS_VERSION_STRING + NEWLINE + NEWLINE; // EIDOS VERSION welcomeMessage += "By Benjamin C. Haller and Philipp W. Messer." + NEWLINE; welcomeMessage += "Copyright (c) 2016–2026 Benjamin C. Haller." + NEWLINE; welcomeMessage += "All rights reserved." + NEWLINE + NEWLINE; welcomeMessage += "Eidos is free software with ABSOLUTELY NO WARRANTY." + NEWLINE; welcomeMessage += "Type license() for license and distribution details." + NEWLINE + NEWLINE; welcomeMessage += "Go to https://github.com/MesserLab/SLiM for source" + NEWLINE; welcomeMessage += "code, documentation, examples, and other information." + NEWLINE + NEWLINE; welcomeMessage += "Welcome to Eidos!" + NEWLINE + NEWLINE; welcomeMessage += "-----------------------------------------------------" + NEWLINE + NEWLINE; welcomeMessage += "Connected to SLiMgui simulation." + NEWLINE; welcomeMessage = welcomeMessage + "SLiM version " + SLIM_VERSION_STRING + "." + NEWLINE + NEWLINE; // SLIM VERSION welcomeMessage += "-----------------------------------------------------" + NEWLINE + NEWLINE; insertPlainText(welcomeMessage); } void QtSLiMConsoleTextEdit::showPrompt(QChar promptChar) { bool inDarkMode = QtSLiMInDarkMode(); QTextCharFormat promptAttrs = textFormatForColor(inDarkMode ? QColor(220, 83, 185) : QColor(170, 13, 145)); QTextCharFormat inputAttrs = textFormatForColor(inDarkMode ? QColor(115, 145, 255) : QColor(28, 0, 207)); moveCursor(QTextCursor::End); setCurrentCharFormat(promptAttrs); insertPlainText(promptChar); moveCursor(QTextCursor::End); setCurrentCharFormat(inputAttrs); insertPlainText(" "); moveCursor(QTextCursor::End); QTextCursor promptCursor(document()); promptCursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); promptCursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 2); promptCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 2); QTextBlockFormat marginBlockFormat; marginBlockFormat.setTopMargin(BLOCK_MARGIN); marginBlockFormat.setBottomMargin(BLOCK_MARGIN); promptCursor.setBlockFormat(marginBlockFormat); // We remember the prompt range for various purposes such as uneditability of old content lastPromptCursor = promptCursor; lastPromptCursor.setKeepPositionOnInsert(true); // When a new prompt appears, one can no longer undo/redo previous changes document()->clearUndoRedoStacks(); } void QtSLiMConsoleTextEdit::showPrompt(void) { showPrompt('>'); } void QtSLiMConsoleTextEdit::showContinuationPrompt(void) { // The user has entered an incomplete script line, so we need to append a newline... moveCursor(QTextCursor::End); insertPlainText(NEWLINE); // ...and issue a continuation prompt to await further input int promptEnd = lastPromptCursor.position(); showPrompt('+'); originalPromptEnd = promptEnd; isContinuationPrompt = true; } void QtSLiMConsoleTextEdit::appendExecution(QString result, QString errorString, QString tokenString, QString parseString, QString executionString) { bool inDarkMode = QtSLiMInDarkMode(); moveCursor(QTextCursor::End); insertPlainText(NEWLINE); if (tokenString.length()) { moveCursor(QTextCursor::End); setCurrentCharFormat(textFormatForColor(inDarkMode ? QColor(100, 56, 32) : QColor(100, 56, 32))); insertPlainText(tokenString); } if (parseString.length()) { moveCursor(QTextCursor::End); setCurrentCharFormat(textFormatForColor(inDarkMode ? QColor(90, 210, 90) : QColor(0, 116, 0))); insertPlainText(parseString); } if (executionString.length()) { moveCursor(QTextCursor::End); setCurrentCharFormat(textFormatForColor(inDarkMode ? QColor(70, 205, 216) : QColor(63, 110, 116))); insertPlainText(executionString); } if (result.length()) { QTextBlockFormat plainBlockFormat; moveCursor(QTextCursor::End); setCurrentCharFormat(textFormatForColor(inDarkMode ? QColor(255, 255, 255) : QColor(0, 0, 0))); textCursor().setBlockFormat(plainBlockFormat); insertPlainText(result); } if (errorString.length()) { QTextBlockFormat marginBlockFormat; marginBlockFormat.setTopMargin(BLOCK_MARGIN); marginBlockFormat.setBottomMargin(BLOCK_MARGIN); moveCursor(QTextCursor::End); setCurrentCharFormat(textFormatForColor(inDarkMode ? QColor(220, 98, 90) : QColor(196, 26, 22))); textCursor().setBlockFormat(marginBlockFormat); insertPlainText(errorString); // If we have an error, it is in the user script, and it has a valid position, // then we can try to highlight it in the input if ((!gEidosErrorContext.currentScript || (gEidosErrorContext.currentScript->UserScriptUTF16Offset() == 0)) && (gEidosErrorContext.errorPosition.characterStartOfErrorUTF16 >= 0) && (gEidosErrorContext.errorPosition.characterEndOfErrorUTF16 >= gEidosErrorContext.errorPosition.characterStartOfErrorUTF16)) { int promptEnd = lastPromptCursor.position(); int errorTokenStart = gEidosErrorContext.errorPosition.characterStartOfErrorUTF16 + promptEnd; int errorTokenEnd = gEidosErrorContext.errorPosition.characterEndOfErrorUTF16 + promptEnd; QTextCursor highlightCursor(lastPromptCursor); highlightCursor.setPosition(errorTokenStart, QTextCursor::MoveAnchor); highlightCursor.setPosition(errorTokenEnd + 1, QTextCursor::KeepAnchor); QTextCharFormat highlightFormat; highlightFormat.setForeground(QBrush(Qt::black)); highlightFormat.setBackground(QBrush(inDarkMode ? QColor(220, 98, 90) : QColor(QColor(Qt::red).lighter(120)))); highlightCursor.setCharFormat(highlightFormat); // note that unlike the error highlighting in the scripting views, this error // highlighting is permanent, and will not be removed by later cursor changes } } // scroll to bottom verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } void QtSLiMConsoleTextEdit::clearToPrompt(void) { if (isContinuationPrompt) { QTextCursor deleteCursor(lastPromptCursor); deleteCursor.setPosition(originalPromptEnd - 2, QTextCursor::MoveAnchor); deleteCursor.movePosition(QTextCursor::Start, QTextCursor::KeepAnchor); deleteCursor.insertText(""); originalPromptEnd = 2; } else { QTextCursor deleteCursor(lastPromptCursor); deleteCursor.setPosition(deleteCursor.anchor(), QTextCursor::MoveAnchor); deleteCursor.movePosition(QTextCursor::Start, QTextCursor::KeepAnchor); deleteCursor.insertText(""); } // Clearing the console is not undoable, and it clears the undo/redo history document()->clearUndoRedoStacks(); } QString QtSLiMConsoleTextEdit::currentCommandAtPrompt(void) { QTextCursor commandCursor(lastPromptCursor); commandCursor.setPosition(commandCursor.position(), QTextCursor::MoveAnchor); commandCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); return commandCursor.selectedText(); } void QtSLiMConsoleTextEdit::setCommandAtPrompt(QString newCommand) { newCommand = newCommand.trimmed(); // trim off whitespace around the command line QTextCursor commandCursor(lastPromptCursor); commandCursor.setPosition(commandCursor.position(), QTextCursor::MoveAnchor); commandCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); commandCursor.setKeepPositionOnInsert(false); commandCursor.insertText(newCommand); moveCursor(QTextCursor::End); } void QtSLiMConsoleTextEdit::registerNewHistoryItem(QString newItem) { // If there is a provisional item on the top of the stack, get rid of it and replace it if (lastHistoryItemIsProvisional) { history.pop_back(); lastHistoryItemIsProvisional = false; } history.push_back(newItem); historyIndex = history.count(); // a new prompt, one beyond the last history item } void QtSLiMConsoleTextEdit::elideContinuationPrompt(void) { // This replaces the continuation prompt, if there is one, with a space, and switches the active prompt back to the original prompt; // the net effect is as if the user entered a newline and two spaces at the original prompt, with no continuation. Note that the // two spaces at the beginning of continuation lines is mirrored in fullInputString, below. if (isContinuationPrompt) { bool inDarkMode = QtSLiMInDarkMode(); QTextCharFormat inputAttrs = textFormatForColor(inDarkMode ? QColor(115, 145, 255) : QColor(28, 0, 207)); QTextCursor fixCursor(lastPromptCursor); fixCursor.setPosition(lastPromptCursor.anchor(), QTextCursor::MoveAnchor); fixCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); fixCursor.insertText(" ", inputAttrs); lastPromptCursor.setPosition(originalPromptEnd - 2, QTextCursor::MoveAnchor); lastPromptCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 2); isContinuationPrompt = false; } } QString QtSLiMConsoleTextEdit::fullInputString(void) { elideContinuationPrompt(); QTextCursor commandCursor(lastPromptCursor); commandCursor.setPosition(commandCursor.position(), QTextCursor::MoveAnchor); commandCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); return commandCursor.selectedText(); } void QtSLiMConsoleTextEdit::previousHistory(void) { if (historyIndex > 0) { // If the user has typed at the current prompt and it is unsaved, save it in the history before going up if (historyIndex == history.count()) { QString commandString = currentCommandAtPrompt(); if (commandString.length() > 0) { // If there is a provisional item on the top of the stack, get rid of it and replace it if (lastHistoryItemIsProvisional) { history.pop_back(); lastHistoryItemIsProvisional = false; --historyIndex; } history.push_back(commandString); lastHistoryItemIsProvisional = true; } } // if the only item on the stack was provisional, and we just replaced it, we may have nowhere up to go if (historyIndex > 0) { --historyIndex; setCommandAtPrompt(history[historyIndex]); } //NSLog(@"moveUp: end:\nhistory = %@\nhistoryIndex = %d\nlastHistoryItemIsProvisional = %@", history, historyIndex, lastHistoryItemIsProvisional ? @"YES" : @"NO"); } } void QtSLiMConsoleTextEdit::nextHistory(void) { if (historyIndex <= history.count()) { // If the user has typed at the current prompt and it is unsaved, save it in the history before going down if (historyIndex == history.count()) { QString commandString = currentCommandAtPrompt(); if (commandString.length() > 0) { // If there is a provisional item on the top of the stack, get rid of it and replace it if (lastHistoryItemIsProvisional) { history.pop_back(); lastHistoryItemIsProvisional = false; --historyIndex; } history.push_back(commandString); lastHistoryItemIsProvisional = true; } else return; } ++historyIndex; if (historyIndex == history.count()) setCommandAtPrompt(""); else setCommandAtPrompt(history[historyIndex]); //NSLog(@"moveDown: end:\nhistory = %@\nhistoryIndex = %d\nlastHistoryItemIsProvisional = %@", history, historyIndex, lastHistoryItemIsProvisional ? @"YES" : @"NO"); } } void QtSLiMConsoleTextEdit::executeCurrentPrompt(void) { QTextCursor endCursor(document()); endCursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); if (isContinuationPrompt && (lastPromptCursor.position() == endCursor.position())) { // If the user has hit return at an empty continuation prompt, we take that as a sign that they want to get out of it QString executionString = fullInputString(); registerNewHistoryItem(executionString); moveCursor(QTextCursor::End); insertPlainText(NEWLINE); // show a new prompt showPrompt(); } else { // The current prompt might be a non-empty continuation prompt, so now we get the full input string from the original prompt QString command = fullInputString(); //qDebug() << "execute " << command; registerNewHistoryItem(command); emit executeScript(command); } } void QtSLiMConsoleTextEdit::keyPressEvent(QKeyEvent *p_event) { // With a completer, we have to call super in some cases to let it handle the completer logic // This code is parallel to the code in QtSLiMTextEdit::keyPressEvent() if (completer) { if (completer->popup()->isVisible()) { // The following keys are forwarded by the completer to the widget switch (p_event->key()) { case Qt::Key_Enter: case Qt::Key_Return: case Qt::Key_Escape: case Qt::Key_Tab: case Qt::Key_Backtab: QtSLiMTextEdit::keyPressEvent(p_event); return; // let super pass these keys on to the completer default: break; } } // if we have a visible completer popup, the key pressed is not one of the special keys above (including escape) // our completion key shortcut is the escape key, so check for that now bool isShortcut = ((p_event->modifiers() == Qt::NoModifier) && p_event->key() == Qt::Key_Escape); // escape if (!isShortcut) { // any key other than escape and the special keys above causes the completion popup to hide // then we drop through to our normal key handling below completer->popup()->hide(); } else { // the completer shortcut has been pressed; let super run the completer QtSLiMTextEdit::keyPressEvent(p_event); return; } } if (p_event->matches(QKeySequence::MoveToPreviousLine)) { // up-arrow pressed; cycle through the command history previousHistory(); p_event->accept(); } else if (p_event->matches(QKeySequence::MoveToNextLine)) { // down-arrow pressed; cycle through the command history nextHistory(); p_event->accept(); } else if (p_event->matches(QKeySequence::InsertLineSeparator) || p_event->matches(QKeySequence::InsertParagraphSeparator)) { // return/enter pressed; execute the statement(s) entered executeCurrentPrompt(); p_event->accept(); } else if (p_event->key() == Qt::Key_Escape) { // escape pressed; this should be handled by the code above, but if we get here just ignore it //qDebug() << "escape!"; p_event->accept(); } else if (p_event->matches(QKeySequence::MoveToEndOfBlock) || p_event->matches(QKeySequence::MoveToEndOfDocument) || p_event->matches(QKeySequence::MoveToEndOfLine) || p_event->matches(QKeySequence::MoveToNextChar) || p_event->matches(QKeySequence::MoveToNextLine) || p_event->matches(QKeySequence::MoveToNextPage) || p_event->matches(QKeySequence::MoveToNextWord) || p_event->matches(QKeySequence::MoveToPreviousChar) || p_event->matches(QKeySequence::MoveToPreviousLine) || p_event->matches(QKeySequence::MoveToPreviousPage) || p_event->matches(QKeySequence::MoveToPreviousWord) || p_event->matches(QKeySequence::MoveToStartOfBlock) || p_event->matches(QKeySequence::MoveToStartOfDocument) || p_event->matches(QKeySequence::MoveToStartOfLine) || p_event->matches(QKeySequence::SelectAll) || p_event->matches(QKeySequence::SelectEndOfBlock) || p_event->matches(QKeySequence::SelectEndOfDocument) || p_event->matches(QKeySequence::SelectEndOfLine) || p_event->matches(QKeySequence::SelectNextChar) || p_event->matches(QKeySequence::SelectNextLine) || p_event->matches(QKeySequence::SelectNextPage) || p_event->matches(QKeySequence::SelectNextWord) || p_event->matches(QKeySequence::SelectPreviousChar) || p_event->matches(QKeySequence::SelectPreviousLine) || p_event->matches(QKeySequence::SelectPreviousPage) || p_event->matches(QKeySequence::SelectPreviousWord) || p_event->matches(QKeySequence::SelectStartOfBlock) || p_event->matches(QKeySequence::SelectStartOfDocument) || p_event->matches(QKeySequence::SelectStartOfLine)) { if (isReadOnly()) { // being read-only interferes with keyboard selection changes, for some odd reason, // so we change ourselves to editable around the call to super setReadOnly(false); QtSLiMTextEdit::keyPressEvent(p_event); updateReadOnly(); // maybe roDTS has changed, so maybe we no longer want to be read-only } else if (p_event->matches(QKeySequence::SelectAll)) { // in the non-read-only case, we want Select All to select everything in the console, rather // than follow the selection-clipping convention below, I think... QtSLiMTextEdit::keyPressEvent(p_event); } else { // in the non-read-only case, the cursor is inside the command area, and we want to clip movement and // selection to stay within the command area; the user can select other content if they want to, but // by default we assume their intention is to work within the command line (except select all, above) QtSLiMTextEdit::keyPressEvent(p_event); QTextCursor selection(textCursor()); int anchor = std::max(selection.anchor(), lastPromptCursor.position()); int position = std::max(selection.position(), lastPromptCursor.position()); selection.setPosition(anchor, QTextCursor::MoveAnchor); selection.setPosition(position, QTextCursor::KeepAnchor); setTextCursor(selection); } } else if (p_event->matches(QKeySequence::Backspace) || p_event->matches(QKeySequence::DeleteStartOfWord) || (p_event->key() == Qt::Key_Backspace)) { // suppress backspace if it would backspace into the prompt; note QKeySequence::Backspace doesn't seem to work! QTextCursor currentCursor(textCursor()); if ((currentCursor.position() == currentCursor.anchor()) && (currentCursor.position() == lastPromptCursor.position())) { p_event->accept(); return; } QtSLiMTextEdit::keyPressEvent(p_event); } else if (p_event->matches(QKeySequence::DeleteCompleteLine)) { // I'm not sure how to produce this on a keyboard, but let's make sure it doesn't delete the prompt setCommandAtPrompt(""); p_event->accept(); } else { // if the key was not handled above, pass the event to super QtSLiMTextEdit::keyPressEvent(p_event); } } void QtSLiMConsoleTextEdit::mousePressEvent(QMouseEvent *p_event) { // keep track of when we are inside a mouse tracking loop insideMouseTracking = true; sawSelectionChange = false; QtSLiMTextEdit::mousePressEvent(p_event); } void QtSLiMConsoleTextEdit::mouseReleaseEvent(QMouseEvent *p_event) { QtSLiMTextEdit::mouseReleaseEvent(p_event); // we're done with the mouse tracking loop; if it changed the selection, we now adjust insideMouseTracking = false; if (sawSelectionChange) adjustSelectionAndReadOnly(); } void QtSLiMConsoleTextEdit::handleSelectionChanged(void) { // This is a handler for both selectionChanged and cursorPositionChanged signals, // because neither one is emitted consistently enough to be usable in general, // unfortunately. So for a given change, we may be called just once (through one // of those signals) or twice. We want to defer our reaction until the end of // the event being handled, because sometimes Qt will change one aspect of the // selection (the anchor, say), send one signal, then change another aspect of // the selection (the position, say), and send another signal; if we react to // the first change before the second change has also been made we will misjudge // what needs to be done. //qDebug() << "handleSelectionChanged"; if (insideMouseTracking) { // when mouse-tracking, selection changes get deferred until tracking is complete // adjustSelectionAndReadOnly() will be called by QtSLiMConsoleTextEdit::mouseReleaseEvent() sawSelectionChange = true; } else { // when not mouse-tracking, selection changes get adjusted at the end of // this event loop callout so we are sure all changes have been made // adjustSelectionAndReadOnly() will be called by a queued connection set up in selfInit() emit selectionWasChangedDuringLastEvent(); } } void QtSLiMConsoleTextEdit::adjustSelectionAndReadOnly(void) { //qDebug() << "adjustSelectionAndReadOnly"; QTextCursor selection(textCursor()); if (selection.position() == selection.anchor()) { // a zero-length selection has been requested; if it falls before the end of the prompt, we adjust it // to fall at the end of the console instead; note we will get a re-entrant call back from this // a special case: if the new position is just to the left of the prompt end, move to the prompt end if (selection.position() == lastPromptCursor.position() - 1) moveCursor(QTextCursor::Right); else if (selection.position() < lastPromptCursor.position()) moveCursor(QTextCursor::End); setReadOnlyDueToSelection(false); } else { // a non-zero-length selection has been requested; we allow any such selection, but if it encompasses // characters before the end of the current prompt, we make ourselves non-editable to prevent changes int start = selection.selectionStart(); if (start < lastPromptCursor.position()) setReadOnlyDueToSelection(true); else setReadOnlyDueToSelection(false); } } // prevent drops outside of the prompt area // thanks to https://github.com/uglide/QtConsole/blob/master/src/qconsole.cpp void QtSLiMConsoleTextEdit::dragMoveEvent(QDragMoveEvent *p_event) { // Figure out where the drop would go #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QTextCursor dropCursor = cursorForPosition(p_event->pos()); #else QTextCursor dropCursor = cursorForPosition(p_event->position().toPoint()); #endif //qDebug() << "dragMoveEvent: " << dropCursor.position(); if (dropCursor.position() < lastPromptCursor.position()) { // Ignore the event if the drop would be outside the command area p_event->ignore(cursorRect(dropCursor)); } else { // Accept the event if the drop is inside the command area p_event->accept(cursorRect(dropCursor)); } } ================================================ FILE: QtSLiM/QtSLiMConsoleTextEdit.h ================================================ // // QtSLiMConsoleTextEdit.h // SLiM // // Created by Ben Haller on 12/6/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMCONSOLETEXTEDIT_H #define QTSLIMCONSOLETEXTEDIT_H #include #include #include #include #include "QtSLiMScriptTextEdit.h" // defines our superclass, QtSLiMTextEdit class QtSLiMConsoleTextEdit : public QtSLiMTextEdit { Q_OBJECT public: QtSLiMConsoleTextEdit(const QString &text, QWidget *p_parent = nullptr); QtSLiMConsoleTextEdit(QWidget *p_parent = nullptr); virtual ~QtSLiMConsoleTextEdit() override; static QTextCharFormat textFormatForColor(QColor color); // Show the initial welcome message void showWelcome(void); // Prompts and spacers void showPrompt(QChar promptChar); void showPrompt(void); void showContinuationPrompt(void); void clearToPrompt(void); // Commands and history void appendExecution(QString result, QString errorString, QString tokenString, QString parseString, QString executionString); QString currentCommandAtPrompt(void); void setCommandAtPrompt(QString newCommand); void registerNewHistoryItem(QString newItem); // Fix the selection to be legal, and adjust the read-only setting accordingly void adjustSelectionAndReadOnly(void); // Read-only state handling; this is a consequence of two independent flags, for us void setReadOnlyDueToSelection(bool flag) { roDTS = flag; updateReadOnly(); } void setReadOnlyDueToActivation(bool flag) { roDTA = flag; updateReadOnly(); } void updateReadOnly(void) { setReadOnly(roDTS | roDTA); } public slots: void previousHistory(void); void nextHistory(void); void executeCurrentPrompt(void); signals: void executeScript(QString script); protected: void selfInit(void); virtual void keyPressEvent(QKeyEvent *p_event) override; // handling input prompts and continuation QTextCursor lastPromptCursor; bool isContinuationPrompt = false; int originalPromptEnd = 0; void elideContinuationPrompt(void); QString fullInputString(void); virtual void scriptStringAndSelection(QString &scriptString, int &position, int &length, int &offset) override; // handling the command history QStringList history; int historyIndex = 0; bool lastHistoryItemIsProvisional = false; // got added to the history by a moveUp: event but is not an executed command // handling read-only status bool roDTS = false; bool roDTA = false; // handling the selection and editability bool insideMouseTracking = false, sawSelectionChange = false; virtual void mousePressEvent(QMouseEvent *p_event) override; virtual void mouseReleaseEvent(QMouseEvent *p_event) override; void handleSelectionChanged(void); virtual void dragMoveEvent(QDragMoveEvent *p_event) override; signals: void selectionWasChangedDuringLastEvent(void); }; #endif // QTSLIMCONSOLETEXTEDIT_H ================================================ FILE: QtSLiM/QtSLiMDebugOutputWindow.cpp ================================================ // // QtSLiMDebugOutputWindow.cpp // SLiM // // Created by Ben Haller on 02/06/2021. // Copyright (c) 2021-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMDebugOutputWindow.h" #include "ui_QtSLiMDebugOutputWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "QtSLiMAppDelegate.h" #include "QtSLiMExtras.h" #include "log_file.h" // // QtSLiMDebugOutputWindow // QtSLiMDebugOutputWindow::QtSLiMDebugOutputWindow(QtSLiMWindow *p_parent) : QWidget(p_parent, Qt::Window), // the console window has us as a parent, but is still a standalone window parentSLiMWindow(p_parent), ui(new Ui::QtSLiMDebugOutputWindow) { ui->setupUi(this); // no window icon #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect setWindowIcon(QIcon()); #endif // prevent this window from keeping the app running when all main windows are closed setAttribute(Qt::WA_QuitOnClose, false); // Restore the saved window position; see https://doc.qt.io/qt-5/qsettings.html#details QSettings settings; settings.beginGroup("QtSLiMDebugOutputWindow"); resize(settings.value("size", QSize(400, 300)).toSize()); move(settings.value("pos", QPoint(25, 445)).toPoint()); settings.endGroup(); // set up the tab bar; annoyingly, this cannot be configured in Designer at all! ui->tabBar->setAcceptDrops(false); ui->tabBar->setDocumentMode(false); ui->tabBar->setDrawBase(false); ui->tabBar->setExpanding(false); ui->tabBar->setMovable(false); ui->tabBar->setShape(QTabBar::RoundedNorth); ui->tabBar->setTabsClosable(false); ui->tabBar->setUsesScrollButtons(false); ui->tabBar->setIconSize(QSize(15, 15)); ui->tabBar->addTab("Debug Output"); ui->tabBar->setTabToolTip(0, "Debug Output"); ui->tabBar->addTab("Run Output"); ui->tabBar->setTabToolTip(1, "Run Output"); ui->tabBar->addTab("Scheduling"); ui->tabBar->setTabToolTip(2, "Scheduling"); resetTabIcons(); connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, [this]() { resetTabIcons(); }); // adjust to dark mode change connect(ui->tabBar, &QTabBar::currentChanged, this, &QtSLiMDebugOutputWindow::selectedTabChanged); // glue UI; no separate file since this is very simple connect(ui->clearOutputButton, &QPushButton::clicked, this, &QtSLiMDebugOutputWindow::clearOutputClicked); ui->clearOutputButton->qtslimSetBaseName("delete"); connect(ui->clearOutputButton, &QPushButton::pressed, this, &QtSLiMDebugOutputWindow::clearOutputPressed); connect(ui->clearOutputButton, &QPushButton::released, this, &QtSLiMDebugOutputWindow::clearOutputReleased); // fix the layout of the window ui->outputHeaderLayout->setSpacing(4); ui->outputHeaderLayout->setContentsMargins(0, 0, 0, 0); // QtSLiMTextEdit attributes ui->debugOutputTextEdit->setOptionClickEnabled(false); ui->debugOutputTextEdit->setCodeCompletionEnabled(false); ui->debugOutputTextEdit->setScriptType(QtSLiMTextEdit::NoScriptType); ui->debugOutputTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::OutputHighlighting); ui->debugOutputTextEdit->setReadOnly(true); ui->runOutputTextEdit->setOptionClickEnabled(false); ui->runOutputTextEdit->setCodeCompletionEnabled(false); ui->runOutputTextEdit->setScriptType(QtSLiMTextEdit::NoScriptType); ui->runOutputTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::OutputHighlighting); ui->runOutputTextEdit->setReadOnly(true); ui->schedulingOutputTextEdit->setOptionClickEnabled(false); ui->schedulingOutputTextEdit->setCodeCompletionEnabled(false); ui->schedulingOutputTextEdit->setScriptType(QtSLiMTextEdit::NoScriptType); ui->schedulingOutputTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::OutputHighlighting); ui->schedulingOutputTextEdit->setReadOnly(true); showDebugOutput(); // make window actions for all global menu items qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); } QtSLiMDebugOutputWindow::~QtSLiMDebugOutputWindow() { delete ui; } void QtSLiMDebugOutputWindow::resetTabIcons(void) { // No icons for now, see how it goes /* QIcon debugTabIcon(QtSLiMImagePath("debug", false)); ui->tabBar->setTabIcon(0, debugTabIcon); ui->tabBar->setTabIcon(1, qtSLiMAppDelegate->applicationIcon()); */ } void QtSLiMDebugOutputWindow::hideAllViews(void) { ui->debugOutputTextEdit->setVisible(false); ui->runOutputTextEdit->setVisible(false); ui->schedulingOutputTextEdit->setVisible(false); for (QTableWidget *logtable : logfileViews) logtable->setVisible(false); for (QtSLiMTextEdit *fileview : fileViews) fileview->setVisible(false); } void QtSLiMDebugOutputWindow::showDebugOutput() { hideAllViews(); ui->debugOutputTextEdit->setVisible(true); } void QtSLiMDebugOutputWindow::showRunOutput() { hideAllViews(); ui->runOutputTextEdit->setVisible(true); } void QtSLiMDebugOutputWindow::showSchedulingOutput() { hideAllViews(); ui->schedulingOutputTextEdit->setVisible(true); } void QtSLiMDebugOutputWindow::showLogFile(int logFileIndex) { QTableWidget *table = logfileViews[logFileIndex]; hideAllViews(); table->setVisible(true); } void QtSLiMDebugOutputWindow::showFile(int fileIndex) { QtSLiMTextEdit *fileview = fileViews[fileIndex]; hideAllViews(); fileview->setVisible(true); } void QtSLiMDebugOutputWindow::tabReceivedInput(int tabIndex) { // set the tab's text color to red when new input is received, if it is not the current tab // the color depends on dark mode; FIXME we should fix it on dark mode change but we don't if (tabIndex != ui->tabBar->currentIndex()) { QColor color = (QtSLiMInDarkMode() ? QColor(255, 128, 128, 255) : QColor(192, 0, 0, 255)); ui->tabBar->setTabTextColor(tabIndex, color); } } void QtSLiMDebugOutputWindow::takeDebugOutput(QString str) { ui->debugOutputTextEdit->moveCursor(QTextCursor::End); ui->debugOutputTextEdit->insertPlainText(str); ui->debugOutputTextEdit->moveCursor(QTextCursor::End); tabReceivedInput(0); } void QtSLiMDebugOutputWindow::takeRunOutput(QString str) { ui->runOutputTextEdit->moveCursor(QTextCursor::End); ui->runOutputTextEdit->insertPlainText(str); ui->runOutputTextEdit->moveCursor(QTextCursor::End); tabReceivedInput(1); } void QtSLiMDebugOutputWindow::takeSchedulingOutput(QString str) { ui->schedulingOutputTextEdit->moveCursor(QTextCursor::End); ui->schedulingOutputTextEdit->insertPlainText(str); ui->schedulingOutputTextEdit->moveCursor(QTextCursor::End); tabReceivedInput(2); } QTableWidget *QtSLiMDebugOutputWindow::logFileTableForPath(const std::string &path) { auto pathIter = std::find(logfilePaths.begin(), logfilePaths.end(), path); QTableWidget *table = nullptr; if (pathIter != logfilePaths.end()) { size_t tableIndex = std::distance(logfilePaths.begin(), pathIter); table = logfileViews[tableIndex]; } return table; } void QtSLiMDebugOutputWindow::takeLogFileOutput(std::vector &lineElements, const std::string &path) { // First, find the index of the log file view we're taking input into // If we didn't find one, make a new one auto pathIter = std::find(logfilePaths.begin(), logfilePaths.end(), path); QTableWidget *table; size_t tableIndex; if (pathIter != logfilePaths.end()) { tableIndex = std::distance(logfilePaths.begin(), pathIter); table = logfileViews[tableIndex]; } else { QLayout *windowLayout = this->layout(); table = new QTableWidget(); table->setSortingEnabled(false); table->setAlternatingRowColors(true); table->setDragEnabled(false); table->setAcceptDrops(false); table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); table->setSelectionMode(QAbstractItemView::NoSelection); table->horizontalHeader()->setResizeContentsPrecision(100); // look at the first 100 rows to determine sizing table->horizontalHeader()->setMinimumSectionSize(100); // wide enough to fit most floating-point output table->horizontalHeader()->setMaximumSectionSize(400); // don't let super-wide output push the table width too far table->horizontalHeader()->setSectionsClickable(false); table->setVisible(false); windowLayout->addWidget(table); // Get right-clicks on table widget headers and items, to run our context menu table->setContextMenuPolicy(Qt::CustomContextMenu); connect(table, &QTableWidget::customContextMenuRequested, this, &QtSLiMDebugOutputWindow::logFileRightClick); table->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(table->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, &QtSLiMDebugOutputWindow::logFileRightClick); // Make a new tab and insert it at the correct position in the tab bar QString filename = QString::fromStdString(Eidos_LastPathComponent(path)); tableIndex = logfilePaths.size(); ui->tabBar->insertTab(tableIndex + 3, filename); ui->tabBar->setTabToolTip(tableIndex + 3, filename); // Add the new view's info logfilePaths.emplace_back(path); logfileViews.emplace_back(table); logfileLineNumbers.emplace_back(-1); } // Then create a new QTableWidgetItem for each item in the row // If this is the first row we have taken, then it's the header row bool firstTime = !table->horizontalHeaderItem(0); if (firstTime) { int col = 0; table->setColumnCount(lineElements.size()); for (const std::string &str : lineElements) { QString qstr = QString::fromStdString(str); QTableWidgetItem *colItem = new QTableWidgetItem(qstr); colItem->setFlags(Qt::ItemIsEnabled); QFont itemFont = colItem->font(); itemFont.setBold(true); colItem->setFont(itemFont); table->setHorizontalHeaderItem(col, colItem); col++; } } else { int rowIndex = table->rowCount(); int lineNumber = logfileLineNumbers[tableIndex] + 1; int col = 0; table->setRowCount(rowIndex + 1); QTableWidgetItem *rowItem = new QTableWidgetItem(QString("%1").arg(lineNumber)); rowItem->setFlags(Qt::NoItemFlags); rowItem->setTextAlignment(Qt::AlignRight); rowItem->setForeground(Qt::darkGray); table->setVerticalHeaderItem(rowIndex, rowItem); logfileLineNumbers[tableIndex] = lineNumber; for (const std::string &str : lineElements) { QString qstr = QString::fromStdString(str); QTableWidgetItem *item = new QTableWidgetItem(qstr); item->setFlags(Qt::ItemIsEnabled); item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); table->setItem(rowIndex, col, item); col++; } table->scrollToBottom(); } // adjust to fit the headers and the initial rows; this is probably rather expensive, // but LogFile usually only fires once per tick or less, so hopefully not a big deal table->resizeColumnsToContents(); tabReceivedInput(tableIndex + 3); } void QtSLiMDebugOutputWindow::logFileRightClick(const QPoint &pos) { for (QTableWidget *table : logfileViews) { if (table->isVisible()) { // found the visible/active table int clickedColumnIndex = table->horizontalHeader()->logicalIndexAt(pos); if (clickedColumnIndex != -1) { int rowCount = table->rowCount(); int columnCount = table->columnCount(); if ((rowCount > 0) && (columnCount > 0)) { std::vector columnIsNumeric; // were any conversion errors to double detected? for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) { columnIsNumeric.push_back(true); for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *item = table->item(rowIndex, columnIndex); if (item) { QString itemText = item->text(); if ((itemText == "NAN") || (itemText == "INF") || (itemText == "-INF")) { // NAN / INF / -INF are acceptable as double values, although // whatever parser the data ends up in might not like it... } else { bool ok; itemText.toDouble(&ok); if (!ok) { columnIsNumeric.back() = false; break; } } } } } // make a context menu QMenu contextMenu("logfile_menu", this); QString columnName = table->horizontalHeaderItem(clickedColumnIndex)->text(); QString copyDataTitle("Copy Data for "); copyDataTitle.append(columnName); QAction *copyDataAction = contextMenu.addAction(copyDataTitle); copyDataAction->setEnabled(true); QString graphData1DTitle("Graph "); graphData1DTitle.append(columnName); QAction *graphData1DAction = contextMenu.addAction(graphData1DTitle); graphData1DAction->setEnabled(columnIsNumeric[clickedColumnIndex]); if (columnIsNumeric[clickedColumnIndex]) { // if the clicked column is numeric, we can potentially plot it as the // dependent variable with a different column as the independent variable bool has2DGraphActions = false; for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) { if ((columnIndex != clickedColumnIndex) && (columnIsNumeric[columnIndex])) { if (!has2DGraphActions) { contextMenu.addSeparator(); { QString graphData2DTitle("Graph "); graphData2DTitle.append(columnName); graphData2DTitle.append(" ~ "); graphData2DTitle.append(table->horizontalHeaderItem(columnIndex)->text()); graphData2DTitle.append(" (line plot)"); QAction *graphData2DAction = contextMenu.addAction(graphData2DTitle); graphData2DAction->setEnabled(true); graphData2DAction->setData(columnIndex); } { QString graphData2DTitle("Graph "); graphData2DTitle.append(columnName); graphData2DTitle.append(" ~ "); graphData2DTitle.append(table->horizontalHeaderItem(columnIndex)->text()); graphData2DTitle.append(" (scatter plot)"); QAction *graphData2DAction = contextMenu.addAction(graphData2DTitle); graphData2DAction->setEnabled(true); graphData2DAction->setData(columnIndex + 10000); // 10000 is "scatter plot" } has2DGraphActions = true; } } } } // could also add items for, e.g., Graph Data for FST ~ cycle // just need to vet all columns to determine which are double/integer // run the context menu synchronously QPoint globalPoint = table->mapToGlobal(pos); QAction *action = contextMenu.exec(globalPoint); if (action == copyDataAction) { QString string("# LogFile data: "); string.append(columnName); string.append("\n"); string.append(slimDateline()); string.append("\n\n"); if (columnIsNumeric[clickedColumnIndex]) { // if the values are all double, we can just dump // the value strings unquoted, as numeric data for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *item = table->item(rowIndex, clickedColumnIndex); if (rowIndex > 0) string.append(QString(", ")); string.append(item->text()); } } else { // otherwise, we dump the value strings quoted // FIXME should worry about quoting issues here for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *item = table->item(rowIndex, clickedColumnIndex); if (rowIndex > 0) string.append(QString(", \"")); else string.append("\""); string.append(item->text()); string.append("\""); } } QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(string); } else if (action == graphData1DAction) { // all values are convertible to double; guaranteed above double *y_values = (double *)malloc(rowCount * sizeof(double)); for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *item = table->item(rowIndex, clickedColumnIndex); QString text = item->text(); double value; if (text == "NAN") value = std::numeric_limits::quiet_NaN(); else if (text == "INF") value = std::numeric_limits::infinity(); else if (text == "-INF") value = -std::numeric_limits::infinity(); else value = text.toDouble(); y_values[rowIndex] = value; } parentSLiMWindow->plotLogFileData_1D(columnName, columnName, y_values, rowCount); } else if (action) // graphData2DAction { // all values are convertible to double; guaranteed above QVariant actiondata = action->data(); int xColumnIndex = actiondata.toInt(); bool makeScatterPlot = false; if (xColumnIndex >= 10000) { xColumnIndex -= 10000; makeScatterPlot = true; } QString xColumnName = table->horizontalHeaderItem(xColumnIndex)->text(); double *x_values = (double *)malloc(rowCount * sizeof(double)); double *y_values = (double *)malloc(rowCount * sizeof(double)); for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *x_item = table->item(rowIndex, xColumnIndex); QTableWidgetItem *y_item = table->item(rowIndex, clickedColumnIndex); QString x_text = x_item->text(); QString y_text = y_item->text(); double x_value, y_value; if (x_text == "NAN") x_value = std::numeric_limits::quiet_NaN(); else if (x_text == "INF") x_value = std::numeric_limits::infinity(); else if (x_text == "-INF") x_value = -std::numeric_limits::infinity(); else x_value = x_text.toDouble(); if (y_text == "NAN") y_value = std::numeric_limits::quiet_NaN(); else if (y_text == "INF") y_value = std::numeric_limits::infinity(); else if (y_text == "-INF") y_value = -std::numeric_limits::infinity(); else y_value = y_text.toDouble(); x_values[rowIndex] = x_value; y_values[rowIndex] = y_value; } QString title = QString("%1 ~ %2").arg(columnName).arg(xColumnName); parentSLiMWindow->plotLogFileData_2D(title, xColumnName, columnName, x_values, y_values, rowCount, makeScatterPlot); } } } } } } EidosValue_SP QtSLiMDebugOutputWindow::dataForColumn(LogFile *logFile, int64_t columnIndex) { const std::string &logfile_path = logFile->UserFilePath(); QTableWidget *table = logFileTableForPath(logfile_path); if (table) { int rowCount = table->rowCount(); int columnCount = table->columnCount(); if ((rowCount > 0) && (columnCount > 0) && (columnIndex < columnCount)) { bool columnIsNumeric = true; // were any conversion errors to double detected? for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *item = table->item(rowIndex, columnIndex); if (item) { QString itemText = item->text(); if ((itemText == "NAN") || (itemText == "INF") || (itemText == "-INF")) { // NAN / INF / -INF are acceptable as double values, although // whatever parser the data ends up in might not like it... } else { bool ok; itemText.toDouble(&ok); if (!ok) { columnIsNumeric = false; break; } } } } if (columnIsNumeric) { EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(rowCount); for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *item = table->item(rowIndex, columnIndex); QString text = item->text(); double value; if (text == "NAN") value = std::numeric_limits::quiet_NaN(); else if (text == "INF") value = std::numeric_limits::infinity(); else if (text == "-INF") value = -std::numeric_limits::infinity(); else value = text.toDouble(); float_result->set_float_no_check(value, rowIndex); } return EidosValue_SP(float_result); } else { EidosValue_String *string_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_String())->Reserve(rowCount); for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QTableWidgetItem *item = table->item(rowIndex, columnIndex); QString text = item->text(); string_result->PushString(text.toStdString()); } return EidosValue_SP(string_result); } } } return gStaticEidosValueNULL; } EidosValue_SP QtSLiMDebugOutputWindow::dataForColumn(LogFile *logFile, const std::string &columnName) { std::vector columnNames = logFile->SortedKeys_StringKeys(); auto columnIter = std::find(columnNames.begin(), columnNames.end(), columnName); if (columnIter != columnNames.end()) return dataForColumn(logFile, std::distance(columnNames.begin(), columnIter)); return gStaticEidosValueNULL; } void QtSLiMDebugOutputWindow::takeFileOutput(std::vector &lines, bool append, const std::string &path) { // First, find the index of the file view we're taking input into // If we didn't find one, make a new one auto pathIter = std::find(filePaths.begin(), filePaths.end(), path); QtSLiMTextEdit *fileview; size_t fileIndex; if (pathIter != filePaths.end()) { fileIndex = std::distance(filePaths.begin(), pathIter); fileview = fileViews[fileIndex]; } else { QLayout *windowLayout = this->layout(); fileview = new QtSLiMTextEdit(); fileview->setUndoRedoEnabled(false); fileview->setOptionClickEnabled(false); fileview->setCodeCompletionEnabled(false); fileview->setScriptType(QtSLiMTextEdit::NoScriptType); fileview->setSyntaxHighlightType(QtSLiMTextEdit::NoHighlighting); fileview->setReadOnly(true); fileview->setVisible(false); windowLayout->addWidget(fileview); // Make a new tab and add it at the end of the tab bar QString filename = QString::fromStdString(Eidos_LastPathComponent(path)); fileIndex = filePaths.size(); ui->tabBar->insertTab(ui->tabBar->count(), filename); ui->tabBar->setTabToolTip(ui->tabBar->count() - 1, filename); // Add the new view's info filePaths.emplace_back(path); fileViews.emplace_back(fileview); } // Then append the new text to the view, as with the built in textviews if (!append) fileview->setPlainText(""); fileview->moveCursor(QTextCursor::End); bool hasPrecedingLines = (fileview->textCursor().position() != 0); for (const std::string &str : lines) { if (hasPrecedingLines) fileview->insertPlainText("\n"); fileview->insertPlainText(QString::fromStdString(str)); hasPrecedingLines = true; } fileview->moveCursor(QTextCursor::End); tabReceivedInput(fileIndex + 3 + logfilePaths.size()); } void QtSLiMDebugOutputWindow::clearAllOutput(void) { ui->debugOutputTextEdit->setPlainText(""); ui->runOutputTextEdit->setPlainText(""); ui->schedulingOutputTextEdit->setPlainText(""); // Remove all tabs but the base three completely; they may not exist again after recycling while (ui->tabBar->count() > 3) ui->tabBar->removeTab(3); // Reset the base three tabs to the default text color ui->tabBar->setTabTextColor(0, ui->tabBar->tabTextColor(-1)); ui->tabBar->setTabTextColor(1, ui->tabBar->tabTextColor(-1)); ui->tabBar->setTabTextColor(2, ui->tabBar->tabTextColor(-1)); logfilePaths.clear(); logfileViews.clear(); logfileLineNumbers.clear(); filePaths.clear(); fileViews.clear(); } void QtSLiMDebugOutputWindow::clearOutputClicked(void) { if (ui->debugOutputTextEdit->isVisible()) ui->debugOutputTextEdit->setPlainText(""); if (ui->runOutputTextEdit->isVisible()) ui->runOutputTextEdit->setPlainText(""); if (ui->schedulingOutputTextEdit->isVisible()) ui->schedulingOutputTextEdit->setPlainText(""); for (QTableWidget *table : logfileViews) if (table->isVisible()) table->setRowCount(0); for (QtSLiMTextEdit *file : fileViews) if (file->isVisible()) file->setPlainText(""); } void QtSLiMDebugOutputWindow::selectedTabChanged(void) { int tabIndex = ui->tabBar->currentIndex(); ui->tabBar->setTabTextColor(tabIndex, ui->tabBar->tabTextColor(-1)); // set to an invalid color to clear to the default color if (tabIndex == 0) { showDebugOutput(); return; } else if (tabIndex == 1) { showRunOutput(); return; } else if (tabIndex == 2) { showSchedulingOutput(); return; } else { tabIndex -= 3; // zero-base the index for logfilePaths if ((tabIndex >= 0) && (tabIndex < (int)logfilePaths.size())) { showLogFile(tabIndex); return; } tabIndex -= logfilePaths.size(); // zero-base the index for filePaths if ((tabIndex >= 0) && (tabIndex < (int)fileViews.size())) { showFile(tabIndex); return; } qDebug() << "unexpected current tab index" << ui->tabBar->currentIndex() << "in selectedTabChanged()"; } } void QtSLiMDebugOutputWindow::clearOutputPressed(void) { ui->clearOutputButton->qtslimSetHighlight(true); } void QtSLiMDebugOutputWindow::clearOutputReleased(void) { ui->clearOutputButton->qtslimSetHighlight(false); } void QtSLiMDebugOutputWindow::closeEvent(QCloseEvent *p_event) { // Save the window position; see https://doc.qt.io/qt-5/qsettings.html#details QSettings settings; settings.beginGroup("QtSLiMDebugOutputWindow"); settings.setValue("size", size()); settings.setValue("pos", pos()); settings.endGroup(); // send our close signal emit willClose(); // use super's default behavior QWidget::closeEvent(p_event); } ================================================ FILE: QtSLiM/QtSLiMDebugOutputWindow.h ================================================ // // QtSLiMDebugOutputWindow.h // SLiM // // Created by Ben Haller on 02/06/2021. // Copyright (c) 2021-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMDEBUGOUTPUTWINDOW_H #define QTSLIMDEBUGOUTPUTWINDOW_H #include #include #include #include "eidos_globals.h" class QCloseEvent; class QtSLiMWindow; class QtSLiMTextEdit; class QTableWidget; class QTableWidgetItem; class LogFile; namespace Ui { class QtSLiMDebugOutputWindow; } class QtSLiMDebugOutputWindow : public QWidget { Q_OBJECT public: QtSLiMWindow *parentSLiMWindow = nullptr; // a copy of parent with the correct class, for convenience explicit QtSLiMDebugOutputWindow(QtSLiMWindow *p_parent = nullptr); virtual ~QtSLiMDebugOutputWindow() override; // Our various output views, which each collect output independently void takeDebugOutput(QString str); void takeRunOutput(QString str); void takeSchedulingOutput(QString str); void takeLogFileOutput(std::vector &lineElements, const std::string &path); void takeFileOutput(std::vector &lines, bool append, const std::string &path); EidosValue_SP dataForColumn(LogFile *logFile, int64_t columnIndex); EidosValue_SP dataForColumn(LogFile *logFile, const std::string &columnName); public slots: void clearAllOutput(void); void clearOutputClicked(void); void showDebugOutput(void); void showRunOutput(void); void showSchedulingOutput(void); void showLogFile(int logFileIndex); void showFile(int fileIndex); signals: void willClose(void); private slots: void tabReceivedInput(int tabIndex); void selectedTabChanged(void); virtual void closeEvent(QCloseEvent *p_event) override; void clearOutputPressed(void); void clearOutputReleased(void); void logFileRightClick(const QPoint &pos); private: Ui::QtSLiMDebugOutputWindow *ui; // all the LogFile paths we have seen, views containing their output, and the last line number emitted std::vector logfilePaths; std::vector logfileViews; std::vector logfileLineNumbers; QTableWidget *logFileTableForPath(const std::string &path); // all the ordinary file paths we have seen, from writeFile() and similar, and output views std::vector filePaths; std::vector fileViews; void resetTabIcons(void); void hideAllViews(void); }; #endif // QTSLIMDEBUGOUTPUTWINDOW_H ================================================ FILE: QtSLiM/QtSLiMDebugOutputWindow.ui ================================================ QtSLiMDebugOutputWindow 0 0 700 500 700 250 Output Viewer 3 3 3 3 3 4 20 20 20 20 Qt::NoFocus <html><head/><body><p>clear the selected output log</p></body></html> :/buttons/delete.png :/buttons/delete_H.png:/buttons/delete.png 20 20 true Qt::Horizontal 10 10 false true false true false true QtSLiMPushButton QPushButton
QtSLiMExtras.h
QtSLiMTextEdit QPlainTextEdit
QtSLiMScriptTextEdit.h
QTabBar QWidget
QTabBar.h
1
================================================ FILE: QtSLiM/QtSLiMEidosConsole.cpp ================================================ // // QtSLiMEidosConsole.cpp // SLiM // // Created by Ben Haller on 12/6/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMEidosConsole.h" #include "ui_QtSLiMEidosConsole.h" #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "QtSLiMVariableBrowser.h" #include "QtSLiMExtras.h" QtSLiMEidosConsole::QtSLiMEidosConsole(QtSLiMWindow *p_parent) : QWidget(p_parent, Qt::Window), // the console window has us as a parent, but is still a standalone window parentSLiMWindow(p_parent), ui(new Ui::QtSLiMEidosConsole) { ui->setupUi(this); interpolateSplitters(); glueUI(); // no window icon #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect setWindowIcon(QIcon()); #endif // prevent this window from keeping the app running when all main windows are closed setAttribute(Qt::WA_QuitOnClose, false); // add a status bar at the bottom; there is a layout in Designer for it already // thanks to https://stackoverflow.com/a/6143818/2752221 statusBar_ = new QtSLiMStatusBar(this); ui->statusBarLayout->addWidget(statusBar_); statusBar_->setMaximumHeight(statusBar_->sizeHint().height()); // set up the script view to syntax highlight ui->scriptTextEdit->setScriptType(QtSLiMTextEdit::EidosScriptType); ui->scriptTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::ScriptHighlighting); // set up the console view to be Eidos script, but without highlighting ui->consoleTextEdit->setScriptType(QtSLiMTextEdit::EidosScriptType); ui->consoleTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::NoHighlighting); // enable option-click in both textedits ui->scriptTextEdit->setOptionClickEnabled(true); ui->consoleTextEdit->setOptionClickEnabled(true); // enable code completion in both textedits ui->scriptTextEdit->setCodeCompletionEnabled(true); ui->consoleTextEdit->setCodeCompletionEnabled(true); // set initial text in console and show the initial prompt QtSLiMConsoleTextEdit *console = ui->consoleTextEdit; console->showWelcome(); console->showPrompt(); console->setFocus(); // Restore the saved window position; see https://doc.qt.io/qt-5/qsettings.html#details QSettings settings; settings.beginGroup("QtSLiMEidosConsole"); resize(settings.value("size", QSize(550, 400)).toSize()); move(settings.value("pos", QPoint(25, 45)).toPoint()); settings.endGroup(); // Enable our UI initially setInterfaceEnabled(true); // Execute a null statement to get our symbols set up, for code completion etc. // Note this has the side effect of creating a random number generator gEidos_RNG for our use. validateSymbolTableAndFunctionMap(); } QtSLiMEidosConsole::~QtSLiMEidosConsole() { delete ui; delete global_symbols; global_symbols = nullptr; if (global_function_map_owned) delete global_function_map; global_function_map = nullptr; } void QtSLiMEidosConsole::interpolateSplitters(void) { #if 1 // add a top-level horizontal splitter const int splitterMargin = 0; QLayout *parentLayout = ui->overallLayout; QVBoxLayout *firstSubLayout = ui->scriptLayout; QVBoxLayout *secondSubLayout = ui->outputLayout; // force geometry calculation, which is lazy setAttribute(Qt::WA_DontShowOnScreen, true); show(); hide(); setAttribute(Qt::WA_DontShowOnScreen, false); // get the geometry we need QMargins marginsP = parentLayout->contentsMargins(); QMargins marginsS1 = firstSubLayout->contentsMargins(); QMargins marginsS2 = secondSubLayout->contentsMargins(); // change fixed-size views to be flexible, so they cooperate with the splitter ui->scriptTextEdit->setMinimumWidth(250); ui->consoleTextEdit->setMinimumWidth(250); // empty out parentLayout QLayoutItem *child; while ((child = parentLayout->takeAt(0)) != nullptr); // make the new top-level widgets and transfer in their contents scriptWidget = new QWidget(nullptr); scriptWidget->setLayout(firstSubLayout); firstSubLayout->setContentsMargins(QMargins(marginsS1.left() + marginsP.left(), marginsS1.top() + marginsP.top(), marginsS1.right() + splitterMargin, marginsS1.bottom() + marginsP.bottom())); outputWidget = new QWidget(nullptr); outputWidget->setLayout(secondSubLayout); secondSubLayout->setContentsMargins(QMargins(marginsS2.left() + splitterMargin, marginsS2.top() + marginsP.top(), marginsS2.right() + marginsP.right(), marginsS2.bottom() + marginsP.bottom())); // make the QSplitter between the left and right and add the subsidiary widgets to it splitter = new QSplitter(Qt::Horizontal, this); splitter->setChildrenCollapsible(true); splitter->addWidget(scriptWidget); splitter->addWidget(outputWidget); splitter->setHandleWidth(splitter->handleWidth() + 3); splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 2); // initially, give 2/3 of the width to the output widget // and finally, add the splitter to the parent layout parentLayout->addWidget(splitter); parentLayout->setContentsMargins(0, 0, 0, 0); #endif } void QtSLiMEidosConsole::closeEvent(QCloseEvent *p_event) { // Save the window position; see https://doc.qt.io/qt-5/qsettings.html#details QSettings settings; settings.beginGroup("QtSLiMEidosConsole"); settings.setValue("size", size()); settings.setValue("pos", pos()); settings.endGroup(); // send our close signal emit willClose(); // use super's default behavior QWidget::closeEvent(p_event); } void QtSLiMEidosConsole::showBrowserClicked(void) { if (!variableBrowser_) { variableBrowser_ = new QtSLiMVariableBrowser(this); if (variableBrowser_) { variableBrowser_->setAttribute(Qt::WA_DeleteOnClose); // wire ourselves up to monitor the console for closing, to free connect(variableBrowser_, &QtSLiMVariableBrowser::willClose, this, [this]() { variableBrowser_ = nullptr; // deleted on close }); } else { qDebug() << "Could not create variable browser"; } } QtSLiMMakeWindowVisibleAndExposed(variableBrowser_); } QStatusBar *QtSLiMEidosConsole::statusBar(void) { return statusBar_; } QtSLiMScriptTextEdit *QtSLiMEidosConsole::scriptTextEdit(void) { return ui->scriptTextEdit; } QtSLiMConsoleTextEdit *QtSLiMEidosConsole::consoleTextEdit(void) { return ui->consoleTextEdit; } QtSLiMVariableBrowser *QtSLiMEidosConsole::variableBrowser(void) { return variableBrowser_; } // enable/disable the user interface as the simulation's state changes void QtSLiMEidosConsole::setInterfaceEnabled(bool enabled) { ui->checkScriptButton->setEnabled(enabled); ui->prettyprintButton->setEnabled(enabled); ui->executeAllButton->setEnabled(enabled); ui->executeSelectionButton->setEnabled(enabled); ui->consoleTextEdit->setReadOnlyDueToActivation(!enabled); } // Throw away the current symbol table void QtSLiMEidosConsole::invalidateSymbolTableAndFunctionMap(void) { if (global_symbols) { delete global_symbols; global_symbols = nullptr; } if (global_function_map) { if (global_function_map_owned) delete global_function_map; global_function_map = nullptr; } if (variableBrowser_) variableBrowser_->reloadBrowser(false); // false tells the browser we're now invalid } // Make a new symbol table from our delegate's current state; this actually executes a minimal script, ";", // to produce the symbol table as a side effect of setting up for the script's execution void QtSLiMEidosConsole::validateSymbolTableAndFunctionMap(void) { if (!global_symbols || !global_function_map) { QString errorString; _executeScriptString(";", nullptr, nullptr, nullptr, &errorString, false); if (errorString.length()) qDebug() << "Error in validateSymbolTableAndFunctionMap: " << errorString; } if (variableBrowser_) variableBrowser_->reloadBrowser(true); } // Low-level script execution QString QtSLiMEidosConsole::_executeScriptString(QString scriptString, QString *tokenString, QString *parseString, QString *executionString, QString *errorString, bool semicolonOptional) { // the back end can't handle Unicode well at present, being based on std::string... scriptString.replace(QChar::ParagraphSeparator, "\n"); scriptString.replace(QChar::LineSeparator, "\n"); std::string script_string(scriptString.toStdString()); EidosScript script(script_string); std::string output; // Unfortunately, running readFromPopulationFile() is too much of a shock for SLiMgui. It invalidates variables that are being displayed in // the variable browser, in such an abrupt way that it causes a crash. Basically, the code in readFromPopulationFile() that "cleans" all // references to mutations and such does not have any way to clean SLiMgui's references, and so those stale references cause a crash. // There is probably a better solution, but for now, we look for code containing readFromPopulationFile() and special-case it. The user // could circumvent this and trigger a crash, so this is just a band-aid; a proper solution is needed. Another problem with this band-aid // is that SLiMgui's display does not refresh to show the new population state. Indeed, that is an issue with anything that changes the // visible state, such as adding new mutations. There needs to be some way for Eidos code to tell SLiMgui that UI refreshing is needed, // and to clean references to variables that are about to invalidated. FIXME bool safeguardReferences = false; if (scriptString.contains("readFromPopulationFile")) safeguardReferences = true; if (safeguardReferences) invalidateSymbolTableAndFunctionMap(); // Make the final semicolon optional if requested; this allows input like "6+7" in the console if (semicolonOptional) script.SetFinalSemicolonOptional(true); // Tokenize try { script.Tokenize(); if (tokenString) { std::ostringstream token_stream; script.PrintTokens(token_stream); std::string &&token_string = token_stream.str(); *tokenString = QString::fromStdString(token_string); } } catch (...) { std::string &&error_string = Eidos_GetUntrimmedRaiseMessage(); *errorString = QString::fromStdString(error_string); // move the error outside of the currentScript context if possible; the ranges // should have already been moved by TranslateErrorContextToUserScript() if (gEidosErrorContext.currentScript == &script) { #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: clearing gEidosErrorContext.currentScript after error in tokenization." << std::endl; #endif gEidosErrorContext.currentScript = nullptr; } else if (gEidosErrorContext.currentScript) { // The error got translated to a script we don't recognize; clear the error info, // all we can do is show the error string to the user, with no position errorString->insert(0, "A tokenization error occurred in a different script context, so the error position cannot be highlighted in the console:\n"); #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: error in tokenization traced to a different script; clearing all error info." << std::endl; #endif ClearErrorContext(); } return nullptr; } // Parse, an "interpreter block" bounded by an EOF rather than a "script block" that requires braces try { script.ParseInterpreterBlockToAST(true); if (parseString) { std::ostringstream parse_stream; script.PrintAST(parse_stream); std::string &&parse_string = parse_stream.str(); *parseString = QString::fromStdString(parse_string); } } catch (...) { std::string &&error_string = Eidos_GetUntrimmedRaiseMessage(); *errorString = QString::fromStdString(error_string); // move the error outside of the currentScript context if possible; the ranges // should have already been moved by TranslateErrorContextToUserScript() if (gEidosErrorContext.currentScript == &script) { #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: clearing gEidosErrorContext.currentScript after error in parsing." << std::endl; #endif gEidosErrorContext.currentScript = nullptr; } else if (gEidosErrorContext.currentScript) { // The error got translated to a script we don't recognize; clear the error info, // all we can do is show the error string to the user, with no position errorString->insert(0, "A parsing error occurred in a different script context, so the error position cannot be highlighted in the console:\n"); #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: error in parsing traced to a different script; clearing all error info." << std::endl; #endif ClearErrorContext(); } return nullptr; } // Get a symbol table and let SLiM add symbols to it if (!global_symbols) { global_symbols = gEidosConstantsSymbolTable; // in SLiMgui this comes from the delegate method eidosConsoleWindowController:symbolsFromBaseSymbols: if (parentSLiMWindow->community && !parentSLiMWindow->invalidSimulation()) global_symbols = parentSLiMWindow->community->SymbolsFromBaseSymbols(global_symbols); // With the advant of global versus local symbol tables, the semantics here have gotten a little tricky. In EidosScribe // we want the console to work in the global variables table directly, which we need to create; that will be our symbol // table. In SLiM, we want the console to work in a local variables table; SLiM has its own global variables table, // which we don't want to clutter up, just as if were were in a callback. So here, we now check whether a global variables // table is already in the chain, and if so, we create a local variables table; otherwise we create a global variables table. bool global_variables_table_exists = false; EidosSymbolTable *scan_table = global_symbols; while (scan_table) { if (scan_table->TableType() == EidosSymbolTableType::kGlobalVariablesTable) { global_variables_table_exists = true; break; } scan_table = scan_table->ChainSymbolTable(); } EidosSymbolTableType console_table_type = global_variables_table_exists ? EidosSymbolTableType::kLocalVariablesTable : EidosSymbolTableType::kGlobalVariablesTable; global_symbols = new EidosSymbolTable(console_table_type, global_symbols); // add a table for script-defined variables on top } // Get a function map from SLiM, or make one ourselves if (!global_function_map) { global_function_map_owned = false; if (parentSLiMWindow->community && !parentSLiMWindow->invalidSimulation()) global_function_map = &parentSLiMWindow->community->FunctionMap(); if (!global_function_map) { global_function_map = new EidosFunctionMap(*EidosInterpreter::BuiltInFunctionMap()); global_function_map_owned = true; } } // Get the EidosContext, if any, from SLiM EidosContext *eidos_context = parentSLiMWindow->community; // Interpret the parsed block parentSLiMWindow->willExecuteScript(); std::ostringstream outstream; // in the Eidos console, one output stream for both types of output EidosInterpreter interpreter(script, *global_symbols, *global_function_map, eidos_context, outstream, outstream #ifdef SLIMGUI , true #endif ); try { if (executionString) interpreter.SetShouldLogExecution(true); EidosValue_SP result = interpreter.EvaluateInterpreterBlock(true, true); // print output, return the last statement value (result not used) output = outstream.str(); if (variableBrowser_) variableBrowser_->reloadBrowser(true); if (executionString) { std::string &&execution_string = interpreter.ExecutionLog(); *executionString = QString::fromStdString(execution_string); } } catch (...) { parentSLiMWindow->didExecuteScript(); output = outstream.str(); std::string &&error_string = Eidos_GetUntrimmedRaiseMessage(); *errorString = QString::fromStdString(error_string); // move the error outside of the currentScript context if possible; the ranges // should have already been moved by TranslateErrorContextToUserScript() if (gEidosErrorContext.currentScript == &script) { #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: clearing gEidosErrorContext.currentScript after error in execution." << std::endl; #endif gEidosErrorContext.currentScript = nullptr; } else if (gEidosErrorContext.currentScript) { // The error got translated to a script we don't recognize; clear the error info, // all we can do is show the error string to the user, with no position errorString->insert(0, "An execution error occurred in a different script context, so the error position cannot be highlighted in the console:\n"); #if EIDOS_DEBUG_ERROR_POSITIONS std::cout << "-[EidosConsoleWindowController _executeScriptString:...]: error in execution traced to a different script; clearing all error info." << std::endl; #endif ClearErrorContext(); } return QString::fromStdString(output); } parentSLiMWindow->didExecuteScript(); // See comment on safeguardReferences above if (safeguardReferences) validateSymbolTableAndFunctionMap(); // Flush buffered output to files after every script execution, so the user sees the results // NOTE THAT THE WORKING DIRECTORY HAS BEEN CHANGED BACK AT THIS POINT! bool flush_success = Eidos_FlushFiles(); if (!flush_success) *errorString = "ERROR (Eidos_FlushFiles): A compressed file buffer failed to write out to disk. Please check file paths, filesystem writeability and permissions, available disk space, and other possible causes of file I/O problems.\n"; return QString::fromStdString(output); } // Execute the given script string, with the terminating semicolon being optional if requested void QtSLiMEidosConsole::executeScriptString(QString scriptString, bool semicolonOptional) { QString tokenString, parseString, executionString, errorString; bool showTokens = false; //[defaults boolForKey:EidosDefaultsShowTokensKey]; bool showParse = false; //[defaults boolForKey:EidosDefaultsShowParseKey]; bool showExecution = false; //[defaults boolForKey:EidosDefaultsShowExecutionKey]; QtSLiMConsoleTextEdit *console = ui->consoleTextEdit; QString result = _executeScriptString(scriptString, showTokens ? &tokenString : nullptr, showParse ? &parseString : nullptr, showExecution ? &executionString : nullptr, &errorString, semicolonOptional); if (errorString.contains("unexpected token 'EOF'")) { // The user has entered an incomplete script line, so we use a continuation prompt console->showContinuationPrompt(); } else { console->appendExecution(result, errorString, tokenString, parseString, executionString); console->showPrompt(); } } // // public slots // void QtSLiMEidosConsole::executeAllClicked(void) { QString all = ui->scriptTextEdit->toPlainText(); ui->consoleTextEdit->setCommandAtPrompt(all); ui->consoleTextEdit->executeCurrentPrompt(); } void QtSLiMEidosConsole::executeSelectionClicked(void) { QTextCursor selectionCursor(ui->scriptTextEdit->textCursor()); if (selectionCursor.selectionStart() == selectionCursor.selectionEnd()) { // zero-length selections get extended to encompass the full line selectionCursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); selectionCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } QString selection = selectionCursor.selectedText(); ui->consoleTextEdit->setCommandAtPrompt(selection); ui->consoleTextEdit->executeCurrentPrompt(); } void QtSLiMEidosConsole::executePromptScript(QString executionString) { executeScriptString(executionString, true); } ================================================ FILE: QtSLiM/QtSLiMEidosConsole.h ================================================ // // QtSLiMEidosConsole.h // SLiM // // Created by Ben Haller on 12/6/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMEIDOSCONSOLE_H #define QTSLIMEIDOSCONSOLE_H #include #include #include class QCloseEvent; class QtSLiMWindow; class QStatusBar; class QSplitter; class QtSLiMScriptTextEdit; class QtSLiMConsoleTextEdit; class QtSLiMVariableBrowser; #include "eidos_script.h" #include "eidos_globals.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" namespace Ui { class QtSLiMEidosConsole; } class QtSLiMEidosConsole : public QWidget { Q_OBJECT public: QtSLiMWindow *parentSLiMWindow = nullptr; // a copy of parent with the correct class, for convenience explicit QtSLiMEidosConsole(QtSLiMWindow *p_parent = nullptr); virtual ~QtSLiMEidosConsole() override; // Enable/disable the user interface as the simulation's state changes void setInterfaceEnabled(bool enabled); // Throw away the current symbol table void invalidateSymbolTableAndFunctionMap(void); // Make a new symbol table from our delegate's current state; this actually executes a minimal script, ";", // to produce the symbol table as a side effect of setting up for the script's execution void validateSymbolTableAndFunctionMap(void); EidosSymbolTable *symbolTable(void) { return global_symbols; } // Execute the given script string, with the terminating semicolon being optional if requested void executeScriptString(QString scriptString, bool semicolonOptional); // Access to key UI items QStatusBar *statusBar(void); QtSLiMScriptTextEdit *scriptTextEdit(void); QtSLiMConsoleTextEdit *consoleTextEdit(void); QtSLiMVariableBrowser *variableBrowser(void); public slots: void executeAllClicked(void); void executeSelectionClicked(void); void showBrowserClicked(void); signals: void willClose(void); // // UI glue, defined in QtSLiMEidosConsole_glue.cpp // private slots: void checkScriptPressed(void); void checkScriptReleased(void); void prettyprintPressed(void); void prettyprintReleased(void); void scriptHelpPressed(void); void scriptHelpReleased(void); void showBrowserPressed(void); void showBrowserReleased(void); void executeSelectionPressed(void); void executeSelectionReleased(void); void executeAllPressed(void); void executeAllReleased(void); void executePromptScript(QString executionString); void clearOutputPressed(void); void clearOutputReleased(void); virtual void closeEvent(QCloseEvent *p_event) override; private: Ui::QtSLiMEidosConsole *ui; QStatusBar *statusBar_ = nullptr; void glueUI(void); bool interfaceEnabled = false; // set to false when the simulation is running or invalid // Variable browser support QtSLiMVariableBrowser *variableBrowser_ = nullptr; // The symbol table for the console interpreter; needs to be wiped whenever the symbol table changes EidosSymbolTable *global_symbols = nullptr; // The function map for the console interpreter; carries over from invocation to invocation EidosFunctionMap *global_function_map = nullptr; bool global_function_map_owned = false; // Execution internals QString _executeScriptString(QString scriptString, QString *tokenString, QString *parseString, QString *executionString, QString *errorString, bool semicolonOptional); // splitter support void interpolateSplitters(void); QWidget *scriptWidget = nullptr; QWidget *outputWidget = nullptr; QSplitter *splitter = nullptr; }; #endif // QTSLIMEIDOSCONSOLE_H ================================================ FILE: QtSLiM/QtSLiMEidosConsole.ui ================================================ QtSLiMEidosConsole 0 0 700 500 700 250 Eidos Console 0 0 0 0 0 6 3 3 3 3 3 3 20 20 20 20 Qt::NoFocus <html><head/><body><p>check script syntax</p></body></html> :/buttons/check.png :/buttons/check_H.png:/buttons/check.png 20 20 true 20 20 20 20 Qt::NoFocus <html><head/><body><p>prettyprint script</p></body></html> :/buttons/prettyprint.png :/buttons/prettyprint_H.png:/buttons/prettyprint.png 20 20 true 20 20 20 20 Qt::NoFocus <html><head/><body><p>scripting help</p></body></html> :/buttons/syntax_help.png :/buttons/syntax_help_H.png:/buttons/syntax_help.png 20 20 true 20 20 20 20 Qt::NoFocus <html><head/><body><p>show Eidos variable browser</p></body></html> :/buttons/show_browser.png :/buttons/show_browser_H.png:/buttons/show_browser.png 20 20 true true Eidos: Qt::Horizontal 40 20 20 20 20 20 Qt::NoFocus <html><head/><body><p>execute selection</p></body></html> :/buttons/execute_selection.png :/buttons/execute_selection_H.png:/buttons/execute_selection.png 20 20 true true 20 20 20 20 Qt::NoFocus <html><head/><body><p>execute full script</p></body></html> :/buttons/execute_script.png :/buttons/execute_script_H.png:/buttons/execute_script.png 20 20 true 3 3 20 20 20 20 Qt::NoFocus <html><head/><body><p>clear output log</p></body></html> :/buttons/delete.png :/buttons/delete_H.png:/buttons/delete.png 20 20 true Console: Qt::Horizontal 40 20 QtSLiMPushButton QPushButton
QtSLiMExtras.h
QtSLiMScriptTextEdit QPlainTextEdit
QtSLiMScriptTextEdit.h
QtSLiMConsoleTextEdit QPlainTextEdit
QtSLiMConsoleTextEdit.h
================================================ FILE: QtSLiM/QtSLiMEidosConsole_glue.cpp ================================================ // // QtSLiMEidosConsole_glue.cpp // SLiM // // Created by Ben Haller on 12/6/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMEidosConsole.h" #include "ui_QtSLiMEidosConsole.h" #include #include #include #include "QtSLiMAppDelegate.h" void QtSLiMEidosConsole::glueUI(void) { connect(ui->consoleTextEdit, &QtSLiMConsoleTextEdit::executeScript, this, &QtSLiMEidosConsole::executePromptScript); // connect all QtSLiMEidosConsole slots connect(ui->checkScriptButton, &QPushButton::clicked, ui->scriptTextEdit, &QtSLiMTextEdit::checkScript); connect(ui->prettyprintButton, &QPushButton::clicked, ui->scriptTextEdit, &QtSLiMTextEdit::prettyprintClicked); connect(ui->scriptHelpButton, &QPushButton::clicked, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_help); connect(ui->browserButton, &QPushButton::clicked, this, &QtSLiMEidosConsole::showBrowserClicked); connect(ui->executeSelectionButton, &QPushButton::clicked, this, &QtSLiMEidosConsole::executeSelectionClicked); connect(ui->executeAllButton, &QPushButton::clicked, this, &QtSLiMEidosConsole::executeAllClicked); connect(ui->clearOutputButton, &QPushButton::clicked, ui->consoleTextEdit, &QtSLiMConsoleTextEdit::clearToPrompt); // set up QtSLiMPushButton "base names" for all buttons ui->checkScriptButton->qtslimSetBaseName("check"); ui->prettyprintButton->qtslimSetBaseName("prettyprint"); ui->scriptHelpButton->qtslimSetBaseName("syntax_help"); ui->browserButton->qtslimSetBaseName("show_browser"); ui->executeSelectionButton->qtslimSetBaseName("execute_selection"); ui->executeAllButton->qtslimSetBaseName("execute_script"); ui->clearOutputButton->qtslimSetBaseName("delete"); // set up all icon-based QPushButtons to change their icon as they track connect(ui->checkScriptButton, &QPushButton::pressed, this, &QtSLiMEidosConsole::checkScriptPressed); connect(ui->checkScriptButton, &QPushButton::released, this, &QtSLiMEidosConsole::checkScriptReleased); connect(ui->prettyprintButton, &QPushButton::pressed, this, &QtSLiMEidosConsole::prettyprintPressed); connect(ui->prettyprintButton, &QPushButton::released, this, &QtSLiMEidosConsole::prettyprintReleased); connect(ui->scriptHelpButton, &QPushButton::pressed, this, &QtSLiMEidosConsole::scriptHelpPressed); connect(ui->scriptHelpButton, &QPushButton::released, this, &QtSLiMEidosConsole::scriptHelpReleased); connect(ui->browserButton, &QPushButton::pressed, this, &QtSLiMEidosConsole::showBrowserPressed); connect(ui->browserButton, &QPushButton::released, this, &QtSLiMEidosConsole::showBrowserReleased); connect(ui->executeSelectionButton, &QPushButton::pressed, this, &QtSLiMEidosConsole::executeSelectionPressed); connect(ui->executeSelectionButton, &QPushButton::released, this, &QtSLiMEidosConsole::executeSelectionReleased); connect(ui->executeAllButton, &QPushButton::pressed, this, &QtSLiMEidosConsole::executeAllPressed); connect(ui->executeAllButton, &QPushButton::released, this, &QtSLiMEidosConsole::executeAllReleased); connect(ui->clearOutputButton, &QPushButton::pressed, this, &QtSLiMEidosConsole::clearOutputPressed); connect(ui->clearOutputButton, &QPushButton::released, this, &QtSLiMEidosConsole::clearOutputReleased); // make window actions for all global menu items qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); } // // private slots // void QtSLiMEidosConsole::checkScriptPressed(void) { ui->checkScriptButton->qtslimSetHighlight(true); } void QtSLiMEidosConsole::checkScriptReleased(void) { ui->checkScriptButton->qtslimSetHighlight(false); } void QtSLiMEidosConsole::prettyprintPressed(void) { ui->prettyprintButton->qtslimSetHighlight(true); } void QtSLiMEidosConsole::prettyprintReleased(void) { ui->prettyprintButton->qtslimSetHighlight(false); } void QtSLiMEidosConsole::scriptHelpPressed(void) { ui->scriptHelpButton->qtslimSetHighlight(true); } void QtSLiMEidosConsole::scriptHelpReleased(void) { ui->scriptHelpButton->qtslimSetHighlight(false); } void QtSLiMEidosConsole::showBrowserPressed(void) { ui->browserButton->qtslimSetHighlight(true); } void QtSLiMEidosConsole::showBrowserReleased(void) { ui->browserButton->qtslimSetHighlight(false); } void QtSLiMEidosConsole::executeSelectionPressed(void) { ui->executeSelectionButton->qtslimSetHighlight(true); } void QtSLiMEidosConsole::executeSelectionReleased(void) { ui->executeSelectionButton->qtslimSetHighlight(false); } void QtSLiMEidosConsole::executeAllPressed(void) { ui->executeAllButton->qtslimSetHighlight(true); } void QtSLiMEidosConsole::executeAllReleased(void) { ui->executeAllButton->qtslimSetHighlight(false); } void QtSLiMEidosConsole::clearOutputPressed(void) { ui->clearOutputButton->qtslimSetHighlight(true); } void QtSLiMEidosConsole::clearOutputReleased(void) { ui->clearOutputButton->qtslimSetHighlight(false); } ================================================ FILE: QtSLiM/QtSLiMEidosPrettyprinter.cpp ================================================ // // QtSLiMEidosPrettyprinter.cpp // SLiM // // Created by Ben Haller on 8/1/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMEidosPrettyprinter.h" #include #include #include #include #include #include #include static int Eidos_indentForStack(std::vector &indentStack, bool startingNewStatement, EidosTokenType nextTokenType) { // Count the number of indents represented by the indent stack. When a control-flow keyword is // followed by a left brace, the indent stack has two items on it, but we want to only indent // one level. int indent = 0; bool previousIndentStackItemWasControlFlow = false; for (size_t stackIndex = 0; stackIndex < indentStack.size(); ++stackIndex) { EidosTokenType stackTokenType = indentStack[stackIndex]->token_type_; // skip over ternary conditionals; they do not generate indent, but are on the stack so we can match elses if (stackTokenType == EidosTokenType::kTokenConditional) continue; if (previousIndentStackItemWasControlFlow && (stackTokenType == EidosTokenType::kTokenLBrace)) ; else ++indent; previousIndentStackItemWasControlFlow = !(stackTokenType == EidosTokenType::kTokenLBrace); } bool lastIndentIsControlFlow = previousIndentStackItemWasControlFlow; // Indent when continuing a statement, but not after a control-flow token. The idea here is that // if you have a structure like: // // if (x) // if (y) // ; // // the indent stack will already dictate that is indented twice; it does not need to // receive the !startingNewStatement extra indent level that we normally add to cause continuing // statements to be indented like: // // x = a + b + c + // d + e + f; // if (!startingNewStatement && !lastIndentIsControlFlow) indent++; // If the next token is a left brace, outdent one level, conventionally. This reflects usage like: // // if (x) // y; // // if (x) // { // y; // } // // This applies only if the last element on the indent stack is a control-flow indent, not a {. // This is the same rule we used when counting indentStack, but applied to nextTokenType. We also // outdent when we see a left brace if we are mid-statement; this covers SLiM callback syntax. // if ((lastIndentIsControlFlow || !startingNewStatement) && (nextTokenType == EidosTokenType::kTokenLBrace)) indent--; // For similar reasons, if the next token is a right brace, always outdent one level if (nextTokenType == EidosTokenType::kTokenRBrace) indent--; return indent; } bool Eidos_prettyprintTokensFromScript(const std::vector &tokens, EidosScript &tokenScript, std::string &pretty) { // We keep a stack of indent-generating tokens: { if else do while for. The purpose of this is // to be able to tell what indent level we're at, and how it changes with a ; or a } token. std::vector indentStack; bool startingNewStatement = true; size_t tokenCount = tokens.size(); for (size_t tokenIndex = 0; tokenIndex < tokenCount; ++tokenIndex) { const EidosToken &token = tokens[tokenIndex]; std::string tokenString(token.token_string_); EidosTokenType nextTokenPeek = (tokenIndex + 1 < tokenCount ? tokens[tokenIndex+1].token_type_ : EidosTokenType::kTokenEOF); // Find the next non-whitespace, non-comment token for lookahead size_t nextSignificantTokenPeekIndex = tokenIndex + 1; EidosTokenType nextSignificantTokenPeek = EidosTokenType::kTokenEOF; while (nextSignificantTokenPeekIndex < tokenCount) { EidosTokenType peek = tokens[nextSignificantTokenPeekIndex].token_type_; if ((peek != EidosTokenType::kTokenWhitespace) && (peek != EidosTokenType::kTokenComment) && (peek != EidosTokenType::kTokenCommentLong)) { nextSignificantTokenPeek = peek; break; } nextSignificantTokenPeekIndex++; } switch (token.token_type_) { // These token types are not used in the AST and should not be present case EidosTokenType::kTokenNone: case EidosTokenType::kTokenBad: return false; // These are virtual tokens that can be ignored case EidosTokenType::kTokenEOF: case EidosTokenType::kTokenInterpreterBlock: case EidosTokenType::kTokenContextFile: case EidosTokenType::kTokenContextEidosBlock: case EidosTokenType::kFirstIdentifierLikeToken: break; // This is where the rubber meets the road; prettyprinting is all about altering whitespace stretches. // We don't want to alter the user's newline decisions, so we count the number of newlines in this // whitespace stretch and always emit the same number. If there are no newlines, we're in whitespace // inside a given line, with tokens on both sides; for the time being we do not alter those at all. // If there are newlines, though, then each newline is changed to be followed by the appropriate number // of tabs as indentation. The indent depends upon the indent stack and some other state about the // context we are in. case EidosTokenType::kTokenWhitespace: { // We use QString to get intelligent treatment of Unicode and UTF-8; std::string is just too dumb. // In SLiMGui this is done with NSString. We want to count newlines in a mac/unix/windows agnostic way. const QString q_tokenString = QString::fromStdString(tokenString); int newlineCount = 0; bool prevWasCR = false, prevWasLF = false; for (QChar qch : q_tokenString) { if (qch == QChar::LineFeed) { if (prevWasCR) { prevWasCR = false; continue; } // CR-LF counts for one newlineCount++; prevWasLF = true; } else if (qch == QChar::CarriageReturn) { if (prevWasLF) { prevWasLF = false; continue; } // LF-CR counts for one newlineCount++; prevWasCR = true; } else if (qch == QChar::ParagraphSeparator) { newlineCount++; prevWasCR = prevWasLF = false; } } if (newlineCount <= 0) { // Normally, whitespace tokens that do not contain a newline occur inside a line, and should be preserved. // A whitespace token that indents the start of a line normally started on the previous line and contains // a newline. However, this is not the case at the very beginning of a script; the first token is special. if (tokenIndex > 0) pretty.append(tokenString); } else { int indent = Eidos_indentForStack(indentStack, startingNewStatement, nextTokenPeek); for (int lineCounter = 0; lineCounter < newlineCount; ++lineCounter) { pretty.append("\n"); for (int tabCounter = 0; tabCounter < indent; ++tabCounter) pretty.append("\t"); } } break; } // We have ended a statement, so we reset our indent levels case EidosTokenType::kTokenSemicolon: { // Pop indent-generating tokens that have expired with the end of this statement; a semicolon terminates // a whole nested series of if else do while for, but does not terminate an enclosing { block. Also, // if there are nested if statements, a semicolon terminates only the first one if the next token is an else. while (indentStack.size() > 0) { const EidosToken *topIndentToken = indentStack.back(); if (topIndentToken->token_type_ == EidosTokenType::kTokenLBrace) break; if ((topIndentToken->token_type_ == EidosTokenType::kTokenIf) && (nextSignificantTokenPeek == EidosTokenType::kTokenElse)) { indentStack.pop_back(); break; } indentStack.pop_back(); } pretty.append(tokenString); break; } // Track braces case EidosTokenType::kTokenLBrace: { indentStack.emplace_back(&token); pretty.append(tokenString); break; } case EidosTokenType::kTokenRBrace: { // First pop the matching left brace if ((indentStack.size() == 0) || (indentStack.back()->token_type_ != EidosTokenType::kTokenLBrace)) { // All other indent-producing tokens should already have been balanced; Eidos has no implicit termination of statements //NSLog(@"Unbalanced '}' token in prettyprinting!"); return false; } indentStack.pop_back(); // Then pop indent-generating tokens above the left brace that have expired with the end of this statement while (indentStack.size() > 0) { const EidosToken *topIndentToken = indentStack.back(); if (topIndentToken->token_type_ == EidosTokenType::kTokenLBrace) break; indentStack.pop_back(); } pretty.append(tokenString); break; } // Control-flow keywords influence our indent level; this might look like the normal statement inner indent, // but it is not, as can be seen when these control-flow keywords are nested like 'if (x) if (y) '. // When an if follows an else, the else is removed by the if, since we don't want two indents; else-if is one indent. case EidosTokenType::kTokenIf: { if (indentStack.size()) { EidosTokenType top_token_type = indentStack.back()->token_type_; if (top_token_type == EidosTokenType::kTokenElse) indentStack.pop_back(); } indentStack.emplace_back(&token); pretty.append(tokenString); break; } case EidosTokenType::kTokenDo: case EidosTokenType::kTokenWhile: case EidosTokenType::kTokenFor: case EidosTokenType::kTokenConditional: // note this does not generate indent, but is put on the stack { indentStack.emplace_back(&token); pretty.append(tokenString); break; } // else can be paired with if or ?. In the former case, the if will be off the stack by the time the else // is encountered, and we put the else on to give us an equivalent indent. In the latter case, we consider // the expressions within the ternary conditional to be statement-level; we don't indent, and we don't push // an else on the stack here, but we remove the conditional that we are completing. case EidosTokenType::kTokenElse: { if (indentStack.size()) { EidosTokenType top_token_type = indentStack.back()->token_type_; if (top_token_type == EidosTokenType::kTokenConditional) { indentStack.pop_back(); pretty.append(tokenString); break; } } indentStack.emplace_back(&token); pretty.append(tokenString); break; } // Comments are preserved verbatim case EidosTokenType::kTokenComment: case EidosTokenType::kTokenCommentLong: pretty.append(tokenString); break; // Tokens for operators are emitted verbatim case EidosTokenType::kTokenColon: case EidosTokenType::kTokenComma: case EidosTokenType::kTokenDot: case EidosTokenType::kTokenPlus: case EidosTokenType::kTokenMinus: case EidosTokenType::kTokenMod: case EidosTokenType::kTokenMult: case EidosTokenType::kTokenExp: case EidosTokenType::kTokenAnd: case EidosTokenType::kTokenOr: case EidosTokenType::kTokenDiv: case EidosTokenType::kTokenAssign: case EidosTokenType::kTokenAssign_R: case EidosTokenType::kTokenEq: case EidosTokenType::kTokenLt: case EidosTokenType::kTokenLtEq: case EidosTokenType::kTokenGt: case EidosTokenType::kTokenGtEq: case EidosTokenType::kTokenNot: case EidosTokenType::kTokenNotEq: pretty.append(tokenString); break; case EidosTokenType::kTokenSingleton: pretty.append(tokenString); break; // Nesting levels of parens and brackets are not tracked at the moment case EidosTokenType::kTokenLParen: case EidosTokenType::kTokenRParen: case EidosTokenType::kTokenLBracket: case EidosTokenType::kTokenRBracket: pretty.append(tokenString); break; // Numbers and identifiers are emitted verbatim case EidosTokenType::kTokenNumber: case EidosTokenType::kTokenIdentifier: pretty.append(tokenString); break; // Strings are emitted verbatim, but their original string needs to be reconstructed; // token_string_ has the outer quotes removed and escape sequences resolved case EidosTokenType::kTokenString: pretty.append(tokenScript.String().substr(static_cast(token.token_start_), static_cast(token.token_end_ - token.token_start_ + 1))); break; // These keywords have no effect on indent level case EidosTokenType::kTokenIn: case EidosTokenType::kTokenNext: case EidosTokenType::kTokenBreak: case EidosTokenType::kTokenReturn: case EidosTokenType::kTokenFunction: pretty.append(tokenString); break; } // Now that we're done processing that token, update startingNewStatement to reflect whether we are within // a statement, of which we have seen at least one token, or starting a new statement. Nonsignificant // tokens (whitespace and comments) do not alter the state of startingNewStatement. if ((token.token_type_ != EidosTokenType::kTokenWhitespace) && (token.token_type_ != EidosTokenType::kTokenComment) && (token.token_type_ != EidosTokenType::kTokenCommentLong)) startingNewStatement = ((token.token_type_ == EidosTokenType::kTokenSemicolon) || (token.token_type_ == EidosTokenType::kTokenLBrace) || (token.token_type_ == EidosTokenType::kTokenRBrace)); } return true; } static inline const EidosToken &NextSignificantToken(const std::vector &tokens, size_t tokenIndex) { // This scans forward from tokenIndex (not including tokenIndex itself) and returns the // next token that is not a whitespace, comment, or other non-significant token. do { if (tokenIndex == tokens.size() - 1) return tokens[tokenIndex]; tokenIndex++; EidosTokenType tokenType = tokens[tokenIndex].token_type_; if ((tokenType != EidosTokenType::kTokenWhitespace) && (tokenType != EidosTokenType::kTokenComment) && (tokenType != EidosTokenType::kTokenCommentLong)) return tokens[tokenIndex]; } while (true); } static inline const EidosToken &PreviousSignificantToken(const std::vector &tokens, size_t tokenIndex) { // This scans backward from tokenIndex (not including tokenIndex itself) and returns the // previous token that is not a whitespace, comment, or other non-significant token. do { if (tokenIndex == 0) return tokens[tokenIndex]; tokenIndex--; EidosTokenType tokenType = tokens[tokenIndex].token_type_; if ((tokenType != EidosTokenType::kTokenWhitespace) && (tokenType != EidosTokenType::kTokenComment) && (tokenType != EidosTokenType::kTokenCommentLong)) return tokens[tokenIndex]; } while (true); } static inline void EmitWhitespace(bool &forceSpace, int &forceNewlineCount, std::string &pretty) { // This emits space before the next token. It should be called immediately before the next token is emitted, so that // other considerations can influence the nature of the whitespace before it is actually appended to the string. if (pretty.length() > 0) { if (forceNewlineCount > 0) { for (int i = 0; i < forceNewlineCount; ++i) pretty.append("\n"); } else if (forceSpace) pretty.append(" "); } forceSpace = false; forceNewlineCount = 0; } bool Eidos_reformatTokensFromScript(const std::vector &tokens, EidosScript &tokenScript, std::string &pretty) { // Completely reformat the script string, changing newlines and spaces as well as line indents. This is different enough // in its logic that it doesn't seem to make sense for it to share code with Eidos_prettyprintTokensFromScript(); it would // just be a mess. However, there is certainly a lot of shared logic, too. Note that we do not deal with indentation at // all here. Instead, we just call Eidos_prettyprintTokensFromScript() at the end to prettyprint the reformatted code. size_t tokenCount = tokens.size(); int parenNestCount = 0; int braceNestCount = 0; bool forceNewlineAfterParenBalance = false; bool resolveWhileSemanticsAfterParenBalance = false; int insideTernaryConditionalCount = 0; bool lastTokenContainedNewline = true; bool lastTokenSuppressesCommentSpacing = true; int functionDeclarationCountdown = 0; bool forceSpace = false; int forceNewlineCount = 0; for (size_t tokenIndex = 0; tokenIndex < tokenCount; ++tokenIndex) { const EidosToken &token = tokens[tokenIndex]; const std::string &tokenString = token.token_string_; EidosTokenType tokenType = token.token_type_; bool nextLastTokenSuppressesCommentSpacing = false; switch (tokenType) { // These token types are not used in the AST and should not be present case EidosTokenType::kTokenNone: case EidosTokenType::kTokenBad: return false; // These are virtual tokens that can be ignored case EidosTokenType::kTokenEOF: case EidosTokenType::kTokenInterpreterBlock: case EidosTokenType::kTokenContextFile: case EidosTokenType::kTokenContextEidosBlock: case EidosTokenType::kFirstIdentifierLikeToken: break; // Whitespace is completely ignored; we do our own whitespace. We do look to see whether a newline is // present, though, so that we can keep comments on the same line as code when that situation exists case EidosTokenType::kTokenWhitespace: lastTokenContainedNewline = (tokenString.find_first_of("\n\r\x0C") != std::string::npos); break; // Comments are copied verbatim, and always get a newline after them; whether they get a newline before depends case EidosTokenType::kTokenComment: case EidosTokenType::kTokenCommentLong: { int postCommentNewlineCount = 1; if (lastTokenContainedNewline) { // we like to have a blank line before standalone comments, unless they follow a brace or a standalone comment forceNewlineCount = std::max(forceNewlineCount, lastTokenSuppressesCommentSpacing ? 1 : 2); nextLastTokenSuppressesCommentSpacing = true; } else { // same-line comments don't get a preceding newline, but if that means we're suppressing newlines, // make up for it after ourselves; we're just a tack-on on top of whatever was already happening forceSpace = true; postCommentNewlineCount = forceNewlineCount; forceNewlineCount = 0; } EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); if ((tokenType == EidosTokenType::kTokenComment) || lastTokenContainedNewline) forceNewlineCount = postCommentNewlineCount; else forceSpace = true; break; } // These tokens get no space before them, even if requested by the previous token; and they are followed by a newline case EidosTokenType::kTokenSemicolon: forceSpace = false; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceNewlineCount = 1; break; // This post-increments the indent level, and is always followed by a newline case EidosTokenType::kTokenLBrace: forceNewlineCount = 1; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceNewlineCount = 1; braceNestCount++; nextLastTokenSuppressesCommentSpacing = true; break; // This pre-decrements the indent level, and is always followed by a newline – two newlines at the topmost level case EidosTokenType::kTokenRBrace: forceNewlineCount = 1; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); braceNestCount--; forceNewlineCount = 1; if ((braceNestCount == 0) && (parenNestCount == 0)) forceNewlineCount = 2; break; // Plus and minus can be binary or unary; emit like kTokenMult if binary, like kTokenNot if unary case EidosTokenType::kTokenPlus: case EidosTokenType::kTokenMinus: { bool isBinary = false; const EidosToken &previous_token = PreviousSignificantToken(tokens, tokenIndex); EidosTokenType prev_type = previous_token.token_type_; if ((prev_type == EidosTokenType::kTokenNumber) || (prev_type == EidosTokenType::kTokenString) || (prev_type == EidosTokenType::kTokenIdentifier) || (prev_type == EidosTokenType::kTokenRParen) || (prev_type == EidosTokenType::kTokenRBracket)) isBinary = true; if (isBinary) forceSpace = true; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); if (isBinary) forceSpace = true; break; } // These tokens should be emitted verbatim, surrounded by single spaces case EidosTokenType::kTokenMod: case EidosTokenType::kTokenMult: case EidosTokenType::kTokenAnd: case EidosTokenType::kTokenOr: case EidosTokenType::kTokenDiv: case EidosTokenType::kTokenConditional: case EidosTokenType::kTokenEq: case EidosTokenType::kTokenLt: case EidosTokenType::kTokenLtEq: case EidosTokenType::kTokenGt: case EidosTokenType::kTokenGtEq: case EidosTokenType::kTokenNotEq: case EidosTokenType::kTokenIn: if ((functionDeclarationCountdown > 0) && ((tokenType == EidosTokenType::kTokenLt) || (tokenType == EidosTokenType::kTokenGt))) { // special treatment for "o" syntax in function declarations forceSpace = false; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); if (tokenType == EidosTokenType::kTokenGt) forceSpace = true; } else { forceSpace = true; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceSpace = true; if (tokenType == EidosTokenType::kTokenConditional) insideTernaryConditionalCount++; } break; // This token gets spaces around it if it's not inside parentheses, like x = y;, but no spaces inside parens, like foo(x=y); case EidosTokenType::kTokenAssign: case EidosTokenType::kTokenAssign_R: if (parenNestCount == 0) forceSpace = true; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); if (parenNestCount == 0) forceSpace = true; break; // These tokens should get a space before them, but none after them pretty.append(tokenString); break; // These tokens should get a space after them, but should force there to be none before them case EidosTokenType::kTokenComma: case EidosTokenType::kTokenSingleton: case EidosTokenType::kTokenReturn: forceSpace = false; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceSpace = true; break; // These tokens should not get spaces on either side of them (unless the adjecent token demands it) case EidosTokenType::kTokenColon: case EidosTokenType::kTokenLBracket: case EidosTokenType::kTokenRBracket: case EidosTokenType::kTokenDot: case EidosTokenType::kTokenExp: case EidosTokenType::kTokenNot: case EidosTokenType::kTokenNext: case EidosTokenType::kTokenBreak: case EidosTokenType::kTokenNumber: EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); break; // Identifiers have special spacing at the top level, because constructs like "s1 1000 early()" don't follow normal rules case EidosTokenType::kTokenIdentifier: if ((parenNestCount == 0) && (braceNestCount == 0) && (functionDeclarationCountdown == 0)) forceSpace = true; if (PreviousSignificantToken(tokens, tokenIndex).token_type_ == EidosTokenType::kTokenIdentifier) forceSpace = true; // always force a space between two adjacent identifiers, like [lif foo=T] EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); if ((parenNestCount == 0) && (braceNestCount == 0) && (NextSignificantToken(tokens, tokenIndex).token_type_ == EidosTokenType::kTokenNumber)) forceSpace = true; break; // Same as the above category, but string tokens have to be emitted in a special way case EidosTokenType::kTokenString: EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenScript.String().substr(static_cast(token.token_start_), static_cast(token.token_end_ - token.token_start_ + 1))); break; // Left parentheses keep track of their nesting state case EidosTokenType::kTokenLParen: EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); parenNestCount++; break; // Right parentheses keep track of their nesting state, and can do some special actions if they balance out case EidosTokenType::kTokenRParen: forceSpace = false; // never a space before a right paren EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); if (--parenNestCount == 0) { if (forceNewlineAfterParenBalance) { forceNewlineAfterParenBalance = false; forceNewlineCount = 1; } if (resolveWhileSemanticsAfterParenBalance) { resolveWhileSemanticsAfterParenBalance = false; // We rely here on the fact that the script parses without errors const EidosToken &nextSigToken = NextSignificantToken(tokens, tokenIndex); if (nextSigToken.token_type_ != EidosTokenType::kTokenSemicolon) { // If the next token is a semicolon, we are terminating a do-while loop // (or we're a while loop with a null statement as its body, which we treat incorrectly) // If the next token is not a semicolon, we are starting a while loop, so we need a newline forceNewlineCount = 1; } } if (functionDeclarationCountdown > 0) functionDeclarationCountdown--; } break; // These tokens are followed by a space, a parenthesized expression, and then a newline case EidosTokenType::kTokenIf: case EidosTokenType::kTokenFor: EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceSpace = true; forceNewlineAfterParenBalance = true; break; // The "do" token starts a do-while loop case EidosTokenType::kTokenDo: EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceNewlineCount = 1; break; // The "while" token either ends a do-while loop, or starts a while loop case EidosTokenType::kTokenWhile: forceSpace = true; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceSpace = true; resolveWhileSemanticsAfterParenBalance = true; break; // The "else" token is handled differently depending on whether it is in a ?else expression or an if-else construct case EidosTokenType::kTokenElse: forceSpace = true; EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceSpace = true; if (insideTernaryConditionalCount) --insideTernaryConditionalCount; else if (NextSignificantToken(tokens, tokenIndex).token_type_ != EidosTokenType::kTokenIf) forceNewlineCount = 1; break; // The function token, for user-defined functions, is particularly tricky since it initiates a signature // We handle this by going into a special mode that chews through the signature declaration case EidosTokenType::kTokenFunction: EmitWhitespace(forceSpace, forceNewlineCount, pretty); pretty.append(tokenString); forceSpace = true; functionDeclarationCountdown = 2; // we will require two close-out right parens to finish the declaration break; } if (tokenType != EidosTokenType::kTokenWhitespace) { lastTokenContainedNewline = false; lastTokenSuppressesCommentSpacing = nextLastTokenSuppressesCommentSpacing; } } // We're done reformatting; now we need to generate a new script and a new token stream, and fix the indentation of our // reformatted string by calling Eidos_prettyprintTokensFromScript(); this avoids duplicating a bunch of logic try { EidosScript indentScript(pretty); indentScript.Tokenize(false, true); // get whitespace and comment tokens const std::vector &indentTokens = indentScript.Tokens(); std::string pretty_indented; bool success = false; success = Eidos_prettyprintTokensFromScript(indentTokens, indentScript, pretty_indented); if (success) pretty = pretty_indented; else return false; } catch (...) { qDebug() << "Reformatted code no longer parsed!"; return false; } return true; } ================================================ FILE: QtSLiM/QtSLiMEidosPrettyprinter.h ================================================ // // QtSLiMEidosPrettyprinter.h // SLiM // // Created by Ben Haller on 8/1/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMEIDOSPRETTYPRINTER_H #define QTSLIMEIDOSPRETTYPRINTER_H #include #include class EidosScript; // Generate a prettyprinted script string into parameter pretty, from the tokens and script supplied bool Eidos_prettyprintTokensFromScript(const std::vector &tokens, EidosScript &tokenScript, std::string &pretty); bool Eidos_reformatTokensFromScript(const std::vector &tokens, EidosScript &tokenScript, std::string &pretty); #endif // QTSLIMEIDOSPRETTYPRINTER_H ================================================ FILE: QtSLiM/QtSLiMExtras.cpp ================================================ // // QtSLiMExtras.cpp // SLiM // // Created by Ben Haller on 7/28/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMExtras.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "QtSLiMPreferences.h" #include "QtSLiMAppDelegate.h" #include "eidos_value.h" bool QtSLiMIsMostlyOnScreen(QWidget *window) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QRect f = window->frameGeometry(); QScreen *screen1 = QGuiApplication::screenAt(f.topLeft()); QScreen *screen2 = QGuiApplication::screenAt(f.topRight()); QScreen *screen3 = QGuiApplication::screenAt(f.bottomLeft()); QScreen *screen4 = QGuiApplication::screenAt(f.bottomRight()); QScreen *screen5 = QGuiApplication::screenAt(f.center()); int cornerCount = (!!screen1) + (!!screen2) + (!!screen3) + (!!screen4); if (!screen5) cornerCount = 0; return (cornerCount >= 2); #else Q_UNUSED(window); return true; #endif } void QtSLiMRelocateQuietly(QWidget *window) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QScreen *primary = QGuiApplication::primaryScreen(); QRect avail = primary ? primary->availableGeometry() : QRect(0,0,1024,768); window->resize(window->size().boundedTo(avail.size())); window->move(avail.topLeft() + QPoint(50, 50)); #else Q_UNUSED(window); #endif } void QtSLiMMakeWindowVisibleAndExposed(QWidget *window) { // we've had lots of problems in SLiMgui with windows not showing when the user expects them to show; this function // is intended to mitigate those issues by doing EVERYTHING we can to make the window visible and exposed. if (!window->isWindow()) { qDebug() << "The widget" << window << "is not a window!"; return; } // if there is a window other than us that is full-screen, we might have trouble getting in front of it // I'm not sure how this is normally dealt with by operating systems, since I never use full-screen mode // on macOS it seems to work OK; the new window goes full-screen also, in front of the other, which // I assume is the standard behavior...? I'll wait for reported bugs on this one, I don't know. FIXME // Fullscreen note: on Windows, "fullscreen" often means a borderless maximized window that can be // overlaid by another normal window that calls show/raise/activate. We intentionally do that here so // SLiMgui presents visibly on the current screen if possible, without minimizing/altering other apps. // If the window still isn't exposed (e.g., truly exclusive fullscreen), we will attempt relocation to // another monitor after a short delay; if that also fails, we flash the app icon to notify the user. -Chris // Still unclear how this will behave on Linux systems, hopefuly the same as macOS? // un-miniaturize the window if it is miniaturized if (window->windowState() & Qt::WindowMinimized) window->setWindowState((window->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); // the standard Qt litany for making a window rise to front: show, raise, activate window->show(); window->raise(); window->activateWindow(); // If not sufficiently visible, relocate to a safe point on the primary screen if (!QtSLiMIsMostlyOnScreen(window)) QtSLiMRelocateQuietly(window); #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) // If still not actually exposed (e.g., exclusive fullscreen), re-check shortly and then try other screens if (QWindow *w = window->windowHandle()) { if (!w->isExposed()) { QPointer safeWindow(window); QTimer::singleShot(200, qApp, [safeWindow]() { if (!safeWindow) return; QWidget *win = safeWindow.data(); QWindow *wh = win->windowHandle(); if (!wh) return; if (wh->isExposed()) return; // became exposed in the meantime; do nothing QScreen *currentScreen = QGuiApplication::screenAt(win->frameGeometry().center()); const QList screens = QGuiApplication::screens(); for (QScreen *screen : screens) { if (screen == currentScreen) continue; QRect avail = screen->availableGeometry(); win->move(avail.topLeft() + QPoint(50, 50)); win->raise(); win->activateWindow(); if (wh->isExposed()) return; } // If we still are not exposed anywhere, alert the user via taskbar/dock qApp->alert(win); }); } } #endif } void QtSLiMClearLayout(QLayout *layout, bool deleteWidgets) { // this code from https://stackoverflow.com/a/7077340/2752221 // thanks to stackoverflow user Darko Maksimovic while (QLayoutItem *item = layout->takeAt(0)) { if (deleteWidgets) { if (QWidget *widget = item->widget()) widget->deleteLater(); } if (QLayout *childLayout = item->layout()) QtSLiMClearLayout(childLayout, deleteWidgets); delete item; } } void QtSLiMFrameRect(const QRect &p_rect, const QColor &p_color, QPainter &p_painter) { p_painter.fillRect(QRect(p_rect.left(), p_rect.top(), p_rect.width(), 1), p_color); // top edge p_painter.fillRect(QRect(p_rect.left(), p_rect.top() + 1, 1, p_rect.height() - 2), p_color); // left edge (without corner pixels) p_painter.fillRect(QRect(p_rect.left() + p_rect.width() - 1, p_rect.top() + 1, 1, p_rect.height() - 2), p_color); // right edge (without corner pixels) p_painter.fillRect(QRect(p_rect.left(), p_rect.top() + p_rect.height() - 1, p_rect.width(), 1), p_color); // bottom edge } void QtSLiMFrameRect(const QRectF &p_rect, const QColor &p_color, QPainter &p_painter, double w) { p_painter.fillRect(QRectF(p_rect.left(), p_rect.top(), p_rect.width(), w), p_color); p_painter.fillRect(QRectF(p_rect.left(), p_rect.top() + w, w, p_rect.height() - 2*w), p_color); p_painter.fillRect(QRectF(p_rect.left() + p_rect.width() - w, p_rect.top() + w, w, p_rect.height() - 2*w), p_color); p_painter.fillRect(QRectF(p_rect.left(), p_rect.top() + p_rect.height() - w, p_rect.width(), w), p_color); } QColor QtSLiMColorWithWhite(double p_white, double p_alpha) { QColor color; color.setRgbF(p_white, p_white, p_white, p_alpha); return color; } QColor QtSLiMColorWithRGB(double p_red, double p_green, double p_blue, double p_alpha) { QColor color; color.setRgbF(p_red, p_green, p_blue, p_alpha); return color; } QColor QtSLiMColorWithHSV(double p_hue, double p_saturation, double p_value, double p_alpha) { QColor color; color.setHsvF(p_hue, p_saturation, p_value, p_alpha); return color; } bool QtSLiMInDarkMode(void) { // We determine whether we're in dark mode heuristically: if the window background color is darker than 50% gray // We don't attempt to cache this value, since it sounds like the change notification for this is buggy QColor windowColor = QPalette().color(QPalette::Window); double windowBrightness = 0.21 * windowColor.redF() + 0.72 * windowColor.greenF() + 0.07 * windowColor.blueF(); return (windowBrightness < 0.5); } QString QtSLiMImagePath(QString baseName, bool highlighted) { bool inDarkMode = QtSLiMInDarkMode(); baseName = (inDarkMode ? ":/buttons_DARK/" : ":/buttons/") + baseName; if (highlighted) baseName += "_H"; if (inDarkMode) baseName += "_DARK"; baseName += ".png"; return baseName; } const double greenBrightness = 0.8; void RGBForFitness(double value, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor) { // apply the scaling factor value = (value - 1.0) * scalingFactor + 1.0; if (value <= 0.5) { // value <= 0.5 is a shade of red, going down to black *colorRed = static_cast(value * 2.0); *colorGreen = 0.0; *colorBlue = 0.0; } else if (value >= 2.0) { // value >= 2.0 is a shade of green, going up to white *colorRed = static_cast((value - 2.0) * greenBrightness / value); *colorGreen = static_cast(greenBrightness); *colorBlue = static_cast((value - 2.0) * greenBrightness / value); } else if (value <= 1.0) { // value <= 1.0 (but > 0.5) goes from red (unfit) to yellow (neutral) *colorRed = 1.0; *colorGreen = static_cast((value - 0.5) * 2.0); *colorBlue = 0.0; } else // 1.0 < value < 2.0 { // value > 1.0 (but < 2.0) goes from yellow (neutral) to green (fit) *colorRed = static_cast(2.0 - value); *colorGreen = static_cast(greenBrightness + (1.0 - greenBrightness) * (2.0 - value)); *colorBlue = 0.0; } } void RGBForSelectionCoeff(double value, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor) { // apply a scaling factor; this could be user-adjustible since different models have different relevant fitness ranges value *= scalingFactor; // and add 1, just so we can re-use the same code as in RGBForFitness() value += 1.0; if (value <= 0.0) { // value <= 0.0 is the darkest shade of red we use *colorRed = 0.5; *colorGreen = 0.0; *colorBlue = 0.0; } else if (value <= 0.5) { // value <= 0.5 is a shade of red, going down toward black *colorRed = static_cast(value + 0.5); *colorGreen = 0.0; *colorBlue = 0.0; } else if (value < 1.0) { // value <= 1.0 (but > 0.5) goes from red (very unfit) to orange (nearly neutral) *colorRed = 1.0; *colorGreen = static_cast((value - 0.5) * 1.0); *colorBlue = 0.0; } else if (value == 1.0) { // exactly neutral mutations are yellow *colorRed = 1.0; *colorGreen = 1.0; *colorBlue = 0.0; } else if (value <= 1.5) { // value > 1.0 (but < 1.5) goes from green (nearly neutral) to cyan (fit) *colorRed = 0.0; *colorGreen = static_cast(greenBrightness); *colorBlue = static_cast((value - 1.0) * 2.0); } else if (value <= 2.0) { // value > 1.5 (but < 2.0) goes from cyan (fit) to blue (very fit) *colorRed = 0.0; *colorGreen = static_cast(greenBrightness * ((2.0 - value) * 2.0)); *colorBlue = 1.0; } else // (value > 2.0) { // value > 2.0 is a shade of blue, going up toward white *colorRed = static_cast((value - 2.0) * 0.75 / value); *colorGreen = static_cast((value - 2.0) * 0.75 / value); *colorBlue = 1.0; } } QtSLiMColorScaleWidget::QtSLiMColorScaleWidget(QWidget *p_parent) : QWidget(p_parent), fitnessTicks({"0.0", "0.5", "1.0", "1.5", "2.0"}), effectTicks({"-1.0", "-0.5", "0.0", "0.5", "1.0"}) { setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); } void QtSLiMColorScaleWidget::paintEvent(QPaintEvent * /*p_paintEvent*/) { // we're designed to fit in a fixed size of 301 x 197; see dispatch_showColorScales() //QRect overallRect = contentsRect(); QPainter painter(this); QRect stripe1 = QRect(15, 33, 271, 20); // odd width so we have a central pixel of exactly yellow QRect stripe2 = QRect(15, 105, 271, 20); // odd width so we have a central pixel of exactly yellow static QFont *labelFont = nullptr; if (!labelFont) { labelFont = new QFont(); #ifdef __linux__ labelFont->setPointSize(10); #else labelFont->setPointSize(12); #endif labelFont->setBold(true); } painter.setFont(*labelFont); const double labelYOffset = -8; // Fitness color scale painter.drawText(stripe1.x(), stripe1.y() + labelYOffset, "Individual fitness scale:"); for (int x = stripe1.left() + 1; x <= (stripe1.left() + 1) + (stripe1.width() - 3); ++x) { const double scalingFactor = 0.8; // this is constant in QtSLiM; there used to be a slider QRect sliver(x, stripe1.top() + 1, 1, stripe1.height() - 2); double sliverFraction = (x - (stripe1.left() + 1)) / (stripe1.width() - 3.0); double fitness = sliverFraction * 2.0; // cover fitness values of 0.0 to 2.0 float r, g, b; RGBForFitness(fitness, &r, &g, &b, scalingFactor); painter.fillRect(sliver, QColor(round(r * 255), round(g * 255), round(b * 255))); } QtSLiMFrameRect(stripe1, Qt::black, painter); // Mutation effect color scale painter.drawText(stripe2.x(), stripe2.y() + labelYOffset, "Mutation effect scale:"); for (int x = stripe2.left() + 1; x <= (stripe2.left() + 1) + (stripe2.width() - 3); ++x) { const double scalingFactor = 0.8; // this is constant in QtSLiM; there used to be a slider QRect sliver(x, stripe2.top() + 1, 1, stripe2.height() - 2); double sliverFraction = (x - (stripe2.left() + 1)) / (stripe2.width() - 3.0); double fitness = sliverFraction * 2.0 - 1; // cover mutation effect values of -1.0 to 1.0 float r, g, b; RGBForSelectionCoeff(fitness, &r, &g, &b, scalingFactor); painter.fillRect(sliver, QColor(round(r * 255), round(g * 255), round(b * 255))); //qDebug() << "x =" << x << " << sliverFraction =" << sliverFraction << " fitness =" << fitness; } QtSLiMFrameRect(stripe2, Qt::black, painter); // Draw axis scales static QFont *tickFont = nullptr; if (!tickFont) { tickFont = new QFont(); #ifdef __linux__ tickFont->setPointSize(8); #else tickFont->setPointSize(10); #endif } painter.setFont(*tickFont); QFontMetricsF fontMetrics(*tickFont); for (int tickIndex = 0; tickIndex < 5; tickIndex++) { bool longTick = (tickIndex % 2 == 0); int tickX = round(stripe1.left() + 1 + (tickIndex / 4.0) * (stripe1.width() - 3.0)); QString tickLabel; double tickLabelWidth; // label stripe 1 tickLabel = fitnessTicks[tickIndex]; #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) tickLabelWidth = fontMetrics.width(tickLabel); // deprecated in 5.11 #else tickLabelWidth = fontMetrics.horizontalAdvance(tickLabel); // added in Qt 5.11 #endif painter.fillRect(tickX, stripe1.bottom() + 1, 1, longTick ? 4 : 2, Qt::black); painter.drawText(QPointF(tickX - tickLabelWidth / 2.0 + 1, stripe1.bottom() + 16), tickLabel); // label stripe 2 tickLabel = effectTicks[tickIndex]; #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) tickLabelWidth = fontMetrics.width(tickLabel); // deprecated in 5.11 #else tickLabelWidth = fontMetrics.horizontalAdvance(tickLabel); // added in Qt 5.11 #endif painter.fillRect(tickX, stripe2.bottom() + 1, 1, longTick ? 4 : 2, Qt::black); painter.drawText(QPointF(tickX - tickLabelWidth / 2.0 + 1, stripe2.bottom() + 16), tickLabel); } // add final notes in italic static QFont *noteFont = nullptr; if (!noteFont) { noteFont = new QFont(); #ifdef __linux__ noteFont->setPointSize(9); #else noteFont->setPointSize(11); #endif noteFont->setItalic(true); } painter.setFont(*noteFont); painter.drawText(stripe1.x(), stripe2.bottom() + 44, "Yellow indicates neutrality on both color scales."); painter.drawText(stripe1.x(), stripe2.bottom() + 58, "Both scales fade out to white for large values."); } // A subclass of QLineEdit that selects all its text when it receives keyboard focus // thanks to https://stackoverflow.com/a/51807268/2752221 QtSLiMGenerationLineEdit::QtSLiMGenerationLineEdit(const QString &contents, QWidget *p_parent) : QLineEdit(contents, p_parent) { connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, [this]() { _ReconfigureAppearance(); }); _ReconfigureAppearance(); } QtSLiMGenerationLineEdit::QtSLiMGenerationLineEdit(QWidget *p_parent) : QLineEdit(p_parent) { connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, [this]() { _ReconfigureAppearance(); }); _ReconfigureAppearance(); } QtSLiMGenerationLineEdit::~QtSLiMGenerationLineEdit() {} void QtSLiMGenerationLineEdit::focusInEvent(QFocusEvent *p_event) { // First let the base class process the event QLineEdit::focusInEvent(p_event); // Then select the text by a single shot timer, so that everything will // be processed before (calling selectAll() directly won't work) QTimer::singleShot(0, this, &QLineEdit::selectAll); } void QtSLiMGenerationLineEdit::setProgress(double p_progress) { double newProgress = std::min(std::max(p_progress, 0.0), 1.0); if (newProgress != progress) { progress = newProgress; update(); } } void QtSLiMGenerationLineEdit::_ReconfigureAppearance() { // Eight states, based on three binary flags; but two states never happen in practice bool darkMode = QtSLiMInDarkMode(); bool enabled = isEnabled(); if (darkMode) { if (enabled) { if (dimmed) setStyleSheet("color: red; background-color: black"); // doesn't happen else setStyleSheet("color: rgb(255, 255, 255); background-color: black"); // not playing } else { if (dimmed) setStyleSheet("color: rgb(40, 40, 40); background-color: black"); // error state (not normally visible) else setStyleSheet("color: rgb(170, 170, 170); background-color: black"); // playing } } else { if (enabled) { if (dimmed) setStyleSheet("color: red; background-color: white"); // doesn't happen else setStyleSheet("color: rgb(0, 0, 0); background-color: white"); // not playing } else { if (dimmed) setStyleSheet("color: rgb(192, 192, 192); background-color: white"); // error state (not normally visible) else setStyleSheet("color: rgb(120, 120, 120); background-color: white"); // playing } } update(); } void QtSLiMGenerationLineEdit::setAppearance(bool p_enabled, bool p_dimmed) { if ((isEnabled() != p_enabled) || (dimmed != p_dimmed)) { setEnabled(p_enabled); dimmed = p_dimmed; _ReconfigureAppearance(); } } void QtSLiMGenerationLineEdit::paintEvent(QPaintEvent *p_paintEvent) { // first let super draw QLineEdit::paintEvent(p_paintEvent); // then overlay a progress bar on top, if requested, and if we are not disabled & dimmed (error state) bool enabled = isEnabled(); if (!enabled && dimmed) return; if (progress > 0.0) { bool darkMode = QtSLiMInDarkMode(); QPainter painter(this); QRect bounds = rect().adjusted(2, 2, -2, -2); bounds.setWidth(round(bounds.width() * progress)); if (darkMode) { // lighten the black background to a dark green; text is unaffected since it's light painter.setCompositionMode(QPainter::CompositionMode_Lighten); painter.fillRect(bounds, QColor(0, 120, 0)); } else { // darken the white background to a light green; text is unaffected since it's dark painter.setCompositionMode(QPainter::CompositionMode_Darken); painter.fillRect(bounds, QColor(180, 255, 180)); } } } void ColorizePropertySignature(const EidosPropertySignature *property_signature, double pointSize, QTextCursor lineCursor) { // // Note this logic is paralleled in the function operator<<(ostream &, const EidosPropertySignature &). // These two should be kept in synch so the user-visible format of signatures is consistent. // QString docSigString = lineCursor.selectedText(); QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); QTextCharFormat ttFormat; QFont displayFont(prefs.displayFontPref()); displayFont.setPointSizeF(pointSize); ttFormat.setFont(displayFont); lineCursor.setCharFormat(ttFormat); bool inDarkMode = QtSLiMInDarkMode(); QTextCharFormat functionAttrs(ttFormat), typeAttrs(ttFormat); functionAttrs.setForeground(QBrush(inDarkMode ? QColor(115, 145, 255) : QColor(28, 0, 207))); typeAttrs.setForeground(QBrush(inDarkMode ? QColor(90, 210, 90) : QColor(0, 116, 0))); QTextCursor propertyNameCursor(lineCursor); propertyNameCursor.setPosition(lineCursor.anchor(), QTextCursor::MoveAnchor); propertyNameCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, static_cast(property_signature->property_name_.length())); propertyNameCursor.setCharFormat(functionAttrs); int nameLength = QString::fromStdString(property_signature->property_name_).length(); int connectorLength = QString::fromStdString(property_signature->PropertySymbol()).length(); int typeLength = docSigString.length() - (nameLength + 4 + connectorLength); QTextCursor typeCursor(lineCursor); typeCursor.setPosition(lineCursor.position(), QTextCursor::MoveAnchor); typeCursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1); typeCursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, typeLength); typeCursor.setCharFormat(typeAttrs); } void ColorizeCallSignature(const EidosCallSignature *call_signature, double pointSize, QTextCursor lineCursor) { // // Note this logic is paralleled in the function operator<<(ostream &, const EidosCallSignature &). // These two should be kept in synch so the user-visible format of signatures is consistent. // QString docSigString = lineCursor.selectedText(); std::ostringstream ss; ss << *call_signature; QString callSigString = QString::fromStdString(ss.str()); if (callSigString.endsWith(" ") && !docSigString.endsWith(" ")) callSigString.chop(7); if (docSigString != callSigString) { qDebug() << "*** " << ((call_signature->CallPrefix().length() > 0) ? "method" : "function") << "signature mismatch:\nold:" << docSigString << "\nnew:" << callSigString; return; } // the signature conforms to expectations, so we can colorize it QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); QTextCharFormat ttFormat; QFont displayFont(prefs.displayFontPref()); displayFont.setPointSizeF(pointSize); ttFormat.setFont(displayFont); lineCursor.setCharFormat(ttFormat); bool inDarkMode = QtSLiMInDarkMode(); QTextCharFormat typeAttrs(ttFormat), functionAttrs(ttFormat), paramAttrs(ttFormat); typeAttrs.setForeground(QBrush(inDarkMode ? QColor(115, 145, 255) : QColor(28, 0, 207))); functionAttrs.setForeground(QBrush(inDarkMode ? QColor(90, 210, 90) : QColor(0, 116, 0))); paramAttrs.setForeground(QBrush(inDarkMode ? QColor(220, 83, 185) : QColor(170, 13, 145))); int prefix_string_len = QString::fromStdString(call_signature->CallPrefix()).length(); int return_type_string_len = QString::fromStdString(StringForEidosValueMask(call_signature->return_mask_, call_signature->return_class_, "", nullptr)).length(); int function_name_string_len = QString::fromStdString(call_signature->call_name_).length(); // colorize return type QTextCursor scanCursor(lineCursor); scanCursor.setPosition(lineCursor.anchor() + prefix_string_len + 1, QTextCursor::MoveAnchor); scanCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, return_type_string_len); scanCursor.setCharFormat(typeAttrs); // colorize call name scanCursor.setPosition(scanCursor.position() + 1, QTextCursor::MoveAnchor); scanCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, function_name_string_len); scanCursor.setCharFormat(functionAttrs); scanCursor.setPosition(scanCursor.position() + 1, QTextCursor::MoveAnchor); // colorize arguments size_t arg_mask_count = call_signature->arg_masks_.size(); if (arg_mask_count == 0) { // colorize "void" scanCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 4); scanCursor.setCharFormat(typeAttrs); } else { for (size_t arg_index = 0; arg_index < arg_mask_count; ++arg_index) { EidosValueMask type_mask = call_signature->arg_masks_[arg_index]; const std::string &arg_name = call_signature->arg_names_[arg_index]; const EidosClass *arg_obj_class = call_signature->arg_classes_[arg_index]; EidosValue_SP arg_default = call_signature->arg_defaults_[arg_index]; // skip private arguments if ((arg_name.length() >= 1) && (arg_name[0] == '_')) continue; scanCursor.setPosition(scanCursor.position() + ((arg_index > 0) ? 2 : 0), QTextCursor::MoveAnchor); // ", " // // Note this logic is paralleled in the function StringForEidosValueMask(). // These two should be kept in synch so the user-visible format of signatures is consistent. // if (arg_name == gEidosStr_ELLIPSIS) { scanCursor.setPosition(scanCursor.position() + 3, QTextCursor::MoveAnchor); // "..." continue; } bool is_optional = !!(type_mask & kEidosValueMaskOptional); bool requires_singleton = !!(type_mask & kEidosValueMaskSingleton); EidosValueMask stripped_mask = type_mask & kEidosValueMaskFlagStrip; int typeLength = 0; if (is_optional) scanCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); // "[" if (stripped_mask == kEidosValueMaskNone) typeLength = 1; // "?" else if (stripped_mask == kEidosValueMaskAny) typeLength = 1; // "*" else if (stripped_mask == kEidosValueMaskAnyBase) typeLength = 1; // "+" else if (stripped_mask == kEidosValueMaskVOID) typeLength = 4; // "void" else if (stripped_mask == kEidosValueMaskNULL) typeLength = 4; // "NULL" else if (stripped_mask == kEidosValueMaskLogical) typeLength = 7; // "logical" else if (stripped_mask == kEidosValueMaskString) typeLength = 6; // "string" else if (stripped_mask == kEidosValueMaskInt) typeLength = 7; // "integer" else if (stripped_mask == kEidosValueMaskFloat) typeLength = 5; // "float" else if (stripped_mask == kEidosValueMaskObject) typeLength = 6; // "object" else if (stripped_mask == kEidosValueMaskNumeric) typeLength = 7; // "numeric" else { if (stripped_mask & kEidosValueMaskVOID) typeLength++; // "v" if (stripped_mask & kEidosValueMaskNULL) typeLength++; // "N" if (stripped_mask & kEidosValueMaskLogical) typeLength++; // "l" if (stripped_mask & kEidosValueMaskInt) typeLength++; // "i" if (stripped_mask & kEidosValueMaskFloat) typeLength++; // "f" if (stripped_mask & kEidosValueMaskString) typeLength++; // "s" if (stripped_mask & kEidosValueMaskObject) typeLength++; // "o" } if (arg_obj_class && (stripped_mask & kEidosValueMaskObject)) { int obj_type_name_len = QString::fromStdString(arg_obj_class->ClassNameForDisplay()).length(); typeLength += (obj_type_name_len + 2); // "<" obj_type_name ">" } if (requires_singleton) typeLength++; // "$" scanCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, typeLength); scanCursor.setCharFormat(typeAttrs); scanCursor.setPosition(scanCursor.position(), QTextCursor::MoveAnchor); if (arg_name.length() > 0) { scanCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); // " " scanCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, QString::fromStdString(arg_name).length()); scanCursor.setCharFormat(paramAttrs); scanCursor.setPosition(scanCursor.position(), QTextCursor::MoveAnchor); } if (is_optional) { if (arg_default && (arg_default != gStaticEidosValueNULLInvisible.get())) { scanCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 3); // " = " std::ostringstream default_string_stream; arg_default->Print(default_string_stream); int default_string_len = QString::fromStdString(default_string_stream.str()).length(); scanCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, default_string_len); } scanCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); // "]" } } } } // A subclass of QHBoxLayout specifically designed to lay out the play controls in the main window QtSLiMPlayControlsLayout::~QtSLiMPlayControlsLayout() { } QSize QtSLiMPlayControlsLayout::sizeHint() const { QSize size(0,0); int n = count(); for (int i = 0; i < n; ++i) { if (i == 2) continue; // the profile button takes no space QLayoutItem *layoutItem = itemAt(i); QSize itemSizeHint = layoutItem->sizeHint(); size.rwidth() += itemSizeHint.width(); size.rheight() = std::max(size.height(), itemSizeHint.height()); } size.rwidth() += (n - 2) * spacing(); // -2 because we exclude spacing for the profile button return size; } QSize QtSLiMPlayControlsLayout::minimumSize() const { QSize size(0,0); int n = count(); for (int i = 0; i < n; ++i) { if (i == 2) continue; // the profile button takes no space QLayoutItem *layoutItem = itemAt(i); QSize itemMinimumSize = layoutItem->minimumSize(); size.rwidth() += itemMinimumSize.width(); size.rheight() = std::max(size.height(), itemMinimumSize.height()); } size.rwidth() += (n - 2) * spacing(); // -2 because we exclude spacing for the profile button return size; } void QtSLiMPlayControlsLayout::setGeometry(const QRect &rect) { QHBoxLayout::setGeometry(rect); int n = count(); int position = rect.x(); QRect playButtonRect; for (int i = 0; i < n; ++i) { if (i == 2) continue; // the profile button takes no space QLayoutItem *layoutItem = itemAt(i); QSize itemSizeHint = layoutItem->sizeHint(); QRect geom(position, rect.y(), itemSizeHint.width(), itemSizeHint.height()); layoutItem->setGeometry(geom); position += itemSizeHint.width() + spacing(); if (i == 1) playButtonRect = geom; } // position the profile button; the button must lie inside the bounds of the parent widget due to clipping QLayoutItem *profileButton = itemAt(2); QSize itemSizeHint = profileButton->sizeHint(); QRect geom(playButtonRect.left() + playButtonRect.width() - 22, rect.y() - 6, itemSizeHint.width(), itemSizeHint.height()); profileButton->setGeometry(geom); } // Heat colors for profiling display #define SLIM_YELLOW_FRACTION 0.10 #define SLIM_SATURATION 0.75 QColor slimColorForFraction(double fraction) { if (fraction < SLIM_YELLOW_FRACTION) { // small fractions fall on a ramp from white (0.0) to yellow (SLIM_YELLOW_FRACTION) return QtSLiMColorWithHSV(1.0 / 6.0, (fraction / SLIM_YELLOW_FRACTION) * SLIM_SATURATION, 1.0, 1.0); } else { // larger fractions ramp from yellow (SLIM_YELLOW_FRACTION) to red (1.0) return QtSLiMColorWithHSV((1.0 / 6.0) * (1.0 - (fraction - SLIM_YELLOW_FRACTION) / (1.0 - SLIM_YELLOW_FRACTION)), SLIM_SATURATION, 1.0, 1.0); } } // Nicely formatted memory usage strings QString stringForByteCount(uint64_t bytes) { if (bytes > 512.0 * 1024.0 * 1024.0 * 1024.0) return QString("%1 TB").arg(bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0), 0, 'f', 2); else if (bytes > 512.0 * 1024.0 * 1024.0) return QString("%1 GB").arg(bytes / (1024.0 * 1024.0 * 1024.0), 0, 'f', 2); else if (bytes > 512.0 * 1024.0) return QString("%1 MB").arg(bytes / (1024.0 * 1024.0), 0, 'f', 2); else if (bytes > 512.0) return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 2); else return QString("%1 bytes").arg(bytes); } QString attributedStringForByteCount(uint64_t bytes, double total, QTextCharFormat &format) { QString byteString = stringForByteCount(bytes); double fraction = bytes / total; QColor fractionColor = slimColorForFraction(fraction); // We modify format for the caller, which they can use to colorize the returned string format.setBackground(fractionColor); return byteString; } QString slimDateline(void) { QDateTime dateTime = QDateTime::currentDateTime(); QString dateTimeString = dateTime.toString("M/d/yy, h:mm:ss AP"); // format: 3/28/20, 8:03:09 PM return QString("# %1").arg(dateTimeString); } // Running a panel to obtain numbers from the user // The goal here is to avoid a proliferation of dumb forms, by programmatically generating the UI QStringList QtSLiMRunLineEditArrayDialog(QWidget *p_parent, QString title, QStringList captions, QStringList values) { if (captions.size() < 1) return QStringList(); if (captions.size() != values.size()) { qDebug("QtSLiMRunLineEditArrayDialog: captions and values are not the same length!"); return QStringList(); } // make the dialog with an overall vertical layout QDialog *dialog = new QDialog(p_parent); QVBoxLayout *verticalLayout = new QVBoxLayout(dialog); // title label { QLabel *titleLabel = new QLabel(dialog); QFont font; font.setBold(true); font.setWeight(QFont::Bold); // used to be 75, which made little sense; fixed for Qt6 titleLabel->setText(title); titleLabel->setFont(font); verticalLayout->addWidget(titleLabel); } // below-title spacer { QSpacerItem *belowTitleSpacer = new QSpacerItem(20, 8, QSizePolicy::Minimum, QSizePolicy::Fixed); verticalLayout->addItem(belowTitleSpacer); } // grid layout QVector lineEdits; { QGridLayout *gridLayout = new QGridLayout(); int rowCount = captions.size(); for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { QString caption = captions[rowIndex]; QString value = values[rowIndex]; QLabel *paramLabel = new QLabel(dialog); paramLabel->setText(caption); gridLayout->addWidget(paramLabel, rowIndex, 1); QLineEdit *paramLineEdit = new QLineEdit(dialog); paramLineEdit->setText(value); paramLineEdit->setFixedWidth(60); paramLineEdit->setAlignment(Qt::AlignRight | Qt::AlignVCenter); gridLayout->addWidget(paramLineEdit, rowIndex, 3); lineEdits.push_back(paramLineEdit); } // spacers, which only need to exist in the first row of the grid { QSpacerItem *leftMarginSpacer = new QSpacerItem(16, 5, QSizePolicy::Fixed, QSizePolicy::Minimum); gridLayout->addItem(leftMarginSpacer, 0, 0); } { QSpacerItem *internalSpacer = new QSpacerItem(20, 5, QSizePolicy::Fixed, QSizePolicy::Minimum); gridLayout->addItem(internalSpacer, 0, 2); } verticalLayout->addLayout(gridLayout); } // button box { QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setOrientation(Qt::Horizontal); buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); verticalLayout->addWidget(buttonBox); QObject::connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); QObject::connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); } // fix sizing dialog->setFixedSize(dialog->sizeHint()); dialog->setSizeGripEnabled(false); // select the first lineEdit and run the dialog lineEdits[0]->selectAll(); int result = dialog->exec(); if (result == QDialog::Accepted) { QStringList returnList; for (QLineEdit *lineEdit : static_cast &>(lineEdits)) returnList.append(lineEdit->text()); delete dialog; return returnList; } else { delete dialog; return QStringList(); } } // A subclass of QPushButton that draws its image with antialiasing, for a better appearance; used for the About panel // See QtSLiMPushButton below for further comments; it uses the same approach, with additional bells and whistles void QtSLiMIconView::paintEvent(QPaintEvent * /* p_paintEvent */) { QPainter painter(this); QRect bounds = rect(); // This uses the icon to draw, which works because of Qt::AA_UseHighDpiPixmaps painter.save(); painter.setRenderHint(QPainter::SmoothPixmapTransform); icon().paint(&painter, bounds, Qt::AlignCenter, isEnabled() ? QIcon::Normal : QIcon::Disabled, QIcon::Off); painter.restore(); } // A subclass of QPushButton that draws its image at screen resolution, for a better appearance on Retina etc. // Turns out setting Qt::AA_UseHighDpiPixmaps fixes that issue; but this subclass also makes the buttons draw // correctly in Qt 5.14.2, where button icons are shifted right one pixel and then clipped in an ugly way. // BCH 1/18/2021: This class now has additional smarts to handle dark mode. First of all, the base name for // the icon used should be set up at creation time with a call to qtslimSetIcon(), with a highlight of false, // presumably; for an icon of :/buttons/foo.png, that would be qtslimSetIcon("foo", false). When the icon // should be changed, either in its base name or highlight state, this can be changed with another such call. // This will lead to the use of one of four image files depending on highlight state and dark mode state: // :/buttons/foo.png, :/buttons/foo_H.png, :/buttons_DARK/foo_DARK.png, or :/buttons_DARK/foo_H_DARK.png. // If the corresponding image file does not exist, an error message will be logged to the console, and the // button will probably not draw properly. All button images should be exactly the same size. QtSLiMPushButton::QtSLiMPushButton(const QIcon &p_icon, const QString &p_text, QWidget *p_parent) : QPushButton(p_icon, p_text, p_parent) { sharedInit(); } QtSLiMPushButton::QtSLiMPushButton(const QString &p_text, QWidget *p_parent) : QPushButton(p_text, p_parent) { sharedInit(); } QtSLiMPushButton::QtSLiMPushButton(QWidget *p_parent) : QPushButton(p_parent) { sharedInit(); } void QtSLiMPushButton::sharedInit(void) { // This button class is designed to work with icon images that include a border and background, // and typically include a transparent background, so we use a style sheet to enforce that setStyleSheet(QString::fromUtf8("QPushButton:pressed {\n" " background-color: #00000000;\n" " border: 0px;\n" "}\n" "QPushButton:checked {\n" " background-color: #00000000;\n" " border: 0px;\n" "}")); } QtSLiMPushButton::~QtSLiMPushButton(void) { qtslimFreeCachedIcons(); } void QtSLiMPushButton::qtslimFreeCachedIcons(void) { if (qtslimIcon) { delete qtslimIcon; qtslimIcon = nullptr; } if (qtslimIcon_H) { delete qtslimIcon_H; qtslimIcon_H = nullptr; } if (qtslimIcon_DARK) { delete qtslimIcon_DARK; qtslimIcon_DARK = nullptr; } if (qtslimIcon_H_DARK) { delete qtslimIcon_H_DARK; qtslimIcon_H_DARK = nullptr; } } bool QtSLiMPushButton::hitButton(const QPoint &mousePosition) const { // I noticed that mouse tracking in QtSLiMPushButton was off; it seemed like the bounds were // kind of inset, and Qt doesn't know the buttons are circular, and so forth. Therefore this. // mousePosition is in the same coordinate system as rect(); we want to consider mousePosition // to be a hit if it is inside the circle or oval bounded by rect(), so let's bust out Pythagoras QRect bounds = rect(); double xd = (mousePosition.x() - bounds.left()) / (double)bounds.width() - 0.5; double yd = (mousePosition.y() - bounds.top()) / (double)bounds.height() - 0.5; double distance = std::sqrt(xd * xd + yd * yd); //qDebug() << "x ==" << x << ", y ==" << y << "; d ==" << d; return (distance <= 0.51); // a little more than 0.5 to provide a little slop } void QtSLiMPushButton::paintEvent(QPaintEvent *p_paintEvent) { // We need a base name to operate; without one, we punt to super and it draws whatever it draws if (qtslimBaseName.length() == 0) { qDebug() << "QtSLiMPushButton::paintEvent: base name not set for object" << objectName(); QPushButton::paintEvent(p_paintEvent); return; } // We have a base name; get the cached icon corresponding to our state QIcon *cachedIcon = qtslimIconForState(qtslimHighlighted, QtSLiMInDarkMode()); if (!cachedIcon) { qDebug() << "QtSLiMPushButton::paintEvent: icon not found for base name" << qtslimBaseName; QPushButton::paintEvent(p_paintEvent); return; } // We got a valid icon; draw with it QPainter painter(this); QRect bounds = rect(); // This uses the icon to draw, which works because of Qt::AA_UseHighDpiPixmaps painter.save(); painter.setRenderHint(QPainter::SmoothPixmapTransform); if (temporaryIcon.isNull()) { cachedIcon->paint(&painter, bounds, Qt::AlignCenter, isEnabled() ? QIcon::Normal : QIcon::Disabled, QIcon::Off); } else { // assume that the temporary icon completely covers the base icon when opacity is 1.0; this avoids artifacts // in the appearance of the button with opacity 1.0 due to double-drawing pixels with partial alpha if (temporaryIconOpacity < 1.0) cachedIcon->paint(&painter, bounds, Qt::AlignCenter, isEnabled() ? QIcon::Normal : QIcon::Disabled, QIcon::Off); if (temporaryIconOpacity > 0.0) { painter.setOpacity(temporaryIconOpacity); temporaryIcon.paint(&painter, bounds, Qt::AlignCenter, isEnabled() ? QIcon::Normal : QIcon::Disabled, QIcon::Off); } } painter.restore(); /* // This code would construct and draw a high-DPI image, but using Qt::AA_UseHighDpiPixmaps is better QTransform transform = painter.combinedTransform(); QRect mappedBounds = transform.mapRect(bounds); QSize size = mappedBounds.size(); QIcon ico = icon(); QPixmap pix = ico.pixmap(size, isEnabled() ? QIcon::Normal : QIcon::Disabled, QIcon::Off); QImage image = pix.toImage(); painter.drawImage(rect(), image); */ } void QtSLiMPushButton::qtslimSetHighlight(bool highlighted) { if (qtslimBaseName.length() == 0) qDebug() << "QtSLiMPushButton::qtslimSetHighlight: base name not set for object" << objectName(); // We're not changing our base name, so we don't need to throw out cached icons qtslimHighlighted = highlighted; update(); } void QtSLiMPushButton::qtslimSetIcon(QString baseName, bool highlighted) { if (baseName == qtslimBaseName) { // We're not changing our base name, so we don't need to throw out cached icons qtslimHighlighted = highlighted; } else { // We're changing base name, so throw out cached icons qtslimBaseName = baseName; qtslimHighlighted = highlighted; qtslimFreeCachedIcons(); } update(); } QIcon *QtSLiMPushButton::qtslimIconForState(bool highlighted, bool darkMode) { if (!highlighted) { if (!darkMode) { if (!qtslimIcon) { QString path = ":/buttons/"; path += qtslimBaseName; path += ".png"; qtslimIcon = new QIcon(path); } return qtslimIcon; } else { if (!qtslimIcon_DARK) { QString path = ":/buttons_DARK/"; path += qtslimBaseName; path += "_DARK.png"; qtslimIcon_DARK = new QIcon(path); } return qtslimIcon_DARK; } } else { if (!darkMode) { if (!qtslimIcon_H) { QString path = ":/buttons/"; path += qtslimBaseName; path += "_H.png"; qtslimIcon_H = new QIcon(path); } return qtslimIcon_H; } else { if (!qtslimIcon_H_DARK) { QString path = ":/buttons_DARK/"; path += qtslimBaseName; path += "_H_DARK.png"; qtslimIcon_H_DARK = new QIcon(path); } return qtslimIcon_H_DARK; } } } // A subclass of QSplitterHandle that does some custom drawing void QtSLiMSplitterHandle::paintEvent(QPaintEvent *p_paintEvent) { QPainter painter(this); QRect bounds = rect(); bool inDarkMode = QtSLiMInDarkMode(); // provide a darkened and beveled appearance QRect begin1Strip, begin2Strip, centerStrip, end2Strip, end1Strip; if (orientation() == Qt::Vertical) { begin1Strip = bounds.adjusted(0, 0, 0, -(bounds.height() - 1)); begin2Strip = bounds.adjusted(0, 1, 0, -(bounds.height() - 2)); centerStrip = bounds.adjusted(0, 2, 0, -2); end2Strip = bounds.adjusted(0, bounds.height() - 2, 0, -1); end1Strip = bounds.adjusted(0, bounds.height() - 1, 0, 0); } else // Qt::Horizontal { begin1Strip = bounds.adjusted(0, 0, -(bounds.width() - 1), 0); begin2Strip = bounds.adjusted(1, 0, -(bounds.width() - 2), 0); centerStrip = bounds.adjusted(2, 0, -2, 0); end2Strip = bounds.adjusted(bounds.width() - 2, 0, -1, 0); end1Strip = bounds.adjusted(bounds.width() - 1, 0, 0, 0); } painter.fillRect(begin1Strip, QtSLiMColorWithWhite(inDarkMode ? 0.227 : 0.773, 1.0)); painter.fillRect(begin2Strip, QtSLiMColorWithWhite(inDarkMode ? 0.000 : 1.000, 1.0)); painter.fillRect(centerStrip, QtSLiMColorWithWhite(inDarkMode ? 0.035 : 0.965, 1.0)); painter.fillRect(end2Strip, QtSLiMColorWithWhite(inDarkMode ? 0.082 : 0.918, 1.0)); painter.fillRect(end1Strip, QtSLiMColorWithWhite(inDarkMode ? 0.278 : 0.722, 1.0)); // On Linux, super draws the knob one pixel to the right of where it ought to be, so we draw it ourselves // This code is modified from QtSplitterHandle in the Qt 5.14.2 sources (it's identical in Qt 5.9.8) // This may turn out to be undesirable, as it assumes that the Linux widget kit is the one I use on Ubuntu #if defined(__linux__) if (orientation() == Qt::Horizontal) { QStyleOption opt(0); opt.rect = contentsRect().adjusted(0, 0, -1, 0); // make the rect one pixel narrower, which shifts the knob opt.palette = palette(); opt.state = QStyle::State_Horizontal; /* // We don't have access to the hover/pressed state as far as I know, but it seems to be unused anyway if (d->hover) opt.state |= QStyle::State_MouseOver; if (d->pressed) opt.state |= QStyle::State_Sunken; */ if (isEnabled()) opt.state |= QStyle::State_Enabled; parentWidget()->style()->drawControl(QStyle::CE_Splitter, &opt, &painter, splitter()); return; } #endif // call super to overlay the splitter knob QSplitterHandle::paintEvent(p_paintEvent); } // A subclass of QStatusBar that draws a top separator, so our splitters abut nicely // BCH 9/20/2020: this now draws the message as HTML text too, allowing colorized signatures QtSLiMStatusBar::QtSLiMStatusBar(QWidget *p_parent) : QStatusBar(p_parent) { // whenever our message changes, we resize vertically to accommodate it connect(this, &QStatusBar::messageChanged, this, [this]() { setHeightFromContent(); }); } void QtSLiMStatusBar::paintEvent(QPaintEvent * /*p_paintEvent*/) { QPainter p(this); QRect bounds = rect(); bool inDarkMode = QtSLiMInDarkMode(); // fill the interior; we no longer try to inherit this from QStatusBar, that was a headache p.fillRect(bounds, QtSLiMColorWithWhite(inDarkMode ? 0.118 : 0.965, 1.0)); // draw the top separator and bevel lines QRect bevelLine = bounds.adjusted(0, 0, 0, -(bounds.height() - 1)); p.fillRect(bevelLine, QtSLiMColorWithWhite(inDarkMode ? 0.278 : 0.722, 1.0)); p.fillRect(bevelLine.adjusted(0, 1, 0, 1), QtSLiMColorWithWhite(inDarkMode ? 0.000 : 1.000, 1.0)); p.fillRect(bevelLine.adjusted(0, (bounds.height() - 1), 0, (bounds.height() - 1)), QtSLiMColorWithWhite(inDarkMode ? 0.082 : 0.918, 1.0)); // draw the message if (!currentMessage().isEmpty()) { // would be nice for these coordinates not to be magic #ifdef __APPLE__ p.translate(QPointF(6, 3)); #else p.translate(QPointF(5, 1)); #endif p.setPen(inDarkMode ? Qt::white : Qt::black); QSizeF pageSize(bounds.width() - 10, 200); // wrap to our width, with a maximum height of 200 (which should never happen) QTextDocument td; td.setPageSize(pageSize); td.setHtml(currentMessage()); td.drawContents(&p, bounds); } } void QtSLiMStatusBar::resizeEvent(QResizeEvent *p_resizeEvent) { // first call super to realize all consequences of the resize QStatusBar::resizeEvent(p_resizeEvent); // Then calculate our new minimum height, as a result of wrapping, and set it in a deferred manner to avoid recursion issues QTimer::singleShot(0, this, &QtSLiMStatusBar::setHeightFromContent); } void QtSLiMStatusBar::setHeightFromContent(void) { // this mirrors the code in QtSLiMStatusBar::paintEvent() QRect bounds = rect(); QSizeF pageSize(bounds.width() - 10, 200); // wrap to our width, with a maximum height of 200 (which should never happen) QTextDocument td; td.setPageSize(pageSize); td.setHtml(currentMessage()); // now get the drawn text height and calculate our minimum height QSizeF textSize = td.documentLayout()->documentSize(); QSize minSizeHint = minimumSizeHint(); QSize oldMinSize = minimumSize(); QSize newMinSize; int newMaxHeight; if (textSize.height() < minSizeHint.height()) { newMinSize = QSize(0, 0); newMaxHeight = minSizeHint.height(); } else { #ifdef __linux__ newMinSize = QSize(minSizeHint.width(), textSize.height() + 0); #else newMinSize = QSize(minSizeHint.width(), textSize.height() + 6); #endif newMaxHeight = newMinSize.height(); } // set the new size only if it is different from the old height, to minimize thrash if (newMinSize != oldMinSize) { setMinimumSize(newMinSize); setMaximumHeight(newMaxHeight); // we have to set the max height also, to make the Eidos console's status bar work properly //qDebug() << "setHeightFromContent():"; //qDebug() << " minSizeHint == " << minSizeHint; //qDebug() << " textSize == " << textSize; //qDebug() << " newMinSize == " << newMinSize; } } QPixmap QtSLiMDarkenPixmap(QPixmap p_pixmap) { QPixmap pixmap(p_pixmap); { QPainter painter(&pixmap); painter.fillRect(pixmap.rect(), QtSLiMColorWithWhite(0.0, 0.35)); } return pixmap; } // find flashing; see https://bugreports.qt.io/browse/QTBUG-83147 static QPalette QtSLiMFlashPalette(QPlainTextEdit *te) { // Returns a palette for QtSLiMTextEdit for highlighting errors, which could depend on platform and dark mode // Note that this is based on the current palette, and derives only the highlight colors QPalette p = te->palette(); p.setColor(QPalette::Highlight, QColor(QColor(Qt::yellow))); p.setColor(QPalette::HighlightedText, QColor(Qt::black)); return p; } void QtSLiMFlashHighlightInTextEdit(QPlainTextEdit *te) { const int delayMillisec = 80; // seems good? 12.5 times per second // set to the flash color te->setPalette(QtSLiMFlashPalette(te)); // set up timers to flash the color again; we don't worry about being called multiple times, // cancelling old timers, etc., because this is so quick that it really doesn't matter; // it sorts itself out more quickly than the user can really notice any discrepancy QTimer::singleShot(delayMillisec, te, [te]() { te->setPalette(qApp->palette(te)); }); QTimer::singleShot(delayMillisec * 2, te, [te]() { te->setPalette(QtSLiMFlashPalette(te)); }); QTimer::singleShot(delayMillisec * 3, te, [te]() { te->setPalette(qApp->palette(te)); }); } // A QLabel that shows shortened text with an ellipsis; see https://stackoverflow.com/a/73316405/2752221 QtSLiMEllipsisLabel::QtSLiMEllipsisLabel(QWidget *parent) : QtSLiMEllipsisLabel("", parent) { } QtSLiMEllipsisLabel::QtSLiMEllipsisLabel(QString text, QWidget *parent) : QLabel(parent) { setText(text); } void QtSLiMEllipsisLabel::setText(QString text) { m_text = text; updateText(); } QSize QtSLiMEllipsisLabel::minimumSizeHint() const { return QSize(0, QLabel::minimumSizeHint().height()); } void QtSLiMEllipsisLabel::resizeEvent(QResizeEvent *p_event) { QLabel::resizeEvent(p_event); updateText(); } void QtSLiMEllipsisLabel::updateText() { QFontMetrics metrics(font()); QString elided = metrics.elidedText(m_text, Qt::ElideRight, width()); QLabel::setText(elided); } void QtSLiMEllipsisLabel::mousePressEvent(QMouseEvent *p_event) { // check the mouse position and only take the click if it is within the displayed label's extent QPoint curPoint = p_event->pos(); int clickX = curPoint.x(); QFontMetrics metrics(font()); int labelLength = metrics.size(0, text()).width(); if ((clickX >= 0) && (clickX <= labelLength + 1)) emit pressed(); // triggers QtSLiMWindow::jumpToPopupButtonPressed() } // Natural sorting (sorting numerically when the first difference is a numeric substring) bool EidosNaturalSort(QString &a, QString &b) { QChar *aptr = a.data(); int alen = a.length(); QChar *bptr = b.data(); int blen = b.length(); do { // look for a shared non-numeric prefix and remove it while ((alen >= 1) && (blen >= 1)) { QChar ach = *aptr; QChar bch = *bptr; if ((ach == bch) && !ach.isDigit()) { aptr++; alen--; bptr++; blen--; } else break; } // parse a leading integer from both strings bool anum = false; int aval = 0; while ((alen >= 1) && aptr->isDigit()) { char ach = aptr->toLatin1(); // we assume that if isDigit() is true, the digit is ASCII if ((ach >= '0') && (ach <= '9')) { aval = aval * 10 + (ach - '0'); aptr++; alen--; anum = true; } } bool bnum = false; int bval = 0; while ((blen >= 1) && bptr->isDigit()) { char bch = bptr->toLatin1(); // we assume that if isDigit() is true, the digit is ASCII if ((bch >= '0') && (bch <= '9')) { bval = bval * 10 + (bch - '0'); bptr++; blen--; bnum = true; } } // look for a shared numeric (integer) prefix if (anum && bnum) { // if a numeric prefix is present in both, compare numerically if (aval != bval) { return aval < bval; } // else if the numeric prefixes are identical, drop through and repeat the process } else { // if one or both strings do not have a numeric prefix, compare alphabetically return a < b; } } while (true); return a < b; } ================================================ FILE: QtSLiM/QtSLiMExtras.h ================================================ // // QtSLiMExtras.h // SLiM // // Created by Ben Haller on 7/28/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMEXTRAS_H #define QTSLIMEXTRAS_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "eidos_property_signature.h" #include "eidos_call_signature.h" class QPaintEvent; // legend positions for QtSLiMGraphView typedef enum { kUnconfigured = -1, kTopLeft = 0, kTopRight, kBottomLeft, kBottomRight } QtSLiM_LegendPosition; // Utility helpers for window visibility/relocation bool QtSLiMIsMostlyOnScreen(QWidget *window); void QtSLiMRelocateQuietly(QWidget *window); void QtSLiMMakeWindowVisibleAndExposed(QWidget *window); void QtSLiMClearLayout(QLayout *layout, bool deleteWidgets = true); void QtSLiMFrameRect(const QRect &p_rect, const QColor &p_color, QPainter &p_painter); void QtSLiMFrameRect(const QRectF &p_rect, const QColor &p_color, QPainter &p_painter, double p_lineWidth); QColor QtSLiMColorWithWhite(double p_white, double p_alpha); QColor QtSLiMColorWithRGB(double p_red, double p_green, double p_blue, double p_alpha); QColor QtSLiMColorWithHSV(double p_hue, double p_saturation, double p_value, double p_alpha); void RGBForFitness(double fitness, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor); void RGBForSelectionCoeff(double selectionCoeff, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor); // A color scale widget that shows the color scales for fitness and selection coefficients class QtSLiMColorScaleWidget : public QWidget { public: QtSLiMColorScaleWidget(QWidget *p_parent); protected: virtual void paintEvent(QPaintEvent *p_paintEvent) override; private: std::vector fitnessTicks, effectTicks; }; // Whether we're in "dark mode" for user interface rendering bool QtSLiMInDarkMode(void); // Standard paths for our images, ending in _H for highlighted, and then in _DARK for dark mode icons, and then in .png QString QtSLiMImagePath(QString baseName, bool highlighted); // A subclass of QLineEdit that selects all its text when it receives keyboard focus // It also supports showing a "progress bar" under its text, and it has a modified // appearance that can be disabled but still show fairly dark text for readability class QtSLiMGenerationLineEdit : public QLineEdit { Q_OBJECT public: QtSLiMGenerationLineEdit(const QString &contents, QWidget *p_parent = nullptr); QtSLiMGenerationLineEdit(QWidget *p_parent = nullptr); virtual ~QtSLiMGenerationLineEdit() override; // can optionally display "progress" in the background of the lineedit void setProgress(double p_progress); // set its appearance/behavior; do not use setEnabled(), use this! void setAppearance(bool p_enabled, bool p_dimmed); protected: virtual void focusInEvent(QFocusEvent *p_event) override; virtual void paintEvent(QPaintEvent *p_paintEvent) override; private: QtSLiMGenerationLineEdit() = delete; QtSLiMGenerationLineEdit(const QtSLiMGenerationLineEdit&) = delete; QtSLiMGenerationLineEdit& operator=(const QtSLiMGenerationLineEdit&) = delete; void _ReconfigureAppearance(void); double progress = 0.0; bool dimmed = false; }; void ColorizePropertySignature(const EidosPropertySignature *property_signature, double pointSize, QTextCursor lineCursor); void ColorizeCallSignature(const EidosCallSignature *call_signature, double pointSize, QTextCursor lineCursor); // A subclass of QHBoxLayout specifically designed to lay out the play controls in the main window class QtSLiMPlayControlsLayout : public QHBoxLayout { Q_OBJECT public: QtSLiMPlayControlsLayout(QWidget *p_parent): QHBoxLayout(p_parent) {} QtSLiMPlayControlsLayout(): QHBoxLayout() {} virtual ~QtSLiMPlayControlsLayout() override; virtual QSize sizeHint() const override; virtual QSize minimumSize() const override; virtual void setGeometry(const QRect &rect) override; }; // Heat colors for profiling display QColor slimColorForFraction(double fraction); // Nicely formatted memory usage strings QString stringForByteCount(uint64_t bytes); QString attributedStringForByteCount(uint64_t bytes, double total, QTextCharFormat &format); // Nicely formatted dateline for output QString slimDateline(void); // Running a panel to obtain numbers from the user QStringList QtSLiMRunLineEditArrayDialog(QWidget *p_parent, QString title, QStringList captions, QStringList values); // A subclass of QPushButton that draws its image with antialiasing, for a better appearance; used for the About panel class QtSLiMIconView : public QPushButton { Q_OBJECT public: QtSLiMIconView(const QIcon &p_icon, const QString &p_text, QWidget *p_parent = nullptr) : QPushButton(p_icon, p_text, p_parent) {} QtSLiMIconView(const QString &p_text, QWidget *p_parent = nullptr) : QPushButton(p_text, p_parent) {} QtSLiMIconView(QWidget *p_parent = nullptr) : QPushButton(p_parent) {} virtual ~QtSLiMIconView(void) override {} protected: virtual void paintEvent(QPaintEvent *p_paintEvent) override; }; // A subclass of QPushButton that draws its image with antialiasing, for a better appearance, and handles dark mode appearance class QtSLiMPushButton : public QPushButton { Q_OBJECT public: QtSLiMPushButton(const QIcon &p_icon, const QString &p_text, QWidget *p_parent = nullptr); QtSLiMPushButton(const QString &p_text, QWidget *p_parent = nullptr); QtSLiMPushButton(QWidget *p_parent = nullptr); virtual ~QtSLiMPushButton(void) override; void qtslimSetBaseName(QString baseName) { qtslimSetIcon(baseName, false); } void qtslimSetHighlight(bool highlighted); void qtslimSetIcon(QString baseName, bool highlighted); // An added feature beyond QPushButton: support for a "temporary icon" drawn on top of the // normal cached icon, with variable opacity. This supports the pulsing debug output button. void setTemporaryIcon(QIcon tempIcon) { temporaryIcon = tempIcon; update(); } void setTemporaryIconOpacity(double opacity) { temporaryIconOpacity = opacity; update(); } void clearTemporaryIcon(void) { temporaryIcon = QIcon(); update(); } protected: void sharedInit(void); virtual bool hitButton(const QPoint &pos) const override; virtual void paintEvent(QPaintEvent *p_paintEvent) override; QString qtslimBaseName; // base name, such as "foo" bool qtslimHighlighted = false; // highlighted state (appends _H to the base name) QIcon *qtslimIcon = nullptr; QIcon *qtslimIcon_H = nullptr; QIcon *qtslimIcon_DARK = nullptr; QIcon *qtslimIcon_H_DARK = nullptr; QIcon temporaryIcon; double temporaryIconOpacity = 0.0; void qtslimFreeCachedIcons(void); QIcon *qtslimIconForState(bool highlighted, bool darkMode); }; // A subclass of QSplitterHandle that does some custom drawing class QtSLiMSplitterHandle : public QSplitterHandle { Q_OBJECT public: QtSLiMSplitterHandle(Qt::Orientation p_orientation, QSplitter *p_parent) : QSplitterHandle(p_orientation, p_parent) {} virtual ~QtSLiMSplitterHandle(void) override {} protected: virtual void paintEvent(QPaintEvent *p_paintEvent) override; }; // A subclass of QSplitter that supplies a custom QSplitterHandle subclass class QtSLiMSplitter : public QSplitter { Q_OBJECT public: QtSLiMSplitter(Qt::Orientation p_orientation, QWidget *p_parent = nullptr) : QSplitter(p_orientation, p_parent) {} QtSLiMSplitter(QWidget *p_parent = nullptr) : QSplitter(p_parent) {} virtual ~QtSLiMSplitter(void) override {} protected: virtual QSplitterHandle *createHandle(void) override { return new QtSLiMSplitterHandle(orientation(), this); } }; // A subclass of QStatusBar that draws a top separator on Linux, so our splitters abut nicely class QtSLiMStatusBar : public QStatusBar { Q_OBJECT public: QtSLiMStatusBar(QWidget *p_parent = nullptr); virtual ~QtSLiMStatusBar(void) override {} protected: virtual void paintEvent(QPaintEvent *p_paintEvent) override; virtual void resizeEvent(QResizeEvent *p_resizeEvent) override; void setHeightFromContent(void); }; // Used to create the dark app icon displayed when running a model QPixmap QtSLiMDarkenPixmap(QPixmap p_pixmap); void QtSLiMFlashHighlightInTextEdit(QPlainTextEdit *te); // A QLabel subclass that shows shortened text with an ellipsis; see https://stackoverflow.com/a/73316405/2752221 class QtSLiMEllipsisLabel : public QLabel { Q_OBJECT public: explicit QtSLiMEllipsisLabel(QWidget *parent = nullptr); explicit QtSLiMEllipsisLabel(QString text, QWidget *parent = nullptr); void setText(QString); virtual QSize minimumSizeHint() const; signals: void pressed(void); protected: void resizeEvent(QResizeEvent *p_event); void mousePressEvent(QMouseEvent *p_event); private: void updateText(); QString m_text; }; // Natural sorting (sorting numerically when the first difference is a numeric substring) bool EidosNaturalSort(QString &a, QString &b); // Incremental sorting // // This is from https://github.com/KukyNekoi/magicode by Erik Regla, released under the GPL 3. // The algorithms involved are described in Paredes & Navarro (2006) "Optimal Incremental // Sorting" and Regla & Paredes (2015) "Worst-case Optimal Incremental Sorting". Thanks very // much to Erik Regla for making this code available for use. #define FIXED_PIVOT_SELECTION 1 // remove to allow random initial pivot selection #define USE_FAT_PARTITION 1 // use three-way-partitioning #define USE_ALPHA_LESS_THAN_P30 1 // use three-way-partitioning template class BareBoneIQS { public: BareBoneIQS(T *target_ptr, std::size_t target_size); ~BareBoneIQS(); inline void swap(std::size_t lhs, std::size_t rhs); inline std::size_t partition(T pivot_value, std::size_t lhs, std::size_t rhs); std::size_t partition_redundant(T pivot_value, std::size_t lhs, std::size_t rhs); inline std::size_t stack_pop(); inline std::size_t stack_peek(); inline void stack_push(std::size_t value); virtual T next(); protected: /** * In this example we used a stack which is the same length of the array. This is only for * testing purposes and can be changed into a proper stack later on if desired * */ std::size_t *stack; std::size_t stack_length; std::size_t target_size; std::size_t extracted_count; T *target_ptr; }; template class BareBoneIIQS : public BareBoneIQS { public: BareBoneIIQS(T *p_target_ptr, std::size_t p_target_size); ~BareBoneIIQS(); T next(); std::size_t bfprt(std::size_t lhs, std::size_t rhs, std::size_t median_length); std::size_t median(std::size_t lhs, std::size_t rhs); }; /* This constructor allows in-place ordering */ template BareBoneIQS::BareBoneIQS(T *p_target_ptr, std::size_t p_target_size){ this->target_ptr = p_target_ptr; this->target_size = p_target_size; this->stack = (std::size_t *) std::malloc(p_target_size * sizeof(std::size_t)) ; this->stack[0] = p_target_size - 1; // index of the last element this->stack_length = 1; //starts with a single element, the top this->extracted_count = 0; // this way, after adding +1, we can partition as whole } template BareBoneIQS::~BareBoneIQS() { std::free(this->stack); } /** * @brief Swaps two elements in the referenced array * * @param lhs left index to be swapped * @param rhs right index to be swapped */ template inline void BareBoneIQS::swap(std::size_t lhs, std::size_t rhs){ T _temp_val = this->target_ptr[lhs]; this->target_ptr[lhs] = this->target_ptr[rhs]; this->target_ptr[rhs] = _temp_val; } /** * @brief Implementation of Hoare's partition algorithm. Can be found on * Cormen's "Introduction to algorithms - 2nd edition" p146 * This implementation is not resistant to the case on which the elements are repeated. * * @param pivot_value the pivot value to use * @param lhs the left boundary for partition algorithm (inclusive) * @param rhs the right boundary for partition algorithm (inclusive) * @return std::size_t the index on which the partition value belongs */ template inline std::size_t BareBoneIQS::partition(T pivot_value, std::size_t lhs, std::size_t rhs){ if(lhs == rhs) return lhs; lhs--; rhs++; while(1){ do{ lhs++; } while(this->target_ptr[lhs] < pivot_value); do{ rhs--; } while(pivot_value < this->target_ptr[rhs]); if (lhs >= rhs) return rhs; this->swap(lhs, rhs); } } /** * Modified version of hoare's algorithm intended to be resistant to redundant elements along the * partition. This scheme is also known as three-way partitioning. Make sure to select the forcing pivot * scheme that matches your problem accordingly * @param pivot_value the pivot value to use * @param lhs the left boundary for partition algorithm (inclusive) * @param rhs the right boundary for partition algorithm (inclusive) * @return std::size_t the index on which the partition value belongs */ template inline std::size_t BareBoneIQS::partition_redundant(T pivot_value, std::size_t lhs, std::size_t rhs) { std::size_t i = lhs - 1; std::size_t k = rhs + 1; while (1) { while (this->target_ptr[++i] < pivot_value); while (this->target_ptr[--k] > pivot_value); if (i >= k) break; this->swap(i, k); } i = k++; while(i > lhs && this->target_ptr[i] == pivot_value) i--; while(k < rhs && this->target_ptr[k] == pivot_value) k++; #ifdef FORCE_PIVOT_SELECTION_LEFT return i; // return left pivot #elif FORCE_PIVOT_SELECTION_RIGHT return k; // return left pivot #else return (i + k) / 2; // if there is a group, then return the middle element to guarantee a position #endif } /** * @brief Pops the last element on the stack * @return std::size_t element at the top of the stack */ template inline std::size_t BareBoneIQS::stack_pop(){ return this->stack[--this->stack_length]; } /** * @brief Peeks the last element on the stack * @return std::size_t element at the top of the stack */ template inline std::size_t BareBoneIQS::stack_peek(){ return this->stack[this->stack_length-1]; } /** * @brief Inserts an element on the top of the stack * @param value the element to insert */ template inline void BareBoneIQS::stack_push(std::size_t value){ this->stack[this->stack_length] = value; this->stack_length++; } /** * @brief Retrieves the next sorted element. The basic idea is to * use quick select to find the smallest element, but store the pivots along the * way in order to shorten future calculations. * * @return T the next sorted element */ template T BareBoneIQS::next() { // This for allows the tail recursion while(1){ // Base condition. If the element referenced by the top of the stack // is the element that we're actually searching, then retrieve it and // resize the search window if (this->extracted_count == this->stack_peek()){ this->extracted_count++; return this->target_ptr[this->stack_pop()]; } // Selects a random pivot from the remaining array #ifdef FIXED_PIVOT_SELECTION std::size_t pivot_idx = this->extracted_count; #else std::size_t rand_range = this->stack_peek() - this->extracted_count; std::size_t pivot_idx = this->extracted_count + (rand() % rand_range); #endif T pivot_value = this->target_ptr[pivot_idx]; // pivot partition and indexing #ifdef USE_FAT_PARTITION pivot_idx = this->partition_redundant(pivot_value, this->extracted_count, this->stack_peek()); #else pivot_idx = this->partition(pivot_value, this->extracted_count, this->stack_peek()); #endif // Push and recurse the loop this->stack_push(pivot_idx); } } /* This constructor allows in-place ordering */ template BareBoneIIQS::BareBoneIIQS(T *p_target_ptr, std::size_t p_target_size): BareBoneIQS(p_target_ptr, p_target_size){ } template BareBoneIIQS::~BareBoneIIQS() { } /** * * @brief Retrieves the next sorted element. The basic idea is to * use quick select to find the smallest element, but store the pivots along the * way in order to shortent future calculations. * * @tparam T The template class/type to use * @return T the next sorted element */ template T BareBoneIIQS::next() { while(1){ // Base condition. If the element referenced by the top of the stack // is the element that we're actually searching, then retrieve it and // resize the search window std::size_t top_element = this->stack_peek(); std::size_t range = top_element - this->extracted_count; std::size_t p70_idx = (std::size_t)std::ceil(range * 0.7); if (this->extracted_count == top_element ){ this->extracted_count++; return this->target_ptr[this->stack_pop()]; } #ifdef FIXED_PIVOT_SELECTION std::size_t pivot_idx = this->extracted_count; #else std::size_t rand_range = this->stack_peek() - this->extracted_count; std::size_t pivot_idx = this->extracted_count + (rand() % rand_range); #endif T pivot_value = this->target_ptr[pivot_idx]; // pivot partition and indexing #ifdef USE_FAT_PARTITION pivot_idx = this->partition_redundant(pivot_value, this->extracted_count, top_element); #else pivot_idx = this->partition(pivot_value, this->extracted_count, top_element); #endif #ifdef REUSE_PIVOTS std::size_t previous_pivot_idx = pivot_idx; #endif // IIQS changes start! only check if range is less than the square root of the total size // First, we need to check if this pointer belongs P70 \union P30 #ifdef USE_ALPHA_LESS_THAN_P30 std::size_t p30_idx = (std::size_t)std::ceil(range * 0.3); // actually, if we don't care about balancing the stack, you can ignore the p30 condition if (p30_idx > pivot_idx || pivot_idx > p70_idx){ #else if (pivot_idx > p70_idx){ #endif // if we enter here, then it's because the index needs to be recomputed. // So, we ditch the index and get a nice approximate median median and reuse previous computation pivot_idx = this->bfprt(this->extracted_count, top_element, 5); pivot_value = this->target_ptr[pivot_idx]; // then we re-partition, assuming that this median is better #ifdef USE_FAT_PARTITION pivot_idx = this->partition_redundant(pivot_value, this->extracted_count, top_element); #else pivot_idx = this->partition(pivot_value, this->extracted_count, top_element); #endif } // I need to see later how it does affect the stack this segment. #ifdef REUSE_PIVOTS if(previous_pivot_idx < pivot_idx){ this->stack_push(pivot_idx); this->stack_push(previous_pivot_idx); continue; } else if(previous_pivot_idx > pivot_idx){ this->stack_push(previous_pivot_idx); this->stack_push(pivot_idx); continue; } #endif // Push and recurse the loop this->stack_push(pivot_idx); } } /** * In-place implementation of bfprt. Instead of the classical implementation when auxiliary structures are used * this implementation forces two phenomena on the array which both are beneficial to IQS. First, given that we force * the selection of the first index, elements near the beginning have a high chance of being good pivots. Second, we * don't use extra memory to allocate those median results. * @tparam T The template class/type to use * @param lhs the left index to sort (inclusive) * @param rhs the right index to sort (inclusive) * @param median_length size of the median to use on bfprt, 5 is commonly used * @return the median value */ template inline std::size_t BareBoneIIQS::bfprt(std::size_t lhs, std::size_t rhs, std::size_t median_length) { std::size_t base_lhs = lhs; std::size_t medians_extracted = 0; while(1){ // reset base conditions lhs = base_lhs; // check base case if( rhs <= base_lhs + median_length) { return this->median(base_lhs, rhs); } // tail recursion step for bfprt while(lhs + median_length <= rhs){ std::size_t median_index = this->median(lhs, lhs + median_length); //move median to the start of the array this->swap(median_index, base_lhs + medians_extracted); // search for next stride lhs += median_length; medians_extracted++; } rhs = medians_extracted + base_lhs; medians_extracted = 0; } } /** * Median selection via quickselect. We can assume that this process is constant, as it is being always executed * with 5 elements (by default, you can change this later) * * @tparam T T The template class/type to use * @param lhs the left boundary for median algorithm (inclusive) * @param rhs the right boundary for median algorithm (inclusive) * @return the median index */ template inline std::size_t BareBoneIIQS::median(std::size_t lhs, std::size_t rhs) { std::sort(this->target_ptr + lhs, this->target_ptr + rhs); return (lhs + rhs) / 2; /* implement heapsort later as it is more cache-friendly for small arrays, I'm too drunk now */ /* explanation: due to how heapsort is implemented, it scatters in-memory operations, that's * on how the tree is represented on the array (the 2k +1 thing), so if you "recurse" long * enough (namely, you're searching for an element on which you need to trash the cache or even * worse, you lose the dram-bursting) then it gets its performance degraded. * * But since on median finding of a fixed set of elements it's small enough to fit on the cache * and to get dram-bursting benefits, it works better than other sorting algorithms in practice. */ } // // Incremental sorting ENDS // #endif // QTSLIMEXTRAS_H ================================================ FILE: QtSLiM/QtSLiMFindPanel.cpp ================================================ // // QtSLiMFindPanel.cpp // SLiM // // Created by Ben Haller on 3/24/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMFindPanel.h" #include "ui_QtSLiMFindPanel.h" #include #include #include #include #include #include #include #include "QtSLiMAppDelegate.h" #include "QtSLiMExtras.h" QtSLiMFindPanel &QtSLiMFindPanel::instance(void) { static QtSLiMFindPanel *inst = nullptr; if (!inst) inst = new QtSLiMFindPanel(nullptr); return *inst; } QtSLiMFindPanel::QtSLiMFindPanel(QWidget *p_parent) : QDialog(p_parent), ui(new Ui::QtSLiMFindPanel) { ui->setupUi(this); QSettings settings; // no window icon #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect setWindowIcon(QIcon()); #endif // prevent this window from keeping the app running when all main windows are closed setAttribute(Qt::WA_QuitOnClose, false); // Connect the panel UI connect(ui->findNextButton, &QPushButton::clicked, this, &QtSLiMFindPanel::findNext); connect(ui->findPreviousButton, &QPushButton::clicked, this, &QtSLiMFindPanel::findPrevious); connect(ui->replaceAndFindButton, &QPushButton::clicked, this, &QtSLiMFindPanel::replaceAndFind); connect(ui->replaceButton, &QPushButton::clicked, this, &QtSLiMFindPanel::replace); connect(ui->replaceAllButton, &QPushButton::clicked, this, &QtSLiMFindPanel::replaceAll); connect(ui->matchCaseCheckBox, &QPushButton::clicked, this, &QtSLiMFindPanel::optionsChanged); connect(ui->wholeWordCheckBox, &QPushButton::clicked, this, &QtSLiMFindPanel::optionsChanged); connect(ui->wrapAroundCheckBox, &QPushButton::clicked, this, &QtSLiMFindPanel::optionsChanged); connect(ui->findTextLineEdit, &QLineEdit::textChanged, this, &QtSLiMFindPanel::findTextChanged); connect(ui->replaceTextLineEdit, &QLineEdit::textChanged, this, &QtSLiMFindPanel::replaceTextChanged); // Set up the find and replace fields ui->findTextLineEdit->setClearButtonEnabled(true); ui->replaceTextLineEdit->setClearButtonEnabled(true); changingFindText = true; ui->findTextLineEdit->clear(); ui->replaceTextLineEdit->clear(); changingFindText = false; // If Qt's clipboard supports a find buffer (currently macOS only), talk to it QClipboard *clipboard = QGuiApplication::clipboard(); if (clipboard && clipboard->supportsFindBuffer()) { // Note that this logs "QMime::convertToMime: unhandled mimetype: text/plain" in Qt 5.9.8 if the // find buffer is empty; there seems to be no way to avoid that log, so whatever QString findText = clipboard->text(QClipboard::FindBuffer); changingFindText = true; ui->findTextLineEdit->setText(findText); changingFindText = false; connect(clipboard, &QClipboard::findBufferChanged, this, &QtSLiMFindPanel::findBufferChanged); } else { ui->findTextLineEdit->setText(settings.value("QtSLiMFindPanel/findText", "").toString()); } ui->replaceTextLineEdit->setText(settings.value("QtSLiMFindPanel/replaceText", "").toString()); fixEnableState(); // Restore saved options settings.beginGroup("QtSLiMFindPanel"); ui->matchCaseCheckBox->setChecked(settings.value("matchCase", false).toBool()); ui->wholeWordCheckBox->setChecked(settings.value("wholeWord", false).toBool()); ui->wrapAroundCheckBox->setChecked(settings.value("wrapAround", true).toBool()); settings.endGroup(); // Clear the status text ui->statusText->clear(); // The initial height should be enforced as the minimum and maximum height setMinimumHeight(height()); setMaximumHeight(height()); // Restore the saved window position; see https://doc.qt.io/qt-5/qsettings.html#details settings.beginGroup("QtSLiMFindPanel"); resize(settings.value("size", QSize(width(), height())).toSize()); move(settings.value("pos", QPoint(25, 45)).toPoint()); settings.endGroup(); // make window actions for all global menu items qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); } QtSLiMFindPanel::~QtSLiMFindPanel(void) { qDebug() << "QtSLiMFindPanel::~QtSLiMFindPanel()"; delete ui; } QPlainTextEdit *QtSLiMFindPanel::targetTextEditRequireModifiable(bool requireModifiable) { // We rely on QtSLiMAppDelegate to track the active window list for us; // our target is the frontmost window that is not our own window // It can be deallocated before us during quit, so we need to check if (!qtSLiMAppDelegate) return nullptr; QWidget *currentFocusWindow = qtSLiMAppDelegate->activeWindowExcluding(this); if (currentFocusWindow) { //qDebug() << "targetTextEditRequireModifiable() found active window" << currentFocusWindow->windowTitle(); // Given a target window, we target the focusWidget *if* it is a textedit QWidget *windowFocusWidget = currentFocusWindow->focusWidget(); QPlainTextEdit *textEdit = dynamic_cast(windowFocusWidget); // if (windowFocusWidget) // qDebug() << " windowFocusWidget" << windowFocusWidget << " " << windowFocusWidget->objectName() << ", textEdit" << textEdit; // else // qDebug() << " NO FOCUSWIDGET"; // If we've found a textedit, return it if it satisfies requirements // There is no fallback, nor should there be; the focused textedit is our target if (textEdit) { if (!textEdit->isEnabled()) return nullptr; if (requireModifiable && textEdit->isReadOnly()) return nullptr; return textEdit; } } else { //qDebug() << "targetTextEditRequireModifiable() : NO ACTIVE WINDOW"; } return nullptr; } void QtSLiMFindPanel::showFindPanel(void) { QtSLiMMakeWindowVisibleAndExposed(this); // When the find panel is raised, it is conventional to select the find text so the user can immediately type to replace it ui->findTextLineEdit->selectAll(); ui->findTextLineEdit->setFocus(); } void QtSLiMFindPanel::closeEvent(QCloseEvent *p_event) { // Save the window position; see https://doc.qt.io/qt-5/qsettings.html#details QSettings settings; settings.beginGroup("QtSLiMFindPanel"); settings.setValue("size", size()); settings.setValue("pos", pos()); settings.endGroup(); // use super's default behavior QDialog::closeEvent(p_event); } bool QtSLiMFindPanel::findForwardWrapBeep(QPlainTextEdit *target, bool forward, bool wrap, bool beepIfNotFound) { // This method is the only place where I looked at its source code, but for this method at least, // thanks to Lorenzo Bettini for his QtFindReplaceDialog project, http://qtfindreplace.sourceforge.net // It is under the LGPL, so to the extent that I did lean on his code here, it is GPL-compatible. if (!target) { qDebug() << "QtSLiMFindPanel::find() called with no target textEdit!"; return false; } QString findString = ui->findTextLineEdit->text(); QTextDocument::FindFlags findFlags; if (!forward) findFlags |= QTextDocument::FindBackward; if (ui->matchCaseCheckBox->isChecked()) findFlags |= QTextDocument::FindCaseSensitively; if (ui->wholeWordCheckBox->isChecked()) findFlags |= QTextDocument::FindWholeWords; // There is a bug, fixed in Qt 5.12.5, where finding backwards fails to find the first occurrence // that it ought to find, in specific circumstances: the selection must start at the start of a // line, and the first previous occurrence must be in the preceding line. The find() method gets // confused by the preceding block's end. See https://bugreports.qt.io/browse/QTBUG-48035. I do // not attempt to work around this bug here; the workaround would be a bit complex, and the bug // has been fixed, and it's unlikely to bite anyone – it's an edge case, and Find Previous is // relatively unusual. But I've put this as a reminder, in case the bug gets reported to me. bool findResult = target->find(findString, findFlags); if (findResult) { target->centerCursor(); QtSLiMFlashHighlightInTextEdit(target); } else if (wrap) { // If we're wrapping around, do the wrap and try again QTextCursor originalCursor(target->textCursor()); if (forward) target->moveCursor(QTextCursor::Start); else target->moveCursor(QTextCursor::End); findResult = target->find(findString, findFlags); if (findResult) { target->centerCursor(); QtSLiMFlashHighlightInTextEdit(target); } else target->setTextCursor(originalCursor); } if (!findResult) { ui->statusText->setText("no match found "); if (beepIfNotFound) qApp->beep(); } return findResult; } void QtSLiMFindPanel::findNext(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(false); QString findString = ui->findTextLineEdit->text(); if (!target || !findString.length()) { qApp->beep(); return; } findForwardWrapBeep(target, true, ui->wrapAroundCheckBox->isChecked(), true); } void QtSLiMFindPanel::findPrevious(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(false); QString findString = ui->findTextLineEdit->text(); if (!target || !findString.length()) { qApp->beep(); return; } findForwardWrapBeep(target, false, ui->wrapAroundCheckBox->isChecked(), true); } void QtSLiMFindPanel::replaceAndFind(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(true); QString findString = ui->findTextLineEdit->text(); if (!target || !findString.length()) { qApp->beep(); return; } // if the selection is non-empty and equals the find string, replace; then find if (target->textCursor().hasSelection()) { QString selectedText = target->textCursor().selectedText(); if (0 == QString::compare(selectedText, ui->findTextLineEdit->text(), ui->matchCaseCheckBox->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive)) target->textCursor().insertText(ui->replaceTextLineEdit->text()); } findForwardWrapBeep(target, true, ui->wrapAroundCheckBox->isChecked(), true); jumpToSelection(); } void QtSLiMFindPanel::replace(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(true); QString findString = ui->findTextLineEdit->text(); if (!target || !findString.length()) { qApp->beep(); return; } // beep if the selection is empty if (!target->textCursor().hasSelection()) { qApp->beep(); return; } target->textCursor().insertText(ui->replaceTextLineEdit->text()); } void QtSLiMFindPanel::replaceAll(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(true); QString findString = ui->findTextLineEdit->text(); if (!target || !findString.length()) { qApp->beep(); return; } // Search from the document start QTextCursor originalCursor(target->textCursor()); bool hasOccurrence = false; int replaceCount = 0; target->moveCursor(QTextCursor::Start); hasOccurrence = findForwardWrapBeep(target, true, false, true); // beeps if none found // Then, assuming we found at least one occurrence, loop replacing and finding if (hasOccurrence) { target->textCursor().beginEditBlock(); // make this a single undoable action while (hasOccurrence) { target->textCursor().insertText(ui->replaceTextLineEdit->text()); replaceCount++; hasOccurrence = findForwardWrapBeep(target, true, false, false); } target->textCursor().endEditBlock(); // undoable action ends } // Restore the original cursor as closely as we can target->setTextCursor(originalCursor); jumpToSelection(); // show the replacement count ui->statusText->setText(QString("replaced %1 occurrence%2 ").arg(replaceCount).arg(replaceCount == 1 ? "" : "s")); } void QtSLiMFindPanel::useSelectionForFind(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(false); QString selectionString = target->textCursor().selectedText(); if (selectionString.length()) ui->findTextLineEdit->setText(selectionString); // this will trigger QtSLiMFindPanel::findTextChanged() else qApp->beep(); } void QtSLiMFindPanel::useSelectionForReplace(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(false); QString selectionString = target->textCursor().selectedText(); if (selectionString.length()) ui->replaceTextLineEdit->setText(selectionString); // this will trigger QtSLiMFindPanel::replaceTextChanged() else qApp->beep(); } void QtSLiMFindPanel::jumpToSelection(void) { ui->statusText->clear(); QPlainTextEdit *target = targetTextEditRequireModifiable(false); target->centerCursor(); QtSLiMFlashHighlightInTextEdit(target); } void QtSLiMFindPanel::jumpToLine(void) { QPlainTextEdit *target = targetTextEditRequireModifiable(false); int lineIndex = target->textCursor().block().blockNumber(); QStringList choices = QtSLiMRunLineEditArrayDialog(target->window(), "Jump to Line:", QStringList{"Line number:"}, QStringList{QString::number(lineIndex)}); if (choices.length() == 1) { lineIndex = choices[0].toInt(); if (lineIndex < 1) { lineIndex = 1; qApp->beep(); } if (lineIndex > target->document()->blockCount()) { lineIndex = target->document()->blockCount(); qApp->beep(); } QTextCursor lineCursor(target->document()); lineCursor.setPosition(0); lineCursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, lineIndex - 1); target->setTextCursor(lineCursor); target->centerCursor(); } } void QtSLiMFindPanel::findBufferChanged(void) { // If the clipboard's find buffer changes, we need to (1) update the find lineEdit, and (2) update our status text // We use changingFindText to avoid responding to find text changes we cause ourselves if (!changingFindText) { QClipboard *clipboard = QGuiApplication::clipboard(); if (clipboard && clipboard->supportsFindBuffer()) { QString findText = clipboard->text(QClipboard::FindBuffer); changingFindText = true; ui->findTextLineEdit->setText(findText); changingFindText = false; ui->statusText->clear(); fixEnableState(); } } } void QtSLiMFindPanel::findTextChanged(void) { // If the find text lineEdit changes, we need to (1) update the clipboard, and (2) update our status text // We use changingFindText to avoid responding to find text changes we cause ourselves if (!changingFindText) { QString findText = ui->findTextLineEdit->text(); if (findText.length()) // don't change the find buffer if we have no find text { QClipboard *clipboard = QGuiApplication::clipboard(); if (clipboard && clipboard->supportsFindBuffer()) { changingFindText = true; clipboard->setText(findText, QClipboard::FindBuffer); changingFindText = false; } else { QSettings settings; settings.setValue("QtSLiMFindPanel/findText", findText); } } ui->statusText->clear(); fixEnableState(); } } void QtSLiMFindPanel::replaceTextChanged(void) { ui->statusText->clear(); // Save the replace string to prefs; unlike findTextChanged() we do this even when the replace string is zero-length QSettings settings; settings.setValue("QtSLiMFindPanel/replaceText", ui->replaceTextLineEdit->text()); } void QtSLiMFindPanel::optionsChanged(void) { ui->statusText->clear(); // When the search options change, we need to write options to prefs QSettings settings; settings.beginGroup("QtSLiMFindPanel"); settings.setValue("matchCase", ui->matchCaseCheckBox->isChecked()); settings.setValue("wholeWord", ui->wholeWordCheckBox->isChecked()); settings.setValue("wrapAround", ui->wrapAroundCheckBox->isChecked()); settings.endGroup(); } void QtSLiMFindPanel::fixEnableState(void) { bool hasFindText = (ui->findTextLineEdit->text().length() > 0); bool hasTarget = (QtSLiMFindPanel::targetTextEditRequireModifiable(false) != nullptr); bool hasModifiableTarget = (QtSLiMFindPanel::targetTextEditRequireModifiable(true) != nullptr); ui->findNextButton->setEnabled(hasFindText && hasTarget); ui->findPreviousButton->setEnabled(hasFindText && hasTarget); ui->replaceAndFindButton->setEnabled(hasFindText && hasModifiableTarget); ui->replaceButton->setEnabled(hasFindText && hasModifiableTarget); ui->replaceAllButton->setEnabled(hasFindText && hasModifiableTarget); } ================================================ FILE: QtSLiM/QtSLiMFindPanel.h ================================================ // // QtSLiMFindPanel.h // SLiM // // Created by Ben Haller on 3/24/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMFINDPANEL_H #define QTSLIMFINDPANEL_H #include class QCloseEvent; class QPlainTextEdit; namespace Ui { class QtSLiMFindPanel; } class QtSLiMFindPanel : public QDialog { Q_OBJECT public: static QtSLiMFindPanel &instance(void); // BCH 2/10/2021: Switched from QTextEdit to QPlainTextEdit for the target of the Find panel, // since we are switching over to QPlainTextEdit for the script/console/output views. // Unfortunate that the design kind of requires it to be one or the other; C++ for the lose. // Once again we need Obj-C protocols. QPlainTextEdit *targetTextEditRequireModifiable(bool requireModifiable); // public for menu enabling public slots: void showFindPanel(void); void findNext(void); void findPrevious(void); void replaceAndFind(void); void replace(void); void replaceAll(void); void useSelectionForFind(void); void useSelectionForReplace(void); void jumpToSelection(void); void jumpToLine(void); void fixEnableState(void); private: // singleton pattern explicit QtSLiMFindPanel(QWidget *p_parent = nullptr); QtSLiMFindPanel(void) = delete; virtual ~QtSLiMFindPanel(void) override; QtSLiMFindPanel(const QtSLiMFindPanel&) = delete; QtSLiMFindPanel& operator=(const QtSLiMFindPanel&) = delete; virtual void closeEvent(QCloseEvent *e) override; bool findForwardWrapBeep(QPlainTextEdit *target, bool forward, bool wrap, bool beepIfNotFound); bool changingFindText = false; private slots: void findBufferChanged(void); void findTextChanged(void); void replaceTextChanged(void); void optionsChanged(void); private: Ui::QtSLiMFindPanel *ui; }; #endif // QTSLIMFINDPANEL_H ================================================ FILE: QtSLiM/QtSLiMFindPanel.ui ================================================ QtSLiMFindPanel 0 0 450 190 16777215 190 Find & Replace true 8 8 8 8 4 Replace: Find: 11 true status text lineedit Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Options: 8 12 Qt::NoFocus Match case Qt::NoFocus Whole word Qt::NoFocus Wrap around 9 Qt::NoFocus Find Next true Qt::NoFocus Find Previous Qt::Vertical QSizePolicy::Fixed 20 10 Qt::NoFocus Replace && Find Qt::NoFocus Replace Qt::NoFocus Replace All ================================================ FILE: QtSLiM/QtSLiMFindRecipe.cpp ================================================ // // QtSLiMFindRecipe.cpp // SLiM // // Created by Ben Haller on 8/6/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMFindRecipe.h" #include "ui_QtSLiMFindRecipe.h" #include "QtSLiMPreferences.h" #include "QtSLiMSyntaxHighlighting.h" #include "QtSLiMAppDelegate.h" #include "QtSLiMExtras.h" #include #include #include #include #include QtSLiMFindRecipe::QtSLiMFindRecipe(QWidget *p_parent) : QDialog(p_parent), ui(new Ui::QtSLiMFindRecipe) { ui->setupUi(this); // change the app icon to our multi-size app icon for best results ui->iconSLiM->setIcon(qtSLiMAppDelegate->applicationIcon()); // load recipes and get ready to search loadRecipes(); constructMatchList(); updateMatchListWidget(); validateOK(); updatePreview(); // set up the script preview with syntax coloring and tab stops QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); double tabWidth = 0; ui->scriptPreviewTextEdit->setFont(prefs.displayFontPref(&tabWidth)); #if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) ui->scriptPreviewTextEdit->setTabStopWidth((int)floor(tabWidth)); // deprecated in 5.10 #else ui->scriptPreviewTextEdit->setTabStopDistance(tabWidth); // added in 5.10 #endif if (prefs.scriptSyntaxHighlightPref()) new QtSLiMScriptHighlighter(ui->scriptPreviewTextEdit->document()); // wire things up connect(ui->keyword1LineEdit, &QLineEdit::textChanged, this, &QtSLiMFindRecipe::keywordChanged); connect(ui->keyword2LineEdit, &QLineEdit::textChanged, this, &QtSLiMFindRecipe::keywordChanged); connect(ui->keyword3LineEdit, &QLineEdit::textChanged, this, &QtSLiMFindRecipe::keywordChanged); connect(ui->matchListWidget, &QListWidget::itemSelectionChanged, this, &QtSLiMFindRecipe::matchListSelectionChanged); connect(ui->matchListWidget, &QListWidget::itemDoubleClicked, this, &QtSLiMFindRecipe::matchListDoubleClicked); } QtSLiMFindRecipe::~QtSLiMFindRecipe() { delete ui; } QStringList QtSLiMFindRecipe::selectedRecipeFilenames(void) { const QList selectedItems = ui->matchListWidget->selectedItems(); QStringList selectedFilenames; for (QListWidgetItem *selectedItem : selectedItems) { int selectedRow = ui->matchListWidget->row(selectedItem); selectedFilenames.append(matchRecipeFilenames[selectedRow]); } return selectedFilenames; } void QtSLiMFindRecipe::loadRecipes(void) { QDir recipesDir(":/recipes/", "Recipe *.*", QDir::NoSort, QDir::Files | QDir::NoSymLinks); QStringList entryList = recipesDir.entryList(QStringList("Recipe *.*")); // the previous name filter seems to be ignored std::sort(entryList.begin(), entryList.end(), EidosNaturalSort); recipeFilenames = entryList; matchRecipeFilenames = entryList; for (QString &filename : recipeFilenames) { QString filePath = ":/recipes/" + filename; QFile recipeFile(filePath); if (recipeFile.open(QFile::ReadOnly | QFile::Text)) { QTextStream fileTextStream(&recipeFile); QString fileContents = fileTextStream.readAll(); recipeContents.push_back(fileContents); } else { recipeContents.push_back("### An error occurred reading the contents of this recipe"); } } } QString QtSLiMFindRecipe::displayStringForRecipeFilename(const QString &name) { if (name.endsWith(".txt")) { // Remove the .txt extension for SLiM models return name.mid(7, name.length() - 11); } else if (name.endsWith(".py")) { // Leave the .py extension for Python models, and add a python // FIXME it would be nice to force these lines to have the same line height, but I can't find a way to do so return name.mid(7, name.length() - 7) + QString(" 🐍"); } return ""; } bool QtSLiMFindRecipe::recipeIndexMatchesKeyword(int recipeIndex, QString &keyword) { // an empty keyword matches all recipes if (keyword.length() == 0) return true; // look for a match in the filename const QString &filename = recipeFilenames[recipeIndex]; if (filename.contains(keyword, Qt::CaseInsensitive)) return true; // look for a match in the file contents const QString &contents = recipeContents[recipeIndex]; if (contents.contains(keyword, Qt::CaseInsensitive)) return true; return false; } void QtSLiMFindRecipe::constructMatchList(void) { matchRecipeFilenames.clear(); QString keyword1 = ui->keyword1LineEdit->text(); QString keyword2 = ui->keyword2LineEdit->text(); QString keyword3 = ui->keyword3LineEdit->text(); for (int i = 0; i < recipeFilenames.size(); ++i) { if (recipeIndexMatchesKeyword(i, keyword1) && recipeIndexMatchesKeyword(i, keyword2) && recipeIndexMatchesKeyword(i, keyword3)) matchRecipeFilenames.append(recipeFilenames[i]); } } void QtSLiMFindRecipe::updateMatchListWidget(void) { QListWidget *matchList = ui->matchListWidget; matchList->clear(); for (const QString &match : static_cast(matchRecipeFilenames)) matchList->addItem(displayStringForRecipeFilename(match)); } void QtSLiMFindRecipe::validateOK(void) { QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok); okButton->setEnabled(ui->matchListWidget->selectedItems().size() > 0); } void QtSLiMFindRecipe::updatePreview(void) { if (ui->matchListWidget->selectedItems().size() == 0) { ui->scriptPreviewTextEdit->clear(); } else if (ui->matchListWidget->selectedItems().size() > 1) { ui->scriptPreviewTextEdit->setPlainText("// Multiple recipes selected"); } else { int selectedRowIndex = ui->matchListWidget->currentRow(); QString &filename = matchRecipeFilenames[selectedRowIndex]; QString filePath = ":/recipes/" + filename; QFile recipeFile(filePath); if (recipeFile.open(QFile::ReadOnly | QFile::Text)) { QTextStream fileTextStream(&recipeFile); QString fileContents = fileTextStream.readAll(); ui->scriptPreviewTextEdit->setPlainText(fileContents); } else { ui->scriptPreviewTextEdit->setPlainText("### An error occurred reading the contents of this recipe"); } highlightPreview(); } } void QtSLiMFindRecipe::highlightPreview(void) { // thanks to https://stackoverflow.com/a/16149381/2752221 QPlainTextEdit *script = ui->scriptPreviewTextEdit; const QString &scriptString = script->toPlainText(); QTextCursor tc = script->textCursor(); tc.select(QTextCursor::Document); QString keyword1 = ui->keyword1LineEdit->text(); QString keyword2 = ui->keyword2LineEdit->text(); QString keyword3 = ui->keyword3LineEdit->text(); const std::vector keywords{keyword1, keyword2, keyword3}; QList extraSelections; QTextCharFormat format; format.setBackground(Qt::yellow); for (const QString &keyword : keywords) { if (keyword.length() == 0) continue; int from = 0; while (true) { int matchPos = scriptString.indexOf(keyword, from, Qt::CaseInsensitive); if (matchPos == -1) break; QTextEdit::ExtraSelection sel; sel.format = format; sel.cursor = tc; // this makes the cursor refer to the document, I think sel.cursor.clearSelection(); // this seems unnecessary to me, but I'm not certain sel.cursor.setPosition(matchPos); sel.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, keyword.length()); extraSelections.append(sel); from = matchPos + 1; } } script->setExtraSelections(extraSelections); } void QtSLiMFindRecipe::keywordChanged() { // FIXME would be nice to preserve the selection across this; see -[FindRecipeController keywordChanged:] constructMatchList(); updateMatchListWidget(); validateOK(); highlightPreview(); } void QtSLiMFindRecipe::matchListSelectionChanged() { validateOK(); updatePreview(); } void QtSLiMFindRecipe::matchListDoubleClicked() { if (ui->matchListWidget->selectedItems().size() > 0) done(QDialog::Accepted); } ================================================ FILE: QtSLiM/QtSLiMFindRecipe.h ================================================ // // QtSLiMFindRecipe.h // SLiM // // Created by Ben Haller on 8/6/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMFINDRECIPE_H #define QTSLIMFINDRECIPE_H #include #include #include namespace Ui { class QtSLiMFindRecipe; } class QtSLiMFindRecipe : public QDialog { Q_OBJECT QStringList recipeFilenames; QStringList recipeContents; QStringList matchRecipeFilenames; public: explicit QtSLiMFindRecipe(QWidget *p_parent = nullptr); virtual ~QtSLiMFindRecipe() override; QStringList selectedRecipeFilenames(void); protected: void loadRecipes(void); QString displayStringForRecipeFilename(const QString &name); bool recipeIndexMatchesKeyword(int recipeIndex, QString &keyword); void constructMatchList(void); void updateMatchListWidget(void); void validateOK(void); void updatePreview(void); void highlightPreview(void); protected slots: void keywordChanged(); void matchListSelectionChanged(); void matchListDoubleClicked(); private: Ui::QtSLiMFindRecipe *ui; }; #endif // QTSLIMFINDRECIPE_H ================================================ FILE: QtSLiM/QtSLiMFindRecipe.ui ================================================ QtSLiMFindRecipe 0 0 824 736 0 0 true 20 20 20 20 20 QLayout::SetMinimumSize true 0 0 48 48 48 48 :/icons/AppIcon128.png:/icons/AppIcon128.png 48 48 false true 3 <html><head/><body><p><span style=" font-weight:600;">Find Recipe</span></p></body></html> Enter the search keywords and select a match from the list. Qt::Horizontal QSizePolicy::Fixed 70 20 12 QLayout::SetMinimumSize Keywords: 2 <html><head/><body><p align="center"><span style=" font-size:11pt;">AND</span></p></body></html> <html><head/><body><p align="center"><span style=" font-size:11pt;">AND</span></p></body></html> Qt::Vertical QSizePolicy::Fixed 20 5 Matches: 0 1 700 110 Qt::ScrollBarAlwaysOn QAbstractItemView::ExtendedSelection 0 2 0 220 Qt::ScrollBarAlwaysOn true Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok QtSLiMIconView QPushButton
QtSLiMExtras.h
buttonBox accepted() QtSLiMFindRecipe accept() 248 254 157 274 buttonBox rejected() QtSLiMFindRecipe reject() 316 260 286 274
================================================ FILE: QtSLiM/QtSLiMGraphView.cpp ================================================ // // QtSLiMGraphView.cpp // SLiM // // Created by Ben Haller on 3/27/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "species.h" #include "subpopulation.h" #include "haplosome.h" #include "mutation_run.h" QFont QtSLiMGraphView::labelFontOfPointSize(double size) { static QFont timesNewRoman("Times New Roman", 10); // Derive a font of the proper size, while leaving the original untouched QFont font(timesNewRoman); #ifdef __linux__ font.setPointSizeF(size * 0.75); #else // font sizes are calibrated for macOS; on Linux they need to be a little smaller font.setPointSizeF(size); #endif return font; } QtSLiMGraphView::QtSLiMGraphView(QWidget *p_parent, QtSLiMWindow *controller) : QWidget(p_parent) { controller_ = controller; setFocalDisplaySpecies(controller_->focalDisplaySpecies()); connect(controller, &QtSLiMWindow::controllerFullUpdateAfterTick, this, &QtSLiMGraphView::updateAfterTick); connect(controller, &QtSLiMWindow::controllerTickFinished, this, &QtSLiMGraphView::controllerTickFinished); connect(controller, &QtSLiMWindow::controllerRecycled, this, &QtSLiMGraphView::controllerRecycled); original_x0_ = 0.0; original_x1_ = 1.0; original_y0_ = 0.0; original_y1_ = 1.0; x0_ = original_x0_; x1_ = original_x1_; y0_ = original_y0_; y1_ = original_y1_; showXAxis_ = true; allowXAxisUserRescale_ = true; showXAxisTicks_ = true; showYAxis_ = true; allowYAxisUserRescale_ = true; showYAxisTicks_ = true; xAxisMin_ = x0_; xAxisMax_ = x1_; xAxisMajorTickInterval_ = 0.5; xAxisMinorTickInterval_ = 0.25; xAxisMajorTickModulus_ = 2; xAxisHistogramStyle_ = false; xAxisTickValuePrecision_ = 1; xAxisLabelsType_ = 1; // default numeric labels yAxisMin_ = y0_; yAxisMax_ = y1_; yAxisMajorTickInterval_ = 0.5; yAxisMinorTickInterval_ = 0.25; yAxisMajorTickModulus_ = 2; yAxisTickValuePrecision_ = 1; yAxisHistogramStyle_ = false; yAxisLog_ = false; yAxisLabelsType_ = 1; // default numeric labels xAxisLabel_ = "This is the x-axis, yo"; yAxisLabel_ = "This is the y-axis, yo"; legendVisible_ = true; showHorizontalGridLines_ = false; showVerticalGridLines_ = false; showGridLinesMajorOnly_ = false; showFullBox_ = false; allowHorizontalGridChange_ = true; allowVerticalGridChange_ = true; allowFullBoxChange_ = true; } void QtSLiMGraphView::addedToWindow(void) { } QtSLiMGraphView::~QtSLiMGraphView() { // It would be nice if we could call these methods automatically for subclasses, but we cannot. By the time // this destructor has been called, the subclass has already been destructed, and a virtual function call // here calls the QtSLiMGraphView implementation, not the subclass implementation. Subclasses that use these // methods must call them themselves in their destructors. QtSLiMGraphView::invalidateDrawingCache(); QtSLiMGraphView::invalidateCachedData(); if (xAxisAt_) { delete xAxisAt_; xAxisAt_ = nullptr; } if (xAxisLabels_) { delete xAxisLabels_; xAxisLabels_ = nullptr; } if (yAxisAt_) { delete yAxisAt_; yAxisAt_ = nullptr; } if (yAxisLabels_) { delete yAxisLabels_; yAxisLabels_ = nullptr; } xAxisLabelsType_ = 1; yAxisLabelsType_ = 1; controller_ = nullptr; } void QtSLiMGraphView::setFocalDisplaySpecies(Species *species) { if (species) { focalSpeciesName_ = species->name_; // focalSpeciesAvatar_ is set by updateSpeciesBadge() } else { focalSpeciesName_ = ""; focalSpeciesAvatar_ = ""; } } Species *QtSLiMGraphView::focalDisplaySpecies(void) { // We look up our focal species object by name every time, since keeping a pointer to it would be unsafe // Before initialize() is done species have not been created, so we return nullptr in that case // Some graph types will have no focal species; in that case we always return nullptr if (focalSpeciesName_.length() == 0) return nullptr; if (controller_ && controller_->community && (controller_->community->Tick() >= 1)) return controller_->community->SpeciesWithName(focalSpeciesName_); return nullptr; } bool QtSLiMGraphView::missingFocalDisplaySpecies(void) { if (focalSpeciesName_.length() == 0) return false; return (focalDisplaySpecies() == nullptr); } void QtSLiMGraphView::updateSpeciesBadge(void) { // graphs that do not have a focal species, such as QtSLiMGraphView_MultispeciesPopSizeOverTime, have no species badge if (focalSpeciesName_.length() == 0) return; // if we do not have a button layout, punt; in some cases we get called by updateAfterTick() before we have been placed in our window QHBoxLayout *enclosingLayout = buttonLayout(); if (!enclosingLayout) return; int layoutCount = enclosingLayout->count(); QLayoutItem *labelItem = (layoutCount > 0) ? enclosingLayout->itemAt(0) : nullptr; QWidget *labelWidget = labelItem ? labelItem->widget() : nullptr; QLabel *speciesLabel = qobject_cast(labelWidget); if (!speciesLabel) { qDebug() << "No species label! enclosingLayout ==" << enclosingLayout << ", layoutCount ==" << layoutCount << ", labelItem ==" << labelItem << ", labelWidget ==" << labelWidget; return; } Species *graphSpecies = focalDisplaySpecies(); // Cache our graphSpecies avatar whenever we're in a valid state, because it could change, // and because we want to be able to display it even when the sim is in an invalid state if (graphSpecies) { if (graphSpecies->community_.all_species_.size() > 1) focalSpeciesAvatar_ = graphSpecies->avatar_; else focalSpeciesAvatar_ = ""; } // Display our current avatar cache; if we have no avatar, hide the label if (focalSpeciesAvatar_.length()) { speciesLabel->setText(QString::fromStdString(focalSpeciesAvatar_)); speciesLabel->setHidden(false); } else { speciesLabel->setText(""); speciesLabel->setHidden(true); } } QHBoxLayout *QtSLiMGraphView::buttonLayout(void) { // Note this method makes assumptions about the layouts in the parent window // It needs to be kept parallel to QtSLiMWindow::graphWindowWithView() QVBoxLayout *topLayout = dynamic_cast(window()->layout()); if (topLayout && (topLayout->count() >= 2)) { QLayoutItem *layoutItem = topLayout->itemAt(1); return dynamic_cast(layoutItem); } return nullptr; } QPushButton *QtSLiMGraphView::actionButton(void) { // Note this method makes assumptions about the layouts in the parent window // It needs to be kept parallel to QtSLiMWindow::graphWindowWithView() QHBoxLayout *enclosingLayout = buttonLayout(); int layoutCount = enclosingLayout ? enclosingLayout->count() : 0; QLayoutItem *buttonItem = (layoutCount > 0) ? enclosingLayout->itemAt(layoutCount - 1) : nullptr; QWidget *buttonWidget = buttonItem ? buttonItem->widget() : nullptr; return qobject_cast(buttonWidget); } QComboBox *QtSLiMGraphView::newButtonInLayout(QHBoxLayout *p_layout) { QComboBox *button = new QComboBox(this); button->setEditable(false); button->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); button->setMinimumContentsLength(2); p_layout->insertWidget(p_layout->count() - 2, button); // left of the spacer and action button return button; } QRect QtSLiMGraphView::interiorRectForBounds(QRect bounds) { // The interiorRect is the area which QtSLiMGraphView clips the plotting within. So it is the live plot // area, within the axes of the plot. In a borderless plot, it is the full interior area of the window, // but afer setting up clipping drawContents() will inset the interiorRect by the margins given to // setBorderless(); the data area will be inset by the margins, but not clipped to the margins. This is // conceptually similar to the 4% expansion of the axis ranges done in bordered plots. QRect interiorRect = bounds; // If the plot is borderless, there is no inset for the interior rect at all if (is_borderless_) return interiorRect; // For now, 10 pixels margin on a side if there is no axis, 40 pixels margin if there is an axis if (showXAxis_) interiorRect.adjust(50, 0, -10, 0); else interiorRect.adjust(10, 0, -10, 0); if (showYAxis_) interiorRect.adjust(0, 50, 0, -10); else interiorRect.adjust(0, 10, 0, -10); return interiorRect; } double QtSLiMGraphView::plotToDeviceX(double plotx, QRect interiorRect) { double fractionAlongSide = (plotx - x0_) / (x1_ - x0_); if (generatingPDF_) { // We go from the left edge of the first pixel to the right edge of the last pixel return (fractionAlongSide * interiorRect.width() + interiorRect.x()); } else { // We go from the center of the first pixel to the center of the last pixel return (fractionAlongSide * (interiorRect.width() - 1.0) + interiorRect.x()) + 0.5; } } double QtSLiMGraphView::plotToDeviceY(double ploty, QRect interiorRect) { double fractionAlongSide = (ploty - y0_) / (y1_ - y0_); if (generatingPDF_) { // We go from the bottom edge of the first pixel to the top edge of the last pixel return (fractionAlongSide * interiorRect.height() + interiorRect.y()); } else { // We go from the center of the first pixel to the center of the last pixel return (fractionAlongSide * (interiorRect.height() - 1.0) + interiorRect.y()) + 0.5; } } double QtSLiMGraphView::roundPlotToDeviceX(double plotx, QRect interiorRect) { double fractionAlongSide = (plotx - x0_) / (x1_ - x0_); if (generatingPDF_) { // We go from the left edge of the first pixel to the right edge of the last pixel return (fractionAlongSide * interiorRect.width() + interiorRect.x()); } else { // We go from the center of the first pixel to the center of the last pixel, rounded off to pixel midpoints return SLIM_SCREEN_ROUND(fractionAlongSide * (interiorRect.width() - 1.0) + interiorRect.x()) + 0.5; } } double QtSLiMGraphView::roundPlotToDeviceY(double ploty, QRect interiorRect) { double fractionAlongSide = (ploty - y0_) / (y1_ - y0_); if (generatingPDF_) { // We go from the bottom edge of the first pixel to the top edge of the last pixel return (fractionAlongSide * interiorRect.height() + interiorRect.y()); } else { // We go from the center of the first pixel to the center of the last pixel, rounded off to pixel midpoints return SLIM_SCREEN_ROUND(fractionAlongSide * (interiorRect.height() - 1.0) + interiorRect.y()) + 0.5; } } void QtSLiMGraphView::willDraw(QPainter & /* painter */, QRect /* interiorRect */) { } QString QtSLiMGraphView::labelTextForTick(double tickValue, int tickValuePrecision, double minorTickInterval) { // This utility method handles negative tickValuePrecision values, which request // output mode 'g' instead of 'f', and also make sure that values extremely close // to zero are output as zero. (The need for the latter correction is because // we use a double value as a for loop index in the plotting code, which is not // really a good idea.) if (std::fabs(tickValue) < std::fabs(minorTickInterval) / 1e6) tickValue = 0.0; if (tickValuePrecision < 0) return QString("%1").arg(tickValue, 0, 'g', -tickValuePrecision); else return QString("%1").arg(tickValue, 0, 'f', tickValuePrecision); } void QtSLiMGraphView::drawAxisTickLabel(QPainter &painter, QString labelText, double xValueForTick, double axisLength, bool isFirstTick, bool isLastTick) { // Draws a tick label. This method thinks of the axis as being the x axis, and assumes that the coordinate system // of the painter has been rotated as needed for that assumption to make sense. The coordinate system should be // shifted so that the axis starts at x==0, and drawing the text with a baseline at y==0 is correct. QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, labelText); double labelWidth = labelBoundingRect.width(); double labelX = xValueForTick - SLIM_SCREEN_ROUND(labelWidth / 2.0); if (tweakXAxisTickLabelAlignment_) { if (isFirstTick && (labelX < 0)) labelX = xValueForTick - 2.0; else if (isLastTick && (labelX + labelWidth > axisLength)) labelX = xValueForTick - SLIM_SCREEN_ROUND(labelWidth) + 2.0; } // draw a debugging line that is positioned where we intend the baseline of the tick label to go //painter.fillRect(QRectF(0, 0, axisLength, 1), Qt::red); painter.drawText(QPointF(labelX, 0), labelText); } void QtSLiMGraphView::drawXAxisTicks(QPainter &painter, QRect interiorRect) { QFont font = QtSLiMGraphView::fontForTickLabels(); QFontMetricsF fontMetrics(font); double capHeight = std::ceil(fontMetrics.capHeight()); painter.setFont(font); painter.setBrush(Qt::black); if (xAxisAt_) { // user-specified tick positions, which may or may not have corresponding label strings int tickCount = (int)xAxisAt_->size(); for (int tickIndex = 0; tickIndex < tickCount; ++tickIndex) { double tickValue = (*xAxisAt_)[tickIndex]; bool isFirstTick = (tickIndex == 0); bool isLastTick = (tickIndex == tickCount - 1); QString labelText; if (xAxisLabelsType_ == 1) labelText = labelTextForTick(tickValue, -8, 1e-100); else if (xAxisLabelsType_ == 2) labelText = (*xAxisLabels_)[tickIndex]; else labelText = " "; // force a major tick mark when labels are turned off bool isMajorTick = labelText.length(); double tickLength = (isMajorTick ? 6 : 3); double xValueForTick; if (generatingPDF_) xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + interiorRect.width() * ((tickValue - x0_) / (x1_ - x0_)) - 0.5); // left edge of pixel else xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + (interiorRect.width() - 1) * ((tickValue - x0_) / (x1_ - x0_))); // left edge of pixel //qDebug() << "tickValue == " << tickValue << ", isMajorTick == " << (isMajorTick ? "YES" : "NO") << ", xValueForTick == " << xValueForTick; painter.fillRect(QRectF(xValueForTick, interiorRect.y() - tickLength, 1, tickLength - (generatingPDF_ ? 0.5 : 0.0)), Qt::black); if (isMajorTick && (xAxisLabelsType_ != 0)) { painter.save(); painter.translate(interiorRect.x(), 41 - capHeight); painter.scale(1.0, -1.0); drawAxisTickLabel(painter, labelText, xValueForTick - interiorRect.x(), interiorRect.width(), isFirstTick, isLastTick); painter.restore(); } } } else { double axisMin = xAxisMin_; double axisMax = xAxisMax_; int tickValuePrecision = xAxisTickValuePrecision_; double tickValue; if (!xAxisHistogramStyle_) { double minorTickInterval = xAxisMinorTickInterval_; int majorTickModulus = xAxisMajorTickModulus_; int tickIndex; for (tickValue = axisMin, tickIndex = 0; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += minorTickInterval, tickIndex++) { bool isFirstTick = (tickIndex == 0); bool isLastTick = (tickValue + minorTickInterval > (axisMax + minorTickInterval / 10.0)); bool isMajorTick = ((tickIndex % majorTickModulus) == 0); double tickLength = (isMajorTick ? 6 : 3); double xValueForTick; if (generatingPDF_) xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + interiorRect.width() * ((tickValue - x0_) / (x1_ - x0_)) - 0.5); // left edge of pixel else xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + (interiorRect.width() - 1) * ((tickValue - x0_) / (x1_ - x0_))); // left edge of pixel //qDebug() << "tickValue == " << tickValue << ", isMajorTick == " << (isMajorTick ? "YES" : "NO") << ", xValueForTick == " << xValueForTick; painter.fillRect(QRectF(xValueForTick, interiorRect.y() - tickLength, 1, tickLength - (generatingPDF_ ? 0.5 : 0.0)), Qt::black); if (isMajorTick && (xAxisLabelsType_ != 0)) { QString labelText = labelTextForTick(tickValue, tickValuePrecision, minorTickInterval); painter.save(); painter.translate(interiorRect.x(), 41 - capHeight); painter.scale(1.0, -1.0); drawAxisTickLabel(painter, labelText, xValueForTick - interiorRect.x(), interiorRect.width(), isFirstTick, isLastTick); painter.restore(); } } } else { // Histogram-style ticks are centered under each bar position, at the 0.5 positions on the axis // So a histogram-style axis declared with min/max of 0/10 actually spans 1..10, with ticks at 0.5..9.5 labeled 1..10 double axisStart = axisMin + 1; for (tickValue = axisStart; tickValue <= axisMax; tickValue++) { bool isFirstTick = (tickValue == axisStart); bool isLastTick = (tickValue == axisMax); bool isMajorTick = isFirstTick || isLastTick; double tickLength = (isMajorTick ? 6 : 3); double xValueForTick; if (generatingPDF_) xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + interiorRect.width() * ((tickValue - 0.5 - x0_) / (x1_ - x0_)) - 0.5); // left edge of pixel else xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + (interiorRect.width() - 1) * ((tickValue - 0.5 - x0_) / (x1_ - x0_))); // left edge of pixel //qDebug() << "tickValue == " << tickValue << ", isMajorTick == " << (isMajorTick ? "YES" : "NO") << ", xValueForTick == " << xValueForTick; painter.fillRect(QRectF(xValueForTick, interiorRect.y() - tickLength, 1, tickLength - (generatingPDF_ ? 0.5 : 0.0)), Qt::black); if (isMajorTick && (xAxisLabelsType_ != 0)) { QString labelText = labelTextForTick(tickValue, tickValuePrecision, 1.0); painter.save(); painter.translate(interiorRect.x(), 41 - capHeight); painter.scale(1.0, -1.0); drawAxisTickLabel(painter, labelText, xValueForTick - interiorRect.x(), interiorRect.width(), isFirstTick, isLastTick); painter.restore(); } } } } } void QtSLiMGraphView::drawXAxis(QPainter &painter, QRect interiorRect) { double yAxisFudge = showYAxis_ ? 1 : (generatingPDF_ ? 0.5 : 0); QRectF axisRect(interiorRect.x() - yAxisFudge, interiorRect.y() - 1, interiorRect.width() + yAxisFudge + (generatingPDF_ ? 0.5 : 0), 1); painter.fillRect(axisRect, Qt::black); // show label QFont font = QtSLiMGraphView::fontForAxisLabels(); QFontMetricsF fontMetrics(font); double capHeight = fontMetrics.capHeight(); painter.setFont(font); painter.setBrush(Qt::black); QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, xAxisLabel_); QPoint drawPoint(interiorRect.x() + (interiorRect.width() - labelBoundingRect.width()) / 2, 0); painter.save(); painter.translate(0, 14 - std::ceil(capHeight / 2.0)); painter.scale(1.0, -1.0); // draw debugging lines that are positioned where we intend the axis label to go //painter.fillRect(QRectF(interiorRect.x(), 0, interiorRect.width(), 1), Qt::blue); //painter.fillRect(QRectF(drawPoint.x(), -capHeight * 0.5, labelBoundingRect.width(), 1), Qt::red); painter.drawText(drawPoint, xAxisLabel_); painter.restore(); } void QtSLiMGraphView::drawYAxisTicks(QPainter &painter, QRect interiorRect) { painter.setFont(QtSLiMGraphView::fontForTickLabels()); painter.setBrush(Qt::black); if (yAxisAt_) { // user-specified tick positions, which may or may not have corresponding label strings int tickCount = (int)yAxisAt_->size(); for (int tickIndex = 0; tickIndex < tickCount; ++tickIndex) { double tickValue = (*yAxisAt_)[tickIndex]; bool isFirstTick = (tickIndex == 0); bool isLastTick = (tickIndex == tickCount - 1); QString labelText; if (yAxisLabelsType_ == 1) labelText = labelTextForTick(tickValue, -8, 1e-100); else if (yAxisLabelsType_ == 2) labelText = (*yAxisLabels_)[tickIndex]; else labelText = " "; // force a major tick mark when labels are turned off bool isMajorTick = labelText.length(); double tickLength = (isMajorTick ? 6 : 3); double yValueForTick; if (generatingPDF_) yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + interiorRect.height() * ((tickValue - y0_) / (y1_ - y0_)) - 0.5); // bottom edge of pixel else yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + (interiorRect.height() - 1) * ((tickValue - y0_) / (y1_ - y0_))); // bottom edge of pixel //qDebug() << "tickValue == " << tickValue << ", isMajorTick == " << (isMajorTick ? "YES" : "NO") << ", yValueForTick == " << yValueForTick; painter.fillRect(QRectF(interiorRect.x() - tickLength, yValueForTick, tickLength - (generatingPDF_ ? 0.5 : 0.0), 1), Qt::black); if (isMajorTick) { painter.save(); painter.translate(41, interiorRect.y()); painter.rotate(90); painter.scale(1.0, -1.0); drawAxisTickLabel(painter, labelText, yValueForTick - interiorRect.y(), interiorRect.height(), isFirstTick, isLastTick); painter.restore(); } } } else { double axisMin = yAxisMin_; double axisMax = yAxisMax_; int tickValuePrecision = yAxisTickValuePrecision_; double tickValue; if (!yAxisHistogramStyle_) { double axisStart = (yAxisLog_ ? round(yAxisMin_) : yAxisMin_); // with a log scale, we leave a little room at the bottom double minorTickInterval = yAxisMinorTickInterval_; int majorTickModulus = yAxisMajorTickModulus_; int tickIndex; for (tickValue = axisStart, tickIndex = 0; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += minorTickInterval, tickIndex++) { bool isFirstTick = (tickIndex == 0); bool isLastTick = (tickValue + minorTickInterval > (axisMax + minorTickInterval / 10.0)); bool isMajorTick = ((tickIndex % majorTickModulus) == 0); double tickLength = (isMajorTick ? 6 : 3); double transformedTickValue = tickValue; if (yAxisLog_ && !isMajorTick) { // with a log scale, adjust the tick positions so they are non-linear; this is hackish double intPart = std::floor(tickValue); double fractPart = tickValue - intPart; double minorTickIndex = fractPart * 9.0; double minorTickOffset = std::log10(minorTickIndex + 1.0); transformedTickValue = intPart + minorTickOffset; //std::cout << intPart << " , " << fractPart << "\n"; } double yValueForTick; if (generatingPDF_) yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + interiorRect.height() * ((transformedTickValue - y0_) / (y1_ - y0_)) - 0.5); // bottom edge of pixel else yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + (interiorRect.height() - 1) * ((transformedTickValue - y0_) / (y1_ - y0_))); // bottom edge of pixel //qDebug() << "tickValue == " << tickValue << ", isMajorTick == " << (isMajorTick ? "YES" : "NO") << ", yValueForTick == " << yValueForTick; painter.fillRect(QRectF(interiorRect.x() - tickLength, yValueForTick, tickLength - (generatingPDF_ ? 0.5 : 0.0), 1), Qt::black); if (isMajorTick) { QString labelText = labelTextForTick(tickValue, tickValuePrecision, minorTickInterval); if (yAxisLog_) { if (std::abs(tickValue - 0.0) < 0.0000001) labelText = "1"; else if (std::abs(tickValue - 1.0) < 0.0000001) labelText = "10"; else if (std::abs(tickValue - 2.0) < 0.0000001) labelText = "100"; else labelText = QString("10^%1").arg((int)round(tickValue)); } painter.save(); painter.translate(41, interiorRect.y()); painter.rotate(90); painter.scale(1.0, -1.0); drawAxisTickLabel(painter, labelText, yValueForTick - interiorRect.y(), interiorRect.height(), isFirstTick, isLastTick); painter.restore(); } } } else { // Histogram-style ticks are centered to the left of each bar position, at the 0.5 positions on the axis // So a histogram-style axis declared with min/max of 0/10 actually spans 1..10, with ticks at 0.5..9.5 labeled 1..10 double axisStart = axisMin + 1; for (tickValue = axisStart; tickValue <= axisMax; tickValue++) { bool isFirstTick = (tickValue == axisStart); bool isLastTick = (tickValue == axisMax); bool isMajorTick = isFirstTick || isLastTick; double tickLength = (isMajorTick ? 6 : 3); double yValueForTick; if (generatingPDF_) yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + interiorRect.height() * ((tickValue - 0.5 - y0_) / (y1_ - y0_)) - 0.5); // bottom edge of pixel else yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + (interiorRect.height() - 1) * ((tickValue - 0.5 - y0_) / (y1_ - y0_))); // bottom edge of pixel //qDebug() << "tickValue == " << tickValue << ", isMajorTick == " << (isMajorTick ? "YES" : "NO") << ", yValueForTick == " << yValueForTick; painter.fillRect(QRectF(interiorRect.x() - tickLength, yValueForTick, tickLength - (generatingPDF_ ? 0.5 : 0.0), 1), Qt::black); if (isMajorTick) { QString labelText = labelTextForTick(tickValue, tickValuePrecision, 1.0); painter.save(); painter.translate(41, interiorRect.y()); painter.rotate(90); painter.scale(1.0, -1.0); drawAxisTickLabel(painter, labelText, yValueForTick - interiorRect.y(), interiorRect.height(), isFirstTick, isLastTick); painter.restore(); } } } } } void QtSLiMGraphView::drawYAxis(QPainter &painter, QRect interiorRect) { double xAxisFudge = showXAxis_ ? 1 : (generatingPDF_ ? 0.5 : 0); QRectF axisRect(interiorRect.x() - 1, interiorRect.y() - xAxisFudge, 1, interiorRect.height() + xAxisFudge + (generatingPDF_ ? 0.5 : 0)); painter.fillRect(axisRect, Qt::black); // show label, rotated QFont font = QtSLiMGraphView::fontForAxisLabels(); QFontMetricsF fontMetrics(font); double capHeight = fontMetrics.capHeight(); painter.setFont(font); painter.setBrush(Qt::black); QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, yAxisLabel_); QPoint drawPoint(interiorRect.y() + (interiorRect.height() - labelBoundingRect.width()) / 2, 0); painter.save(); painter.translate(11 + std::ceil(capHeight / 2.0), 0.0); painter.rotate(90); painter.scale(1.0, -1.0); // draw debugging lines that are positioned where we intend the axis label to go //painter.fillRect(QRectF(interiorRect.y(), 0, interiorRect.height(), 1), Qt::blue); //painter.fillRect(QRectF(drawPoint.x(), -capHeight * 0.5, labelBoundingRect.width(), 1), Qt::red); painter.drawText(drawPoint, yAxisLabel_); painter.restore(); } void QtSLiMGraphView::drawFullBox(QPainter &painter, QRect interiorRect) { QRect axisRect; // upper x axis int yAxisFudge = showYAxis_ ? 1 : 0; axisRect = QRect(interiorRect.x() - yAxisFudge, interiorRect.y() + interiorRect.height(), interiorRect.width() + yAxisFudge + 1, 1); painter.fillRect(axisRect, Qt::black); // right-hand y axis int xAxisFudge = showXAxis_ ? 1 : 0; axisRect = QRect(interiorRect.x() + interiorRect.width(), interiorRect.y() - xAxisFudge, 1, interiorRect.height() + xAxisFudge + 1); painter.fillRect(axisRect, Qt::black); } void QtSLiMGraphView::drawVerticalGridLines(QPainter &painter, QRect interiorRect) { // we assume that no grid lines fall outside of the axis range QColor gridColor = QtSLiMGraphView::gridLineColor(); double axisMin = xAxisMin_; double axisMax = xAxisMax_; double minorTickInterval = xAxisMinorTickInterval_; double tickValue; for (tickValue = axisMin; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += minorTickInterval) { double xValueForTick; if (generatingPDF_) xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + interiorRect.width() * ((tickValue - x0_) / (x1_ - x0_)) - 0.5); // left edge of pixel else xValueForTick = SLIM_SCREEN_ROUND(interiorRect.x() + (interiorRect.width() - 1) * ((tickValue - x0_) / (x1_ - x0_))); // left edge of pixel if (std::abs(xValueForTick - interiorRect.x()) < 1.25) continue; if ((std::abs(xValueForTick - (interiorRect.x() + interiorRect.width() - 1)) < 1.25) && showFullBox_) continue; painter.fillRect(QRectF(xValueForTick, interiorRect.y(), 1, interiorRect.height()), gridColor); } } void QtSLiMGraphView::drawHorizontalGridLines(QPainter &painter, QRect interiorRect) { // we assume that no grid lines fall outside of the axis range QColor gridColor = QtSLiMGraphView::gridLineColor(); double axisMin = yAxisMin_; double axisMax = yAxisMax_; double minorTickInterval = yAxisMinorTickInterval_; double tickValue; double axisStart = (yAxisLog_ ? round(axisMin) : axisMin); // with a log scale, we leave a little room at the bottom double tickValueIncrement = (showGridLinesMajorOnly_ ? yAxisMajorTickInterval_ : minorTickInterval); for (tickValue = axisStart; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += tickValueIncrement) { double yValueForTick; if (generatingPDF_) yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + interiorRect.height() * ((tickValue - y0_) / (y1_ - y0_)) - 0.5); // bottom edge of pixel else yValueForTick = SLIM_SCREEN_ROUND(interiorRect.y() + (interiorRect.height() - 1) * ((tickValue - y0_) / (y1_ - y0_))); // bottom edge of pixel if (std::abs(yValueForTick - interiorRect.y()) < 1.25) continue; if ((std::abs(yValueForTick - (interiorRect.y() + interiorRect.height() - 1)) < 1.25) && showFullBox_) continue; painter.fillRect(QRectF(interiorRect.x(), yValueForTick, interiorRect.width(), 1), gridColor); } } void QtSLiMGraphView::drawMessage(QPainter &painter, QString messageString, QRect p_rect) { painter.setFont(QtSLiMGraphView::labelFontOfPointSize(16)); painter.setBrush(QtSLiMColorWithWhite(0.4, 1.0)); painter.drawText(p_rect, Qt::AlignHCenter | Qt::AlignVCenter, messageString); } void QtSLiMGraphView::drawGraph(QPainter &painter, QRect interiorRect) { painter.fillRect(interiorRect, QtSLiMColorWithHSV(0.15, 0.15, 1.0, 1.0)); } QtSLiMLegendSpec QtSLiMGraphView::legendKey(void) { return QtSLiMLegendSpec(); } int QtSLiMGraphView::lineCountForLegend(QtSLiMLegendSpec &legend) { // check for duplicate labels, which get uniqued into a single line // displayedLabels maps from label to index, but we don't use the index here; parallel with drawLegend() QMap displayedLabels; int lineCount = 0; for (const QtSLiMLegendEntry &legendEntry : legend) { if (legendEntry.entry_type == QtSLiM_LegendEntryType::kTitle) { // title items do not participate in the duplicate process; they always stand alone lineCount++; } else { QString labelString = legendEntry.label; auto existingEntry = displayedLabels.find(labelString); if (existingEntry == displayedLabels.end()) { // not a duplicate displayedLabels.insert(labelString, 0); lineCount++; } } } return lineCount; } double QtSLiMGraphView::graphicsWidthForLegend(QtSLiMLegendSpec &legend, double legendLineHeight) { // with a specified width, use that width if (legend_graphicsWidth != -1) return legend_graphicsWidth; // otherwise, the default graphics width depends upon whether there are any duplicate // entries and lines; we want to make the area a bit wider if we have points on top of lines const double legendGraphicsWidth_default = legendLineHeight; int entryCount = static_cast(legend.size()); int lineCount = lineCountForLegend(legend); // remove duplicate lines from the count if (entryCount != lineCount) { for (const QtSLiMLegendEntry &legendEntry : legend) { if (legendEntry.entry_type == QtSLiM_LegendEntryType::kLine) { // duplicates entries, and some entries are lines; expand return legendGraphicsWidth_default * 2.0; } } } return legendGraphicsWidth_default; } QSizeF QtSLiMGraphView::legendSize(QPainter &painter) { // This method must be synchronized with QtSLiMGraphView::drawLegend() QtSLiMLegendSpec legend = legendKey(); int entryCount = static_cast(legend.size()); if (entryCount == 0) return QSize(); const double legendLabelPointSize = ((legend_labelSize == -1) ? 10 : legend_labelSize); const double legendLineHeight = ((legend_lineHeight == -1) ? legendLabelPointSize : legend_lineHeight); const double legendInteriorMargin = ((legend_interiorMargin == -1) ? 5 : legend_interiorMargin); const double legendGraphicsWidth = graphicsWidthForLegend(legend, legendLineHeight); int lineCount = lineCountForLegend(legend); // remove duplicate lines from the count QSizeF legend_size = QSize(0, legendLineHeight * lineCount + legendInteriorMargin * (lineCount - 1)); for (const QtSLiMLegendEntry &legendEntry : legend) { // we don't bother removing duplicate lines here, we just measure them twice; no harm QString labelString = legendEntry.label; // incorporate the width of the label into the width of the legend QRectF labelBoundingBox = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, labelString); double labelWidth = labelBoundingBox.width(); // items other than title entries have a graphics box and an interior margin as well if (legendEntry.entry_type != QtSLiM_LegendEntryType::kTitle) labelWidth += legendGraphicsWidth + legendInteriorMargin; labelWidth = SLIM_SCREEN_ROUND(labelWidth); legend_size.setWidth(std::max(legend_size.width(), labelWidth)); } return legend_size; } void QtSLiMGraphView::drawLegend(QPainter &painter, QRectF legendRect) { // This method must be synchronized with QtSLiMGraphView::legendSize() // drawLegendInInteriorRect() has already done the frame/fill, including margins, for us QtSLiMLegendSpec legend = legendKey(); int entryCount = static_cast(legend.size()); if (entryCount == 0) return; const double legendLabelPointSize = ((legend_labelSize == -1) ? 10 : legend_labelSize); const double legendLineHeight = ((legend_lineHeight == -1) ? legendLabelPointSize : legend_lineHeight); const double legendInteriorMargin = ((legend_interiorMargin == -1) ? 5 : legend_interiorMargin); const double legendGraphicsWidth = graphicsWidthForLegend(legend, legendLineHeight); QFont legendFont = labelFontOfPointSize(legendLabelPointSize); QFontMetricsF legendFontMetrics(legendFont); double capHeight = legendFontMetrics.capHeight(); const double labelVerticalAdjust = (legendLineHeight - capHeight) / 2.0; double swatchSize = capHeight * 1.5; int lineCount = lineCountForLegend(legend); // remove duplicate lines from the count QMap displayedLabels; // maps from label to index #if 0 // show the legend layout, for debugging for (int index = 0; index < lineCount; ++index) { int positionIndex = (lineCount - 1) - index; // top to bottom QRectF entryBox(legendRect.x(), legendRect.y() + positionIndex * (legendLineHeight + legendInteriorMargin), legendRect.width(), legendLineHeight); QRectF graphicsBox(entryBox); QRectF labelBox(entryBox); graphicsBox.setWidth(legendGraphicsWidth); labelBox.adjust(legendGraphicsWidth + legendInteriorMargin, 0, 0, 0); painter.fillRect(entryBox, Qt::white); painter.fillRect(graphicsBox, QtSLiMColorWithWhite(0.85, 1.0)); painter.fillRect(labelBox, QtSLiMColorWithWhite(0.85, 1.0)); QtSLiMFrameRect(entryBox, Qt::black, painter, 1.0); } #endif int nextLinePosition = lineCount - 1; // top to bottom for (int index = 0; index < entryCount; ++index) { const QtSLiMLegendEntry &legendEntry = legend[index]; QString labelString = legendEntry.label; // check for duplicate labels, which get uniqued into a single line int positionIndex; bool isDuplicate = false; if (legendEntry.entry_type == QtSLiM_LegendEntryType::kTitle) { // title items do not participate in the duplicate process; they always stand alone positionIndex = (nextLinePosition--); } else { auto existingEntry = displayedLabels.find(labelString); if (existingEntry == displayedLabels.end()) { // not a duplicate positionIndex = (nextLinePosition--); displayedLabels.insert(labelString, positionIndex); } else { // duplicate; use the previously determined position positionIndex = existingEntry.value(); isDuplicate = true; } } QRectF entryBox(legendRect.x(), legendRect.y() + positionIndex * (legendLineHeight + legendInteriorMargin), legendRect.width(), legendLineHeight); QRectF graphicsBox(entryBox); QRectF labelBox(entryBox); graphicsBox.setWidth(legendGraphicsWidth); labelBox.adjust(legendGraphicsWidth + legendInteriorMargin, 0, 0, 0); // draw the graphics in graphicsBox switch (legendEntry.entry_type) { case QtSLiM_LegendEntryType::kTitle: { // use the full entry box for title items labelBox = entryBox; break; } case QtSLiM_LegendEntryType::kSwatch: { QRectF swatchBox = graphicsBox; // make the width and height be, at most, swatchSize (a scaled factor of the capHeight) { double widthAdj = (swatchBox.width() > swatchSize) ? (swatchBox.width() - swatchSize) / 2 : 0; double heightAdj = (swatchBox.height() > swatchSize) ? (swatchBox.height() - swatchSize) / 2 : 0; swatchBox.adjust(widthAdj, heightAdj, -widthAdj, -heightAdj); } // make sure the swatch is square, by shrinking it if (swatchBox.width() != swatchBox.height()) { double widthAdj = (swatchBox.width() > swatchBox.height()) ? (swatchBox.width() - swatchBox.height()) / 2 : 0; double heightAdj = (swatchBox.height() > swatchBox.width()) ? (swatchBox.height() - swatchBox.width()) / 2 : 0; swatchBox.adjust(widthAdj, heightAdj, -widthAdj, -heightAdj); } QColor swatchColor = legendEntry.swatch_color; painter.fillRect(swatchBox, swatchColor); QtSLiMFrameRect(swatchBox, Qt::black, painter, 1.0); break; } case QtSLiM_LegendEntryType::kLine: { double lineWidth = legendEntry.line_lwd; QColor lineColor = legendEntry.line_color; QPainterPath linePath; QPen linePen(lineColor, lineWidth); double y = SLIM_SCREEN_ROUND(graphicsBox.center().y()) + 0.5; linePen.setCapStyle(Qt::FlatCap); linePath.moveTo(graphicsBox.left(), y); linePath.lineTo(graphicsBox.right(), y); painter.strokePath(linePath, linePen); break; } case QtSLiM_LegendEntryType::kPoint: { drawPointSymbol(painter, graphicsBox.center().x(), graphicsBox.center().y(), legendEntry.point_symbol, legendEntry.point_color, legendEntry.point_border, /* alpha */ 1.0, legendEntry.point_lwd, legendEntry.point_size); break; } default: break; } // if the entry is not a duplicate, draw the text label if (!isDuplicate) { double labelX = labelBox.x(); double labelY = labelBox.y() + labelVerticalAdjust; labelY = painter.transform().map(QPointF(labelX, labelY)).y(); painter.setWorldMatrixEnabled(false); painter.drawText(QPointF(labelX, labelY), labelString); painter.setWorldMatrixEnabled(true); } } } void QtSLiMGraphView::drawLegendInInteriorRect(QPainter &painter, QRect interiorRect) { // set the legend label font for the methods we call, which will rely on it const double legendLabelPointSize = ((legend_labelSize == -1) ? 10 : legend_labelSize); QFont legendFont = labelFontOfPointSize(legendLabelPointSize); painter.setFont(legendFont); // assess the size of the legend, given all configuration preferences QSizeF legend_size = legendSize(painter); int legendWidth = static_cast(ceil(legend_size.width())); int legendHeight = static_cast(ceil(legend_size.height())); if ((legendWidth > 0) && (legendHeight > 0)) { // legend_entryMargin provides the margin between and around each entry, within the legend's box; +1 for the width of the legend's frame const double legendExteriorMargin = ((legend_exteriorMargin == -1) ? 9 : legend_exteriorMargin) + 1; QRect legendRect(0, 0, legendWidth + legendExteriorMargin + legendExteriorMargin, legendHeight + legendExteriorMargin + legendExteriorMargin); // positional inset from the edge, outside the legend's box; -1 so an inset of zero matches the "full box" int legendInset = ((legend_inset == -1) ? 3 : legend_inset) - 1; // position the legend in the chosen corner with the chosen inset QtSLiM_LegendPosition position = legend_position_; if (position == QtSLiM_LegendPosition::kUnconfigured) position = QtSLiM_LegendPosition::kTopRight; if ((position == QtSLiM_LegendPosition::kTopLeft) || (position == QtSLiM_LegendPosition::kTopRight)) legendRect.moveTop(interiorRect.y() + interiorRect.height() - (legendRect.height() + legendInset)); else if ((position == QtSLiM_LegendPosition::kBottomLeft) || (position == QtSLiM_LegendPosition::kBottomRight)) legendRect.moveTop(interiorRect.y() + legendInset); if ((position == QtSLiM_LegendPosition::kTopRight) || (position == QtSLiM_LegendPosition::kBottomRight)) legendRect.moveLeft(interiorRect.x() + interiorRect.width() - (legendRect.width() + legendInset)); else if ((position == QtSLiM_LegendPosition::kTopLeft) || (position == QtSLiM_LegendPosition::kBottomLeft)) legendRect.moveLeft(interiorRect.x() + legendInset); // frame the legend and erase it with a slightly gray wash painter.fillRect(legendRect, QtSLiMColorWithWhite(0.95, 1.0)); QtSLiMFrameRect(legendRect, QtSLiMColorWithWhite(0.3, 1.0), painter); // inset and draw the legend content legendRect.adjust(legendExteriorMargin, legendExteriorMargin, -legendExteriorMargin, -legendExteriorMargin); drawLegend(painter, legendRect); } } void QtSLiMGraphView::setBorderless(bool isBorderless, double marginLeft, double marginTop, double marginRight, double marginBottom) { // Convert to/from a borderless plot; this is used only by custom plots is_borderless_ = isBorderless; borderless_margin_left_ = marginLeft; borderless_margin_top_ = marginTop; borderless_margin_right_ = marginRight; borderless_margin_bottom_ = marginBottom; // x0/y0/x1/y1 do not change, but changing our borderless value reconfigures the axis ranges; unless they have been // explicitly set by the user in the QtSLiM UI, they get re-evaluated based upon the original axis range if (!xAxisIsUIRescaled_) { // we generally try to maintain original_x0_ and original_x1_ in sync with x0_ and x1_, but this is the // only place where original_x0_ and original_x1_ actually matter outside of where they are locally set // up -- the only place where we re-set x0_ and x1_ back to their original values, because we want to // re-generate a new axis range based upon a changed is_borderless_ value x0_ = original_x0_; x1_ = original_x1_; //std::cout << "is_borderless_ == " << (is_borderless_ ? "T" : "F") << std::endl; //std::cout << " BEFORE: x0_ == " << x0_ << ", x1_ == " << x1_ << ", xAxisMin_ == " << xAxisMin_ << ", xAxisMax_ == " << xAxisMax_ << std::endl; configureAxisForRange(x0_, x1_, xAxisMin_, xAxisMax_, xAxisMajorTickInterval_, xAxisMinorTickInterval_, xAxisMajorTickModulus_, xAxisTickValuePrecision_); //std::cout << " AFTER: x0_ == " << x0_ << ", x1_ == " << x1_ << ", xAxisMin_ == " << xAxisMin_ << ", xAxisMax_ == " << xAxisMax_ << std::endl; } if (!yAxisIsUIRescaled_) { // we generally try to maintain original_y0_ and original_y1_ in sync with y0_ and y1_, but this is the // only place where original_y0_ and original_y1_ actually matter outside of where they are locally set // up -- the only place where we re-set y0_ and y1_ back to their original values, because we want to // re-generate a new axis range based upon a changed is_borderless_ value y0_ = original_y0_; y1_ = original_y1_; configureAxisForRange(y0_, y1_, yAxisMin_, yAxisMax_, yAxisMajorTickInterval_, yAxisMinorTickInterval_, yAxisMajorTickModulus_, yAxisTickValuePrecision_); } } void QtSLiMGraphView::drawContents(QPainter &painter) { // Set to a default color of black; I thought Qt did this for me, but apparently not painter.setPen(Qt::black); // Erase background QRect bounds = rect(); if (!generatingPDF_) painter.fillRect(bounds, Qt::white); // Get our controller and test for validity, so subclasses don't have to worry about this if (!controller_ || controller_->invalidSimulation()) { drawMessage(painter, "invalid\nsimulation", bounds); } else if (controller_->community->Tick() == 0) { drawMessage(painter, "no\ndata", bounds); } else if (missingFocalDisplaySpecies()) { // The species name no longer refers to a species in the community drawMessage(painter, "missing\nspecies", bounds); } else if (disableMessage().length() > 0) { drawMessage(painter, disableMessage(), bounds); } else { QRect interiorRect = interiorRectForBounds(bounds); // flip the coordinate system so (0, 0) is at lower left // see https://stackoverflow.com/questions/4413570/use-window-viewport-to-flip-qpainter-y-axis painter.save(); painter.translate(0, height()); painter.scale(1.0, -1.0); willDraw(painter, interiorRect); // Draw grid lines, if requested, and if tick marks are turned on for the corresponding axis if (showHorizontalGridLines_ && showYAxis_ && showYAxisTicks_) drawHorizontalGridLines(painter, interiorRect); if (showVerticalGridLines_ && showXAxis_ && showXAxisTicks_) drawVerticalGridLines(painter, interiorRect); // Draw the interior of the graph; this will be overridden by the subclass // We clip the interior drawing to the interior rect, so outliers get clipped out painter.save(); painter.setClipRect(interiorRect, Qt::IntersectClip); // When drawing borderless, we have margins by which we inset interiorRect, but we clip to bounds if (is_borderless_) interiorRect.adjust(borderless_margin_left_, borderless_margin_bottom_, -borderless_margin_right_, -borderless_margin_top_); drawGraph(painter, interiorRect); painter.restore(); // If we're caching, skip all overdrawing, since it cannot be cached (it would then appear under new drawing that supplements the cache) if (!cachingNow_) { // Overdraw axes, ticks, and axis labels, if requested if (!is_borderless_) { if (showXAxis_) drawXAxis(painter, interiorRect); if (showYAxis_) drawYAxis(painter, interiorRect); if (showFullBox_) drawFullBox(painter, interiorRect); if (showXAxis_ && showXAxisTicks_) drawXAxisTicks(painter, interiorRect); if (showYAxis_ && showYAxisTicks_) drawYAxisTicks(painter, interiorRect); } else { // when borderless, the "full box", if requested, frames the full bounds if (showFullBox_) QtSLiMFrameRect(bounds, Qt::black, painter); } // Overdraw the legend if (legendVisible_) drawLegendInInteriorRect(painter, interiorRect); } // unflip painter.restore(); } } void QtSLiMGraphView::paintEvent(QPaintEvent * /* p_paintEvent */) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); drawContents(painter); } void QtSLiMGraphView::invalidateCachedData() { // GraphView has no cached data, but it supports the idea of it // If anything ever gets added here, calls to super will need to be added in subclasses } void QtSLiMGraphView::invalidateDrawingCache(void) { // GraphView has no drawing cache, but it supports the idea of one // If anything ever gets added here, calls to super will need to be added in subclasses } void QtSLiMGraphView::graphWindowResized(void) { invalidateDrawingCache(); } void QtSLiMGraphView::resizeEvent(QResizeEvent *p_event) { // this override is private; subclassers should override graphWindowResized() graphWindowResized(); QWidget::resizeEvent(p_event); } void QtSLiMGraphView::controllerRecycled(void) { updateSpeciesBadge(); invalidateDrawingCache(); invalidateCachedData(); // recycling reverts custom axis settings from axis() back to the default // the design of what reverts on a recycle and what doesn't is kind of ad hoc... if (xAxisAt_) { delete xAxisAt_; xAxisAt_ = nullptr; } if (xAxisLabels_) { delete xAxisLabels_; xAxisLabels_ = nullptr; } if (yAxisAt_) { delete yAxisAt_; yAxisAt_ = nullptr; } if (yAxisLabels_) { delete yAxisLabels_; yAxisLabels_ = nullptr; } xAxisLabelsType_ = 1; yAxisLabelsType_ = 1; update(); QPushButton *action = actionButton(); if (action) action->setEnabled(!controller_->invalidSimulation() && !missingFocalDisplaySpecies()); } void QtSLiMGraphView::controllerTickFinished(void) { } void QtSLiMGraphView::updateAfterTick(void) { updateSpeciesBadge(); update(); QPushButton *action = actionButton(); if (action) action->setEnabled(!controller_->invalidSimulation() && !missingFocalDisplaySpecies()); } bool QtSLiMGraphView::providesStringForData(void) { // subclassers that override stringForData() should also override this to return true // this is an annoying substitute for Obj-C's respondsToSelector: return false; } QString QtSLiMGraphView::stringForData(void) { QString string("# Graph data: "); string.append(graphTitle()); string.append("\n"); string.append(slimDateline()); string.append("\n\n"); appendStringForData(string); // Get rid of extra commas, as a service to subclasses string.replace(", \n", "\n"); return string; } void QtSLiMGraphView::actionButtonRunMenu(QtSLiMPushButton *p_actionButton) { contextMenuEvent(nullptr); // This is not called by Qt, for some reason (nested tracking loops?), so we call it explicitly p_actionButton->qtslimSetHighlight(false); } void QtSLiMGraphView::subclassAddItemsToMenu(QMenu & /* contextMenu */, QContextMenuEvent * /* event */) { } QString QtSLiMGraphView::disableMessage(void) { return ""; } bool QtSLiMGraphView::writeToFile(QString fileName) { QSize graphSize = size(); QPdfWriter pdfwriter(fileName); QPageSize pageSize = QPageSize(graphSize, QString(), QPageSize::ExactMatch); QMarginsF margins(0, 0, 0, 0); pdfwriter.setCreator("SLiMgui"); pdfwriter.setResolution(72); // match the screen? pdfwriter.setPageSize(pageSize); pdfwriter.setPageMargins(margins); QPainter painter; if (painter.begin(&pdfwriter)) { generatingPDF_ = true; drawContents(painter); generatingPDF_ = false; painter.end(); return true; } else { return false; } } void QtSLiMGraphView::contextMenuEvent(QContextMenuEvent *p_event) { if (!controller_->invalidSimulation() && !missingFocalDisplaySpecies()) // && ![[controller window] attachedSheet]) { bool addedItems = false; QMenu contextMenu("graph_menu", this); QAction *aboutGraph = nullptr; QAction *legendToggle = nullptr; QAction *gridHToggle = nullptr; QAction *gridVToggle = nullptr; QAction *boxToggle = nullptr; QAction *changeBinCount = nullptr; QAction *changeHeatmapMargins = nullptr; QAction *changeXAxisScale = nullptr; QAction *changeYAxisScale = nullptr; QAction *copyGraph = nullptr; QAction *exportGraph = nullptr; QAction *copyData = nullptr; QAction *exportData = nullptr; // Show a description of the graph aboutGraph = contextMenu.addAction("About This Graph..."); contextMenu.addSeparator(); // Toggle legend visibility if (legendKey().size() > 0) { legendToggle = contextMenu.addAction(legendVisible_ ? "Hide Legend" : "Show Legend"); addedItems = true; } // Toggle horizontal grid line visibility if (allowHorizontalGridChange_ && showYAxis_ && showYAxisTicks_) { gridHToggle = contextMenu.addAction(showHorizontalGridLines_ ? "Hide Horizontal Grid" : "Show Horizontal Grid"); addedItems = true; } // Toggle vertical grid line visibility if (allowVerticalGridChange_ && showXAxis_ && showXAxisTicks_) { gridVToggle = contextMenu.addAction(showVerticalGridLines_ ? "Hide Vertical Grid" : "Show Vertical Grid"); addedItems = true; } // Toggle box visibility if (allowFullBoxChange_ && showXAxis_ && showYAxis_) { boxToggle = contextMenu.addAction(showFullBox_ ? "Hide Full Box" : "Show Full Box"); addedItems = true; } // Add a separator if we had any visibility-toggle menu items above if (addedItems) contextMenu.addSeparator(); addedItems = false; // Rescale axes if (histogramBinCount_ && allowBinCountRescale_) { changeBinCount = contextMenu.addAction("Change Bin Count..."); addedItems = true; } if (allowHeatmapMarginsChange_) { changeHeatmapMargins = contextMenu.addAction(heatmapMargins_ ? "Remove Patch Margins" : "Add Patch Margins"); addedItems = true; } if (showXAxis_ && showXAxisTicks_ && allowXAxisUserRescale_) { changeXAxisScale = contextMenu.addAction("Change X Axis Scale..."); addedItems = true; } if (showYAxis_ && showYAxisTicks_ && allowYAxisUserRescale_) { changeYAxisScale = contextMenu.addAction("Change Y Axis Scale..."); addedItems = true; } // Add a separator if we had any visibility-toggle menu items above if (addedItems) contextMenu.addSeparator(); addedItems = false; (void)addedItems; // dead store above is deliberate // Allow a subclass to introduce menu items here, above the copy/export menu items, which belong at the bottom; // we are responsible for adding a separator afterwards if needed int preSubclassItemCount = contextMenu.actions().count(); subclassAddItemsToMenu(contextMenu, p_event); if (preSubclassItemCount != contextMenu.actions().count()) contextMenu.addSeparator(); // Copy/export the graph image { // BCH 4/21/2020: FIXME the "as ..." names here are temporary, until the bug below is fixed and we can copy PDF... copyGraph = contextMenu.addAction("Copy Graph as Bitmap"); exportGraph = contextMenu.addAction("Export Graph as PDF..."); } // Copy/export the data to the clipboard if (providesStringForData()) { contextMenu.addSeparator(); copyData = contextMenu.addAction("Copy Data"); exportData = contextMenu.addAction("Export Data..."); } // Run the context menu synchronously QPoint menuPos = (p_event ? p_event->globalPos() : QCursor::pos()); QAction *action = contextMenu.exec(menuPos); // Act upon the chosen action; we just do it right here instead of dealing with slots if (action) { if (action == aboutGraph) { QString title = graphTitle(); QString about = aboutString(); QMessageBox messageBox(this); messageBox.setText(title); messageBox.setInformativeText(about); messageBox.setIcon(QMessageBox::Information); // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::WindowModal seems // to be safe, I can't find a way to make this message box block the UI without doing it deliberately messageBox.setWindowModality(Qt::WindowModal); messageBox.exec(); } if (action == legendToggle) { legendVisible_ = !legendVisible_; update(); } if (action == gridHToggle) { showHorizontalGridLines_ = !showHorizontalGridLines_; update(); } if (action == gridVToggle) { showVerticalGridLines_ = !showVerticalGridLines_; update(); } if (action == boxToggle) { showFullBox_ = !showFullBox_; update(); } if (action == changeBinCount) { QStringList choices = QtSLiMRunLineEditArrayDialog(window(), "Choose a bin count:", QStringList("Bin count:"), QStringList(QString::number(histogramBinCount_))); if (choices.length() == 1) { int newBinCount = choices[0].toInt(); if ((newBinCount > 1) && (newBinCount <= 500)) { histogramBinCount_ = newBinCount; invalidateDrawingCache(); invalidateCachedData(); update(); } else qApp->beep(); } } if (action == changeHeatmapMargins) { heatmapMargins_ = 1- heatmapMargins_; // toggle update(); } if (action == changeXAxisScale) { QStringList choices = QtSLiMRunLineEditArrayDialog(window(), "Choose a configuration for the axis:", QStringList{"Minimum value:", "Maximum value:", "Interval between major ticks:", "Minor tick divisions per major tick interval:", "Tick label precision:"}, QStringList{QString::number(xAxisMin_), QString::number(xAxisMax_), QString::number(xAxisMajorTickInterval_), QString::number(xAxisMajorTickModulus_),QString::number(xAxisTickValuePrecision_)}); if (choices.length() == 5) { xAxisMin_ = choices[0].toDouble(); xAxisMax_ = choices[1].toDouble(); xAxisMajorTickInterval_ = choices[2].toDouble(); xAxisMajorTickModulus_ = std::max(choices[3].toInt(), 1); // zero causes a crash; better would be to validate that it is an integer value, etc. xAxisTickValuePrecision_ = choices[4].toInt(); xAxisMinorTickInterval_ = xAxisMajorTickInterval_ / xAxisMajorTickModulus_; xAxisIsUserRescaled_ = true; xAxisIsUIRescaled_ = true; // for now, these are the same, except in custom plots original_x0_ = xAxisMin_; original_x1_ = xAxisMax_; x0_ = original_x0_; x1_ = original_x1_; invalidateDrawingCache(); update(); } } if (action == changeYAxisScale) { if (!yAxisLog_) { QStringList choices = QtSLiMRunLineEditArrayDialog(window(), "Choose a configuration for the axis:", QStringList{"Minimum value:", "Maximum value:", "Interval between major ticks:", "Minor tick divisions per major tick interval:", "Tick label precision:"}, QStringList{QString::number(yAxisMin_), QString::number(yAxisMax_), QString::number(yAxisMajorTickInterval_), QString::number(yAxisMajorTickModulus_),QString::number(yAxisTickValuePrecision_)}); if (choices.length() == 5) { yAxisMin_ = choices[0].toDouble(); yAxisMax_ = choices[1].toDouble(); yAxisMajorTickInterval_ = choices[2].toDouble(); yAxisMajorTickModulus_ = std::max(choices[3].toInt(), 1); // zero causes a crash; better would be to validate that it is an integer value, etc. yAxisTickValuePrecision_ = choices[4].toInt(); yAxisMinorTickInterval_ = yAxisMajorTickInterval_ / yAxisMajorTickModulus_; yAxisIsUserRescaled_ = true; yAxisIsUIRescaled_ = true; // for now, these are the same, except in custom plots original_y0_ = yAxisMin_; original_y1_ = yAxisMax_; y0_ = original_y0_; y1_ = original_y1_; invalidateDrawingCache(); update(); } } else { QStringList choices = QtSLiMRunLineEditArrayDialog(window(), "Choose a maximum log-scale power:", QStringList{"Maximum value (10^x):"}, QStringList{QString::number(yAxisMax_)}); if (choices.length() == 1) { int newPower = choices[0].toInt(); if ((newPower >= 1) && (newPower <= 10)) { yAxisMax_ = (double)newPower; // for now, these are the same, except in custom plots original_y1_ = yAxisMax_; y1_ = original_y1_; invalidateDrawingCache(); update(); } else qApp->beep(); } } } if (action == copyGraph) { #if 0 // FIXME this clipboard data is not usable on macOS, apparently because the MIME tag doesn't // come through properly; see https://bugreports.qt.io/browse/QTBUG-83164 // I can't find a workaround so I'll wait for them to respond QBuffer buffer; buffer.open(QIODevice::WriteOnly); { QSize graphSize = size(); QPdfWriter pdfwriter(&buffer); QPageSize pageSize = QPageSize(graphSize, QString(), QPageSize::ExactMatch); QMarginsF margins(0, 0, 0, 0); pdfwriter.setCreator("SLiMgui"); pdfwriter.setResolution(72); // match the screen? pdfwriter.setPageSize(pageSize); pdfwriter.setPageMargins(margins); QPainter painter(&pdfwriter); generatingPDF_ = true; drawContents(painter); generatingPDF_ = false; } buffer.close(); QClipboard *clipboard = QGuiApplication::clipboard(); QMimeData mimeData; mimeData.setData("application/pdf", buffer.data()); clipboard->setMimeData(&mimeData); #else // Until the above bug gets fixed, we'll copy raster data to the clipboard instead QPixmap pixmap(size()); render(&pixmap); QImage image = pixmap.toImage(); QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setImage(image); #endif } if (action == exportGraph) { // FIXME maybe this should use QtSLiMDefaultSaveDirectory? see QtSLiMWindow::saveAs() QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); QFileInfo fileInfo(QDir(desktopPath), "graph.pdf"); QString path = fileInfo.absoluteFilePath(); QString fileName = QFileDialog::getSaveFileName(this, "Export Graph", path); if (!fileName.isEmpty()) { bool success = writeToFile(fileName); if (!success) qApp->beep(); } } if (action == copyData) { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(stringForData()); } if (action == exportData) { // FIXME maybe this should use QtSLiMDefaultSaveDirectory? see QtSLiMWindow::saveAs() QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); QFileInfo fileInfo(QDir(desktopPath), "data.txt"); QString path = fileInfo.absoluteFilePath(); QString fileName = QFileDialog::getSaveFileName(this, "Export Data", path); if (!fileName.isEmpty()) { QFile file(fileName); if (file.open(QFile::WriteOnly | QFile::Text)) file.write(stringForData().toUtf8()); else qApp->beep(); } } } } } void QtSLiMGraphView::setXAxisRangeFromTick(void) { Community *community = controller_->community; // We can't get the estimated last tick until tick ranges are known if (!community || (community->Tick() < 1)) return; slim_tick_t lastTick = community->EstimatedLastTick(); // The last tick could be just about anything, so we need some smart axis setup code here – a problem we neglect elsewhere // since we use hard-coded axis setups in other places. The goal is to (1) have the axis max be >= lastTick, (2) have the axis // max be == lastTick if lastTick is a reasonably round number (a single-digit multiple of a power of 10, say), (3) have just a few // other major tick intervals drawn, so labels don't collide or look crowded, and (4) have a few minor tick intervals in between // the majors. Labels that are single-digit multiples of powers of 10 are to be strongly preferred. double lower10power = pow(10.0, floor(log10(lastTick))); // 8000 gives 1000, 1000 gives 1000, 10000 gives 10000 double lower5mult = lower10power / 2.0; // 8000 gives 500, 1000 gives 500, 10000 gives 5000 double axisMax = ceil(lastTick / lower5mult) * lower5mult; // 8000 gives 8000, 7500 gives 7500, 1100 gives 1500 double contained5mults = axisMax / lower5mult; // 8000 gives 16, 7500 gives 15, 1100 gives 3, 1000 gives 2 if (contained5mults <= 3) { // We have a max like 1500 that divides into 5mults well, so do that xAxisMax_ = axisMax; xAxisMajorTickInterval_ = lower5mult; xAxisMinorTickInterval_ = lower5mult / 5; xAxisMajorTickModulus_ = 5; xAxisTickValuePrecision_ = 0; // for now, these are the same, except in custom plots original_x1_ = xAxisMax_; x1_ = original_x1_; } else { // We have a max like 7000 that does not divide into 5mults well; for simplicity, we just always divide these in two xAxisMax_ = axisMax; xAxisMajorTickInterval_ = axisMax / 2; xAxisMinorTickInterval_ = axisMax / 4; xAxisMajorTickModulus_ = 2; xAxisTickValuePrecision_ = 0; // for now, these are the same, except in custom plots original_x1_ = xAxisMax_; x1_ = original_x1_; } } void QtSLiMGraphView::configureAxisForRange(double &dim0, double &dim1, double &axisMin, double &axisMax, double &majorTickInterval, double &minorTickInterval, int &majorTickModulus, int &tickValuePrecision) { if (is_borderless_) { // For borderless plots, we leave the axis range untouched; the axis range does not get outset. // However, drawContents() will inset the interiorRect by the margins given to setBorderless(); // that provides a similar way of framing the data with margins around them. axisMin = dim0; axisMax = dim1; } else { // We call down to our R-inspired axis calculation methods to figure out a good axis layout. // The call here, to _GScale(), is parallel to the point in R's plot.window() function where // it calls down to GScale() for each of the two axes. Note that this branch modifies dim0 // and dim1; we keep the "original_" versions of the axis ranges so that we can back that out // in setBorderless(). int nDivisions; //qDebug() << "configureAxisForRange() : original dim0 ==" << dim0 << ", dim1 ==" << dim1; _GScale(dim0, dim1, axisMin, axisMax, nDivisions); //qDebug() << " after axisGScale() : dim0 ==" << dim0 << ", dim1 ==" << dim1; //qDebug() << " after axisGScale() : axisMin ==" << axisMin << ", axisMax ==" << axisMax << ", nDivisions ==" << nDivisions; // We go beyond R a little, designating some ticks as "major" (getting a label, and a longer tick // mark) and others "minor" (just a short tick mark with no label). We do that after the R-based // tick calculations are done, just assigned roles based on the number of divisions; this could // probably be improved. It's a good idea primarily because we tend to display plots at a much // smaller default size than R, and so there just isn't room for every tick mark to get a label. switch (nDivisions) { case 2: case 4: case 6: case 8: case 10: majorTickInterval = (axisMax - axisMin) / 2.0; minorTickInterval = (axisMax - axisMin) / nDivisions; majorTickModulus = nDivisions / 2; break; case 3: case 9: majorTickInterval = (axisMax - axisMin) / 3.0; minorTickInterval = (axisMax - axisMin) / nDivisions; majorTickModulus = nDivisions / 3; break; default: majorTickInterval = axisMax - axisMin; minorTickInterval = majorTickInterval; majorTickModulus = 1; break; } //qDebug() << " majorTickInterval ==" << majorTickInterval << ", minorTickInterval ==" << minorTickInterval << ", majorTickModulus ==" << majorTickModulus; // We now use a negative tick precision to ask the tick-plotting code to use output mode 'g' // instead of 'f', with the tick precision meaning the number of significant digits, not // the number of digits after the decimal point. This is used only by this method; old- // style QtSLiM plots still use mode 'f'. The precision value chosen here is arbitrary, // but note that trailing zeros are removed by mode 'g', so this precision will only be // used if it is needed; and mode 'g' also switches to scientific notation if it is more // concise. tickValuePrecision = -8; //qDebug() << " tickValuePrecision ==" << tickValuePrecision; } } QtSLiMLegendSpec QtSLiMGraphView::subpopulationLegendKey(std::vector &subpopsToDisplay, bool drawSubpopsGray) { QtSLiMLegendSpec legend_key; // put "All" first, if it occurs in subpopsToDisplay if (std::find(subpopsToDisplay.begin(), subpopsToDisplay.end(), -1) != subpopsToDisplay.end()) legend_key.emplace_back("All", Qt::black); if (drawSubpopsGray) { legend_key.emplace_back("pX", QtSLiMColorWithWhite(0.5, 1.0)); } else { for (auto subpop_id : subpopsToDisplay) { if (subpop_id != -1) { QString labelString = QString("p%1").arg(subpop_id); legend_key.emplace_back(labelString, controller_->whiteContrastingColorForIndex(subpop_id)); } } } return legend_key; } QtSLiMLegendSpec QtSLiMGraphView::mutationTypeLegendKey(void) { Species *graphSpecies = focalDisplaySpecies(); if (!graphSpecies) return QtSLiMLegendSpec(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); // if we only have one mutation type, do not show a legend if (mutationTypeCount < 2) return QtSLiMLegendSpec(); QtSLiMLegendSpec legend_key; // first we put in placeholders, of swatch type std::map &mutTypes = graphSpecies->mutation_types_; for (size_t index = 0; index < mutTypes.size(); ++index) legend_key.emplace_back("placeholder", QColor()); // then we replace the placeholders with lines, but we do it out of order, according to mutation_type_index_ values for (auto mutationTypeIter : mutTypes) { MutationType *mutationType = mutationTypeIter.second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! QString labelString = QString("m%1").arg(mutationType->mutation_type_id_); QtSLiMLegendEntry &entry = legend_key[static_cast(mutationTypeIndex)]; entry.label = labelString; entry.swatch_color = controller_->blackContrastingColorForIndex(mutationTypeIndex); } return legend_key; } void QtSLiMGraphView::drawPointSymbol(QPainter &painter, double x, double y, int symbol, QColor symbolColor, QColor borderColor, double alpha, double lineWidth, double size) { size = size * 3.5; // this scales what size=1 looks like if (alpha != 1.0) { symbolColor.setAlphaF(alpha); borderColor.setAlphaF(alpha); } switch (symbol) { case 0: // square outline { QPainterPath symbolStrokePath; symbolStrokePath.addRect(x - size, y - size, size * 2, size * 2); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 1: // circle outline { QPainterPath symbolStrokePath; symbolStrokePath.addEllipse(x - size * 1.13, y - size * 1.13, size * 2 * 1.13, size * 2 * 1.13); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 2: // triangle outline pointing up { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x, y + size * 1.4); symbolStrokePath.lineTo(x + 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolStrokePath.lineTo(x - 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolStrokePath.closeSubpath(); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 3: // orthogonal cross { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x, y + size); symbolStrokePath.lineTo(x, y - size); symbolStrokePath.moveTo(x + size, y); symbolStrokePath.lineTo(x - size, y); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 4: // diagonal cross { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x + size * 0.7071, y + size * 0.7071); symbolStrokePath.lineTo(x - size * 0.7071, y - size * 0.7071); symbolStrokePath.moveTo(x + size * 0.7071, y - size * 0.7071); symbolStrokePath.lineTo(x - size * 0.7071, y + size * 0.7071); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 5: // diamond outline { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x + size * 1.3, y); symbolStrokePath.lineTo(x, y - size * 1.3); symbolStrokePath.lineTo(x - size * 1.3, y); symbolStrokePath.lineTo(x, y + size * 1.3); symbolStrokePath.closeSubpath(); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 6: // triangle outline pointing down { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x, y - size * 1.4); symbolStrokePath.lineTo(x + 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolStrokePath.lineTo(x - 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolStrokePath.closeSubpath(); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 7: // square outline with diagonal cross { QPainterPath symbolStrokePath; symbolStrokePath.addRect(x - size, y - size, size * 2, size * 2); symbolStrokePath.moveTo(x + size * 0.93, y + size * 0.93); symbolStrokePath.lineTo(x - size * 0.93, y - size * 0.93); symbolStrokePath.moveTo(x + size * 0.93, y - size * 0.93); symbolStrokePath.lineTo(x - size * 0.93, y + size * 0.93); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 8: // 8-pointed asterisk { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x, y + size); symbolStrokePath.lineTo(x, y - size); symbolStrokePath.moveTo(x + size, y); symbolStrokePath.lineTo(x - size, y); symbolStrokePath.moveTo(x + size * 0.7071, y + size * 0.7071); symbolStrokePath.lineTo(x - size * 0.7071, y - size * 0.7071); symbolStrokePath.moveTo(x + size * 0.7071, y - size * 0.7071); symbolStrokePath.lineTo(x - size * 0.7071, y + size * 0.7071); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 9: // diamond with orthogonal cross { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x + size * 1.3, y); symbolStrokePath.lineTo(x, y - size * 1.3); symbolStrokePath.lineTo(x - size * 1.3, y); symbolStrokePath.lineTo(x, y + size * 1.3); symbolStrokePath.closeSubpath(); symbolStrokePath.moveTo(x, y + size * 1.2); symbolStrokePath.lineTo(x, y - size * 1.2); symbolStrokePath.moveTo(x + size * 1.2, y); symbolStrokePath.lineTo(x - size * 1.2, y); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 10: // circle outline with orthogonal cross { QPainterPath symbolStrokePath; symbolStrokePath.addEllipse(x - size * 1.13, y - size * 1.13, size * 2 * 1.13, size * 2 * 1.13); symbolStrokePath.moveTo(x, y + size * 1.05); symbolStrokePath.lineTo(x, y - size * 1.05); symbolStrokePath.moveTo(x + size * 1.05, y); symbolStrokePath.lineTo(x - size * 1.05, y); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 11: // six-pointed star outline (overlapping triangles) { QPainterPath symbolStrokePath; symbolStrokePath.moveTo(x, y + size * 1.4); symbolStrokePath.lineTo(x + 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolStrokePath.lineTo(x - 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolStrokePath.closeSubpath(); symbolStrokePath.moveTo(x, y - size * 1.4); symbolStrokePath.lineTo(x + 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolStrokePath.lineTo(x - 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolStrokePath.closeSubpath(); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 12: // square outline with orthogonal cross { QPainterPath symbolStrokePath; symbolStrokePath.addRect(x - size, y - size, size * 2, size * 2); symbolStrokePath.moveTo(x, y + size * 0.9); symbolStrokePath.lineTo(x, y - size * 0.9); symbolStrokePath.moveTo(x + size * 0.9, y); symbolStrokePath.lineTo(x - size * 0.9, y); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 13: // circle outline with diagonal cross { QPainterPath symbolStrokePath; symbolStrokePath.addEllipse(x - size * 1.13, y - size * 1.13, size * 2 * 1.13, size * 2 * 1.13); symbolStrokePath.moveTo(x + size * 0.75, y + size * 0.75); symbolStrokePath.lineTo(x - size * 0.75, y - size * 0.75); symbolStrokePath.moveTo(x + size * 0.75, y - size * 0.75); symbolStrokePath.lineTo(x - size * 0.75, y + size * 0.75); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 14: // square with embedded triangle { QPainterPath symbolStrokePath; symbolStrokePath.addRect(x - size, y - size, size * 2, size * 2); symbolStrokePath.moveTo(x - size, y - size); symbolStrokePath.lineTo(x, y + size); symbolStrokePath.lineTo(x + size, y - size); painter.strokePath(symbolStrokePath, QPen(symbolColor, lineWidth)); break; } case 15: // square filled { QPainterPath symbolFillPath; symbolFillPath.addRect(x - size, y - size, size * 2, size * 2); painter.fillPath(symbolFillPath, symbolColor); break; } case 16: // circle filled { QPainterPath symbolFillPath; symbolFillPath.addEllipse(x - size * 1.13, y - size * 1.13, size * 2 * 1.13, size * 2 * 1.13); painter.fillPath(symbolFillPath, symbolColor); break; } case 17: // triangle filled pointing up { QPainterPath symbolFillPath; symbolFillPath.moveTo(x, y + size * 1.4); symbolFillPath.lineTo(x + 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolFillPath.lineTo(x - 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolFillPath.closeSubpath(); painter.fillPath(symbolFillPath, symbolColor); break; } case 18: // diamond filled { QPainterPath symbolFillPath; symbolFillPath.moveTo(x + size * 1.3, y); symbolFillPath.lineTo(x, y - size * 1.3); symbolFillPath.lineTo(x - size * 1.3, y); symbolFillPath.lineTo(x, y + size * 1.3); symbolFillPath.closeSubpath(); painter.fillPath(symbolFillPath, symbolColor); break; } case 19: // triangle filled pointing down { QPainterPath symbolFillPath; symbolFillPath.moveTo(x, y - size * 1.4); symbolFillPath.lineTo(x + 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolFillPath.lineTo(x - 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolFillPath.closeSubpath(); painter.fillPath(symbolFillPath, symbolColor); break; } case 20: // six-pointed star filled (overlapping triangles) { QPainterPath symbolFillPath; symbolFillPath.moveTo(x, y + size * 1.4); symbolFillPath.lineTo(x + 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolFillPath.lineTo(x - 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolFillPath.closeSubpath(); symbolFillPath.moveTo(x, y - size * 1.4); symbolFillPath.lineTo(x - 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolFillPath.lineTo(x + 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolFillPath.closeSubpath(); symbolFillPath.setFillRule(Qt::WindingFill); painter.fillPath(symbolFillPath, symbolColor); break; } case 21: // circle filled and stroked { QPainterPath symbolFillPath; symbolFillPath.addEllipse(x - size * 1.13, y - size * 1.13, size * 2 * 1.13, size * 2 * 1.13); painter.fillPath(symbolFillPath, symbolColor); painter.strokePath(symbolFillPath, QPen(borderColor, lineWidth)); break; } case 22: // square filled and stroked { QPainterPath symbolFillPath; symbolFillPath.addRect(x - size, y - size, size * 2, size * 2); painter.fillPath(symbolFillPath, symbolColor); painter.strokePath(symbolFillPath, QPen(borderColor, lineWidth)); break; } case 23: // diamond filled and stroked { QPainterPath symbolFillPath; symbolFillPath.moveTo(x + size * 1.3, y); symbolFillPath.lineTo(x, y - size * 1.3); symbolFillPath.lineTo(x - size * 1.3, y); symbolFillPath.lineTo(x, y + size * 1.3); symbolFillPath.closeSubpath(); painter.fillPath(symbolFillPath, symbolColor); painter.strokePath(symbolFillPath, QPen(borderColor, lineWidth)); break; } case 24: // triangle filled and stroked pointing up { QPainterPath symbolFillPath; symbolFillPath.moveTo(x, y + size * 1.4); symbolFillPath.lineTo(x + 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolFillPath.lineTo(x - 0.8660 * size * 1.4, y - 0.5 * size * 1.4); symbolFillPath.closeSubpath(); painter.fillPath(symbolFillPath, symbolColor); painter.strokePath(symbolFillPath, QPen(borderColor, lineWidth)); break; } case 25: // triangle filled and stroked pointing down { QPainterPath symbolFillPath; symbolFillPath.moveTo(x, y - size * 1.4); symbolFillPath.lineTo(x + 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolFillPath.lineTo(x - 0.8660 * size * 1.4, y + 0.5 * size * 1.4); symbolFillPath.closeSubpath(); painter.fillPath(symbolFillPath, symbolColor); painter.strokePath(symbolFillPath, QPen(borderColor, lineWidth)); break; } default: // other symbols draw nothing break; } } void QtSLiMGraphView::drawGroupedBarplot(QPainter &painter, QRect interiorRect, double *buffer, int subBinCount, int mainBinCount, double firstBinValue, double mainBinWidth) { // Decide on a display style; if we have lots of width, then we draw bars with a fill color, spaced out nicely, // but if we are cramped for space then we draw solid black bars. Note the latter style does not really // work with sub-bins; not much we can do about that, since we don't have the room to draw... int interiorWidth = interiorRect.width(); int totalBarCount = subBinCount * mainBinCount; int drawStyle = 0; if (totalBarCount * 7 + 1 <= interiorWidth) // room for space, space, space, frame, fill, fill, frame... drawStyle = 0; else if (totalBarCount * 5 + 1 <= interiorWidth) // room for space, frame, fill, fill, frame... drawStyle = 1; else if (totalBarCount * 2 + 1 <= interiorWidth) // room for frame, fill, [frame]... drawStyle = 2; else drawStyle = 3; if (generatingPDF_ && (drawStyle == 3)) drawStyle = 2; for (int mainBinIndex = 0; mainBinIndex < mainBinCount; ++mainBinIndex) { double binMinValue = mainBinIndex * mainBinWidth + firstBinValue; double binMaxValue = (mainBinIndex + 1) * mainBinWidth + firstBinValue; double barLeft = roundPlotToDeviceX(binMinValue, interiorRect); double barRight = roundPlotToDeviceX(binMaxValue, interiorRect); double lineWidth = generatingPDF_ ? 0.3 : 1.0; double halfLineWidth = lineWidth / 2.0; switch (drawStyle) { case 0: barLeft += (1.0 + halfLineWidth); barRight -= (1.0 + halfLineWidth); break; case 1: barLeft += halfLineWidth; barRight -= halfLineWidth; break; case 2: barLeft -= halfLineWidth; barRight += halfLineWidth; break; case 3: barLeft -= halfLineWidth; barRight += halfLineWidth; break; } for (int subBinIndex = 0; subBinIndex < subBinCount; ++subBinIndex) { int actualBinIndex = subBinIndex + mainBinIndex * subBinCount; double binValue = buffer[actualBinIndex]; double barBottom = interiorRect.y() - (generatingPDF_ ? 0.5 : 1.0); double barTop; QRectF barRect; if (fabs(binValue - 0) < 0.00000001) continue; if (generatingPDF_) { barTop = plotToDeviceY(binValue, interiorRect); barRect = QRectF(barLeft, barBottom, barRight - barLeft, barTop - barBottom); } else { barTop = roundPlotToDeviceY(binValue, interiorRect) + 0.5; barRect = QRectF(qRound(barLeft), qRound(barBottom), qRound(barRight - barLeft), qRound(barTop - barBottom)); } if (barRect.height() > 0.0) { // subdivide into sub-bars for each mutation type, if there is more than one if (subBinCount > 1) { double barWidth = barRect.width(); double subBarWidth = (barWidth - lineWidth) / subBinCount; double subbarLeft = SLIM_SCREEN_ROUND(barRect.x() + subBinIndex * subBarWidth); double subbarRight = SLIM_SCREEN_ROUND(barRect.x() + (subBinIndex + 1) * subBarWidth) + lineWidth; if (generatingPDF_) { barRect.setX(subbarLeft); barRect.setWidth(subbarRight - subbarLeft); } else { barRect.setX(qRound(subbarLeft)); barRect.setWidth(qRound(subbarRight - subbarLeft)); } } // fill and frame if (drawStyle == 3) { painter.fillRect(barRect, Qt::black); } else { painter.fillRect(barRect, controller_->blackContrastingColorForIndex(subBinIndex)); QtSLiMFrameRect(barRect, Qt::black, painter, lineWidth); } } } } } void QtSLiMGraphView::drawBarplot(QPainter &painter, QRect interiorRect, double *buffer, int binCount, double firstBinValue, double binWidth) { drawGroupedBarplot(painter, interiorRect, buffer, 1, binCount, firstBinValue, binWidth); } void QtSLiMGraphView::drawHeatmap(QPainter &painter, QRect interiorRect, double *buffer, int xBinCount, int yBinCount) { int intHeatMapMargins = (generatingPDF_ ? 0 : heatmapMargins_); // when generating a PDF we use an inset for accuracy double patchWidth = (interiorRect.width() - intHeatMapMargins) / (double)xBinCount; double patchHeight = (interiorRect.height() - intHeatMapMargins) / (double)yBinCount; for (int xc = 0; xc < xBinCount; ++xc) { for (int yc = 0; yc < yBinCount; ++yc) { double value = buffer[xc + yc * xBinCount]; double patchX1 = SLIM_SCREEN_ROUND(interiorRect.left() + patchWidth * xc) + intHeatMapMargins; double patchX2 = SLIM_SCREEN_ROUND(interiorRect.left() + patchWidth * (xc + 1)); double patchY1 = SLIM_SCREEN_ROUND(interiorRect.top() + patchHeight * yc) + intHeatMapMargins; double patchY2 = SLIM_SCREEN_ROUND(interiorRect.top() + patchHeight * (yc + 1)); QRectF patchRect(patchX1, patchY1, patchX2 - patchX1, patchY2 - patchY1); if (generatingPDF_) patchRect.adjust(0.5 * heatmapMargins_, 0.5 * heatmapMargins_, -0.5 * heatmapMargins_, -0.5 * heatmapMargins_); double r, g, b; if (value == -10000) { r = 0.25; g = 0.25; b = 1.0; // a special "no value" color for the 2D SFS plot } else Eidos_ColorPaletteLookup(1.0 - value, EidosColorPalette::kPalette_hot, r, g, b); painter.fillRect(patchRect, QtSLiMColorWithRGB(r, g, b, 1.0)); } } } bool QtSLiMGraphView::addSubpopulationsToMenu(QComboBox *subpopButton, slim_objectid_t selectedSubpopID, slim_objectid_t avoidSubpopID) { Species *graphSpecies = focalDisplaySpecies(); slim_objectid_t firstTag = -1; // QComboBox::currentIndexChanged signals will be sent during rebuilding; this flag // allows client code to avoid (over-)reacting to those signals. rebuildingMenu_ = true; // Depopulate and populate the menu subpopButton->clear(); if (graphSpecies) { Population &population = graphSpecies->population_; for (auto popIter : population.subpops_) { slim_objectid_t subpopID = popIter.first; QString subpopString = QString("p%1").arg(subpopID); subpopButton->addItem(subpopString, subpopID); // Remember the first item we add; we will use this item's tag to make a selection if needed // If we have a tag to avoid, avoid it, preferring the second item if necessary if (firstTag == -1) firstTag = subpopID; if (firstTag == avoidSubpopID) firstTag = subpopID; } } //[subpopulationButton slimSortMenuItemsByTag]; // If it is empty, disable it bool hasItems = (subpopButton->count() >= 1); subpopButton->setEnabled(hasItems); // Done rebuilding the menu, resume change messages rebuildingMenu_ = false; // Fix the selection and then select the chosen subpopulation if (hasItems) { int indexOfTag = subpopButton->findData(selectedSubpopID); if (indexOfTag == -1) selectedSubpopID = -1; if (selectedSubpopID == -1) selectedSubpopID = firstTag; if (selectedSubpopID == avoidSubpopID) selectedSubpopID = firstTag; subpopButton->setCurrentIndex(subpopButton->findData(selectedSubpopID)); // this signal, emitted after rebuildingMenu_ is set to false, is the one that sticks emit subpopButton->QComboBox::currentIndexChanged(subpopButton->currentIndex()); } return hasItems; // true if we found at least one subpop to add to the menu, false otherwise } bool QtSLiMGraphView::addMutationTypesToMenu(QComboBox *mutTypeButton, int selectedMutIDIndex) { Species *graphSpecies = focalDisplaySpecies(); int firstTag = -1; // QComboBox::currentIndexChanged signals will be sent during rebuilding; this flag // allows client code to avoid (over-)reacting to those signals. rebuildingMenu_ = true; // Depopulate and populate the menu mutTypeButton->clear(); if (graphSpecies) { std::map &mutationTypes = graphSpecies->mutation_types_; for (auto mutTypeIter : mutationTypes) { MutationType *mutationType = mutTypeIter.second; slim_objectid_t mutationTypeID = mutationType->mutation_type_id_; int mutationTypeIndex = mutationType->mutation_type_index_; QString mutationTypeString = QString("m%1").arg(mutationTypeID); mutTypeButton->addItem(mutationTypeString, mutationTypeIndex); // Remember the first item we add; we will use this item's tag to make a selection if needed if (firstTag == -1) firstTag = mutationTypeIndex; } } //[mutationTypeButton slimSortMenuItemsByTag]; // If it is empty, disable it bool hasItems = (mutTypeButton->count() >= 1); mutTypeButton->setEnabled(hasItems); // Done rebuilding the menu, resume change messages rebuildingMenu_ = false; // Fix the selection and then select the chosen mutation type if (hasItems) { int indexOfTag = mutTypeButton->findData(selectedMutIDIndex); if (indexOfTag == -1) selectedMutIDIndex = -1; if (selectedMutIDIndex == -1) selectedMutIDIndex = firstTag; mutTypeButton->setCurrentIndex(mutTypeButton->findData(selectedMutIDIndex)); // this signal, emitted after rebuildingMenu_ is set to false, is the one that sticks emit mutTypeButton->QComboBox::currentIndexChanged(mutTypeButton->currentIndex()); } return hasItems; // true if we found at least one muttype to add to the menu, false otherwise } size_t QtSLiMGraphView::tallyGUIMutationReferences(slim_objectid_t subpop_id, int muttype_index) { // // this code is a slightly modified clone of the code in Population::TallyMutationReferences; here we scan only the // subpopulation that is being displayed in this graph, and tally into gui_scratch_reference_count only // BCH 4/21/2023: This could use mutrun use counts to run faster... // Species *graphSpecies = focalDisplaySpecies(); if (!graphSpecies) return 0; Population &population = graphSpecies->population_; size_t subpop_total_haplosome_count = 0; Mutation *mut_block_ptr = gSLiM_Mutation_Block; { int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); const MutationIndex *registry_iter_end = registry + registry_size; for (const MutationIndex *registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) (mut_block_ptr + *registry_iter)->gui_scratch_reference_count_ = 0; } Subpopulation *subpop = graphSpecies->SubpopulationWithID(subpop_id); if (subpop) // tally only within our chosen subpop { int haplosome_count_per_individual = subpop->HaplosomeCountPerIndividual(); for (Individual *ind : subpop->parent_individuals_) { Haplosome **haplosomes = ind->haplosomes_; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; if (!haplosome->IsNull()) { int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; const MutationIndex *haplosome_iter = mutrun->begin_pointer_const(); const MutationIndex *haplosome_end_iter = mutrun->end_pointer_const(); for (; haplosome_iter != haplosome_end_iter; ++haplosome_iter) { const Mutation *mutation = mut_block_ptr + *haplosome_iter; if (mutation->mutation_type_ptr_->mutation_type_index_ == muttype_index) (mutation->gui_scratch_reference_count_)++; } } subpop_total_haplosome_count++; } } } } return subpop_total_haplosome_count; } size_t QtSLiMGraphView::tallyGUIMutationReferences(const std::vector &haplosomes, int muttype_index) { // // this code is a slightly modified clone of the code in Population::TallyMutationReferences; here we scan only the // subpopulation that is being displayed in this graph, and tally into gui_scratch_reference_count only // BCH 4/21/2023: This could use mutrun use counts to run faster... // Species *graphSpecies = focalDisplaySpecies(); if (!graphSpecies) return 0; Population &population = graphSpecies->population_; Mutation *mut_block_ptr = gSLiM_Mutation_Block; { int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); const MutationIndex *registry_iter_end = registry + registry_size; for (const MutationIndex *registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) (mut_block_ptr + *registry_iter)->gui_scratch_reference_count_ = 0; } for (const Haplosome *haplosome : haplosomes) { if (!haplosome->IsNull()) { int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; const MutationIndex *haplosome_iter = mutrun->begin_pointer_const(); const MutationIndex *haplosome_end_iter = mutrun->end_pointer_const(); for (; haplosome_iter != haplosome_end_iter; ++haplosome_iter) { const Mutation *mutation = mut_block_ptr + *haplosome_iter; if (mutation->mutation_type_ptr_->mutation_type_index_ == muttype_index) (mutation->gui_scratch_reference_count_)++; } } } } return haplosomes.size(); } // // Axis tick calculations // // This code is based upon the code in R 4.3.2. R is open source under the GPL, so // we are free to use it in Eidos/SLiM which is also GPL. The GPL license is already // incorporated in this distribution. Thanks to all the contributors to this code in // R, which provides a nice algorithm. // // In QtSLiM's adapted code, I have removed a bunch of debugging code, removed the // log-axis case, removed support for axis min > max, removed all the par()-based // graphics-parameter stuff, removed various errors and warnings, etc. These changes // simplified the code, at the cost of making it less general and robust. QtSLiM // doesn't really want to be reporting random internal warnings and errors to the // user, though; if we hit one of the edge cases that R used to handle, then que sera, // sera. // // modified from R-4.3.2/src/library/graphics/src/graphics.c : void GScale(double min, double max, int axis, pGEDevDesc dd) void QtSLiMGraphView::_GScale(double &minValue, double &maxValue, double &axisMin, double &axisMax, int &nDivisions) { #define EPS_FAC_1 16 // number of divisions; this comes from lab[0] in R, but for us we just always use the default of 5 nDivisions = 5; double temp = std::max(std::fabs(maxValue), std::fabs(minValue)); if (temp == 0) /* min = max = 0 */ { minValue = -1; maxValue = 1; } else { // careful to avoid overflow (and underflow) here: double tf = (temp > 1) ? (temp * DBL_EPSILON) * EPS_FAC_1 : (temp * EPS_FAC_1 ) * DBL_EPSILON; if (tf == 0) tf = DBL_MIN; if (std::fabs(maxValue - minValue) < tf) { temp *= 1e-2; minValue -= temp; maxValue += temp; } } if (true) { // R axis style 'r': (regular) first extends the data range by 4 percent at each end // and then finds an axis with pretty labels that fits within the extended range. temp = (temp > 100 ? 0.04 * maxValue - 0.04 * minValue // not to overflow : 0.04 * (maxValue - minValue)); // is negative iff max < min // careful now to not get to +/- Inf : double d; d = minValue - temp; if (std::isfinite(d)) minValue = d; else minValue = (d < 0) ? -DBL_MAX : DBL_MAX; d = maxValue + temp; if (std::isfinite(d)) maxValue = d; else maxValue = (d < 0) ? -DBL_MAX : DBL_MAX; } else { // R axis style 'i': (internal) just finds an axis with pretty labels that fits // within the original data range. Presently inaccessible in QtSLiM. } // Computation of [xy]axp[0:2] == (min,max,n) : axisMin = minValue; axisMax = maxValue; _GAxisPars(&axisMin, &axisMax, &nDivisions); #undef EPS_FAC_1 } // modified from R-4.3.2/src/main/graphics.c : void GAxisPars(double *min, double *max, int *n, Rboolean log, int axis) void QtSLiMGraphView::_GAxisPars(double *minValue, double *maxValue, int *nDivisions) { #define EPS_FAC_2 16 /* save only for the extreme case (EPS_FAC_2): */ double min_o = *minValue, max_o = *maxValue; _GEPretty(minValue, maxValue, nDivisions); double t_ = std::max(fabs(*maxValue), fabs(*minValue)), tf = // careful to avoid overflow (and underflow) here: (t_ > 1) ? (t_ * DBL_EPSILON) * EPS_FAC_2 : (t_ * EPS_FAC_2 ) * DBL_EPSILON; if (tf == 0) tf = DBL_MIN; if (fabs(*maxValue - *minValue) <= tf) { /* Treat this case somewhat similar to the (min ~= max) case above */ /* Too much accuracy here just shows machine differences */ /* No pretty()ing anymore */ *minValue = min_o; *maxValue = max_o; double eps = .005 * (*maxValue - *minValue);/* .005: not to go to DBL_MIN/MAX */ *minValue += eps; *maxValue -= eps; *nDivisions = 1; } #undef EPS_FAC_2 } // modified R-4.3.2/src/main/graphics.c : static void GLPretty(double *ul, double *uh, int *n) void QtSLiMGraphView::_GEPretty(double *lo, double *up, int *nDivisions) { /* Set scale and ticks for linear scales. * * Pre: x1 == lo < up == x2 ; nDivisions >= 1 * Post: x1 <= y1 := lo < up =: y2 <= x2; nDivisions >= 1 */ if (*nDivisions <= 0) return; if (!std::isfinite(*lo) || !std::isfinite(*up)) // also catch NA etc return; // For *finite* boundaries, now allow (*up - *lo) = +/- inf as R_pretty() now does double ns = *lo, nu = *up; double unit, high_u_fact[3] = { .8, 1.7, 1.125 }; // = (h, h5 , f_min) = (high.u.bias, u5.bias, f_min) unit = _R_pretty(&ns, &nu, nDivisions, /* min_n = */ 1, /* shrink_sml = */ 0.25, high_u_fact, 2 /* do eps_correction in any case */); // The following is ugly since it kind of happens already in R_pretty(..): #define rounding_eps 1e-10 /* <- compatible to seq*(); was 1e-7 till 2017-08-14 */ if (nu >= ns + 1) { int mod = 0; if ( ns * unit < *lo - rounding_eps * unit) { ns++; mod++; } if (nu > ns + 1 && nu * unit > *up + rounding_eps * unit) { nu--; mod++; } if (mod) *nDivisions = (int)(nu - ns); } *lo = ns * unit; *up = nu * unit; } // modified from R-4.3.2/src/appl/pretty.c : double R_pretty(double *lo, double *up, int *ndiv, int min_n, double shrink_sml, const double high_u_fact[], int eps_correction, int return_bounds) double QtSLiMGraphView::_R_pretty(double *lo, double *up, int *nDivisions, int min_n, double shrink_sml, const double high_u_fact[], // = (h, h5, f_min) below int eps_correction) { /* From version 0.65 on, we had rounding_eps := 1e-5, before, r..eps = 0 * then, 1e-7 was consistent with seq.default() and seq.int() till 2010-02-03, * where it was changed to 1e-10 for seq*(), and in 2017-08-14 for pretty(): */ #define rounding_eps 1e-10 // (h, h5, f_min) = c(high.u.bias, u5.bias, f.min) in base::pretty.default(): #define h high_u_fact[0] #define h5 high_u_fact[1] #define f_min high_u_fact[2] double // save input boundaries lo_ = *lo, up_ = *up, dx = up_ - lo_, cell, U; bool i_small; /* cell := "scale" here */ if (dx == 0 && up_ == 0) { /* up == lo == 0 */ cell = 1; i_small = true; } else { cell = std::max(fabs(lo_),fabs(up_)); /* U = upper bound on cell/unit */ U = 1 + ((h5 >= 1.5*h+.5) ? 1/(1+h) : 1.5/(1+h5)); U *= std::max(1,*nDivisions) * DBL_EPSILON; // avoid overflow for large nDivisions /* added times 3, as several calculations here */ i_small = dx < cell * U * 3; } /*OLD: cell = FLT_EPSILON+ dx / *nDivisions; FLT_EPSILON = 1.192e-07 */ if (i_small) { if(cell > 10) cell = 9 + cell/10; cell *= shrink_sml; if(min_n > 1) cell /= min_n; } else { cell = dx; if (std::isfinite(dx)) { if (*nDivisions > 1) cell /= *nDivisions; } else { // up - lo = +Inf (overflow; both are finite) if (*nDivisions >= 2) { cell = up_ / (*nDivisions) - lo_ / (*nDivisions); } } } // f_min: arg, default = 2^-20, was 20. till R 4.1.0 (2021-05) #define MAX_F 1.25 // was 10. " " " double subsmall = f_min*DBL_MIN; if (subsmall == 0.) // subnormals underflowing to zero (not yet seen!) subsmall = DBL_MIN; if (cell < subsmall) { // possibly subnormal cell = subsmall; } else if (cell > DBL_MAX/MAX_F) { cell = DBL_MAX/MAX_F; } #undef MAX_F /* NB: the power can be negative and this relies on exact calculation, which glibc's exp10 does not achieve */ double base = pow(10.0, floor(log10(cell))); /* base <= cell < 10*base */ /* unit : from { 1,2,5,10 } * base * such that |u - cell| is small, * favoring larger (if h > 1, else smaller) u values; * favor '5' more than '2' if h5 > h (default h5 = .5 + 1.5 h) */ double unit = base; if ((U = 2*base)-cell < h*(cell-unit)) { unit = U; if ((U = 5*base)-cell < h5*(cell-unit)) { unit = U; if ((U =10*base)-cell < h*(cell-unit)) unit = U; }} /* Result (c := cell, b := base, u := unit): * c in [ 1, (2+ h)/ (1+h) ] b ==> u= b * c in ( (2+ h) /(1+h), (5+2h5)/(1+h5)] b ==> u= 2b * c in ( (5+2h5)/(1+h5), (10+5h)/(1+h) ] b ==> u= 5b * c in ((10+5h) /(1+h), 10 ) b ==> u=10b * * ===> 2/5 *(2+h)/(1+h) <= c/u <= (2+h)/(1+h) */ double ns = floor(lo_/unit+rounding_eps); double nu = ceil (up_/unit-rounding_eps); if (eps_correction && (eps_correction > 1 || !i_small)) { // FIXME?: assumes 0 <= lo <= up (what if lo <= up < 0 ?) if (lo_ != 0.) *lo *= (1- DBL_EPSILON); else *lo = -DBL_MIN; if (up_ != 0.) *up *= (1+ DBL_EPSILON); else *up = +DBL_MIN; } while (ns*unit > *lo + rounding_eps*unit) ns--; while (!std::isfinite(ns*unit)) ns++; while (nu*unit < *up - rounding_eps*unit) nu++; while (!std::isfinite(nu*unit)) nu--; int k = (int)(0.5 + nu - ns); if (k < min_n) { /* ensure that nu - ns == min_n */ k = min_n - k; if (lo_ == 0. && ns == 0. && up_ != 0.) { nu += k; } else if (up_ == 0. && nu == 0. && lo_ != 0.) { ns -= k; } else if (ns >= 0.) { nu += k/2; ns -= k/2 + k%2;/* ==> nu-ns = old(nu-ns) + min_n -k = min_n */ } else { ns -= k/2; nu += k/2 + k%2; } *nDivisions = min_n; } else { *nDivisions = k; } *lo = ns; *up = nu; return unit; #undef h #undef h5 } ================================================ FILE: QtSLiM/QtSLiMGraphView.h ================================================ // // QtSLiMGraphView.h // SLiM // // Created by Ben Haller on 3/27/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_H #define QTSLIMGRAPHVIEW_H #include #include #include #include #include #include "QtSLiMExtras.h" #include "QtSLiMWindow.h" class QHBoxLayout; class QComboBox; class QPushButton; // A quick and dirty macro to enable rounding of coordinates to the nearest pixel only when we are not generating PDF // FIXME this ought to be revisited in the light of Retina displays, printing, etc. #define SLIM_SCREEN_ROUND(x) (generatingPDF_ ? (x) : round(x)) // Legend support enum class QtSLiM_LegendEntryType : int { kUninitialized = -1, kTitle = 0, kSwatch, kLine, kPoint }; class QtSLiMLegendEntry { public: // the text label for the legend entry QString label; // the type of legend entry QtSLiM_LegendEntryType entry_type = QtSLiM_LegendEntryType::kUninitialized; // attributes for a swatch component (QtSLiM_LegendEntryType::kSwatch) QColor swatch_color; // attributes for a line component (QtSLiM_LegendEntryType::kLine); same as for plotLines() double line_lwd; QColor line_color; // attributes for a point component (QtSLiM_LegendEntryType::kPoint); same as for plotPoints() int point_symbol; QColor point_color; QColor point_border; double point_lwd; double point_size; // constructor for an entry that shows a color swatch QtSLiMLegendEntry(QString p_label, QColor p_swatch_color) : label(p_label), entry_type(QtSLiM_LegendEntryType::kSwatch), swatch_color(p_swatch_color) {}; // constructor for an entry that shows a line segment QtSLiMLegendEntry(QString p_label, double p_line_lwd, QColor p_line_color) : label(p_label), entry_type(QtSLiM_LegendEntryType::kLine), line_lwd(p_line_lwd), line_color(p_line_color) {}; // constructor for an entry that shows a point symbol QtSLiMLegendEntry(QString p_label, int p_point_symbol, QColor p_point_color, QColor p_point_border, double p_point_lwd, double p_point_size) : label(p_label), entry_type(QtSLiM_LegendEntryType::kPoint), point_symbol(p_point_symbol), point_color(p_point_color), point_border(p_point_border), point_lwd(p_point_lwd), point_size(p_point_size) {}; // constructor for an entry that shows a title QtSLiMLegendEntry(QString p_label) : label(p_label), entry_type(QtSLiM_LegendEntryType::kTitle) {}; }; typedef std::vector QtSLiMLegendSpec; class QtSLiMGraphView : public QWidget { Q_OBJECT public: static QFont labelFontOfPointSize(double size); static inline QColor gridLineColor(void) { return QtSLiMColorWithWhite(0.85, 1.0); } QtSLiMGraphView(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView() override; virtual QString graphTitle(void) = 0; virtual QString aboutString(void) = 0; virtual void drawGraph(QPainter &painter, QRect interiorRect); void setBorderless(bool isBorderless, double marginLeft, double marginTop, double marginRight, double marginBottom); bool writeToFile(QString fileName); public slots: virtual void addedToWindow(void); virtual void invalidateCachedData(void); // subclasses must call this themselves in their destructor - super cannot do it! virtual void invalidateDrawingCache(void); // subclasses must call this themselves in their destructor - super cannot do it! virtual void graphWindowResized(void); virtual void controllerRecycled(void); // subclasses must call super: QtSLiMGraphView::controllerRecycled() virtual void controllerTickFinished(void); virtual void updateAfterTick(void); // subclasses must call super: QtSLiMGraphView::updateAfterTick() virtual void updateSpeciesBadge(void); void actionButtonRunMenu(QtSLiMPushButton *actionButton); protected: QtSLiMWindow *controller_ = nullptr; std::string focalSpeciesName_; // we keep the name of our focal species, since a pointer would be unsafe std::string focalSpeciesAvatar_; // cached so we can display it even when the simulation is invalid void setFocalDisplaySpecies(Species *species); Species *focalDisplaySpecies(void); bool missingFocalDisplaySpecies(void); // true if the graph has a focal display species but can't find it // Base graphing functionality QRect interiorRectForBounds(QRect bounds); double plotToDeviceX(double plotx, QRect interiorRect); double plotToDeviceY(double ploty, QRect interiorRect); double roundPlotToDeviceX(double plotx, QRect interiorRect); // rounded off to the nearest midpixel double roundPlotToDeviceY(double ploty, QRect interiorRect); // rounded off to the nearest midpixel inline QFont fontForAxisLabels(void) { return labelFontOfPointSize(axisLabelSize_); } inline QFont fontForTickLabels(void) { return labelFontOfPointSize(tickLabelSize_); } QString labelTextForTick(double tickValue, int tickValuePrecision, double minorTickInterval); void drawAxisTickLabel(QPainter &painter, QString labelText, double xValueForTick, double axisLength, bool isFirstTick, bool isLastTick); void drawXAxisTicks(QPainter &painter, QRect interiorRect); void drawXAxis(QPainter &painter, QRect interiorRect); void drawYAxisTicks(QPainter &painter, QRect interiorRect); void drawYAxis(QPainter &painter, QRect interiorRect); void drawFullBox(QPainter &painter, QRect interiorRect); void drawVerticalGridLines(QPainter &painter, QRect interiorRect); void drawHorizontalGridLines(QPainter &painter, QRect interiorRect); void drawMessage(QPainter &painter, QString messageString, QRect rect); void drawLegendInInteriorRect(QPainter &painter, QRect interiorRect); // Mandatory subclass overrides virtual void appendStringForData(QString &string) = 0; // Optional subclass overrides virtual void willDraw(QPainter &painter, QRect interiorRect); virtual bool providesStringForData(void); virtual QtSLiMLegendSpec legendKey(void); virtual QSizeF legendSize(QPainter &painter); virtual void drawLegend(QPainter &painter, QRectF legendRect); virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event); virtual QString disableMessage(void); // Adding new widgets at the bottom of the window QHBoxLayout *buttonLayout(void); QPushButton *actionButton(void); QComboBox *newButtonInLayout(QHBoxLayout *layout); // Prefab additions void setXAxisRangeFromTick(void); void configureAxisForRange(double &dim0, double &dim1, double &axisMin, double &axisMax, double &majorTickInterval, double &minorTickInterval, int &majorTickModulus, int &tickValuePrecision); QtSLiMLegendSpec subpopulationLegendKey(std::vector &subpopsToDisplay, bool drawSubpopsGray); QtSLiMLegendSpec mutationTypeLegendKey(void); void drawGroupedBarplot(QPainter &painter, QRect interiorRect, double *buffer, int subBinCount, int mainBinCount, double firstBinValue, double mainBinWidth); void drawBarplot(QPainter &painter, QRect interiorRect, double *buffer, int binCount, double firstBinValue, double binWidth); void drawHeatmap(QPainter &painter, QRect interiorRect, double *buffer, int xBinCount, int yBinCount); bool addSubpopulationsToMenu(QComboBox *subpopButton, slim_objectid_t selectedSubpopID, slim_objectid_t avoidSubpopID = -1); bool addMutationTypesToMenu(QComboBox *mutTypeButton, int selectedMutIDIndex); size_t tallyGUIMutationReferences(slim_objectid_t subpop_id, int muttype_index); size_t tallyGUIMutationReferences(const std::vector &haplosomes, int muttype_index); // Properties; initialized in the constructor, these defaults are just zero-fill // The bounds in user coordinates (x0_/x1_/y0_/y1_) are the actual extents of the // axes; this is somewhat unrelated to the positions at which tick and labels are // drawn, which are prettified. The axis limits (xAxisMin_, xAxisMax_, ...) express // the limits of tick marks; they are the conceptual limits of the axes, but // typically there are margins of 4% or so outside of that range so the data isn't // right up against the edges of the plot. The original axis limits provided // by the user are kept in original_x0_ etc., since x0_ and xAxisMin_ do not // necessarily reflect the original values; they can get changed for aesthetics. // See QtSLiMGraphView::setBorderless() for the reason we need the original values. double original_x0_ = 0.0, original_x1_ = 0.0, original_y0_ = 0.0, original_y1_ = 0.0; double x0_ = 0.0, x1_ = 0.0, y0_ = 0.0, y1_ = 0.0; // coordinate space bounds double axisLabelSize_ = 15; double tickLabelSize_ = 10; bool showXAxis_ = false; bool allowXAxisUserRescale_ = false; bool xAxisIsUserRescaled_ = false; // the user gave a custom axis scale in any way (such as in createPlot()) bool xAxisIsUIRescaled_ = false; // the user gave a custom axis scale in the QtSLiM UI, which has precedence bool showXAxisTicks_ = false; double xAxisMin_ = 0.0, xAxisMax_ = 0.0; double xAxisMajorTickInterval_ = 0.0, xAxisMinorTickInterval_ = 0.0; int xAxisMajorTickModulus_ = 0; int xAxisTickValuePrecision_ = 0; // negative values request output mode 'g' instead of 'f' bool xAxisHistogramStyle_ = false; QString xAxisLabel_; std::vector *xAxisAt_ = nullptr; int xAxisLabelsType_ = 0; // 0 == F (don't show labels), 1 == T (numeric position labels), 2 == use xAxisLabels_ std::vector *xAxisLabels_ = nullptr; bool showYAxis_ = false; bool allowYAxisUserRescale_ = false; bool yAxisIsUserRescaled_ = false; // the user gave a custom axis scale in any way (such as in createPlot()) bool yAxisIsUIRescaled_ = false; // the user gave a custom axis scale in the QtSLiM UI, which has precedence bool showYAxisTicks_ = false; double yAxisMin_ = 0.0, yAxisMax_ = 0.0; double yAxisMajorTickInterval_ = 0.0, yAxisMinorTickInterval_ = 0.0; int yAxisMajorTickModulus_ = 0; int yAxisTickValuePrecision_ = 0; // negative values request output mode 'g' instead of 'f' bool yAxisHistogramStyle_ = false; bool yAxisLog_ = false; QString yAxisLabel_; std::vector *yAxisAt_ = nullptr; int yAxisLabelsType_ = 0; // 0 == F (don't show labels), 1 == T (numeric position labels), 2 == use yAxisLabels_ std::vector *yAxisLabels_ = nullptr; bool legendVisible_ = false; QtSLiM_LegendPosition legend_position_ = QtSLiM_LegendPosition::kUnconfigured; int legend_inset = -1; double legend_labelSize = -1; double legend_lineHeight = -1; double legend_graphicsWidth = -1; double legend_exteriorMargin = -1; double legend_interiorMargin = -1; bool showHorizontalGridLines_ = false; bool showVerticalGridLines_ = false; bool showGridLinesMajorOnly_ = false; bool showFullBox_ = false; bool allowHorizontalGridChange_ = false; bool allowVerticalGridChange_ = false; bool allowFullBoxChange_ = false; bool tweakXAxisTickLabelAlignment_ = false; // borderless plots show no axes, no ticks, no labels; this feature is used only for custom plots // for borderless plots, the plot region is the whole window area, and the data range fills the plot // that is inset by the margins given here, measured in pixels, to allow a bit of overflow bool is_borderless_ = false; double borderless_margin_left_ = 0.0; double borderless_margin_right_ = 0.0; double borderless_margin_top_ = 0.0; double borderless_margin_bottom_ = 0.0; // Prefab additions properties int histogramBinCount_ = 0; bool allowBinCountRescale_ = false; int heatmapMargins_ = 0; bool allowHeatmapMarginsChange_ = false; bool rebuildingMenu_ = false; // set to true during addSubpopulationsToMenu() / addMutationTypesToMenu() // set to YES during a copy: operation, to allow customization bool generatingPDF_ = false; // caching for drawing speed is up to subclasses, if they want to do it, but we provide minimal support // in GraphView to make it work smoothly; this flag is to prevent recursion in the drawing code, and to // disable drawing of things that don't belong in a cache, such as the legend bool cachingNow_ = false; protected: int lineCountForLegend(QtSLiMLegendSpec &legend); double graphicsWidthForLegend(QtSLiMLegendSpec &legend, double legendLineHeight); void drawPointSymbol(QPainter &painter, double x, double y, int symbol, QColor symbolColor, QColor borderColor, double alpha, double lineWidth, double size); private: virtual void paintEvent(QPaintEvent *p_paintEvent) override; void drawContents(QPainter &painter); virtual void resizeEvent(QResizeEvent *p_event) override; virtual void contextMenuEvent(QContextMenuEvent *p_event) override; QString stringForData(void); // Internal axis layout code, based on R; presently used only by custom graphs void _GScale(double &minValue, double &maxValue, double &axisMin, double &axisMax, int &nDivisions); void _GAxisPars(double *minValue, double *maxValue, int *nDivisions); void _GEPretty(double *lo, double *up, int *nDivisions); double _R_pretty(double *lo, double *up, int *nDivisions, int min_n, double shrink_sml, const double high_u_fact[], int eps_correction); }; #endif // QTSLIMGRAPHVIEW_H ================================================ FILE: QtSLiM/QtSLiMGraphView_1DPopulationSFS.cpp ================================================ // // QtSLiMGraphView_1DPopulationSFS.cpp // SLiM // // Created by Ben Haller on 3/27/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_1DPopulationSFS.h" #include "QtSLiMWindow.h" #include QtSLiMGraphView_1DPopulationSFS::QtSLiMGraphView_1DPopulationSFS(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 10; allowBinCountRescale_ = true; xAxisMajorTickInterval_ = 0.2; xAxisMinorTickInterval_ = 0.1; xAxisMajorTickModulus_ = 2; xAxisTickValuePrecision_ = 1; xAxisLabel_ = "Mutation frequency"; yAxisLabel_ = "Proportion of mutations"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = false; showHorizontalGridLines_ = true; } QtSLiMGraphView_1DPopulationSFS::~QtSLiMGraphView_1DPopulationSFS() { } QString QtSLiMGraphView_1DPopulationSFS::graphTitle(void) { return "1D Population SFS"; } QString QtSLiMGraphView_1DPopulationSFS::aboutString(void) { return "The 1D Population SFS graph shows a Site Frequency Spectrum (SFS) for the entire population. Since " "mutation occurrence counts across the whole population might be very large, the x axis here is the " "frequency of a given mutation, from 0.0 to 1.0, rather than an occurrence count. The y axis is the " "proportion of all mutations that fall within a given binned frequency range. The number of frequency " "bins can be customized from the action menu. The 1D Sample SFS graph provides an alternative that " "might also be useful."; } double *QtSLiMGraphView_1DPopulationSFS::populationSFS(int mutationTypeCount) { static uint32_t *spectrum = nullptr; // used for tallying static double *doubleSpectrum = nullptr; // not used for tallying, to avoid precision issues static size_t spectrumBins = 0; int binCount = histogramBinCount_; size_t usedSpectrumBins = static_cast(binCount * mutationTypeCount); // allocate our bins if (!spectrum || (spectrumBins < usedSpectrumBins)) { spectrumBins = usedSpectrumBins; spectrum = static_cast(realloc(spectrum, spectrumBins * sizeof(uint32_t))); doubleSpectrum = static_cast(realloc(doubleSpectrum, spectrumBins * sizeof(double))); } // clear our bins for (size_t i = 0; i < usedSpectrumBins; ++i) spectrum[i] = 0; // get the selected chromosome range Species *graphSpecies = focalDisplaySpecies(); // tally into our bins Population &pop = graphSpecies->population_; pop.TallyMutationReferencesAcrossPopulation(/* p_clock_for_mutrun_experiments */ false); // update tallies; usually this will just use the cache set up by Population::MaintainRegistry() Mutation *mut_block_ptr = gSLiM_Mutation_Block; slim_refcount_t *refcount_block_ptr = gSLiM_Mutation_Refcounts; int registry_size; const MutationIndex *registry = pop.MutationRegistry(®istry_size); for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; Chromosome *mut_chromosome = graphSpecies->Chromosomes()[mutation->chromosome_index_]; double totalHaplosomeCount = ((mut_chromosome->total_haplosome_count_ == 0) ? 1 : mut_chromosome->total_haplosome_count_); // prevent a zero count from producing NAN frequencies below slim_refcount_t mutationRefCount = *(refcount_block_ptr + mutation->BlockIndex()); double mutationFrequency = mutationRefCount / totalHaplosomeCount; int mutationBin = static_cast(floor(mutationFrequency * binCount)); int mutationTypeIndex = mutation->mutation_type_ptr_->mutation_type_index_; if (mutationBin == binCount) mutationBin = binCount - 1; (spectrum[mutationTypeIndex + mutationBin * mutationTypeCount])++; // bins in sequence for each mutation type within one frequency bin, then again for the next frequency bin, etc. } // normalize within each mutation type for (int mutationTypeIndex = 0; mutationTypeIndex < mutationTypeCount; ++mutationTypeIndex) { uint32_t total = 0; for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; total += spectrum[binIndex]; } for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; doubleSpectrum[binIndex] = (total > 0) ? (spectrum[binIndex] / static_cast(total)) : 0.0; } } // return the final tally; note this is a pointer in to our static ivar, and must not be freed! return doubleSpectrum; } void QtSLiMGraphView_1DPopulationSFS::drawGraph(QPainter &painter, QRect interiorRect) { int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); double *spectrum = populationSFS(mutationTypeCount); // plot our histogram bars drawGroupedBarplot(painter, interiorRect, spectrum, mutationTypeCount, binCount, 0.0, (1.0 / binCount)); } QtSLiMLegendSpec QtSLiMGraphView_1DPopulationSFS::legendKey(void) { return mutationTypeLegendKey(); // we use the prefab mutation type legend } bool QtSLiMGraphView_1DPopulationSFS::providesStringForData(void) { return true; } void QtSLiMGraphView_1DPopulationSFS::appendStringForData(QString &string) { // get the selected chromosome range Species *graphSpecies = focalDisplaySpecies(); int binCount = histogramBinCount_; int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); double *plotData = populationSFS(mutationTypeCount); for (auto mutationTypeIter : graphSpecies->mutation_types_) { MutationType *mutationType = mutationTypeIter.second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! string.append(QString("\"m%1\", ").arg(mutationType->mutation_type_id_)); for (int i = 0; i < binCount; ++i) { int histIndex = mutationTypeIndex + i * mutationTypeCount; string.append(QString("%1, ").arg(plotData[histIndex], 0, 'f', 4)); } string.append("\n"); } } ================================================ FILE: QtSLiM/QtSLiMGraphView_1DPopulationSFS.h ================================================ // // QtSLiMGraphView_1DPopulationSFS.h // SLiM // // Created by Ben Haller on 3/27/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_1DPOPULATIONSFS_H #define QTSLIMGRAPHVIEW_1DPOPULATIONSFS_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_1DPopulationSFS : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_1DPopulationSFS(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_1DPopulationSFS() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual QtSLiMLegendSpec legendKey(void) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; private: double *populationSFS(int mutationTypeCount); }; #endif // QTSLIMGRAPHVIEW_1DPOPULATIONSFS_H ================================================ FILE: QtSLiM/QtSLiMGraphView_1DSampleSFS.cpp ================================================ // // QtSLiMGraphView_1DSampleSFS.cpp // SLiM // // Created by Ben Haller on 8/20/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_1DSampleSFS.h" #include #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "subpopulation.h" #include "mutation_type.h" QtSLiMGraphView_1DSampleSFS::QtSLiMGraphView_1DSampleSFS(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 20; // this is also the haplosome sample size allowBinCountRescale_ = false; original_x0_ = 0; original_x1_ = histogramBinCount_; x0_ = original_x0_; x1_ = original_x1_; xAxisMin_ = x0_; xAxisMax_ = x1_; xAxisHistogramStyle_ = true; xAxisTickValuePrecision_ = 0; original_y0_ = -0.05; // on log scale; we want a frequency of 1 to show slightly above baseline original_y1_ = 3.0; // on log scale; maximum power of 10 y0_ = original_y0_; y1_ = original_y1_; yAxisMin_ = y0_; yAxisMax_ = y1_; yAxisMajorTickInterval_ = 1; yAxisMinorTickInterval_ = 1/9.0; yAxisMajorTickModulus_ = 9; // 9 ticks per major; ticks at 1:10 are represented by values 0:9, and 0 and 9 both need to be modulo 0 yAxisLog_ = true; // changes positioning of ticks, grid lines, etc. xAxisLabel_ = "Count in sample"; yAxisLabel_ = "Number of mutations"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; showGridLinesMajorOnly_ = true; allowHorizontalGridChange_ = true; allowVerticalGridChange_ = false; allowFullBoxChange_ = true; selectedSubpopulation1ID_ = 1; selectedMutationTypeIndex_ = -1; } void QtSLiMGraphView_1DSampleSFS::addedToWindow(void) { // Make our pop-up menu buttons QHBoxLayout *button_layout = buttonLayout(); if (button_layout) { subpopulation1Button_ = newButtonInLayout(button_layout); connect(subpopulation1Button_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_1DSampleSFS::subpopulation1PopupChanged); mutationTypeButton_ = newButtonInLayout(button_layout); connect(mutationTypeButton_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_1DSampleSFS::mutationTypePopupChanged); addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); } } QtSLiMGraphView_1DSampleSFS::~QtSLiMGraphView_1DSampleSFS() { // We are responsible for our own destruction QtSLiMGraphView_1DSampleSFS::invalidateCachedData(); } void QtSLiMGraphView_1DSampleSFS::subpopulation1PopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulation1Button_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulation1ID_ != newSubpopID)) { selectedSubpopulation1ID_ = newSubpopID; invalidateCachedData(); update(); } } void QtSLiMGraphView_1DSampleSFS::mutationTypePopupChanged(int /* index */) { int newMutTypeIndex = mutationTypeButton_->currentData().toInt(); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedMutationTypeIndex_ != newMutTypeIndex)) { selectedMutationTypeIndex_ = newMutTypeIndex; invalidateCachedData(); update(); } } void QtSLiMGraphView_1DSampleSFS::controllerRecycled(void) { if (!controller_->invalidSimulation()) update(); // Remake our popups, whether or not the controller is valid addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_1DSampleSFS::graphTitle(void) { return "1D Sample SFS"; } QString QtSLiMGraphView_1DSampleSFS::aboutString(void) { return "The 1D Sample SFS graph shows a Site Frequency Spectrum (SFS) for a sample of haplosomes taken " "(with replacement) from a given subpopulation, for mutations of a given mutation type. The x axis " "here is the occurrence count of a given mutation within the sample, from 1 to the sample size. The " "y axis is the number of mutations in the sample with that specific occurrence count, on a log " "scale. The y axis range and the sample size can be customized from the action menu. The 1D " "Population SFS graph provides an alternative that might also be useful."; } void QtSLiMGraphView_1DSampleSFS::invalidateCachedData(void) { if (sfs1dbuf_) { free(sfs1dbuf_); sfs1dbuf_ = nullptr; } QtSLiMGraphView::invalidateCachedData(); } void QtSLiMGraphView_1DSampleSFS::updateAfterTick(void) { // Rebuild the subpop and muttype menus; this has the side effect of checking and fixing our selections, and that, // in turn, will have the side effect of invaliding our cache and fetching new data if needed addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); invalidateCachedData(); QtSLiMGraphView::updateAfterTick(); } QString QtSLiMGraphView_1DSampleSFS::disableMessage(void) { Species *graphSpecies = focalDisplaySpecies(); if (graphSpecies) { Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); MutationType *muttype = graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_); //qDebug() << "muttype " << muttype << " for id " << selectedMutationTypeIndex_; //if (!subpop1 || !muttype) // return "no\ndata"; if (!subpop1) return "no\nsubpop"; if (!muttype) return "no\nmuttype"; } return ""; } void QtSLiMGraphView_1DSampleSFS::drawGraph(QPainter &painter, QRect interiorRect) { uint64_t *sfs1dbuf = mutation1DSFS(); if (sfs1dbuf) { // plot our histogram bars double *sfsTransformed = (double *)calloc(histogramBinCount_, sizeof(double)); for (int i = 0; i < histogramBinCount_; ++i) { uint64_t value = sfs1dbuf[i]; sfsTransformed[i] = (value == 0) ? -1000 : log10(value); } drawBarplot(painter, interiorRect, sfsTransformed, histogramBinCount_, 0.0, 1.0); free(sfsTransformed); } } bool QtSLiMGraphView_1DSampleSFS::providesStringForData(void) { return true; } void QtSLiMGraphView_1DSampleSFS::appendStringForData(QString &string) { uint64_t *plotData = mutation1DSFS(); for (int i = 0; i < histogramBinCount_; ++i) string.append(QString("%1, ").arg(plotData[i])); string.append("\n"); } void QtSLiMGraphView_1DSampleSFS::changeSampleSize(void) { // Similar to "Change Bin Count...", just different branding QStringList choices = QtSLiMRunLineEditArrayDialog(window(), "Choose a sample size:", QStringList("Sample size:"), QStringList(QString::number(histogramBinCount_))); if (choices.length() == 1) { int newSampleSize = choices[0].toInt(); if ((newSampleSize > 1) && (newSampleSize <= 500)) { histogramBinCount_ = newSampleSize; xAxisMax_ = histogramBinCount_; original_x1_ = histogramBinCount_; // the same as xAxisMax_, for base plots x1_ = original_x1_; invalidateCachedData(); update(); } else qApp->beep(); } } void QtSLiMGraphView_1DSampleSFS::subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent * /* event */) { contextMenu.addAction("Change Sample Size...", this, &QtSLiMGraphView_1DSampleSFS::changeSampleSize); } uint64_t *QtSLiMGraphView_1DSampleSFS::mutation1DSFS(void) { if (!sfs1dbuf_) { Species *graphSpecies = focalDisplaySpecies(); Population &population = graphSpecies->population_; // Find our subpops and mutation type Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); MutationType *muttype = graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_); if (!subpop1 || !muttype) return nullptr; // Get frequencies for a sample taken (with replacement) from subpop1 { std::vector sampleHaplosomes; std::vector &subpopIndividuals = subpop1->parent_individuals_; size_t subpopHaplosomeCount = subpopIndividuals.size() * 2; if (subpopHaplosomeCount) for (int i = 0; i < histogramBinCount_ - 1; ++i) { slim_popsize_t haplosome_index = random() % subpopHaplosomeCount; slim_popsize_t individual_index = haplosome_index >> 1; Haplosome *haplosome = subpopIndividuals[individual_index]->haplosomes_[haplosome_index & 0x01]; sampleHaplosomes.emplace_back(haplosome); } tallyGUIMutationReferences(sampleHaplosomes, selectedMutationTypeIndex_); } // Tally into our bins sfs1dbuf_ = static_cast(calloc(histogramBinCount_, sizeof(uint64_t))); Mutation *mut_block_ptr = gSLiM_Mutation_Block; int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; slim_refcount_t mutationRefCount = mutation->gui_scratch_reference_count_; int mutationBin = mutationRefCount - 1; if (mutationBin >= 0) // mutationBin is -1 if the mutation is not present in the sample at all (sfs1dbuf_[mutationBin])++; } } // Return the final tally; note that we retain ownership of this buffer and only free it when we want to force a recache return sfs1dbuf_; } ================================================ FILE: QtSLiM/QtSLiMGraphView_1DSampleSFS.h ================================================ // // QtSLiMGraphView_1DSampleSFS.h // SLiM // // Created by Ben Haller on 8/20/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_1DSAMPLESFS_H #define QTSLIMGRAPHVIEW_1DSAMPLESFS_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_1DSampleSFS : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_1DSampleSFS(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_1DSampleSFS() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event) override; virtual QString disableMessage(void) override; public slots: virtual void addedToWindow(void) override; virtual void invalidateCachedData(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void subpopulation1PopupChanged(int index); void mutationTypePopupChanged(int index); void changeSampleSize(void); private: // pop-up menu buttons QComboBox *subpopulation1Button_ = nullptr; QComboBox *mutationTypeButton_ = nullptr; // The subpop and mutation type selected; -1 indicates no current selection (which will be fixed as soon as the menu is populated) slim_objectid_t selectedSubpopulation1ID_; int selectedMutationTypeIndex_; uint64_t *sfs1dbuf_ = nullptr; uint64_t *mutation1DSFS(void); }; #endif // QTSLIMGRAPHVIEW_1DSAMPLESFS_H ================================================ FILE: QtSLiM/QtSLiMGraphView_2DPopulationSFS.cpp ================================================ // // QtSLiMGraphView_2DPopulationSFS.cpp // SLiM // // Created by Ben Haller on 8/22/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_2DPopulationSFS.h" #include #include #include #include #include #include "mutation_type.h" QtSLiMGraphView_2DPopulationSFS::QtSLiMGraphView_2DPopulationSFS(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 20; allowBinCountRescale_ = true; heatmapMargins_ = 0; allowHeatmapMarginsChange_ = true; xAxisLabel_ = "Frequency in p1"; yAxisLabel_ = "Frequency in p2"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = false; showHorizontalGridLines_ = false; showVerticalGridLines_ = false; showFullBox_ = true; allowHorizontalGridChange_ = false; allowVerticalGridChange_ = false; allowFullBoxChange_ = false; // Default to plotting p1 against p2, with no default mutation type selectedSubpopulation1ID_ = 1; selectedSubpopulation2ID_ = 2; selectedMutationTypeIndex_ = -1; } void QtSLiMGraphView_2DPopulationSFS::addedToWindow(void) { // Make our pop-up menu buttons QHBoxLayout *button_layout = buttonLayout(); if (button_layout) { subpopulation1Button_ = newButtonInLayout(button_layout); connect(subpopulation1Button_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_2DPopulationSFS::subpopulation1PopupChanged); subpopulation2Button_ = newButtonInLayout(button_layout); connect(subpopulation2Button_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_2DPopulationSFS::subpopulation2PopupChanged); mutationTypeButton_ = newButtonInLayout(button_layout); connect(mutationTypeButton_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_2DPopulationSFS::mutationTypePopupChanged); addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addSubpopulationsToMenu(subpopulation2Button_, selectedSubpopulation2ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); } } QtSLiMGraphView_2DPopulationSFS::~QtSLiMGraphView_2DPopulationSFS() { } void QtSLiMGraphView_2DPopulationSFS::subpopulation1PopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulation1Button_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulation1ID_ != newSubpopID)) { selectedSubpopulation1ID_ = newSubpopID; xAxisLabel_ = QString("Frequency in p%1").arg(selectedSubpopulation1ID_); invalidateCachedData(); update(); } } void QtSLiMGraphView_2DPopulationSFS::subpopulation2PopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulation2Button_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulation2ID_ != newSubpopID)) { selectedSubpopulation2ID_ = newSubpopID; yAxisLabel_ = QString("Frequency in p%1").arg(selectedSubpopulation2ID_); invalidateCachedData(); update(); } } void QtSLiMGraphView_2DPopulationSFS::mutationTypePopupChanged(int /* index */) { int newMutTypeIndex = mutationTypeButton_->currentData().toInt(); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedMutationTypeIndex_ != newMutTypeIndex)) { selectedMutationTypeIndex_ = newMutTypeIndex; invalidateCachedData(); update(); } } void QtSLiMGraphView_2DPopulationSFS::controllerRecycled(void) { if (!controller_->invalidSimulation()) update(); // Remake our popups, whether or not the controller is valid addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addSubpopulationsToMenu(subpopulation2Button_, selectedSubpopulation2ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_2DPopulationSFS::graphTitle(void) { return "2D Population SFS"; } QString QtSLiMGraphView_2DPopulationSFS::aboutString(void) { return "The 2D Population SFS graph shows a Site Frequency Spectrum (SFS) for two entire subpopulations in " "the population, for mutations of a given mutation type. Since mutation occurrence counts across " "whole subpopulations might be very large, the x and y axes here are the frequencies of a given mutation " "in the two subpopulations, from 0.0 to 1.0 on each axis, rather than occurrence counts. The z axis, " "represented with color, is the proportion of mutations (among those present in either of the two " "subpopulations) that fall within a binned range of frequencies in the two subpopulations; a proportion " "of zero is represented by white, and the maximum observed proportion is represented by black (rescaled each " "time the graph redisplays), with heat colors from yellow (low) through red and up to black (high). The " "number of frequency bins can be customized from the action menu. The 2D Sample SFS graph provides an " "alternative that might also be useful."; } void QtSLiMGraphView_2DPopulationSFS::updateAfterTick(void) { // Rebuild the subpop and muttype menus; this has the side effect of checking and fixing our selections, and that, // in turn, will have the side effect of invaliding our cache and fetching new data if needed addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addSubpopulationsToMenu(subpopulation2Button_, selectedSubpopulation2ID_, selectedSubpopulation1ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); invalidateCachedData(); QtSLiMGraphView::updateAfterTick(); } QString QtSLiMGraphView_2DPopulationSFS::disableMessage(void) { Species *graphSpecies = focalDisplaySpecies(); if (graphSpecies) { Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); Subpopulation *subpop2 = graphSpecies->SubpopulationWithID(selectedSubpopulation2ID_); MutationType *muttype = graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_); if (!subpop1 || !subpop2 || !muttype) return "no\ndata"; } return ""; } void QtSLiMGraphView_2DPopulationSFS::drawGraph(QPainter &painter, QRect interiorRect) { double *sfs2dbuf = mutation2DSFS(); if (sfs2dbuf) { drawHeatmap(painter, interiorRect, sfs2dbuf, histogramBinCount_, histogramBinCount_); free(sfs2dbuf); } } bool QtSLiMGraphView_2DPopulationSFS::providesStringForData(void) { return true; } void QtSLiMGraphView_2DPopulationSFS::appendStringForData(QString &string) { double *plotData = mutation2DSFS(); for (int yc = 0; yc < histogramBinCount_; ++yc) { for (int xc = 0; xc < histogramBinCount_; ++xc) string.append(QString("%1, ").arg(plotData[xc + yc * histogramBinCount_], 0, 'f', 4)); string.append("\n"); } free(plotData); } double *QtSLiMGraphView_2DPopulationSFS::mutation2DSFS(void) { Species *graphSpecies = focalDisplaySpecies(); Population &population = graphSpecies->population_; int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); const MutationIndex *registry_iter_end = registry + registry_size; // Find our subpops and mutation type Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); Subpopulation *subpop2 = graphSpecies->SubpopulationWithID(selectedSubpopulation2ID_); MutationType *muttype = graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_); if (!subpop1 || !subpop2 || !muttype) return nullptr; // Get frequencies in subpop1 and subpop2 Mutation *mut_block_ptr = gSLiM_Mutation_Block; std::vector refcounts1, refcounts2; size_t subpop1_total_haplosome_count, subpop2_total_haplosome_count; { subpop1_total_haplosome_count = tallyGUIMutationReferences(selectedSubpopulation1ID_, selectedMutationTypeIndex_); for (const MutationIndex *registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) { const Mutation *mutation = mut_block_ptr + *registry_iter; if (mutation->mutation_type_ptr_->mutation_type_index_ == selectedMutationTypeIndex_) refcounts1.emplace_back(mutation->gui_scratch_reference_count_); } if (subpop1_total_haplosome_count == 0) subpop1_total_haplosome_count = 1; // counts will all be zero; prevent NAN frequency, make it zero instead } { subpop2_total_haplosome_count = tallyGUIMutationReferences(selectedSubpopulation2ID_, selectedMutationTypeIndex_); for (const MutationIndex *registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) { const Mutation *mutation = mut_block_ptr + *registry_iter; if (mutation->mutation_type_ptr_->mutation_type_index_ == selectedMutationTypeIndex_) refcounts2.emplace_back(mutation->gui_scratch_reference_count_); } if (subpop2_total_haplosome_count == 0) subpop2_total_haplosome_count = 1; // counts will all be zero; prevent NAN frequency, make it zero instead } // Tally up the binned 2D SFS from the 1D data double *sfs2dbuf = (double *)calloc(histogramBinCount_ * histogramBinCount_, sizeof(double)); size_t refcounts_size = refcounts1.size(); for (size_t refcount_index = 0; refcount_index < refcounts_size; ++refcount_index) { slim_refcount_t count1 = refcounts1[refcount_index]; slim_refcount_t count2 = refcounts2[refcount_index]; // exclude mutations that are not present in either subpopulation if ((count1 > 0) || (count2 > 0)) { double freq1 = count1 / (double)subpop1_total_haplosome_count; double freq2 = count2 / (double)subpop2_total_haplosome_count; int bin1 = static_cast(round(freq1 * (histogramBinCount_ - 1))); int bin2 = static_cast(round(freq2 * (histogramBinCount_ - 1))); sfs2dbuf[bin1 + bin2 * histogramBinCount_]++; } } // Normalize the bin counts to [0,1]; 0 is reserved for actual zero counts, the rest are on a log scale double maxCount = 0; for (int i = 0; i < histogramBinCount_ * histogramBinCount_; ++i) maxCount = std::max(maxCount, sfs2dbuf[i]); if (maxCount > 0.0) { double logMaxCount = std::log10(maxCount + 1); for (int i = 0; i < histogramBinCount_ * histogramBinCount_; ++i) { double value = sfs2dbuf[i]; if (value != 0.0) sfs2dbuf[i] = std::log10(value + 1) / logMaxCount; } } // return the final tally; note the caller takes responsibility for freeing this buffer! return sfs2dbuf; } ================================================ FILE: QtSLiM/QtSLiMGraphView_2DPopulationSFS.h ================================================ // // QtSLiMGraphView_2DPopulationSFS.h // SLiM // // Created by Ben Haller on 8/18/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_2DPOPULATIONSFS_H #define QTSLIMGRAPHVIEW_2DPOPULATIONSFS_H #include "QtSLiMGraphView.h" class MutationType; class QtSLiMGraphView_2DPopulationSFS : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_2DPopulationSFS(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_2DPopulationSFS() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual QString disableMessage(void) override; public slots: virtual void addedToWindow(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void subpopulation1PopupChanged(int index); void subpopulation2PopupChanged(int index); void mutationTypePopupChanged(int index); private: // pop-up menu buttons QComboBox *subpopulation1Button_ = nullptr; QComboBox *subpopulation2Button_ = nullptr; QComboBox *mutationTypeButton_ = nullptr; // The subpop and mutation type selected; -1 indicates no current selection (which will be fixed as soon as the menu is populated) slim_objectid_t selectedSubpopulation1ID_; slim_objectid_t selectedSubpopulation2ID_; int selectedMutationTypeIndex_; double *mutation2DSFS(void); }; #endif // QTSLIMGRAPHVIEW_2DPOPULATIONSFS_H ================================================ FILE: QtSLiM/QtSLiMGraphView_2DSampleSFS.cpp ================================================ // // QtSLiMGraphView_2DSampleSFS.cpp // SLiM // // Created by Ben Haller on 8/18/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_2DSampleSFS.h" #include #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "subpopulation.h" #include "mutation_type.h" QtSLiMGraphView_2DSampleSFS::QtSLiMGraphView_2DSampleSFS(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 21; // this is the haplosome sample size + 1 allowBinCountRescale_ = false; original_x0_ = -1; // zero is included, unlike the 1D plot original_x1_ = histogramBinCount_ - 1; x0_ = original_x0_; x1_ = original_x1_; xAxisMin_ = x0_; xAxisMax_ = x1_; xAxisHistogramStyle_ = true; xAxisTickValuePrecision_ = 0; original_y0_ = -1; // zero is included, unlike the 1D plot original_y1_ = histogramBinCount_ - 1; y0_ = original_y0_; y1_ = original_y1_; yAxisMin_ = y0_; yAxisMax_ = y1_; yAxisHistogramStyle_ = true; yAxisTickValuePrecision_ = 0; zAxisMax_ = 1000; // 10^3 heatmapMargins_ = 0; allowHeatmapMarginsChange_ = true; xAxisLabel_ = "Count in p1 sample"; yAxisLabel_ = "Count in p2 sample"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = false; showHorizontalGridLines_ = false; showVerticalGridLines_ = false; showFullBox_ = true; allowHorizontalGridChange_ = false; allowVerticalGridChange_ = false; allowFullBoxChange_ = false; // Default to plotting p1 against p2, with no default mutation type selectedSubpopulation1ID_ = 1; selectedSubpopulation2ID_ = 2; selectedMutationTypeIndex_ = -1; } void QtSLiMGraphView_2DSampleSFS::addedToWindow(void) { // Make our pop-up menu buttons QHBoxLayout *button_layout = buttonLayout(); if (button_layout) { subpopulation1Button_ = newButtonInLayout(button_layout); connect(subpopulation1Button_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_2DSampleSFS::subpopulation1PopupChanged); subpopulation2Button_ = newButtonInLayout(button_layout); connect(subpopulation2Button_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_2DSampleSFS::subpopulation2PopupChanged); mutationTypeButton_ = newButtonInLayout(button_layout); connect(mutationTypeButton_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_2DSampleSFS::mutationTypePopupChanged); addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addSubpopulationsToMenu(subpopulation2Button_, selectedSubpopulation2ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); } } QtSLiMGraphView_2DSampleSFS::~QtSLiMGraphView_2DSampleSFS() { // We are responsible for our own destruction QtSLiMGraphView_2DSampleSFS::invalidateCachedData(); } void QtSLiMGraphView_2DSampleSFS::subpopulation1PopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulation1Button_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulation1ID_ != newSubpopID)) { selectedSubpopulation1ID_ = newSubpopID; xAxisLabel_ = QString("Count in p%1 sample").arg(selectedSubpopulation1ID_); invalidateCachedData(); update(); } } void QtSLiMGraphView_2DSampleSFS::subpopulation2PopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulation2Button_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulation2ID_ != newSubpopID)) { selectedSubpopulation2ID_ = newSubpopID; yAxisLabel_ = QString("Count in p%1 sample").arg(selectedSubpopulation2ID_); invalidateCachedData(); update(); } } void QtSLiMGraphView_2DSampleSFS::mutationTypePopupChanged(int /* index */) { int newMutTypeIndex = mutationTypeButton_->currentData().toInt(); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedMutationTypeIndex_ != newMutTypeIndex)) { selectedMutationTypeIndex_ = newMutTypeIndex; invalidateCachedData(); update(); } } void QtSLiMGraphView_2DSampleSFS::controllerRecycled(void) { if (!controller_->invalidSimulation()) update(); // Remake our popups, whether or not the controller is valid addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addSubpopulationsToMenu(subpopulation2Button_, selectedSubpopulation2ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_2DSampleSFS::graphTitle(void) { return "2D Sample SFS"; } QString QtSLiMGraphView_2DSampleSFS::aboutString(void) { return "The 2D Sample SFS graph shows a Site Frequency Spectrum (SFS) for a sample of haplosomes taken " "(with replacement) from two given subpopulations, for mutations of a given mutation type. The x and y axes " "here are the occurrence counts of a given mutation within the two samples, from 0 to the sample size. The " "z axis, represented with color, is the number of mutations in the samples with those specific occurrence " "counts; a count of zero is represented by white, and the chosen maximum count is represented by black, " "with heat colors from yellow (low) through red and up to black (high). The lower left bin is always blue, " "representing the fact that mutations not present in either sample are not included in the graph, and thus " "there is no count to depict for that bin. The z axis maximum and the sample size can be customized from " "the action menu. The 2D Population SFS graph provides an alternative that might also be useful."; } void QtSLiMGraphView_2DSampleSFS::invalidateCachedData(void) { if (sfs2dbuf_) { free(sfs2dbuf_); sfs2dbuf_ = nullptr; } QtSLiMGraphView::invalidateCachedData(); } void QtSLiMGraphView_2DSampleSFS::updateAfterTick(void) { // Rebuild the subpop and muttype menus; this has the side effect of checking and fixing our selections, and that, // in turn, will have the side effect of invaliding our cache and fetching new data if needed addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); addSubpopulationsToMenu(subpopulation2Button_, selectedSubpopulation2ID_, selectedSubpopulation1ID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); invalidateCachedData(); QtSLiMGraphView::updateAfterTick(); } QString QtSLiMGraphView_2DSampleSFS::disableMessage(void) { Species *graphSpecies = focalDisplaySpecies(); if (graphSpecies) { Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); Subpopulation *subpop2 = graphSpecies->SubpopulationWithID(selectedSubpopulation2ID_); MutationType *muttype = graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_); if (!subpop1 || !subpop2 || !muttype) return "no\ndata"; } return ""; } void QtSLiMGraphView_2DSampleSFS::willDraw(QPainter &painter, QRect /* interiorRect */) { QRect bounds = rect(); if (!cachingNow_) { painter.setFont(QtSLiMGraphView::fontForTickLabels()); painter.setBrush(Qt::black); QString rangeString = QString("z ∈ [0, %1]").arg((long)zAxisMax_); QPoint drawPoint(bounds.x() + 10, bounds.y() + 10); drawPoint = painter.transform().map(drawPoint); painter.setWorldMatrixEnabled(false); painter.drawText(drawPoint, rangeString); painter.setWorldMatrixEnabled(true); } } void QtSLiMGraphView_2DSampleSFS::drawGraph(QPainter &painter, QRect interiorRect) { uint64_t *sfs2dbuf = mutation2DSFS(); if (sfs2dbuf) { // plot our histogram bars double *sfsTransformed = (double *)calloc(histogramBinCount_ * histogramBinCount_, sizeof(double)); double logZMax = log10(zAxisMax_); for (int i = 1; i < histogramBinCount_ * histogramBinCount_; ++i) { uint64_t value = sfs2dbuf[i]; sfsTransformed[i] = (value == 0) ? -1000 : (log10(value) / logZMax); } sfsTransformed[0] = -10000; // a special value that assigns a "no value" color to this square drawHeatmap(painter, interiorRect, sfsTransformed, histogramBinCount_, histogramBinCount_); free(sfsTransformed); } } bool QtSLiMGraphView_2DSampleSFS::providesStringForData(void) { return true; } void QtSLiMGraphView_2DSampleSFS::appendStringForData(QString &string) { uint64_t *plotData = mutation2DSFS(); for (int yc = 0; yc < histogramBinCount_; ++yc) { for (int xc = 0; xc < histogramBinCount_; ++xc) string.append(QString("%1, ").arg(plotData[xc + yc * histogramBinCount_])); string.append("\n"); } } void QtSLiMGraphView_2DSampleSFS::changeZAxisScale(void) { QStringList choices = QtSLiMRunLineEditArrayDialog(window(), "Choose a z-axis maximum:", QStringList{"Maximum value:"}, QStringList{QString::number(zAxisMax_)}); if (choices.length() == 1) { int newZMax = choices[0].toInt(); if ((newZMax >= 10) && (newZMax <= 1000000)) { zAxisMax_ = (double)newZMax; invalidateCachedData(); update(); } else qApp->beep(); } } void QtSLiMGraphView_2DSampleSFS::changeSampleSize(void) { // Similar to "Change Bin Count...", just different branding QStringList choices = QtSLiMRunLineEditArrayDialog(window(), "Choose a sample size:", QStringList("Sample size:"), QStringList(QString::number(histogramBinCount_ - 1))); if (choices.length() == 1) { int newSampleSize = choices[0].toInt(); if ((newSampleSize > 1) && (newSampleSize <= 500)) { histogramBinCount_ = newSampleSize + 1; xAxisMax_ = histogramBinCount_ - 1; yAxisMax_ = histogramBinCount_ - 1; original_x1_ = xAxisMax_; // the same as xAxisMax_, for base plots original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots x1_ = original_x1_; y1_ = original_y1_; invalidateCachedData(); update(); } else qApp->beep(); } } void QtSLiMGraphView_2DSampleSFS::subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent * /* event */) { contextMenu.addAction("Change Z Axis Scale...", this, &QtSLiMGraphView_2DSampleSFS::changeZAxisScale); contextMenu.addAction("Change Sample Size...", this, &QtSLiMGraphView_2DSampleSFS::changeSampleSize); } uint64_t *QtSLiMGraphView_2DSampleSFS::mutation2DSFS(void) { if (!sfs2dbuf_) { Species *graphSpecies = focalDisplaySpecies(); Population &population = graphSpecies->population_; int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); const MutationIndex *registry_iter_end = registry + registry_size; Mutation *mut_block_ptr = gSLiM_Mutation_Block; // Find our subpops and mutation type Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); Subpopulation *subpop2 = graphSpecies->SubpopulationWithID(selectedSubpopulation2ID_); MutationType *muttype = graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_); if (!subpop1 || !subpop2 || !muttype) return nullptr; // Get frequencies for a sample taken from subpop1 { std::vector sample1Haplosomes; std::vector &subpopIndividuals = subpop1->parent_individuals_; size_t subpopHaplosomeCount = subpopIndividuals.size() * 2; if (subpopHaplosomeCount) for (int i = 0; i < histogramBinCount_ - 1; ++i) { slim_popsize_t haplosome_index = random() % subpopHaplosomeCount; slim_popsize_t individual_index = haplosome_index >> 1; Haplosome *haplosome = subpopIndividuals[individual_index]->haplosomes_[haplosome_index & 0x01]; sample1Haplosomes.emplace_back(haplosome); } tallyGUIMutationReferences(sample1Haplosomes, selectedMutationTypeIndex_); } std::vector refcounts1; for (const MutationIndex *registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) { const Mutation *mutation = mut_block_ptr + *registry_iter; if (mutation->mutation_type_ptr_->mutation_type_index_ == selectedMutationTypeIndex_) refcounts1.emplace_back(mutation->gui_scratch_reference_count_); } // Get frequencies for a sample taken from subpop2 { std::vector sample2Haplosomes; std::vector &subpopIndividuals = subpop2->parent_individuals_; size_t subpopHaplosomeCount = subpopIndividuals.size() * 2; if (subpopHaplosomeCount) for (int i = 0; i < histogramBinCount_ - 1; ++i) { slim_popsize_t haplosome_index = random() % subpopHaplosomeCount; slim_popsize_t individual_index = haplosome_index >> 1; Haplosome *haplosome = subpopIndividuals[individual_index]->haplosomes_[haplosome_index & 0x01]; sample2Haplosomes.emplace_back(haplosome); } tallyGUIMutationReferences(sample2Haplosomes, selectedMutationTypeIndex_); } std::vector refcounts2; for (const MutationIndex *registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) { const Mutation *mutation = mut_block_ptr + *registry_iter; if (mutation->mutation_type_ptr_->mutation_type_index_ == selectedMutationTypeIndex_) refcounts2.emplace_back(mutation->gui_scratch_reference_count_); } // Tally up the binned 2D SFS from the 1D data sfs2dbuf_ = (uint64_t *)calloc(histogramBinCount_ * histogramBinCount_, sizeof(uint64_t)); size_t refcounts_size = refcounts1.size(); for (size_t refcount_index = 0; refcount_index < refcounts_size; ++refcount_index) { slim_refcount_t mutationRefCount1 = refcounts1[refcount_index]; slim_refcount_t mutationRefCount2 = refcounts2[refcount_index]; if ((mutationRefCount1 > 0) || (mutationRefCount2 > 0)) // exclude mutations not present in either sample sfs2dbuf_[mutationRefCount1 + mutationRefCount2 * histogramBinCount_]++; } } // Return the final tally; note that we retain ownership of this buffer and only free it when we want to force a recache return sfs2dbuf_; } ================================================ FILE: QtSLiM/QtSLiMGraphView_2DSampleSFS.h ================================================ // // QtSLiMGraphView_2DSampleSFS.h // SLiM // // Created by Ben Haller on 8/18/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_2DSAMPLESFS_H #define QTSLIMGRAPHVIEW_2DSAMPLESFS_H #include "QtSLiMGraphView.h" class MutationType; class QtSLiMGraphView_2DSampleSFS : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_2DSampleSFS(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_2DSampleSFS() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void willDraw(QPainter &painter, QRect interiorRect) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event) override; virtual QString disableMessage(void) override; public slots: virtual void addedToWindow(void) override; virtual void invalidateCachedData(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void subpopulation1PopupChanged(int index); void subpopulation2PopupChanged(int index); void mutationTypePopupChanged(int index); void changeZAxisScale(void); void changeSampleSize(void); private: // pop-up menu buttons QComboBox *subpopulation1Button_ = nullptr; QComboBox *subpopulation2Button_ = nullptr; QComboBox *mutationTypeButton_ = nullptr; // The subpop and mutation type selected; -1 indicates no current selection (which will be fixed as soon as the menu is populated) slim_objectid_t selectedSubpopulation1ID_; slim_objectid_t selectedSubpopulation2ID_; int selectedMutationTypeIndex_; double zAxisMax_ = 0.0; uint64_t *sfs2dbuf_ = nullptr; uint64_t *mutation2DSFS(void); }; #endif // QTSLIMGRAPHVIEW_2DSAMPLESFS_H ================================================ FILE: QtSLiM/QtSLiMGraphView_AgeDistribution.cpp ================================================ // // QtSLiMGraphView_AgeDistribution.cpp // SLiM // // Created by Ben Haller on 8/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_AgeDistribution.h" #include #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "subpopulation.h" QtSLiMGraphView_AgeDistribution::QtSLiMGraphView_AgeDistribution(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 10; // max age (no age 0 since we display after tick increment); this rescales automatically allowBinCountRescale_ = false; original_x0_ = 0; original_x1_ = histogramBinCount_; x0_ = original_x0_; x1_ = original_x1_; xAxisMin_ = x0_; xAxisMax_ = x1_; xAxisHistogramStyle_ = true; xAxisTickValuePrecision_ = 0; tweakXAxisTickLabelAlignment_ = true; xAxisLabel_ = "Age"; yAxisLabel_ = "Frequency"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = false; showHorizontalGridLines_ = true; allowHorizontalGridChange_ = true; allowVerticalGridChange_ = false; allowFullBoxChange_ = true; selectedSubpopulation1ID_ = 1; } void QtSLiMGraphView_AgeDistribution::addedToWindow(void) { // Make our pop-up menu buttons QHBoxLayout *button_layout = buttonLayout(); if (button_layout) { subpopulation1Button_ = newButtonInLayout(button_layout); connect(subpopulation1Button_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_AgeDistribution::subpopulation1PopupChanged); addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); } } QtSLiMGraphView_AgeDistribution::~QtSLiMGraphView_AgeDistribution() { } void QtSLiMGraphView_AgeDistribution::subpopulation1PopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulation1Button_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulation1ID_ != newSubpopID)) { selectedSubpopulation1ID_ = newSubpopID; // Reset our autoscaling x axis histogramBinCount_ = 10; xAxisMax_ = histogramBinCount_; original_x1_ = xAxisMax_; // the same as xAxisMax_, for base plots x1_ = original_x1_; invalidateCachedData(); update(); } } void QtSLiMGraphView_AgeDistribution::controllerRecycled(void) { if (!controller_->invalidSimulation()) update(); // Remake our popups, whether or not the controller is valid addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); // Reset our autoscaling x axis histogramBinCount_ = 10; xAxisMax_ = histogramBinCount_; original_x1_ = xAxisMax_; // the same as xAxisMax_, for base plots x1_ = original_x1_; // Reset our autoscaling y axis yAxisMax_ = 1.0; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 0.5; yAxisMinorTickInterval_ = 0.25; QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_AgeDistribution::graphTitle(void) { return "Age Distribution"; } QString QtSLiMGraphView_AgeDistribution::aboutString(void) { return "The Age Distribution graph shows the distribution of age values within a chosen subpopulation. The " "x axis is individual age (in cycles, in the SLiM sense of the term); the y axis is the frequency " "of a given age in the population, normalized to a total of 1.0. This graph is only meaningful " "for nonWF models; WF models have non-overlapping generations without age structure. Note that " "display occurs after the cycle counter increments, so new offspring will have age 1."; } void QtSLiMGraphView_AgeDistribution::updateAfterTick(void) { // Rebuild the subpop and muttype menus; this has the side effect of checking and fixing our selections, and that, // in turn, will have the side effect of invaliding our cache and fetching new data if needed addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); invalidateCachedData(); QtSLiMGraphView::updateAfterTick(); } QString QtSLiMGraphView_AgeDistribution::disableMessage(void) { if (controller_ && !controller_->invalidSimulation()) { if (controller_->community->ModelType() == SLiMModelType::kModelTypeWF) return "requires a\nnonWF model"; Species *graphSpecies = focalDisplaySpecies(); if (graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_) == nullptr) return "no\ndata"; } return ""; } void QtSLiMGraphView_AgeDistribution::drawGraph(QPainter &painter, QRect interiorRect) { int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); bool tallySexesSeparately = graphSpecies->sex_enabled_; double *ageDist = ageDistribution(&binCount, tallySexesSeparately); int totalBinCount = tallySexesSeparately ? (binCount * 2) : binCount; if (ageDist) { // rescale the x axis if needed if (binCount != histogramBinCount_) { histogramBinCount_ = binCount; xAxisMax_ = histogramBinCount_; original_x1_ = xAxisMax_; // the same as xAxisMax_, for base plots x1_ = original_x1_; invalidateCachedData(); } // rescale the y axis if needed double maxFreq = 0.000000001; // guarantee a non-zero axis range for (int binIndex = 0; binIndex < totalBinCount; ++binIndex) maxFreq = std::max(maxFreq, ageDist[binIndex]); double ceilingFreq = std::ceil(maxFreq * 5.0) / 5.0; // 0.2 / 0.4 / 0.6 / 0.8 / 1.0 if ((ceilingFreq > yAxisMax_) || ((ceilingFreq < yAxisMax_) && (maxFreq + 0.05 < ceilingFreq))) // require a margin of error to jump down { yAxisMax_ = ceilingFreq; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = ceilingFreq / 2.0; yAxisMinorTickInterval_ = ceilingFreq / 4.0; } // plot our histogram bars if (tallySexesSeparately) drawGroupedBarplot(painter, interiorRect, ageDist, 2, histogramBinCount_, 0.0, 1.0); else drawBarplot(painter, interiorRect, ageDist, histogramBinCount_, 0.0, 1.0); free(ageDist); } } QtSLiMLegendSpec QtSLiMGraphView_AgeDistribution::legendKey(void) { Species *graphSpecies = focalDisplaySpecies(); bool tallySexesSeparately = graphSpecies->sex_enabled_; if (tallySexesSeparately) { QtSLiMLegendSpec legend_key; legend_key.emplace_back("M", controller_->blackContrastingColorForIndex(0)); legend_key.emplace_back("F", controller_->blackContrastingColorForIndex(1)); return legend_key; } else { return QtSLiMLegendSpec(); } } bool QtSLiMGraphView_AgeDistribution::providesStringForData(void) { return true; } void QtSLiMGraphView_AgeDistribution::appendStringForData(QString &string) { int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); bool tallySexesSeparately = graphSpecies->sex_enabled_; double *ageDist = ageDistribution(&binCount, tallySexesSeparately); if (ageDist) { if (tallySexesSeparately) { string.append("M : "); for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(ageDist[i * 2], 0, 'f', 4)); string.append("\n\nF : "); for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(ageDist[i * 2 + 1], 0, 'f', 4)); } else { for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(ageDist[i], 0, 'f', 4)); } free(ageDist); } string.append("\n"); } double *QtSLiMGraphView_AgeDistribution::ageDistribution(int *binCount, bool tallySexesSeparately) { // Find our subpop Species *graphSpecies = focalDisplaySpecies(); Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); if (!subpop1) return nullptr; // Find the maximum age and choose the new bin count slim_age_t maxAge = 1; for (const Individual *individual : subpop1->parent_individuals_) maxAge = std::max(maxAge, individual->age_); // compare to the logic in QtSLiMGraphView_LifetimeReproduction::reproductionDistribution(); // it is different here because we subtract 1 from every age in the tallying code below, // there is no bin for age==0, and age==1 goes into bin #0; confusing! if (maxAge > *binCount) *binCount = (slim_age_t)(std::ceil(maxAge / 10.0) * 10.0); int newBinCount = *binCount; // Tally into our bins int totalBinCount = (tallySexesSeparately ? newBinCount * 2 : newBinCount); double *ageTallies = static_cast(calloc(totalBinCount, sizeof(double))); for (const Individual *individual : subpop1->parent_individuals_) { slim_age_t age = individual->age_ - 1; // age 1 is bin 0, age binCount is bin binCount-1 if (age < 0) age = 0; if (age >= newBinCount) age = newBinCount - 1; if (tallySexesSeparately) { if (individual->sex_ == IndividualSex::kFemale) ageTallies[age * 2 + 1]++; else ageTallies[age * 2]++; } else { ageTallies[age]++; } } // Normalize to 1 if (tallySexesSeparately) { // males double totalTallies = 0.0; for (int i = 0; i < newBinCount; ++i) totalTallies += ageTallies[i * 2]; if (totalTallies > 0.0) for (int i = 0; i < newBinCount; ++i) ageTallies[i * 2] /= totalTallies; // females totalTallies = 0.0; for (int i = 0; i < newBinCount; ++i) totalTallies += ageTallies[i * 2 + 1]; if (totalTallies > 0.0) for (int i = 0; i < newBinCount; ++i) ageTallies[i * 2 + 1] /= totalTallies; } else { double totalTallies = 0.0; for (int i = 0; i < newBinCount; ++i) totalTallies += ageTallies[i]; if (totalTallies > 0.0) for (int i = 0; i < newBinCount; ++i) ageTallies[i] /= totalTallies; } // Return the final tally; note that the caller takes ownership of the buffer return ageTallies; } ================================================ FILE: QtSLiM/QtSLiMGraphView_AgeDistribution.h ================================================ // // QtSLiMGraphView_AgeDistribution.h // SLiM // // Created by Ben Haller on 8/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_AGEDISTRIBUTION_H #define QTSLIMGRAPHVIEW_AGEDISTRIBUTION_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_AgeDistribution : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_AgeDistribution(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_AgeDistribution() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual QtSLiMLegendSpec legendKey(void) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual QString disableMessage(void) override; public slots: virtual void addedToWindow(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void subpopulation1PopupChanged(int index); private: // pop-up menu buttons QComboBox *subpopulation1Button_ = nullptr; // The subpop selected; -1 indicates no current selection (which will be fixed as soon as the menu is populated) slim_objectid_t selectedSubpopulation1ID_; double *ageDistribution(int *binCount, bool tallySexesSeparately); }; #endif // QTSLIMGRAPHVIEW_AGEDISTRIBUTION_H ================================================ FILE: QtSLiM/QtSLiMGraphView_CustomPlot.cpp ================================================ // // QtSLiMGraphView_CustomPlot.cpp // SLiM // // Created by Ben Haller on 1/19/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_CustomPlot.h" #include #include #include #include #include #include "QtSLiM_Plot.h" #include #include #include QtSLiMGraphView_CustomPlot::QtSLiMGraphView_CustomPlot(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { title_ = "Custom Plot"; // will be replaced xAxisLabel_ = "x"; yAxisLabel_ = "y"; // user-rescaling of the axes should work fine, but will switch to the "base plot" way of handling // the data range and the axis ticks, so some functionality will be disabled, such as auto-resizing // the data range and axes to fit newly added data; so it goes allowXAxisUserRescale_ = true; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; tweakXAxisTickLabelAlignment_ = true; setFocalDisplaySpecies(nullptr); QtSLiMGraphView_CustomPlot::updateAfterTick(); } void QtSLiMGraphView_CustomPlot::freeData(void) { // discard all plot data for (double *x1buffer : x1data_) free(x1buffer); for (double *y1buffer : y1data_) free(y1buffer); for (double *x2buffer : x2data_) free(x2buffer); for (double *y2buffer : y2data_) free(y2buffer); for (std::vector *labelsbuffer : labels_) delete labelsbuffer; for (std::vector *symbolbuffer : symbol_) delete symbolbuffer; for (std::vector *colorbuffer : color_) delete colorbuffer; for (std::vector *borderbuffer : border_) delete borderbuffer; for (std::vector *alphabuffer : alpha_) delete alphabuffer; for (std::vector *lwdbuffer : line_width_) delete lwdbuffer; for (std::vector *sizebuffer : size_) delete sizebuffer; for (std::vector *anglebuffer : angle_) delete anglebuffer; plot_type_.clear(); x1data_.clear(); y1data_.clear(); x2data_.clear(); y2data_.clear(); data_count_.clear(); labels_.clear(); symbol_.clear(); color_.clear(); border_.clear(); alpha_.clear(); line_width_.clear(); size_.clear(); angle_.clear(); xadj_.clear(); yadj_.clear(); image_.clear(); // reset the legend state legend_added_ = false; legend_position_ = QtSLiM_LegendPosition::kUnconfigured; legend_inset = -1; legend_labelSize = -1; legend_lineHeight = -1; legend_graphicsWidth = -1; legend_exteriorMargin = -1; legend_interiorMargin = -1; legend_entries_.clear(); } QtSLiMGraphView_CustomPlot::~QtSLiMGraphView_CustomPlot() { // We are responsible for our own destruction // We own our corresponding Eidos object of class Plot, and free it here. It is not under retain/release, // and this should occur only at a "long-term boundary" since it is triggered by the plot window closing // in SLiMgui. Note that sometimes eidos_plot_object_ is nullptr, such as when the custom plot was created // in SLiMgui's UI from LogFile data; this is fine, it just means the window is not controllable from script. if (eidos_plot_object_) { delete eidos_plot_object_; eidos_plot_object_ = nullptr; } freeData(); } void QtSLiMGraphView_CustomPlot::setTitle(QString title) { title_ = title; QWidget *graphWindow = window(); if (graphWindow) graphWindow->setWindowTitle(title); } void QtSLiMGraphView_CustomPlot::setXLabel(QString x_label) { xAxisLabel_ = x_label; update(); } void QtSLiMGraphView_CustomPlot::setYLabel(QString y_label) { yAxisLabel_ = y_label; update(); } void QtSLiMGraphView_CustomPlot::setShowHorizontalGrid(bool showHorizontalGrid) { showHorizontalGridLines_ = showHorizontalGrid; update(); } void QtSLiMGraphView_CustomPlot::setShowVerticalGrid(bool showVerticalGrid) { showVerticalGridLines_ = showVerticalGrid; update(); } void QtSLiMGraphView_CustomPlot::setShowFullBox(bool showFullBox) { showFullBox_ = showFullBox; update(); } void QtSLiMGraphView_CustomPlot::setAxisLabelSize(double axisLabelSize) { axisLabelSize_ = axisLabelSize; update(); } void QtSLiMGraphView_CustomPlot::setTickLabelSize(double tickLabelSize) { tickLabelSize_ = tickLabelSize; update(); } void QtSLiMGraphView_CustomPlot::setLegendPosition(QtSLiM_LegendPosition position) { legend_position_ = position; update(); } void QtSLiMGraphView_CustomPlot::setDataRanges(double *x_range, double *y_range) { // this is called by QtSLiMWindow::eidos_createPlot(), to set up for the user's specified ranges // nullptr for an axis indicates that we want that axis to be controlled by the range of the data // otherwise, we set up the min and max values for the axis from the given (two-valued) range buffer if (x_range) { original_x0_ = x_range[0]; original_x1_ = x_range[1]; x0_ = original_x0_; x1_ = original_x1_; configureAxisForRange(x0_, x1_, xAxisMin_, xAxisMax_, xAxisMajorTickInterval_, xAxisMinorTickInterval_, xAxisMajorTickModulus_, xAxisTickValuePrecision_); xAxisIsUserRescaled_ = true; xAxisIsUIRescaled_ = false; } else { // allow any user configuration in the UI to persist through a recycle // if a range was set by createPlot(), though, we want to reset that if (!xAxisIsUIRescaled_) xAxisIsUserRescaled_ = false; } if (y_range) { original_y0_ = y_range[0]; original_y1_ = y_range[1]; y0_ = original_y0_; y1_ = original_y1_; configureAxisForRange(y0_, y1_, yAxisMin_, yAxisMax_, yAxisMajorTickInterval_, yAxisMinorTickInterval_, yAxisMajorTickModulus_, yAxisTickValuePrecision_); yAxisIsUserRescaled_ = true; yAxisIsUIRescaled_ = false; } else { // allow any user configuration in the UI to persist through a recycle // if a range was set by createPlot(), though, we want to reset that if (!yAxisIsUIRescaled_) yAxisIsUserRescaled_ = false; } } void QtSLiMGraphView_CustomPlot::setAxisConfiguration(int side, std::vector *at, int labels_type, std::vector *labels) { // This method is called by the Eidos method Plot::axis() to customize axis display. // Note that Plot::ExecuteMethod_axis() does a bunch of bounds-checking and such for us. if (side == 1) { // x-axis configuration if (xAxisAt_) { delete xAxisAt_; xAxisAt_ = nullptr; } if (xAxisLabels_) { delete xAxisLabels_; xAxisLabels_ = nullptr; } if (at) { xAxisAt_ = at; allowXAxisUserRescale_ = false; } else { allowXAxisUserRescale_ = true; } xAxisLabelsType_ = labels_type; xAxisLabels_ = labels; } else if (side == 2) { // y-axis configuration if (yAxisAt_) { delete yAxisAt_; yAxisAt_ = nullptr; } if (yAxisLabels_) { delete yAxisLabels_; yAxisLabels_ = nullptr; } if (at) { yAxisAt_ = at; allowYAxisUserRescale_ = false; } else { allowYAxisUserRescale_ = true; } yAxisLabelsType_ = labels_type; yAxisLabels_ = labels; } } void QtSLiMGraphView_CustomPlot::dataRange(std::vector &data_vector, double *p_min, double *p_max) { // This method accumulates the min/max for the range of our data, in either x or y // It excludes NAN and INF values from the range; such values are not plotted double min = std::numeric_limits::infinity(); double max = -std::numeric_limits::infinity(); for (int data_index = 0; data_index < (int)data_vector.size(); ++data_index) { // lines from abline() are not included in the data range; they are decoration if ((plot_type_[data_index] == QtSLiM_CustomPlotType::kABLines) || (plot_type_[data_index] == QtSLiM_CustomPlotType::kHLines) || (plot_type_[data_index] == QtSLiM_CustomPlotType::kVLines)) continue; double *point_data = data_vector[data_index]; if (point_data) { int point_count = data_count_[data_index]; for (int point_index = 0; point_index < point_count; ++point_index) { double point_value = point_data[point_index]; if (std::isfinite(point_value)) { min = std::min(min, point_value); max = std::max(max, point_value); } } } } *p_min = min; *p_max = max; } void QtSLiMGraphView_CustomPlot::rescaleAxesForDataRange(void) { // this is called when new data is added to a plot, to rescale the axes as needed // set up axes based on the data range; we try to apply a little intelligence, but if the user // wants really intelligent axis ranges, they can set them up themselves... double x1min, x1max, y1min, y1max, x2min, x2max, y2min, y2max; dataRange(x1data_, &x1min, &x1max); dataRange(y1data_, &y1min, &y1max); dataRange(x2data_, &x2min, &x2max); dataRange(y2data_, &y2min, &y2max); double xmin = std::min(x1min, x2min); double xmax = std::max(x1max, x2max); double ymin = std::min(y1min, y2min); double ymax = std::max(y1max, y2max); //std::cout << "-----" << std::endl; //std::cout << " xmin == " << xmin << ", x1min == " << x1min << ", x2min << " << x2min << std::endl; //std::cout << " xmax == " << xmax << ", x1max == " << x1max << ", x2max << " << x2max << std::endl; //std::cout << " ymin == " << ymin << ", y1min == " << y1min << ", y2min << " << y2min << std::endl; //std::cout << " ymax == " << ymax << ", y1max == " << y1max << ", y2max << " << y2max << std::endl; has_finite_data_ = false; if (std::isfinite(xmin) && std::isfinite(xmax) && std::isfinite(ymin) && std::isfinite(ymax)) { if (!xAxisIsUserRescaled_) { original_x0_ = xmin; original_x1_ = xmax; x0_ = original_x0_; x1_ = original_x1_; configureAxisForRange(x0_, x1_, xAxisMin_, xAxisMax_, xAxisMajorTickInterval_, xAxisMinorTickInterval_, xAxisMajorTickModulus_, xAxisTickValuePrecision_); } if (!yAxisIsUserRescaled_) { original_y0_ = ymin; original_y1_ = ymax; y0_ = original_y0_; y1_ = original_y1_; configureAxisForRange(y0_, y1_, yAxisMin_, yAxisMax_, yAxisMajorTickInterval_, yAxisMinorTickInterval_, yAxisMajorTickModulus_, yAxisTickValuePrecision_); } has_finite_data_ = true; } } void QtSLiMGraphView_CustomPlot::addABLineData(double *a_values, double *b_values, double *h_values, double *v_values, int data_count, std::vector *color, std::vector *alpha, std::vector *lwd) { if (a_values) { plot_type_.push_back(QtSLiM_CustomPlotType::kABLines); x1data_.push_back(a_values); y1data_.push_back(b_values); } else if (h_values) { plot_type_.push_back(QtSLiM_CustomPlotType::kHLines); x1data_.push_back(h_values); y1data_.push_back(nullptr); } else if (v_values) { plot_type_.push_back(QtSLiM_CustomPlotType::kVLines); x1data_.push_back(v_values); y1data_.push_back(nullptr); } x2data_.push_back(nullptr); // unused for abline y2data_.push_back(nullptr); // unused for abline labels_.push_back(nullptr); // unused for abline data_count_.push_back(data_count); symbol_.push_back(nullptr); // unused for abline color_.push_back(color); border_.push_back(nullptr); // unused for abline alpha_.push_back(alpha); line_width_.push_back(lwd); size_.push_back(nullptr); // unused for abline angle_.push_back(nullptr); // unused for abline xadj_.push_back(-1); // unused for abline yadj_.push_back(-1); // unused for abline image_.push_back(QImage()); // unused for abline //rescaleAxesForDataRange(); // not needed for abline update(); } void QtSLiMGraphView_CustomPlot::addLineData(double *x_values, double *y_values, int data_count, std::vector *color, std::vector *alpha, std::vector *lwd) { plot_type_.push_back(QtSLiM_CustomPlotType::kLines); x1data_.push_back(x_values); y1data_.push_back(y_values); x2data_.push_back(nullptr); // unused for lines y2data_.push_back(nullptr); // unused for lines labels_.push_back(nullptr); // unused for lines data_count_.push_back(data_count); symbol_.push_back(nullptr); // unused for lines color_.push_back(color); border_.push_back(nullptr); // unused for lines alpha_.push_back(alpha); line_width_.push_back(lwd); size_.push_back(nullptr); // unused for lines angle_.push_back(nullptr); // unused for lines xadj_.push_back(-1); // unused for lines yadj_.push_back(-1); // unused for lines image_.push_back(QImage()); // unused for lines rescaleAxesForDataRange(); update(); } void QtSLiMGraphView_CustomPlot::addRectData(double *x1_values, double *y1_values, double *x2_values, double *y2_values, int data_count, std::vector *color, std::vector *border, std::vector *alpha, std::vector *lwd) { plot_type_.push_back(QtSLiM_CustomPlotType::kRects); x1data_.push_back(x1_values); y1data_.push_back(y1_values); x2data_.push_back(x2_values); y2data_.push_back(y2_values); labels_.push_back(nullptr); // unused for rects data_count_.push_back(data_count); symbol_.push_back(nullptr); // unused for rects color_.push_back(color); border_.push_back(border); alpha_.push_back(alpha); line_width_.push_back(lwd); size_.push_back(nullptr); // unused for rects angle_.push_back(nullptr); // unused for rects xadj_.push_back(-1); // unused for rects yadj_.push_back(-1); // unused for rects image_.push_back(QImage()); // unused for rects rescaleAxesForDataRange(); update(); } void QtSLiMGraphView_CustomPlot::addSegmentData(double *x1_values, double *y1_values, double *x2_values, double *y2_values, int data_count, std::vector *color, std::vector *alpha, std::vector *lwd) { plot_type_.push_back(QtSLiM_CustomPlotType::kSegments); x1data_.push_back(x1_values); y1data_.push_back(y1_values); x2data_.push_back(x2_values); y2data_.push_back(y2_values); labels_.push_back(nullptr); // unused for segments data_count_.push_back(data_count); symbol_.push_back(nullptr); // unused for segments color_.push_back(color); border_.push_back(nullptr); // unused for segments alpha_.push_back(alpha); line_width_.push_back(lwd); size_.push_back(nullptr); // unused for segments angle_.push_back(nullptr); // unused for segments xadj_.push_back(-1); // unused for segments yadj_.push_back(-1); // unused for segments image_.push_back(QImage()); // unused for segments rescaleAxesForDataRange(); update(); } void QtSLiMGraphView_CustomPlot::addMarginTextData(double *x_values, double *y_values, std::vector *labels, int data_count, std::vector *color, std::vector *alpha, std::vector *size, double *adj, std::vector *angle) { plot_type_.push_back(QtSLiM_CustomPlotType::kMarginText); x1data_.push_back(x_values); y1data_.push_back(y_values); x2data_.push_back(nullptr); // unused for text y2data_.push_back(nullptr); // unused for text labels_.push_back(labels); data_count_.push_back(data_count); symbol_.push_back(nullptr); // unused for text color_.push_back(color); border_.push_back(nullptr); // unused for text alpha_.push_back(alpha); line_width_.push_back(nullptr); // unused for text size_.push_back(size); angle_.push_back(angle); // unused for text xadj_.push_back(adj[0]); yadj_.push_back(adj[1]); image_.push_back(QImage()); // unused for text rescaleAxesForDataRange(); update(); } void QtSLiMGraphView_CustomPlot::addPointData(double *x_values, double *y_values, int data_count, std::vector *symbol, std::vector *color, std::vector *border, std::vector *alpha, std::vector *lwd, std::vector *size) { plot_type_.push_back(QtSLiM_CustomPlotType::kPoints); x1data_.push_back(x_values); y1data_.push_back(y_values); x2data_.push_back(nullptr); // unused for points y2data_.push_back(nullptr); // unused for points labels_.push_back(nullptr); // unused for points data_count_.push_back(data_count); symbol_.push_back(symbol); color_.push_back(color); border_.push_back(border); alpha_.push_back(alpha); line_width_.push_back(lwd); size_.push_back(size); angle_.push_back(nullptr); // unused for points xadj_.push_back(-1); // unused for points yadj_.push_back(-1); // unused for points image_.push_back(QImage()); // unused for points rescaleAxesForDataRange(); update(); } void QtSLiMGraphView_CustomPlot::addTextData(double *x_values, double *y_values, std::vector *labels, int data_count, std::vector *color, std::vector *alpha, std::vector *size, double *adj, std::vector *angle) { plot_type_.push_back(QtSLiM_CustomPlotType::kText); x1data_.push_back(x_values); y1data_.push_back(y_values); x2data_.push_back(nullptr); // unused for text y2data_.push_back(nullptr); // unused for text labels_.push_back(labels); data_count_.push_back(data_count); symbol_.push_back(nullptr); // unused for text color_.push_back(color); border_.push_back(nullptr); // unused for text alpha_.push_back(alpha); line_width_.push_back(nullptr); // unused for text size_.push_back(size); angle_.push_back(angle); // unused for text xadj_.push_back(adj[0]); yadj_.push_back(adj[1]); image_.push_back(QImage()); // unused for text rescaleAxesForDataRange(); update(); } void QtSLiMGraphView_CustomPlot::addImageData(double *x_values, double *y_values, int data_count, QImage image, std::vector *alpha) { plot_type_.push_back(QtSLiM_CustomPlotType::kImage); x1data_.push_back(x_values); y1data_.push_back(y_values); x2data_.push_back(nullptr); // unused for image y2data_.push_back(nullptr); // unused for image labels_.push_back(nullptr); // unused for image data_count_.push_back(data_count); symbol_.push_back(nullptr); // unused for image color_.push_back(nullptr); // unused for image border_.push_back(nullptr); // unused for image alpha_.push_back(alpha); line_width_.push_back(nullptr); // unused for image size_.push_back(nullptr); // unused for image angle_.push_back(nullptr); // unused for image xadj_.push_back(-1); // unused for image yadj_.push_back(-1); // unused for image image_.push_back(image); rescaleAxesForDataRange(); update(); } void QtSLiMGraphView_CustomPlot::addLegend(QtSLiM_LegendPosition position, int inset, double labelSize, double lineHeight, double graphicsWidth, double exteriorMargin, double interiorMargin) { legend_added_ = true; legend_position_ = position; legend_inset = inset; legend_labelSize = labelSize; legend_lineHeight = lineHeight; legend_graphicsWidth = graphicsWidth; legend_exteriorMargin = exteriorMargin; legend_interiorMargin = interiorMargin; update(); } void QtSLiMGraphView_CustomPlot::addLegendLineEntry(QString label, QColor color, double lwd) { legend_entries_.emplace_back(label, lwd, color); update(); } void QtSLiMGraphView_CustomPlot::addLegendPointEntry(QString label, int symbol, QColor color, QColor border, double lwd, double size) { legend_entries_.emplace_back(label, symbol, color, border, lwd, size); update(); } void QtSLiMGraphView_CustomPlot::addLegendSwatchEntry(QString label, QColor color) { legend_entries_.emplace_back(label, color); update(); } void QtSLiMGraphView_CustomPlot::addLegendTitleEntry(QString label) { legend_entries_.emplace_back(label); update(); } QString QtSLiMGraphView_CustomPlot::graphTitle(void) { return title_; } QString QtSLiMGraphView_CustomPlot::aboutString(void) { return "The Custom Plot graph type displays user-provided data that is supplied " "in script with createPlot() and subsequent calls."; } void QtSLiMGraphView_CustomPlot::drawGraph(QPainter &painter, QRect interiorRect) { for (int i = 0; i < (int)plot_type_.size(); ++i) { QtSLiM_CustomPlotType plot_type = plot_type_[i]; switch (plot_type) { case QtSLiM_CustomPlotType::kLines: drawLines(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kSegments: drawSegments(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kRects: drawRects(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kMarginText: drawMarginText(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kPoints: drawPoints(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kText: drawText(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kABLines: drawABLines(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kHLines: drawHLines(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kVLines: drawVLines(painter, interiorRect, i); break; case QtSLiM_CustomPlotType::kImage: drawImage(painter, interiorRect, i); break; } } } void QtSLiMGraphView_CustomPlot::appendStringForData(QString & /* string */) { // No data string } QtSLiMLegendSpec QtSLiMGraphView_CustomPlot::legendKey(void) { return legend_entries_; } void QtSLiMGraphView_CustomPlot::controllerRecycled(void) { freeData(); update(); QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_CustomPlot::disableMessage(void) { if ((plot_type_.size() == 0) || !has_finite_data_) return "no\ndata"; return ""; } void QtSLiMGraphView_CustomPlot::drawABLines(QPainter &painter, QRect interiorRect, int dataIndex) { double *adata = x1data_[dataIndex]; double *bdata = y1data_[dataIndex]; int lineCount = data_count_[dataIndex]; std::vector &lineColors = *color_[dataIndex]; // might be one value or N values std::vector &lineAlphas = *alpha_[dataIndex]; // might be one value or N values std::vector &lineWidths = *line_width_[dataIndex]; // might be one value or N values for (int lineIndex = 0; lineIndex < lineCount; ++lineIndex) { QPainterPath linePath; double user_a = adata[lineIndex]; double user_b = bdata[lineIndex]; if (std::isfinite(user_a) && std::isfinite(user_b)) { // slope-intercept: y = a + bx double user_x1 = x0_ - 100000.0; double user_x2 = x1_ + 100000.0; double user_y1 = user_a + user_b * user_x1; double user_y2 = user_a + user_b * user_x2; QPointF devicePoint1(plotToDeviceX(user_x1, interiorRect), plotToDeviceY(user_y1, interiorRect)); QPointF devicePoint2(plotToDeviceX(user_x2, interiorRect), plotToDeviceY(user_y2, interiorRect)); QColor lineColor = lineColors[lineIndex % lineColors.size()]; double lineAlpha = lineAlphas[lineIndex % lineAlphas.size()]; double lineWidth = lineWidths[lineIndex % lineWidths.size()]; linePath.moveTo(devicePoint1); linePath.lineTo(devicePoint2); if (lineAlpha != 1.0) lineColor.setAlphaF(lineAlpha); painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } } void QtSLiMGraphView_CustomPlot::drawHLines(QPainter &painter, QRect interiorRect, int dataIndex) { double *hdata = x1data_[dataIndex]; int lineCount = data_count_[dataIndex]; std::vector &lineColors = *color_[dataIndex]; // might be one value or N values std::vector &lineAlphas = *alpha_[dataIndex]; // might be one value or N values std::vector &lineWidths = *line_width_[dataIndex]; // might be one value or N values for (int lineIndex = 0; lineIndex < lineCount; ++lineIndex) { QPainterPath linePath; double user_h = hdata[lineIndex]; if (std::isfinite(user_h)) { // round the y-coordinate for display to make the line look nicer, especially for lwd 1.0 QPointF devicePoint1(plotToDeviceX(x0_ - 100000.0, interiorRect), roundPlotToDeviceY(user_h, interiorRect)); QPointF devicePoint2(plotToDeviceX(x1_ + 100000.0, interiorRect), roundPlotToDeviceY(user_h, interiorRect)); QColor lineColor = lineColors[lineIndex % lineColors.size()]; double lineAlpha = lineAlphas[lineIndex % lineAlphas.size()]; double lineWidth = lineWidths[lineIndex % lineWidths.size()]; linePath.moveTo(devicePoint1); linePath.lineTo(devicePoint2); if (lineAlpha != 1.0) lineColor.setAlphaF(lineAlpha); painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } } void QtSLiMGraphView_CustomPlot::drawVLines(QPainter &painter, QRect interiorRect, int dataIndex) { double *vdata = x1data_[dataIndex]; int lineCount = data_count_[dataIndex]; std::vector &lineColors = *color_[dataIndex]; // might be one value or N values std::vector &lineAlphas = *alpha_[dataIndex]; // might be one value or N values std::vector &lineWidths = *line_width_[dataIndex]; // might be one value or N values for (int lineIndex = 0; lineIndex < lineCount; ++lineIndex) { QPainterPath linePath; double user_v = vdata[lineIndex]; if (std::isfinite(user_v)) { // round the x-coordinate for display to make the line look nicer, especially for lwd 1.0 QPointF devicePoint1(roundPlotToDeviceX(user_v, interiorRect), plotToDeviceY(y0_ - 100000.0, interiorRect)); QPointF devicePoint2(roundPlotToDeviceX(user_v, interiorRect), plotToDeviceY(y1_ + 100000.0, interiorRect)); QColor lineColor = lineColors[lineIndex % lineColors.size()]; double lineAlpha = lineAlphas[lineIndex % lineAlphas.size()]; double lineWidth = lineWidths[lineIndex % lineWidths.size()]; linePath.moveTo(devicePoint1); linePath.lineTo(devicePoint2); if (lineAlpha != 1.0) lineColor.setAlphaF(lineAlpha); painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } } void QtSLiMGraphView_CustomPlot::drawLines(QPainter &painter, QRect interiorRect, int dataIndex) { double *xdata = x1data_[dataIndex]; double *ydata = y1data_[dataIndex]; int vertexCount = data_count_[dataIndex]; QColor lineColor = (*color_[dataIndex])[0]; // guaranteed to be only one value, for lines() double lineAlpha = (*alpha_[dataIndex])[0]; // guaranteed to be only one value, for lines() double lineWidth = (*line_width_[dataIndex])[0]; // guaranteed to be only one value, for lines() if (lineAlpha != 1.0) { // This strokes each line segment as a separate path, so that when successive line segments cross, the alpha // value affects their area of overlap. This is arguably more likely to be what the user expects. However, // it doesn't draw the line joins nicely, with bevels and such, so the line path as a whole might less pretty. // We therefore use this drawing method only when alpha is not 1.0. lineColor.setAlphaF(lineAlpha); for (int vertexIndex = 0; vertexIndex < vertexCount - 1; ++vertexIndex) { QPainterPath linePath; double user_x1 = xdata[vertexIndex]; double user_y1 = ydata[vertexIndex]; double user_x2 = xdata[vertexIndex + 1]; double user_y2 = ydata[vertexIndex + 1]; if (!std::isnan(user_x1) && !std::isnan(user_y1) && !std::isnan(user_x2) && !std::isnan(user_y2)) { QPointF devicePoint1(plotToDeviceX(user_x1, interiorRect), plotToDeviceY(user_y1, interiorRect)); QPointF devicePoint2(plotToDeviceX(user_x2, interiorRect), plotToDeviceY(user_y2, interiorRect)); linePath.moveTo(devicePoint1); linePath.lineTo(devicePoint2); painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } } else { QPainterPath linePath; bool startedLine = false; for (int vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { double user_x = xdata[vertexIndex]; double user_y = ydata[vertexIndex]; if (!std::isnan(user_x) && !std::isnan(user_y)) { QPointF devicePoint(plotToDeviceX(user_x, interiorRect), plotToDeviceY(user_y, interiorRect)); if (startedLine) linePath.lineTo(devicePoint); else linePath.moveTo(devicePoint); startedLine = true; } else { // a NAN value for x or y interrupts the line being plotted; INF values are plotted, but don't affect axis ranges startedLine = false; } } if (lineAlpha != 1.0) lineColor.setAlphaF(lineAlpha); painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } void QtSLiMGraphView_CustomPlot::drawMarginText(QPainter &painter, QRect interiorRect, int dataIndex) { double *xdata = x1data_[dataIndex]; double *ydata = y1data_[dataIndex]; std::vector &labels = *labels_[dataIndex]; int pointCount = data_count_[dataIndex]; std::vector &textColors = *color_[dataIndex]; std::vector &textAlphas = *alpha_[dataIndex]; std::vector &textAngles = *angle_[dataIndex]; std::vector &pointSizes = *size_[dataIndex]; double xadj = xadj_[dataIndex]; double yadj = yadj_[dataIndex]; // move clipping area outward to encompass our entire parent widget QRect bounds = rect(); painter.save(); painter.setClipRect(bounds, Qt::ReplaceClip); //QtSLiMFrameRect(interiorRect, Qt::green, painter); // set up to get the font and font info double lastPointSize = -1; QFont labelFont; double capHeight = 0; for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { double user_x = xdata[pointIndex]; double user_y = ydata[pointIndex]; if (std::isfinite(user_x) && std::isfinite(user_y)) { // for mtext(), coordinates inside the plot area are in [0,1] QString &labelText = labels[pointIndex]; double x = user_x * interiorRect.width() + interiorRect.x(); double y = user_y * interiorRect.height() + interiorRect.y(); //qDebug() << "labelText ==" << labelText << ", user_x ==" << user_x << ", user_y ==" << user_y << ", x ==" << x << ", y ==" << y; // translate the painter so (x, y) is at (0, 0) painter.save(); painter.translate(x, y); x = 0; y = 0; double pointSize = pointSizes[pointIndex % pointSizes.size()]; if (pointSize != lastPointSize) { labelFont = QtSLiMGraphView::labelFontOfPointSize(pointSize); capHeight = QFontMetricsF(labelFont).capHeight(); painter.setFont(labelFont); lastPointSize = pointSize; } QColor textColor = textColors[pointIndex % textColors.size()]; double alpha = textAlphas[pointIndex % textAlphas.size()]; if (alpha != 1.0) textColor.setAlphaF(alpha); painter.setPen(textColor); QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, labelText); // labelBoundingRect is useful for its width, which seems to be calculated correctly; its height, however, is oddly large, and // is not useful, so we use the capHeight from the font metrics instead. This means that vertically centered (yadj == 0.5) is // the midpoint between the baseline and the capHeight, which I think is probably the best behavior. double labelWidth = labelBoundingRect.width(); double labelHeight = capHeight; double labelX = x - SLIM_SCREEN_ROUND(labelWidth * xadj); double labelY = y - SLIM_SCREEN_ROUND(labelHeight * yadj); //qDebug() << " labelBoundingRect ==" << labelBoundingRect << ", labelWidth ==" << labelWidth << ", labelHeight ==" << labelHeight << ", capHeight ==" << capHeight; //qDebug() << " labelX ==" << labelX << ", labelY ==" << labelY; // rotate the coordinate system around (x, y); for example, -10.0 is 10 degrees clockwise double textAngle = textAngles[pointIndex]; if (textAngle != 0.0) painter.rotate(-textAngles[pointIndex]); #if 0 // draw the axes for the text around the pivot point QPainterPath linePath; linePath.moveTo(-50, 0); linePath.lineTo(50, 0); linePath.moveTo(0, -50); linePath.lineTo(0, 50); painter.strokePath(linePath, QPen(Qt::green, 1.0)); #endif // flip vertically so the text is upright, and then use -labelY since we're flipped painter.scale(1.0, -1.0); painter.drawText(QPointF(labelX, -labelY), labelText); painter.restore(); } else { // a NAN or INF value for x or y is not plotted } } painter.restore(); } void QtSLiMGraphView_CustomPlot::drawRects(QPainter &painter, QRect interiorRect, int dataIndex) { double *x1data = x1data_[dataIndex]; double *y1data = y1data_[dataIndex]; double *x2data = x2data_[dataIndex]; double *y2data = y2data_[dataIndex]; int segmentCount = data_count_[dataIndex]; std::vector &colors = *color_[dataIndex]; std::vector &borderColors = *border_[dataIndex]; std::vector &alphas = *alpha_[dataIndex]; std::vector &lineWidths = *line_width_[dataIndex]; for (int segmentIndex = 0; segmentIndex < segmentCount; ++segmentIndex) { double user_x1 = x1data[segmentIndex]; double user_y1 = y1data[segmentIndex]; double user_x2 = x2data[segmentIndex]; double user_y2 = y2data[segmentIndex]; if (!std::isnan(user_x1) && !std::isnan(user_y1) && !std::isnan(user_x2) && !std::isnan(user_y2)) { double device_x1 = plotToDeviceX(user_x1, interiorRect); double device_y1 = plotToDeviceY(user_y1, interiorRect); double device_x2 = plotToDeviceX(user_x2, interiorRect); double device_y2 = plotToDeviceY(user_y2, interiorRect); QColor color = colors[segmentIndex % colors.size()]; QColor borderColor = borderColors[segmentIndex % borderColors.size()]; double alpha = alphas[segmentIndex % alphas.size()]; double lineWidth = lineWidths[segmentIndex % lineWidths.size()]; if (color.alphaF() != 0.0f) { // fill the rect if (alpha != 1.0) color.setAlphaF(alpha); QRectF rect(device_x1, device_y1, device_x2 - device_x1, device_y2 - device_y1); painter.fillRect(rect, color); } if (borderColor.alphaF() != 0.0f) { // frame the rect if (alpha != 1.0) borderColor.setAlphaF(alpha); QPainterPath linePath; linePath.moveTo(device_x1, device_y1); linePath.lineTo(device_x2, device_y1); linePath.lineTo(device_x2, device_y2); linePath.lineTo(device_x1, device_y2); linePath.closeSubpath(); painter.strokePath(linePath, QPen(borderColor, lineWidth, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin)); } } } } void QtSLiMGraphView_CustomPlot::drawSegments(QPainter &painter, QRect interiorRect, int dataIndex) { double *x1data = x1data_[dataIndex]; double *y1data = y1data_[dataIndex]; double *x2data = x2data_[dataIndex]; double *y2data = y2data_[dataIndex]; int segmentCount = data_count_[dataIndex]; std::vector &colors = *color_[dataIndex]; std::vector &alphas = *alpha_[dataIndex]; std::vector &lineWidths = *line_width_[dataIndex]; for (int segmentIndex = 0; segmentIndex < segmentCount; ++segmentIndex) { QPainterPath linePath; double user_x1 = x1data[segmentIndex]; double user_y1 = y1data[segmentIndex]; double user_x2 = x2data[segmentIndex]; double user_y2 = y2data[segmentIndex]; if (!std::isnan(user_x1) && !std::isnan(user_y1) && !std::isnan(user_x2) && !std::isnan(user_y2)) { QPointF devicePoint1(plotToDeviceX(user_x1, interiorRect), plotToDeviceY(user_y1, interiorRect)); QPointF devicePoint2(plotToDeviceX(user_x2, interiorRect), plotToDeviceY(user_y2, interiorRect)); QColor color = colors[segmentIndex % colors.size()]; double alpha = alphas[segmentIndex % alphas.size()]; double lineWidth = lineWidths[segmentIndex % lineWidths.size()]; if (alpha != 1.0) color.setAlphaF(alpha); linePath.moveTo(devicePoint1); linePath.lineTo(devicePoint2); painter.strokePath(linePath, QPen(color, lineWidth)); } } } void QtSLiMGraphView_CustomPlot::drawPoints(QPainter &painter, QRect interiorRect, int dataIndex) { double *xdata = x1data_[dataIndex]; double *ydata = y1data_[dataIndex]; int pointCount = data_count_[dataIndex]; std::vector &symbols = *symbol_[dataIndex]; std::vector &symbolColors = *color_[dataIndex]; std::vector &borderColors = *border_[dataIndex]; std::vector &alphas = *alpha_[dataIndex]; std::vector &lineWidths = *line_width_[dataIndex]; std::vector &sizes = *size_[dataIndex]; for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { double user_x = xdata[pointIndex]; double user_y = ydata[pointIndex]; // given that the line width, color, etc. can change with each symbol, we just plot each symbol individually if (std::isfinite(user_x) && std::isfinite(user_y)) { double x = plotToDeviceX(user_x, interiorRect); double y = plotToDeviceY(user_y, interiorRect); int symbol = symbols[pointIndex % symbols.size()]; QColor symbolColor = symbolColors[pointIndex % symbolColors.size()]; QColor borderColor = borderColors[pointIndex % borderColors.size()]; double alpha = alphas[pointIndex % alphas.size()]; double lineWidth = lineWidths[pointIndex % lineWidths.size()]; double size = sizes[pointIndex % sizes.size()]; drawPointSymbol(painter, x, y, symbol, symbolColor, borderColor, alpha, lineWidth, size); } } } void QtSLiMGraphView_CustomPlot::drawText(QPainter &painter, QRect interiorRect, int dataIndex) { double *xdata = x1data_[dataIndex]; double *ydata = y1data_[dataIndex]; std::vector &labels = *labels_[dataIndex]; int pointCount = data_count_[dataIndex]; std::vector &textColors = *color_[dataIndex]; std::vector &textAlphas = *alpha_[dataIndex]; std::vector &textAngles = *angle_[dataIndex]; std::vector &pointSizes = *size_[dataIndex]; double xadj = xadj_[dataIndex]; double yadj = yadj_[dataIndex]; // set up to get the font and font info double lastPointSize = -1; QFont labelFont; double capHeight = 0; for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { double user_x = xdata[pointIndex]; double user_y = ydata[pointIndex]; if (std::isfinite(user_x) && std::isfinite(user_y)) { QString &labelText = labels[pointIndex]; double x = plotToDeviceX(user_x, interiorRect); double y = plotToDeviceY(user_y, interiorRect); //qDebug() << "labelText ==" << labelText << ", user_x ==" << user_x << ", user_y ==" << user_y << ", x ==" << x << ", y ==" << y; // translate the painter so (x, y) is at (0, 0) painter.save(); painter.translate(x, y); x = 0; y = 0; double pointSize = pointSizes[pointIndex % pointSizes.size()]; if (pointSize != lastPointSize) { labelFont = QtSLiMGraphView::labelFontOfPointSize(pointSize); capHeight = QFontMetricsF(labelFont).capHeight(); painter.setFont(labelFont); lastPointSize = pointSize; } QColor textColor = textColors[pointIndex % textColors.size()]; double alpha = textAlphas[pointIndex % textAlphas.size()]; if (alpha != 1.0) textColor.setAlphaF(alpha); painter.setPen(textColor); QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, labelText); // labelBoundingRect is useful for its width, which seems to be calculated correctly; its height, however, is oddly large, and // is not useful, so we use the capHeight from the font metrics instead. This means that vertically centered (yadj == 0.5) is // the midpoint between the baseline and the capHeight, which I think is probably the best behavior. double labelWidth = labelBoundingRect.width(); double labelHeight = capHeight; double labelX = x - SLIM_SCREEN_ROUND(labelWidth * xadj); double labelY = y - SLIM_SCREEN_ROUND(labelHeight * yadj); //qDebug() << " labelBoundingRect ==" << labelBoundingRect << ", labelWidth ==" << labelWidth << ", labelHeight ==" << labelHeight << ", capHeight ==" << capHeight; //qDebug() << " labelX ==" << labelX << ", labelY ==" << labelY; // rotate the coordinate system around (x, y); for example, -10.0 is 10 degrees clockwise double textAngle = textAngles[pointIndex]; if (textAngle != 0.0) painter.rotate(-textAngles[pointIndex]); #if 0 // draw the axes for the text around the pivot point QPainterPath linePath; linePath.moveTo(-50, 0); linePath.lineTo(50, 0); linePath.moveTo(0, -50); linePath.lineTo(0, 50); painter.strokePath(linePath, QPen(Qt::green, 1.0)); #endif // flip vertically so the text is upright, and then use -labelY since we're flipped painter.scale(1.0, -1.0); painter.drawText(QPointF(labelX, -labelY), labelText); painter.restore(); } else { // a NAN or INF value for x or y is not plotted } } } void QtSLiMGraphView_CustomPlot::drawImage(QPainter &painter, QRect interiorRect, int dataIndex) { double *xdata = x1data_[dataIndex]; double *ydata = y1data_[dataIndex]; double alpha = (*alpha_[dataIndex])[0]; double user_x1 = xdata[0], user_y1 = ydata[0]; double user_x2 = xdata[1], user_y2 = ydata[1]; // for image() we always want to use pixel edges, as in PDF, not pixel centers, so that the plotted image // uses up the full pixels at the edges of the plot area if the image fills the whole plot area bool old_generatingPDF_ = generatingPDF_; generatingPDF_ = true; double x1 = plotToDeviceX(user_x1, interiorRect); double y1 = plotToDeviceY(user_y1, interiorRect); double x2 = plotToDeviceX(user_x2, interiorRect); double y2 = plotToDeviceY(user_y2, interiorRect); generatingPDF_ = old_generatingPDF_; // the coordinates are absolute, but Qt wants them as width/height double target_width = x2 - x1; double target_height = y2 - y1; // get the image data const QImage &image = image_[dataIndex]; QRectF target(x1, y1, target_width, target_height); if (alpha != 1.0) painter.setOpacity(alpha); // we do not want antialiasing of images drawn here; unfortunately that is difficult, because Qt ignores its render hints // in some cases and still gives us an interpolated image, so we also have to scale the image itself sometimes bool old_antialiasing = painter.testRenderHint(QPainter::Antialiasing); bool old_smoothpixmap = painter.testRenderHint(QPainter::SmoothPixmapTransform); painter.setRenderHint(QPainter::Antialiasing, false); painter.setRenderHint(QPainter::SmoothPixmapTransform, false); if (generatingPDF_) { const QImage scaledImage = image.scaled(target_width, target_height, Qt::IgnoreAspectRatio, Qt::FastTransformation); painter.drawImage(target, scaledImage); } else { // for on-screen display I have not seen it smooth the rescale, and I don't want the overhead of making a new image every time... painter.drawImage(target, image); } painter.setRenderHint(QPainter::Antialiasing, old_antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform, old_smoothpixmap); if (alpha != 1.0) painter.setOpacity(1.0); } ================================================ FILE: QtSLiM/QtSLiMGraphView_CustomPlot.h ================================================ // // QtSLiMGraphView_CustomPlot.h // SLiM // // Created by Ben Haller on 1/19/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_CUSTOMPLOT_H #define QTSLIMGRAPHVIEW_CUSTOMPLOT_H #include #include #include "QtSLiMGraphView.h" class Plot; enum class QtSLiM_CustomPlotType : int { kLines, kSegments, kRects, kPoints, kMarginText, kText, kABLines, // from abline() kHLines, // from abline() kVLines, // from abline() kImage, }; class QtSLiMGraphView_CustomPlot : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_CustomPlot(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_CustomPlot() override; Plot *eidosPlotObject(void) { return eidos_plot_object_; } void setEidosPlotObject(Plot *plot_object) { eidos_plot_object_ = plot_object; } // takes ownership void freeData(void); void setTitle(QString title); void setXLabel(QString x_label); void setYLabel(QString y_label); void setShowHorizontalGrid(bool showHorizontalGrid); void setShowVerticalGrid(bool showVerticalGrid); void setShowFullBox(bool showFullBox); void setAxisLabelSize(double axisLabelSize); void setTickLabelSize(double tickLabelSize); void setLegendPosition(QtSLiM_LegendPosition position); void setDataRanges(double *x_range, double *y_range); void setAxisConfiguration(int side, std::vector *at, int labels_type, std::vector *labels); void addABLineData(double *a_values, double *b_values, double *h_values, double *v_values, int data_count, std::vector *color, std::vector *alpha, std::vector *lwd); void addImageData(double *x_values, double *y_values, int data_count, QImage image, std::vector *alpha); void addLineData(double *x_values, double *y_values, int data_count, std::vector *color, std::vector *alpha, std::vector *lwd); void addMarginTextData(double *x_values, double *y_values, std::vector *labels, int data_count, std::vector *color, std::vector *alpha, std::vector *size, double *adj, std::vector *angle); void addPointData(double *x_values, double *y_values, int data_count, std::vector *symbol, std::vector *color, std::vector *border, std::vector *alpha, std::vector *lwd, std::vector *size); void addRectData(double *x1_values, double *y1_values, double *x2_values, double *y2_values, int data_count, std::vector *color, std::vector *border, std::vector *alpha, std::vector *lwd); void addSegmentData(double *x1_values, double *y1_values, double *x2_values, double *y2_values, int data_count, std::vector *color, std::vector *alpha, std::vector *lwd); void addTextData(double *x_values, double *y_values, std::vector *labels, int data_count, std::vector *color, std::vector *alpha, std::vector *size, double *adj, std::vector *angle); void addLegend(QtSLiM_LegendPosition position, int inset, double labelSize, double lineHeight, double graphicsWidth, double exteriorMargin, double interiorMargin); void addLegendLineEntry(QString label, QColor color, double lwd); void addLegendPointEntry(QString label, int symbol, QColor color, QColor border, double lwd, double size); void addLegendSwatchEntry(QString label, QColor color); void addLegendTitleEntry(QString label); virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual QString disableMessage(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual void appendStringForData(QString &string) override; bool legendAdded(void) { return legend_added_; } virtual QtSLiMLegendSpec legendKey(void) override; public slots: virtual void controllerRecycled(void) override; private: Plot *eidos_plot_object_ = nullptr; // OWNED POINTER QString title_; // we can keep any number of sets of lines and points; they get plotted in the order supplied to us // some parameters don't apply to a given plot type; placeholder values are inserted for those bool has_finite_data_; std::vector plot_type_; std::vector x1data_; std::vector y1data_; std::vector x2data_; std::vector y2data_; std::vector data_count_; // the count for the xdata / ydata buffers std::vector *> labels_; // one label per point std::vector *> symbol_; // one symbol per point, OR one symbol for all points std::vector *> color_; // one color per point, OR one color for all points std::vector *> border_; // one border color per point, OR one for all points std::vector *> alpha_; // one alpha per point, OR one alpha for all points std::vector *> line_width_; // one lwd per point, OR one lwd for all points std::vector *> size_; // one size per point, OR one size for all points std::vector *> angle_; // one angle per point, OR one angle for all points std::vector xadj_; // one xadj for all points std::vector yadj_; // one yadj for all points std::vector image_; // one QImage per image data; QImage() if unused void dataRange(std::vector &data, double *p_min, double *p_max); void rescaleAxesForDataRange(void); void drawABLines(QPainter &painter, QRect interiorRect, int dataIndex); void drawHLines(QPainter &painter, QRect interiorRect, int dataIndex); void drawVLines(QPainter &painter, QRect interiorRect, int dataIndex); void drawLines(QPainter &painter, QRect interiorRect, int dataIndex); void drawMarginText(QPainter &painter, QRect interiorRect, int dataIndex); void drawSegments(QPainter &painter, QRect interiorRect, int dataIndex); void drawRects(QPainter &painter, QRect interiorRect, int dataIndex); void drawPoints(QPainter &painter, QRect interiorRect, int dataIndex); void drawText(QPainter &painter, QRect interiorRect, int dataIndex); void drawImage(QPainter &painter, QRect interiorRect, int dataIndex); bool legend_added_ = false; // set to true by addLegend() QtSLiMLegendSpec legend_entries_; // unlike most graph types, we keep our legend around }; #endif // QTSLIMGRAPHVIEW_CUSTOMPLOT_H ================================================ FILE: QtSLiM/QtSLiMGraphView_FitnessOverTime.cpp ================================================ // // QtSLiMGraphView_FitnessOverTime.cpp // SLiM // // Created by Ben Haller on 3/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_FitnessOverTime.h" #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" QtSLiMGraphView_FitnessOverTime::QtSLiMGraphView_FitnessOverTime(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { //setXAxisRangeFromTick(); // the end tick is not yet known setDefaultYAxisRange(); xAxisLabel_ = "Tick"; yAxisLabel_ = "Fitness (rescaled)"; allowXAxisUserRescale_ = true; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; tweakXAxisTickLabelAlignment_ = true; showSubpopulations_ = true; drawLines_ = true; QtSLiMGraphView_FitnessOverTime::updateAfterTick(); } void QtSLiMGraphView_FitnessOverTime::setDefaultYAxisRange(void) { original_y0_ = 0.9; original_y1_ = 1.1; // dynamic y0_ = original_y0_; y1_ = original_y1_; yAxisMin_ = y0_; yAxisMax_ = y1_; yAxisMajorTickInterval_ = 0.1; yAxisMinorTickInterval_ = 0.02; yAxisMajorTickModulus_ = 5; yAxisTickValuePrecision_ = 1; } QtSLiMGraphView_FitnessOverTime::~QtSLiMGraphView_FitnessOverTime() { // We are responsible for our own destruction QtSLiMGraphView_FitnessOverTime::invalidateDrawingCache(); } void QtSLiMGraphView_FitnessOverTime::invalidateDrawingCache(void) { delete drawingCache_; drawingCache_ = nullptr; drawingCacheTick_ = 0; } void QtSLiMGraphView_FitnessOverTime::controllerRecycled(void) { if (!controller_->invalidSimulation()) { if (!yAxisIsUserRescaled_) setDefaultYAxisRange(); //if (!xAxisIsUserRescaled_) // setXAxisRangeFromTick(); // the end tick is not yet known update(); } QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_FitnessOverTime::graphTitle(void) { return "Fitness ~ Time"; } QString QtSLiMGraphView_FitnessOverTime::aboutString(void) { return "The Fitness ~ Time graph shows mean fitness as a function of time. The mean fitness " "of the population is shown with a thick black line, while those of subpopulations " "are shown with thinner colored lines. Fixation events during the model run are " "shown with light blue vertical lines at the tick in which they occurred. The " "fitness shown is 'rescaled', meaning that when non-neutral mutations fix and are 'substituted' by " "SLiM they are no longer included in fitness calculations, so the y axis is 'rescaled'; " "this is mainly relevant to WF models. It is also 'rescaled' in the sense that it " "excludes subpopulation fitnessScaling values (to emphasize individual fitness effects " "over density-dependence); this is mainly relevant to nonWF models."; } void QtSLiMGraphView_FitnessOverTime::updateAfterTick(void) { Species *graphSpecies = focalDisplaySpecies(); if (!controller_->invalidSimulation() && graphSpecies && !yAxisIsUserRescaled_) { // BCH 3/20/2024: We set the x axis range each tick, because the end tick is now invalid until after initialize() callbacks if (!xAxisIsUserRescaled_) setXAxisRangeFromTick(); Population &pop = graphSpecies->population_; double minHistory = std::numeric_limits::infinity(); double maxHistory = -std::numeric_limits::infinity(); bool showSubpops = showSubpopulations_ && (pop.fitness_histories_.size() > 2); for (auto history_record_iter : pop.fitness_histories_) { if (showSubpops || (history_record_iter.first == -1)) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; // find the min and max history value for (int i = 0; i < historyLength; ++i) { double historyEntry = history[i]; if (!std::isnan(historyEntry)) { if (historyEntry > maxHistory) maxHistory = historyEntry; if (historyEntry < minHistory) minHistory = historyEntry; } } } } // set axis range to encompass the data if (!std::isinf(minHistory) && !std::isinf(maxHistory)) { if ((minHistory < 0.9) || (maxHistory > 1.1)) // if we're outside our original axis range... { double axisMin = (minHistory < 0.5 ? 0.0 : 0.5); // either 0.0 or 0.5 double axisMax = ceil(maxHistory * 2.0) / 2.0; // 1.5, 2.0, 2.5, ... if (axisMax < 1.5) axisMax = 1.5; if ((fabs(axisMin - yAxisMin_) > 0.0000001) || (fabs(axisMax - yAxisMax_) > 0.0000001)) { yAxisMin_ = axisMin; original_y0_ = yAxisMin_; // the same as yAxisMin_, for base plots y0_ = original_y0_; yAxisMax_ = axisMax; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 0.5; yAxisMinorTickInterval_ = 0.25; yAxisMajorTickModulus_ = 2; yAxisTickValuePrecision_ = 1; QtSLiMGraphView_FitnessOverTime::invalidateDrawingCache(); } } } } QtSLiMGraphView::updateAfterTick(); } void QtSLiMGraphView_FitnessOverTime::drawPointGraph(QPainter &painter, QRect interiorRect) { Community *community = controller_->community; Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; slim_tick_t completedTicks = community->Tick() - 1; // The tick counter can get set backwards, in which case our drawing cache is invalid – it contains drawing of things in the // future that may no longer happen. So we need to detect that case and invalidate our cache. if (!cachingNow_ && drawingCache_ && (drawingCacheTick_ > completedTicks)) { //qDebug() << "backward tick change detected, invalidating drawing cache"; invalidateDrawingCache(); } // If we're not caching, then: if our cache is invalid OR we have crossed a 1000-tick boundary since we last cached, cache an image if (!cachingNow_ && (!drawingCache_ || ((completedTicks / 1000) > (drawingCacheTick_ / 1000)))) { invalidateDrawingCache(); //qDebug() << "making new cache at tick " << community->Tick(); cachingNow_ = true; QPixmap *cache = new QPixmap(interiorRect.size()); cache->fill(Qt::transparent); // transparent so grid lines don't get overwritten by drawPixmap() QPainter cachePainter(cache); drawGraph(cachePainter, cache->rect()); drawingCache_ = cache; drawingCacheTick_ = completedTicks; cachingNow_ = false; } // Now draw our cache, if we have one if (drawingCache_) { //qDebug() << "drawing cache:" << drawingCache_->rect() << ", drawingCacheTick_ == " << drawingCacheTick_; painter.drawPixmap(interiorRect, *drawingCache_, drawingCache_->rect()); } // Draw fixation events std::vector &substitutions = pop.substitutions_; for (const Substitution *substitution : substitutions) { slim_tick_t fixation_tick = substitution->fixation_tick_; // If we are caching, draw all events; if we are not, draw only those that are not already in the cache if (!cachingNow_ && (fixation_tick < drawingCacheTick_)) continue; double substitutionX = plotToDeviceX(fixation_tick, interiorRect); QRectF substitutionRect(substitutionX - 0.5, interiorRect.x(), 1.0, interiorRect.height()); painter.fillRect(substitutionRect, QtSLiMColorWithRGB(0.2, 0.2, 1.0, 0.2)); } // Draw the fitness history as a scatter plot; better suited to caching of the image bool showSubpops = showSubpopulations_ && (pop.fitness_histories_.size() > 2); bool drawSubpopsGray = (showSubpops && (pop.fitness_histories_.size() > 8)); // 7 subpops + pop // First draw subpops, then draw the mean population fitness for (int iter = (showSubpops ? 0 : 1); iter <= 1; ++iter) { QColor pointColor = ((iter == 0) ? QtSLiMColorWithWhite(0.5, 1.0) : Qt::black); for (auto history_record_iter : pop.fitness_histories_) { if (((iter == 0) && (history_record_iter.first != -1)) || ((iter == 1) && (history_record_iter.first == -1))) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; // If we're caching now, draw all points; otherwise, if we have a cache, draw only additional points slim_tick_t firstHistoryEntryToDraw = (cachingNow_ ? 0 : (drawingCache_ ? drawingCacheTick_ : 0)); for (slim_tick_t i = firstHistoryEntryToDraw; (i < historyLength) && (i < completedTicks); ++i) { double historyEntry = history[i]; if (!std::isnan(historyEntry)) { QPointF historyPoint(plotToDeviceX(i, interiorRect), plotToDeviceY(historyEntry, interiorRect)); if ((iter == 0) && !drawSubpopsGray) pointColor = controller_->whiteContrastingColorForIndex(history_record_iter.first); painter.fillRect(QRectF(historyPoint.x() - 0.5, historyPoint.y() - 0.5, 1.0, 1.0), pointColor); } } } } } } void QtSLiMGraphView_FitnessOverTime::drawLineGraph(QPainter &painter, QRect interiorRect) { Community *community = controller_->community; Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; slim_tick_t completedTicks = community->Tick() - 1; // Draw fixation events std::vector &substitutions = pop.substitutions_; for (const Substitution *substitution : substitutions) { slim_tick_t fixation_tick = substitution->fixation_tick_; double substitutionX = plotToDeviceX(fixation_tick, interiorRect); QRectF substitutionRect(substitutionX - 0.5, interiorRect.x(), 1.0, interiorRect.height()); painter.fillRect(substitutionRect, QtSLiMColorWithRGB(0.2, 0.2, 1.0, 0.2)); } // Draw the fitness history as a line plot bool showSubpops = showSubpopulations_ && (pop.fitness_histories_.size() > 2); bool drawSubpopsGray = (showSubpops && (pop.fitness_histories_.size() > 8)); // 7 subpops + pop // First draw subpops, then draw the mean population fitness for (int iter = (showSubpops ? 0 : 1); iter <= 1; ++iter) { QColor lineColor = (iter == 0) ? QtSLiMColorWithWhite(0.5, 1.0) : Qt::black; double lineWidth = (iter == 0) ? 1.0 : 1.5; for (auto history_record_iter : pop.fitness_histories_) { if (((iter == 0) && (history_record_iter.first != -1)) || ((iter == 1) && (history_record_iter.first == -1))) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; QPainterPath linePath; bool startedLine = false; for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) { double historyEntry = history[i]; if (std::isnan(historyEntry)) { startedLine = false; } else { QPointF historyPoint(plotToDeviceX(i, interiorRect), plotToDeviceY(historyEntry, interiorRect)); if (startedLine) linePath.lineTo(historyPoint); else linePath.moveTo(historyPoint); startedLine = true; } } if ((iter == 0) && !drawSubpopsGray) lineColor = controller_->whiteContrastingColorForIndex(history_record_iter.first); painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } } } void QtSLiMGraphView_FitnessOverTime::drawGraph(QPainter &painter, QRect interiorRect) { if (drawLines_) drawLineGraph(painter, interiorRect); else drawPointGraph(painter, interiorRect); } bool QtSLiMGraphView_FitnessOverTime::providesStringForData(void) { return true; } void QtSLiMGraphView_FitnessOverTime::appendStringForData(QString &string) { Community *community = controller_->community; Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; slim_tick_t completedTicks = community->Tick() - 1; // Fixation events string.append("# Fixation ticks:\n"); std::vector &substitutions = pop.substitutions_; for (const Substitution *substitution : substitutions) { slim_tick_t fixation_tick = substitution->fixation_tick_; string.append(QString("%1, ").arg(fixation_tick)); } // Fitness history bool showSubpops = showSubpopulations_ && (pop.fitness_histories_.size() > 2); string.append("\n\n# Fitness history:\n"); for (int iter = 0; iter <= (showSubpops ? 1 : 0); ++iter) { for (auto history_record_iter : pop.fitness_histories_) { if (((iter == 0) && (history_record_iter.first == -1)) || ((iter == 1) && (history_record_iter.first != -1))) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; if (iter == 1) string.append(QString("\n\n# Fitness history (subpopulation p%1):\n").arg(history_record_iter.first)); for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) string.append(QString("%1, ").arg(history[i], 0, 'f', 4)); string.append("\n"); } } } } QtSLiMLegendSpec QtSLiMGraphView_FitnessOverTime::legendKey(void) { if (!showSubpopulations_) return QtSLiMLegendSpec(); Species *graphSpecies = focalDisplaySpecies(); std::vector subpopsToDisplay; for (auto history_record_iter : graphSpecies->population_.fitness_histories_) subpopsToDisplay.emplace_back(history_record_iter.first); return subpopulationLegendKey(subpopsToDisplay, subpopsToDisplay.size() > 8); } void QtSLiMGraphView_FitnessOverTime::toggleShowSubpopulations(void) { showSubpopulations_ = !showSubpopulations_; invalidateDrawingCache(); update(); } void QtSLiMGraphView_FitnessOverTime::toggleDrawLines(void) { drawLines_ = !drawLines_; invalidateDrawingCache(); update(); } void QtSLiMGraphView_FitnessOverTime::subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent * /* event */) { contextMenu.addAction(showSubpopulations_ ? "Hide Subpopulations" : "Show Subpopulations", this, &QtSLiMGraphView_FitnessOverTime::toggleShowSubpopulations); contextMenu.addAction(drawLines_ ? "Draw Points (Faster)" : "Draw Lines (Slower)", this, &QtSLiMGraphView_FitnessOverTime::toggleDrawLines); } ================================================ FILE: QtSLiM/QtSLiMGraphView_FitnessOverTime.h ================================================ // // QtSLiMGraphView_FitnessOverTime.h // SLiM // // Created by Ben Haller on 3/31/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_FITNESSOVERTIME_H #define QTSLIMGRAPHVIEW_FITNESSOVERTIME_H #include #include "QtSLiMGraphView.h" class QPixmap; class QtSLiMGraphView_FitnessOverTime : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_FitnessOverTime(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_FitnessOverTime() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event) override; public slots: virtual void invalidateDrawingCache(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void toggleShowSubpopulations(void); void toggleDrawLines(void); protected: virtual QtSLiMLegendSpec legendKey(void) override; private: bool showSubpopulations_ = false; bool drawLines_ = false; QPixmap *drawingCache_ = nullptr; slim_tick_t drawingCacheTick_ = 0; void setDefaultYAxisRange(void); void drawPointGraph(QPainter &painter, QRect interiorRect); void drawLineGraph(QPainter &painter, QRect interiorRect); }; #endif // QTSLIMGRAPHVIEW_FITNESSOVERTIME_H ================================================ FILE: QtSLiM/QtSLiMGraphView_FixationTimeHistogram.cpp ================================================ // // QtSLiMGraphView_FixationTimeHistogram.cpp // SLiM // // Created by Ben Haller on 3/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_FixationTimeHistogram.h" #include #include "QtSLiMWindow.h" QtSLiMGraphView_FixationTimeHistogram::QtSLiMGraphView_FixationTimeHistogram(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 10; //allowBinCountRescale_ = true; // not supported yet original_x1_ = 1000; x1_ = original_x1_; xAxisMax_ = x1_; xAxisMajorTickInterval_ = 200; xAxisMinorTickInterval_ = 100; xAxisMajorTickModulus_ = 2; xAxisTickValuePrecision_ = 0; xAxisLabel_ = "Mutation fixation time"; yAxisLabel_ = "Proportion of fixed mutations"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; } QtSLiMGraphView_FixationTimeHistogram::~QtSLiMGraphView_FixationTimeHistogram() { } QString QtSLiMGraphView_FixationTimeHistogram::graphTitle(void) { return "Mutation Fixation Time"; } QString QtSLiMGraphView_FixationTimeHistogram::aboutString(void) { return "The Mutation Fixation Time graph shows a histogram of mutation fixation times, " "for those mutations that have fixed. The proportions are calculated and plotted " "separately for each mutation type, for comparison."; } double *QtSLiMGraphView_FixationTimeHistogram::fixationTimeData(void) { int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); slim_tick_t *histogram = graphSpecies->population_.mutation_fixation_times_; int64_t histogramBins = static_cast(graphSpecies->population_.mutation_fixation_tick_slots_); // fewer than binCount * mutationTypeCount may exist static double *rebin = nullptr; static size_t rebinBins = 0; size_t usedRebinBins = static_cast(binCount * mutationTypeCount); // re-bin for display; SLiM bins every 10 ticks, but right now we want to plot every 100 ticks as a bin if (!rebin || (rebinBins < usedRebinBins)) { rebinBins = usedRebinBins; rebin = static_cast(realloc(rebin, rebinBins * sizeof(double))); } for (size_t i = 0; i < usedRebinBins; ++i) rebin[i] = 0.0; for (int i = 0; i < binCount * 10; ++i) { for (int j = 0; j < mutationTypeCount; ++j) { int histIndex = j + i * mutationTypeCount; if (histIndex < histogramBins) rebin[j + (i / 10) * mutationTypeCount] += histogram[histIndex]; } } // normalize within each mutation type for (int mutationTypeIndex = 0; mutationTypeIndex < mutationTypeCount; ++mutationTypeIndex) { int64_t total = 0; for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; total += static_cast(rebin[binIndex]); } if (total == 0) total = 1; // if counts are all zero, avoid a divide by zero below and just end up with 0 instead for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; rebin[binIndex] /= static_cast(total); } } return rebin; } void QtSLiMGraphView_FixationTimeHistogram::drawGraph(QPainter &painter, QRect interiorRect) { double *plotData = fixationTimeData(); int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); // plot our histogram bars drawGroupedBarplot(painter, interiorRect, plotData, mutationTypeCount, binCount, 0.0, 100.0); } QtSLiMLegendSpec QtSLiMGraphView_FixationTimeHistogram::legendKey(void) { return mutationTypeLegendKey(); // we use the prefab mutation type legend } bool QtSLiMGraphView_FixationTimeHistogram::providesStringForData(void) { return true; } void QtSLiMGraphView_FixationTimeHistogram::appendStringForData(QString &string) { double *plotData = fixationTimeData(); int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); for (auto mutationTypeIter : graphSpecies->mutation_types_) { MutationType *mutationType = mutationTypeIter.second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! string.append(QString("\"m%1\", ").arg(mutationType->mutation_type_id_)); for (int i = 0; i < binCount; ++i) { int histIndex = mutationTypeIndex + i * mutationTypeCount; string.append(QString("%1, ").arg(plotData[histIndex], 0, 'f', 4)); } string.append("\n"); } } ================================================ FILE: QtSLiM/QtSLiMGraphView_FixationTimeHistogram.h ================================================ // // QtSLiMGraphView_FixationTimeHistogram.h // SLiM // // Created by Ben Haller on 3/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_FIXATIONTIMEHISTOGRAM_H #define QTSLIMGRAPHVIEW_FIXATIONTIMEHISTOGRAM_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_FixationTimeHistogram : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_FixationTimeHistogram(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_FixationTimeHistogram() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual QtSLiMLegendSpec legendKey(void) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; private: double *fixationTimeData(void); }; #endif // QTSLIMGRAPHVIEW_FIXATIONTIMEHISTOGRAM_H ================================================ FILE: QtSLiM/QtSLiMGraphView_FrequencyTrajectory.cpp ================================================ // // QtSLiMGraphView_FrequencyTrajectory.cpp // SLiM // // Created by Ben Haller on 4/1/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_FrequencyTrajectory.h" #include #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "subpopulation.h" QtSLiMGraphView_FrequencyTrajectory::QtSLiMGraphView_FrequencyTrajectory(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { //setXAxisRangeFromTick(); // the end tick is not yet known xAxisLabel_ = "Tick"; yAxisLabel_ = "Frequency"; allowXAxisUserRescale_ = true; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; tweakXAxisTickLabelAlignment_ = true; // Start with no selected subpop or mutation-type; these will be set to the first menu item added when menus are constructed selectedSubpopulationID_ = -1; selectedMutationTypeIndex_ = -1; // Start plotting lost mutations, by default plotLostMutations_ = true; plotFixedMutations_ = true; plotActiveMutations_ = true; useColorsForPlotting_ = true; } void QtSLiMGraphView_FrequencyTrajectory::addedToWindow(void) { // Make our pop-up menu buttons QHBoxLayout *button_layout = buttonLayout(); if (button_layout) { subpopulationButton_ = newButtonInLayout(button_layout); connect(subpopulationButton_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_FrequencyTrajectory::subpopulationPopupChanged); mutationTypeButton_ = newButtonInLayout(button_layout); connect(mutationTypeButton_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_FrequencyTrajectory::mutationTypePopupChanged); addSubpopulationsToMenu(subpopulationButton_, selectedSubpopulationID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); } // We want to display a "recycle to start gathering data at the start of the run" message justAddedToWindow_ = true; } QtSLiMGraphView_FrequencyTrajectory::~QtSLiMGraphView_FrequencyTrajectory() { // We are responsible for our own destruction QtSLiMGraphView_FrequencyTrajectory::invalidateCachedData(); } void QtSLiMGraphView_FrequencyTrajectory::invalidateCachedData(void) { // first free all the MutationFrequencyHistory objects we've stored for (auto &item : frequencyHistoryDict_) delete item.second; for (auto &item : frequencyHistoryColdStorageLost_) delete item; for (auto &item : frequencyHistoryColdStorageFixed_) delete item; // then clear out the storage frequencyHistoryDict_.clear(); frequencyHistoryColdStorageLost_.clear(); frequencyHistoryColdStorageFixed_.clear(); justAddedToWindow_ = true; } void QtSLiMGraphView_FrequencyTrajectory::fetchDataForFinishedTick(void) { Community *community = controller_->community; Species *graphSpecies = focalDisplaySpecies(); if (!graphSpecies) return; Population &population = graphSpecies->population_; int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); const MutationIndex *registry_iter_end = registry + registry_size; // Check that the subpop and muttype we're supposed to be surveying exists; if not, bail. bool hasSubpop = true, hasMuttype = true; if (!graphSpecies->SubpopulationWithID(selectedSubpopulationID_)) hasSubpop = addSubpopulationsToMenu(subpopulationButton_, selectedSubpopulationID_); if (!graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_)) hasMuttype = addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); if (!hasSubpop || !hasMuttype) return; // Start by zeroing out the "updated" flags; this is how we find dead mutations for (auto &pair_ref : frequencyHistoryDict_) pair_ref.second->updated = false; // Tally reference counts within selectedSubpopulationID_ size_t subpop_total_haplosome_count = tallyGUIMutationReferences(selectedSubpopulationID_, selectedMutationTypeIndex_); if (subpop_total_haplosome_count == 0) subpop_total_haplosome_count = 1; // refcounts will all be zero; prevent NAN values below, make them 0 instead // Now we can run through the mutations and use the tallies in gui_scratch_reference_count to update our histories Mutation *mut_block_ptr = gSLiM_Mutation_Block; for (const MutationIndex *registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) { const Mutation *mutation = mut_block_ptr + *registry_iter; slim_refcount_t refcount = mutation->gui_scratch_reference_count_; if (refcount) { uint16_t value = static_cast((static_cast(refcount) * static_cast(UINT16_MAX)) / subpop_total_haplosome_count); slim_mutationid_t mutationID = mutation->mutation_id_; auto history_iter = frequencyHistoryDict_.find(mutationID); //NSLog(@"mutation refcount %d has uint16_t value %d, found history %p for id %lld", refcount, value, history, (long long int)mutation->mutation_id_); if (history_iter != frequencyHistoryDict_.end()) { // We have a history for this mutation, so we just need to add an entry; this sets the updated flag MutationFrequencyHistory *history = history_iter->second; history->addEntry(value); } else { // No history, so we make one starting at this tick; this also sets the updated flag // Note we use community->Tick() - 1, because the tick counter has already been advanced to the next tick MutationFrequencyHistory *history = new MutationFrequencyHistory(value, mutation, community->Tick() - 1); frequencyHistoryDict_.emplace(mutationID, history); } } } // OK, now every mutation that has frequency >0 in our subpop has got a current entry. But what about mutations that used to circulate, // but don't any more? These could still be active in a different subpop, or they might be gone – lost or fixed. For the former case, // we need to add an entry with frequency zero. For the latter case, we need to put their history into "cold storage" for efficiency. std::vector historiesToAddToColdStorage; for (auto entry_iter : frequencyHistoryDict_) { MutationFrequencyHistory *history = entry_iter.second; if (!history->updated) { slim_mutationid_t historyID = history->mutationID; bool mutationStillExists = false; for (const MutationIndex *mutation_iter = registry; mutation_iter != registry_iter_end; ++mutation_iter) { const Mutation *mutation = mut_block_ptr + *mutation_iter; slim_mutationid_t mutationID = mutation->mutation_id_; if (historyID == mutationID) { mutationStillExists = true; break; } } if (mutationStillExists) { // The mutation is still around, so just add a zero entry for it history->addEntry(0); } else { // The mutation is gone, so we need to put its history into cold storage, but we can't modify // our dictionary since we are enumerating it, so we just make a record and do it below historiesToAddToColdStorage.emplace_back(history); } } } // Now, if historiesToAddToColdStorage is non-nil, we have histories to put into cold storage; do it now for (MutationFrequencyHistory *history : historiesToAddToColdStorage) { // The remaining tricky bit is that we have to figure out whether the vanished mutation was fixed or lost; we do this by // scanning through all our Substitution objects, which use the same unique IDs as Mutations use. We need to know this // for two reasons: to add the final entry for the mutation, and to put it into the correct cold storage array. slim_mutationid_t mutationID = history->mutationID; bool wasFixed = false; std::vector &substitutions = population.substitutions_; for (const Substitution *substitution : substitutions) { if (substitution->mutation_id_ == mutationID) { wasFixed = true; break; } } if (wasFixed) { history->addEntry(UINT16_MAX); frequencyHistoryColdStorageFixed_.emplace_back(history); } else { history->addEntry(0); frequencyHistoryColdStorageLost_.emplace_back(history); } auto history_iter = frequencyHistoryDict_.find(mutationID); frequencyHistoryDict_.erase(history_iter); } //NSLog(@"frequencyHistoryDict has %lld entries, frequencyHistoryColdStorageLost has %lld entries, frequencyHistoryColdStorageFixed has %lld entries", (long long int)[frequencyHistoryDict count], (long long int)[frequencyHistoryColdStorageLost count], (long long int)[frequencyHistoryColdStorageFixed count]); lastTick_ = community->Tick(); justAddedToWindow_ = false; } void QtSLiMGraphView_FrequencyTrajectory::subpopulationPopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulationButton_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulationID_ != newSubpopID)) { selectedSubpopulationID_ = newSubpopID; invalidateCachedData(); fetchDataForFinishedTick(); update(); } } void QtSLiMGraphView_FrequencyTrajectory::mutationTypePopupChanged(int /* index */) { int newMutTypeIndex = mutationTypeButton_->currentData().toInt(); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedMutationTypeIndex_ != newMutTypeIndex)) { selectedMutationTypeIndex_ = newMutTypeIndex; invalidateCachedData(); fetchDataForFinishedTick(); update(); } } void QtSLiMGraphView_FrequencyTrajectory::controllerRecycled(void) { if (!controller_->invalidSimulation()) { //if (!xAxisIsUserRescaled_) // setXAxisRangeFromTick(); // the end tick is not yet known update(); } // Remake our popups, whether or not the controller is valid invalidateCachedData(); addSubpopulationsToMenu(subpopulationButton_, selectedSubpopulationID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); // We do not want to display our "recycle to start gathering at the start" message after a recycle justAddedToWindow_ = false; QtSLiMGraphView::controllerRecycled(); } void QtSLiMGraphView_FrequencyTrajectory::controllerTickFinished(void) { QtSLiMGraphView::controllerTickFinished(); // Check for an unexpected change in Tick(), in which case we invalidate all our histories and start over Community *community = controller_->community; if (lastTick_ != community->Tick() - 1) { invalidateCachedData(); update(); } // Fetch and store the frequencies for all mutations of the selected mutation type(s), within the subpopulation selected fetchDataForFinishedTick(); } QString QtSLiMGraphView_FrequencyTrajectory::graphTitle(void) { return "Mutation Frequency Trajectories"; } QString QtSLiMGraphView_FrequencyTrajectory::aboutString(void) { return "The Mutation Frequency Trajectories graph shows historical trajectories of mutation " "frequencies over time, within a given subpopulation and for a given " "mutation type. Color represents whether a given mutation was " "lost (red), fixed population-wide and substituted by SLiM (blue), or is " "still segregating (black). These categories can be separately enabled " "or disabled in the action menu. Because of the large amount of data " "recorded for this graph, recording is only enabled when the graph is " "open, and only for the selected subpopulation and mutation type; to " "fill in missing data, it is necessary to recycle and run when the graph " "window is already open and configured as desired."; } void QtSLiMGraphView_FrequencyTrajectory::updateAfterTick(void) { // BCH 3/20/2024: We set the x axis range each tick, because the end tick is now invalid until after initialize() callbacks if (!xAxisIsUserRescaled_) setXAxisRangeFromTick(); // Rebuild the subpop and muttype menus; this has the side effect of checking and fixing our selections, and that, // in turn, will have the side effect of invaliding our cache and fetching new data if needed addSubpopulationsToMenu(subpopulationButton_, selectedSubpopulationID_); addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); QtSLiMGraphView::updateAfterTick(); } QString QtSLiMGraphView_FrequencyTrajectory::disableMessage(void) { Species *graphSpecies = focalDisplaySpecies(); if (graphSpecies) { // check that we have a valid subpop and muttype bool hasSubpop = true, hasMuttype = true; if (!graphSpecies->SubpopulationWithID(selectedSubpopulationID_)) hasSubpop = addSubpopulationsToMenu(subpopulationButton_, selectedSubpopulationID_); if (!graphSpecies->MutationTypeWithIndex(selectedMutationTypeIndex_)) hasMuttype = addMutationTypesToMenu(mutationTypeButton_, selectedMutationTypeIndex_); if (!hasSubpop || !hasMuttype) return "no\ndata"; // check that we have some history recorded qDebug() << "justAddedToWindow_ ==" << justAddedToWindow_; if (justAddedToWindow_) return "initiating data collection;\nrecycle and run to\ncollect data from the\nstart of the simulation"; } return ""; } void QtSLiMGraphView_FrequencyTrajectory::drawHistory(QPainter &painter, MutationFrequencyHistory *history, QRect interiorRect) { size_t entryCount = history->entryCount; if (entryCount > 1) // one entry just generates a moveto { uint16_t *entries = history->entries; QPainterPath linePath; uint16_t firstValue = *entries; double firstFrequency = static_cast(firstValue) / UINT16_MAX; slim_tick_t tick = history->baseTick; QPointF firstPoint(plotToDeviceX(tick, interiorRect), plotToDeviceY(firstFrequency, interiorRect)); linePath.moveTo(firstPoint); for (size_t entryIndex = 1; entryIndex < entryCount; ++entryIndex) { uint16_t value = entries[entryIndex]; double frequency = static_cast(value) / UINT16_MAX; QPointF nextPoint(plotToDeviceX(++tick, interiorRect), plotToDeviceY(frequency, interiorRect)); linePath.lineTo(nextPoint); } painter.drawPath(linePath); } } void QtSLiMGraphView_FrequencyTrajectory::drawGraph(QPainter &painter, QRect interiorRect) { painter.setBrush(Qt::NoBrush); painter.setPen(QPen(Qt::black, 1.0)); // Go through all our history entries and draw a line for each. First we draw the ones in cold storage, then the active ones. if (plotLostMutations_) { if (useColorsForPlotting_) painter.setPen(QPen(Qt::red, 1.0)); for (MutationFrequencyHistory *history : frequencyHistoryColdStorageLost_) drawHistory(painter, history, interiorRect); } if (plotFixedMutations_) { if (useColorsForPlotting_) painter.setPen(QPen(QtSLiMColorWithRGB(0.4, 0.4, 1.0, 1.0), 1.0)); for (MutationFrequencyHistory *history : frequencyHistoryColdStorageFixed_) drawHistory(painter, history, interiorRect); } if (plotActiveMutations_) { if (useColorsForPlotting_) painter.setPen(QPen(Qt::black, 1.0)); for (auto history_pair : frequencyHistoryDict_) drawHistory(painter, history_pair.second, interiorRect); } } void QtSLiMGraphView_FrequencyTrajectory::toggleShowLostMutations(void) { plotLostMutations_ = !plotLostMutations_; update(); } void QtSLiMGraphView_FrequencyTrajectory::toggleShowFixedMutations(void) { plotFixedMutations_ = !plotFixedMutations_; update(); } void QtSLiMGraphView_FrequencyTrajectory::toggleShowActiveMutations(void) { plotActiveMutations_ = !plotActiveMutations_; update(); } void QtSLiMGraphView_FrequencyTrajectory::toggleUseColorsForPlotting(void) { useColorsForPlotting_ = !useColorsForPlotting_; update(); } void QtSLiMGraphView_FrequencyTrajectory::subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent * /* event */) { contextMenu.addAction(plotLostMutations_ ? "Hide Lost Mutations" : "Show Lost Mutations", this, &QtSLiMGraphView_FrequencyTrajectory::toggleShowLostMutations); contextMenu.addAction(plotFixedMutations_ ? "Hide Fixed Mutations" : "Show Fixed Mutations", this, &QtSLiMGraphView_FrequencyTrajectory::toggleShowFixedMutations); contextMenu.addAction(plotActiveMutations_ ? "Hide Active Mutations" : "Show Active Mutations", this, &QtSLiMGraphView_FrequencyTrajectory::toggleShowActiveMutations); contextMenu.addSeparator(); contextMenu.addAction(useColorsForPlotting_ ? "Black Plot Lines" : "Colored Plot Lines", this, &QtSLiMGraphView_FrequencyTrajectory::toggleUseColorsForPlotting); } QtSLiMLegendSpec QtSLiMGraphView_FrequencyTrajectory::legendKey(void) { if (!useColorsForPlotting_) return QtSLiMLegendSpec(); QtSLiMLegendSpec legend_key; if (plotLostMutations_) legend_key.emplace_back("lost", Qt::red); if (plotFixedMutations_) legend_key.emplace_back("fixed", QtSLiMColorWithRGB(0.4, 0.4, 1.0, 1.0)); if (plotActiveMutations_) legend_key.emplace_back("active", Qt::black); return legend_key; } bool QtSLiMGraphView_FrequencyTrajectory::providesStringForData(void) { return true; } void QtSLiMGraphView_FrequencyTrajectory::appendEntriesToString(std::vector &array, QString &string, slim_tick_t completedTicks) { for (MutationFrequencyHistory *history : array) { int entryCount = static_cast(history->entryCount); slim_tick_t baseTick = history->baseTick; for (slim_tick_t tick = 1; tick <= completedTicks; ++tick) { if (tick < baseTick) string.append("NA, "); else if (tick - baseTick < entryCount) string.append(QString("%1, ").arg(static_cast(history->entries[tick - baseTick]) / UINT16_MAX, 0, 'f', 4)); else string.append("NA, "); } string.append("\n"); } } void QtSLiMGraphView_FrequencyTrajectory::appendStringForData(QString &string) { Community *community = controller_->community; slim_tick_t completedTicks = community->Tick() - 1; if (plotLostMutations_) { string.append("# Lost mutations:\n"); appendEntriesToString(frequencyHistoryColdStorageLost_, string, completedTicks); string.append("\n\n"); } if (plotFixedMutations_) { string.append("# Fixed mutations:\n"); appendEntriesToString(frequencyHistoryColdStorageFixed_, string, completedTicks); string.append("\n\n"); } if (plotActiveMutations_) { string.append("# Active mutations:\n"); std::vector allActive; for (auto &pair_ref : frequencyHistoryDict_) allActive.emplace_back(pair_ref.second); appendEntriesToString(allActive, string, completedTicks); string.append("\n\n"); } } // // MutationFrequencyHistory // MutationFrequencyHistory::MutationFrequencyHistory(uint16_t value, const Mutation *mutation, slim_tick_t tick) { mutationID = mutation->mutation_id_; mutationType = mutation->mutation_type_ptr_; baseTick = tick; bufferSize = 0; entryCount = 0; entries = nullptr; addEntry(value); } MutationFrequencyHistory::~MutationFrequencyHistory() { if (entries) { free(entries); entries = nullptr; bufferSize = 0; entryCount = 0; } } ================================================ FILE: QtSLiM/QtSLiMGraphView_FrequencyTrajectory.h ================================================ // // QtSLiMGraphView_FrequencyTrajectory.h // SLiM // // Created by Ben Haller on 4/1/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_FREQUENCYTRAJECTORY_H #define QTSLIMGRAPHVIEW_FREQUENCYTRAJECTORY_H #include #include #include #include "QtSLiMGraphView.h" #include "mutation.h" #include "mutation_type.h" class MutationFrequencyHistory; class QtSLiMGraphView_FrequencyTrajectory : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_FrequencyTrajectory(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_FrequencyTrajectory() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event) override; virtual QString disableMessage(void) override; public slots: virtual void addedToWindow(void) override; virtual void invalidateCachedData(void) override; virtual void controllerRecycled(void) override; virtual void controllerTickFinished(void) override; virtual void updateAfterTick(void) override; void toggleShowLostMutations(void); void toggleShowFixedMutations(void); void toggleShowActiveMutations(void); void toggleUseColorsForPlotting(void); void subpopulationPopupChanged(int index); void mutationTypePopupChanged(int index); protected: virtual QtSLiMLegendSpec legendKey(void) override; private: // Mutation history storage std::unordered_map frequencyHistoryDict_; // unordered_map of active MutationFrequencyHistory objects, with slim_mutationid_t keys std::vector frequencyHistoryColdStorageLost_; // vector of MutationFrequencyHistory objects that have been lost std::vector frequencyHistoryColdStorageFixed_; // vector of MutationFrequencyHistory objects that have been fixed slim_tick_t lastTick_ = 0; // the last tick data was gathered for; used to detect a backward move in time bool justAddedToWindow_ = true; // true when the plot window has just been created // pop-up menu buttons QComboBox *subpopulationButton_ = nullptr; QComboBox *mutationTypeButton_ = nullptr; // The subpop and mutation type selected; -1 indicates no current selection (which will be fixed as soon as the menu is populated) slim_objectid_t selectedSubpopulationID_; int selectedMutationTypeIndex_; // User-selected display prefs bool plotLostMutations_ = false; bool plotFixedMutations_ = false; bool plotActiveMutations_ = false; bool useColorsForPlotting_ = false; void fetchDataForFinishedTick(void); void drawHistory(QPainter &painter, MutationFrequencyHistory *history, QRect interiorRect); void appendEntriesToString(std::vector &array, QString &string, slim_tick_t completedTicks); }; // We want to keep a history of frequency values for each mutation of the chosen mutation type in the // chosen subpopulation. The history of a mutation should persist after it has vanished, and if a // new mutation object gets allocated at the same memory location, it should be treated as a distinct // mutation; so we can't use pointers to identify mutations. Instead, we keep data on them using a // unique 64-bit ID generated only when SLiM is running under SLiMgui. At the end of a tick, // we loop through all mutations in the registry, and add an entry for that mutation in our data store. // This is probably O(n^2), but so it goes. It should only be used for mutation types that generate // few mutations; if somebody tries to plot every mutation in a common mutation-type, they will suffer. class MutationFrequencyHistory { public: // The 64-bit mutation ID is how we keep track of the mutation we reference; its pointer might go stale and be reused. slim_mutationid_t mutationID; // We keep a flag that we use to figure out if our mutation is dead; if it is, we can be moved into cold storage. bool updated; // Mostly we are just a malloced array of uint16s. The data we're storing is doubles, conceptually, but to minimize our memory footprint // (which might be very large!) we convert the doubles, which are guaranteed to be in the range [0.0, 1.0], to uint16s in the range // [0, UINT16_MAX] (65535). The buffer size is the number of entries allocated, the entry count is the number used so far, and the // base tick is the first tick recorded; the assumption is that entries are then sequential without gaps. uint32_t bufferSize, entryCount; slim_tick_t baseTick; uint16_t *entries; // Remember our mutation type so we can set our line color, etc., if we wish const MutationType *mutationType; public: MutationFrequencyHistory(uint16_t value, const Mutation *mutation, slim_tick_t tick); ~MutationFrequencyHistory(); inline void addEntry(uint16_t value) { if (entryCount == bufferSize) { // We need to expand bufferSize += 1024; entries = static_cast(realloc(entries, bufferSize * sizeof(uint16_t))); } entries[entryCount++] = value; updated = true; } }; #endif // QTSLIMGRAPHVIEW_FREQUENCYTRAJECTORY_H ================================================ FILE: QtSLiM/QtSLiMGraphView_LifetimeReproduction.cpp ================================================ // // QtSLiMGraphView_LifetimeReproduction.cpp // SLiM // // Created by Ben Haller on 11/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_LifetimeReproduction.h" #include #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "subpopulation.h" QtSLiMGraphView_LifetimeReproduction::QtSLiMGraphView_LifetimeReproduction(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 11; // max reproductive output (from 0 to 10); this rescales automatically allowBinCountRescale_ = false; original_x0_ = -1; // zero is included original_x1_ = histogramBinCount_ - 1; x0_ = original_x0_; x1_ = original_x1_; xAxisMin_ = x0_; xAxisMax_ = x1_; xAxisHistogramStyle_ = true; xAxisTickValuePrecision_ = 0; tweakXAxisTickLabelAlignment_ = true; xAxisLabel_ = "Lifetime reproduction"; yAxisLabel_ = "Frequency"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = false; showHorizontalGridLines_ = true; allowHorizontalGridChange_ = true; allowVerticalGridChange_ = false; allowFullBoxChange_ = true; selectedSubpopulation1ID_ = 1; } void QtSLiMGraphView_LifetimeReproduction::addedToWindow(void) { // Make our pop-up menu buttons QHBoxLayout *button_layout = buttonLayout(); if (button_layout) { subpopulation1Button_ = newButtonInLayout(button_layout); connect(subpopulation1Button_, QOverload::of(&QComboBox::currentIndexChanged), this, &QtSLiMGraphView_LifetimeReproduction::subpopulation1PopupChanged); addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); } } QtSLiMGraphView_LifetimeReproduction::~QtSLiMGraphView_LifetimeReproduction() { } void QtSLiMGraphView_LifetimeReproduction::subpopulation1PopupChanged(int /* index */) { slim_objectid_t newSubpopID = SLiMClampToObjectidType(subpopulation1Button_->currentData().toInt()); // don't react to non-changes and changes during rebuilds if (!rebuildingMenu_ && (selectedSubpopulation1ID_ != newSubpopID)) { selectedSubpopulation1ID_ = newSubpopID; // Reset our autoscaling x axis histogramBinCount_ = 11; xAxisMax_ = histogramBinCount_ - 1; original_x1_ = xAxisMax_; // the same as xAxisMax_, for base plots x1_ = original_x1_; invalidateCachedData(); update(); } } void QtSLiMGraphView_LifetimeReproduction::controllerRecycled(void) { if (!controller_->invalidSimulation()) update(); // Remake our popups, whether or not the controller is valid addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); // Reset our autoscaling x axis histogramBinCount_ = 11; xAxisMax_ = histogramBinCount_ - 1; original_x1_ = xAxisMax_; // the same as xAxisMax_, for base plots x1_ = original_x1_; // Reset our autoscaling y axis yAxisMax_ = 1.0; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 0.5; yAxisMinorTickInterval_ = 0.25; QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_LifetimeReproduction::graphTitle(void) { return "Lifetime Reproductive Output"; } QString QtSLiMGraphView_LifetimeReproduction::aboutString(void) { return "The Lifetime Reproductive Output graph shows the distribution of lifetime reproductive output within " "a chosen subpopulation, for individuals that died in the last tick. The x axis is individual " "lifetime reproductive output; the y axis is the frequency of a given lifetime reproductive output " "in the population, normalized to a total of 1.0."; } void QtSLiMGraphView_LifetimeReproduction::updateAfterTick(void) { // Rebuild the subpop and muttype menus; this has the side effect of checking and fixing our selections, and that, // in turn, will have the side effect of invaliding our cache and fetching new data if needed addSubpopulationsToMenu(subpopulation1Button_, selectedSubpopulation1ID_); invalidateCachedData(); QtSLiMGraphView::updateAfterTick(); } QString QtSLiMGraphView_LifetimeReproduction::disableMessage(void) { Species *graphSpecies = focalDisplaySpecies(); if (graphSpecies) { if (graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_) == nullptr) return "no\ndata"; } return ""; } void QtSLiMGraphView_LifetimeReproduction::drawGraph(QPainter &painter, QRect interiorRect) { int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); bool tallySexesSeparately = graphSpecies->sex_enabled_; double *reproductionDist = reproductionDistribution(&binCount, tallySexesSeparately); int totalBinCount = tallySexesSeparately ? (binCount * 2) : binCount; if (reproductionDist) { // rescale the x axis if needed if (binCount != histogramBinCount_) { histogramBinCount_ = binCount; xAxisMax_ = histogramBinCount_ - 1; original_x1_ = xAxisMax_; // the same as xAxisMax_, for base plots x1_ = original_x1_; invalidateCachedData(); } // rescale the y axis if needed double maxFreq = 0.000000001; // guarantee a non-zero axis range for (int binIndex = 0; binIndex < totalBinCount; ++binIndex) maxFreq = std::max(maxFreq, reproductionDist[binIndex]); double ceilingFreq = std::ceil(maxFreq * 5.0) / 5.0; // 0.2 / 0.4 / 0.6 / 0.8 / 1.0 if ((ceilingFreq > yAxisMax_) || ((ceilingFreq < yAxisMax_) && (maxFreq + 0.05 < ceilingFreq))) // require a margin of error to jump down { yAxisMax_ = ceilingFreq; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = ceilingFreq / 2.0; yAxisMinorTickInterval_ = ceilingFreq / 4.0; } // plot our histogram bars; note that xAxisMin_ is -1 so we use that as a minimum if (tallySexesSeparately) drawGroupedBarplot(painter, interiorRect, reproductionDist, 2, histogramBinCount_, -1.0, 1.0); else drawBarplot(painter, interiorRect, reproductionDist, histogramBinCount_, -1.0, 1.0); free(reproductionDist); } } QtSLiMLegendSpec QtSLiMGraphView_LifetimeReproduction::legendKey(void) { Species *graphSpecies = focalDisplaySpecies(); bool tallySexesSeparately = graphSpecies->sex_enabled_; if (tallySexesSeparately) { QtSLiMLegendSpec legend_key; legend_key.emplace_back("M", controller_->blackContrastingColorForIndex(0)); legend_key.emplace_back("F", controller_->blackContrastingColorForIndex(1)); return legend_key; } else { return QtSLiMLegendSpec(); } } bool QtSLiMGraphView_LifetimeReproduction::providesStringForData(void) { return true; } void QtSLiMGraphView_LifetimeReproduction::appendStringForData(QString &string) { int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); bool tallySexesSeparately = graphSpecies->sex_enabled_; double *reproductionDist = reproductionDistribution(&binCount, tallySexesSeparately); if (reproductionDist) { if (tallySexesSeparately) { string.append("M : "); for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(reproductionDist[i * 2], 0, 'f', 4)); string.append("\n\nF : "); for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(reproductionDist[i * 2 + 1], 0, 'f', 4)); } else { for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(reproductionDist[i], 0, 'f', 4)); } free(reproductionDist); } string.append("\n"); } double *QtSLiMGraphView_LifetimeReproduction::reproductionDistribution(int *binCount, bool tallySexesSeparately) { // Find our subpop Species *graphSpecies = focalDisplaySpecies(); Subpopulation *subpop1 = graphSpecies->SubpopulationWithID(selectedSubpopulation1ID_); if (!subpop1) return nullptr; // Find the maximum reproductive output and choose the new bin count int32_t maxReproduction = 0; for (int32_t reproduction : subpop1->lifetime_reproductive_output_F_) maxReproduction = std::max(maxReproduction, reproduction); for (int32_t reproduction : subpop1->lifetime_reproductive_output_MH_) maxReproduction = std::max(maxReproduction, reproduction); // compare to the logic in QtSLiMGraphView_AgeDistribution::ageDistribution(); // it is different here because reproduction count 0 gets a bin, so if the // max reproduction is 12, we need 13 bins; confusing! if (maxReproduction >= *binCount) *binCount = (int)(std::ceil(maxReproduction / 10.0) * 10.0 + 1); // +1 because zero gets an entry int newBinCount = *binCount; // Tally into our bins int totalBinCount = (tallySexesSeparately ? newBinCount * 2 : newBinCount); double *reproductionTallies = static_cast(calloc(totalBinCount, sizeof(double))); for (int reproduction : subpop1->lifetime_reproductive_output_F_) { if (tallySexesSeparately) reproductionTallies[reproduction * 2 + 1]++; else reproductionTallies[reproduction]++; } for (int reproduction : subpop1->lifetime_reproductive_output_MH_) { if (tallySexesSeparately) reproductionTallies[reproduction * 2]++; else reproductionTallies[reproduction]++; } // Normalize to 1 if (tallySexesSeparately) { // males double totalTallies = 0.0; for (int i = 0; i < newBinCount; ++i) totalTallies += reproductionTallies[i * 2]; if (totalTallies > 0.0) for (int i = 0; i < newBinCount; ++i) reproductionTallies[i * 2] /= totalTallies; // females totalTallies = 0.0; for (int i = 0; i < newBinCount; ++i) totalTallies += reproductionTallies[i * 2 + 1]; if (totalTallies > 0.0) for (int i = 0; i < newBinCount; ++i) reproductionTallies[i * 2 + 1] /= totalTallies; } else { double totalTallies = 0.0; for (int i = 0; i < newBinCount; ++i) totalTallies += reproductionTallies[i]; if (totalTallies > 0.0) for (int i = 0; i < newBinCount; ++i) reproductionTallies[i] /= totalTallies; } // Return the final tally; note that the caller takes ownership of the buffer return reproductionTallies; } ================================================ FILE: QtSLiM/QtSLiMGraphView_LifetimeReproduction.h ================================================ // // QtSLiMGraphView_LifetimeReproduction.h // SLiM // // Created by Ben Haller on 11/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_LIFETIMEREPRODUCTION_H #define QTSLIMGRAPHVIEW_LIFETIMEREPRODUCTION_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_LifetimeReproduction : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_LifetimeReproduction(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_LifetimeReproduction() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual QtSLiMLegendSpec legendKey(void) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual QString disableMessage(void) override; public slots: virtual void addedToWindow(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void subpopulation1PopupChanged(int index); private: // pop-up menu buttons QComboBox *subpopulation1Button_ = nullptr; // The subpop selected; -1 indicates no current selection (which will be fixed as soon as the menu is populated) slim_objectid_t selectedSubpopulation1ID_; double *reproductionDistribution(int *binCount, bool tallySexesSeparately); }; #endif // QTSLIMGRAPHVIEW_LIFETIMEREPRODUCTION_H ================================================ FILE: QtSLiM/QtSLiMGraphView_LossTimeHistogram.cpp ================================================ // // QtSLiMGraphView_LossTimeHistogram.cpp // SLiM // // Created by Ben Haller on 3/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_LossTimeHistogram.h" #include #include "QtSLiMWindow.h" QtSLiMGraphView_LossTimeHistogram::QtSLiMGraphView_LossTimeHistogram(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 10; //allowBinCountRescale_ = true; // not supported yet original_x1_ = 100; x1_ = original_x1_; xAxisMax_ = x1_; xAxisMajorTickInterval_ = 20; xAxisMinorTickInterval_ = 10; xAxisMajorTickModulus_ = 2; xAxisTickValuePrecision_ = 0; xAxisLabel_ = "Mutation loss time"; yAxisLabel_ = "Proportion of lost mutations"; allowXAxisUserRescale_ = false; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; } QtSLiMGraphView_LossTimeHistogram::~QtSLiMGraphView_LossTimeHistogram() { } QString QtSLiMGraphView_LossTimeHistogram::graphTitle(void) { return "Mutation Loss Time"; } QString QtSLiMGraphView_LossTimeHistogram::aboutString(void) { return "The Mutation Loss Time graph shows a histogram of mutation loss times, for " "those mutations that have been lost. The proportions are calculated and plotted " "separately for each mutation type, for comparison."; } double *QtSLiMGraphView_LossTimeHistogram::lossTimeData(void) { int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); slim_tick_t *histogram = graphSpecies->population_.mutation_loss_times_; int64_t histogramBins = static_cast(graphSpecies->population_.mutation_loss_tick_slots_); // fewer than binCount * mutationTypeCount may exist static double *rebin = nullptr; static size_t rebinBins = 0; size_t usedRebinBins = static_cast(binCount * mutationTypeCount); // re-bin for display; use double, normalize, etc. if (!rebin || (rebinBins < usedRebinBins)) { rebinBins = usedRebinBins; rebin = static_cast(realloc(rebin, rebinBins * sizeof(double))); } for (size_t i = 0; i < usedRebinBins; ++i) rebin[i] = 0.0; for (int i = 0; i < binCount; ++i) { for (int j = 0; j < mutationTypeCount; ++j) { int histIndex = j + i * mutationTypeCount; if (histIndex < histogramBins) rebin[histIndex] += histogram[histIndex]; } } // normalize within each mutation type for (int mutationTypeIndex = 0; mutationTypeIndex < mutationTypeCount; ++mutationTypeIndex) { int64_t total = 0; for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; total += static_cast(rebin[binIndex]); } for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; rebin[binIndex] /= static_cast(total); } } return rebin; } void QtSLiMGraphView_LossTimeHistogram::drawGraph(QPainter &painter, QRect interiorRect) { double *plotData = lossTimeData(); int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); // plot our histogram bars drawGroupedBarplot(painter, interiorRect, plotData, mutationTypeCount, binCount, 0.0, 10.0); } QtSLiMLegendSpec QtSLiMGraphView_LossTimeHistogram::legendKey(void) { return mutationTypeLegendKey(); // we use the prefab mutation type legend } bool QtSLiMGraphView_LossTimeHistogram::providesStringForData(void) { return true; } void QtSLiMGraphView_LossTimeHistogram::appendStringForData(QString &string) { double *plotData = lossTimeData(); int binCount = histogramBinCount_; Species *graphSpecies = focalDisplaySpecies(); int mutationTypeCount = static_cast(graphSpecies->mutation_types_.size()); for (auto mutationTypeIter : graphSpecies->mutation_types_) { MutationType *mutationType = mutationTypeIter.second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! string.append(QString("\"m%1\", ").arg(mutationType->mutation_type_id_)); for (int i = 0; i < binCount; ++i) { int histIndex = mutationTypeIndex + i * mutationTypeCount; string.append(QString("%1, ").arg(plotData[histIndex], 0, 'f', 4)); } string.append("\n"); } } ================================================ FILE: QtSLiM/QtSLiMGraphView_LossTimeHistogram.h ================================================ // // QtSLiMGraphView_LossTimeHistogram.h // SLiM // // Created by Ben Haller on 3/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_LOSSTIMEHISTOGRAM_H #define QTSLIMGRAPHVIEW_LOSSTIMEHISTOGRAM_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_LossTimeHistogram : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_LossTimeHistogram(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_LossTimeHistogram() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual QtSLiMLegendSpec legendKey(void) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; private: double *lossTimeData(void); }; #endif // QTSLIMGRAPHVIEW_LOSSTIMEHISTOGRAM_H ================================================ FILE: QtSLiM/QtSLiMGraphView_MultispeciesPopSizeOverTime.cpp ================================================ // // QtSLiMGraphView_MultispeciesPopSizeOverTime.cpp // SLiM // // Created by Ben Haller on 4/25/2022. // Copyright (c) 2022-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_MultispeciesPopSizeOverTime.h" #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" QtSLiMGraphView_MultispeciesPopSizeOverTime::QtSLiMGraphView_MultispeciesPopSizeOverTime(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { // super assumes that we are species-specific; we tell it we are not setFocalDisplaySpecies(nullptr); //setXAxisRangeFromTick(); // the end tick is not yet known setDefaultYAxisRange(); xAxisLabel_ = "Tick"; yAxisLabel_ = "Number of individuals"; allowXAxisUserRescale_ = true; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; tweakXAxisTickLabelAlignment_ = true; showSubpopulations_ = true; drawLines_ = true; QtSLiMGraphView_MultispeciesPopSizeOverTime::updateAfterTick(); } void QtSLiMGraphView_MultispeciesPopSizeOverTime::setDefaultYAxisRange(void) { original_y0_ = 0.0; original_y1_ = 100.0; // dynamic y0_ = original_y0_; y1_ = original_y1_; yAxisMin_ = y0_; yAxisMax_ = y1_; yAxisMajorTickInterval_ = 50; yAxisMinorTickInterval_ = 10; yAxisMajorTickModulus_ = 5; yAxisTickValuePrecision_ = 0; } QtSLiMGraphView_MultispeciesPopSizeOverTime::~QtSLiMGraphView_MultispeciesPopSizeOverTime() { // We are responsible for our own destruction QtSLiMGraphView_MultispeciesPopSizeOverTime::invalidateDrawingCache(); } void QtSLiMGraphView_MultispeciesPopSizeOverTime::invalidateDrawingCache(void) { delete drawingCache_; drawingCache_ = nullptr; drawingCacheTick_ = 0; } void QtSLiMGraphView_MultispeciesPopSizeOverTime::controllerRecycled(void) { if (!controller_->invalidSimulation()) { if (!yAxisIsUserRescaled_) setDefaultYAxisRange(); //if (!xAxisIsUserRescaled_) // setXAxisRangeFromTick(); // the end tick is not yet known update(); } QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_MultispeciesPopSizeOverTime::graphTitle(void) { return "Multispecies Population Size ~ Time"; } QString QtSLiMGraphView_MultispeciesPopSizeOverTime::aboutString(void) { return "The Multispecies Population Size ~ Time graph shows species (and subpopulation) " "size as a function of time. The size of each species is shown with a thick " "bright line, while those of subpopulations are shown with thinner pastel lines."; } void QtSLiMGraphView_MultispeciesPopSizeOverTime::updateAfterTick(void) { // BCH 3/20/2024: We set the x axis range each tick, because the end tick is now invalid until after initialize() callbacks if (!controller_->invalidSimulation() && !xAxisIsUserRescaled_) setXAxisRangeFromTick(); if (!controller_->invalidSimulation() && !yAxisIsUserRescaled_) { Community *community = controller_->community; slim_popsize_t maxHistory = 0; for (Species *species : community->all_species_) { Population &pop = species->population_; bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); for (auto history_record_iter : pop.subpop_size_histories_) { if (showSubpops || (history_record_iter.first == -1)) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; // find the min and max history value for (int i = 0; i < historyLength; ++i) maxHistory = std::max(maxHistory, history[i]); } } } // set axis range to encompass the data if (maxHistory > yAxisMax_) { if (maxHistory <= 1000) { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 100.0) * 100.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 200; yAxisMinorTickInterval_ = 100; yAxisMajorTickModulus_ = 2; } else if (maxHistory <= 10000) { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 1000.0) * 1000.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 2000; yAxisMinorTickInterval_ = 1000; yAxisMajorTickModulus_ = 2; } else if (maxHistory <= 100000) { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 10000.0) * 10000.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 20000; yAxisMinorTickInterval_ = 10000; yAxisMajorTickModulus_ = 2; } else { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 100000.0) * 100000.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 200000; yAxisMinorTickInterval_ = 100000; yAxisMajorTickModulus_ = 2; } QtSLiMGraphView_MultispeciesPopSizeOverTime::invalidateDrawingCache(); } } QtSLiMGraphView::updateAfterTick(); } void QtSLiMGraphView_MultispeciesPopSizeOverTime::drawPointGraph(QPainter &painter, QRect interiorRect) { Community *community = controller_->community; slim_tick_t completedTicks = community->Tick() - 1; // The tick counter can get set backwards, in which case our drawing cache is invalid – it contains drawing of things in the // future that may no longer happen. So we need to detect that case and invalidate our cache. if (!cachingNow_ && drawingCache_ && (drawingCacheTick_ > completedTicks)) { //qDebug() << "backward tick change detected, invalidating drawing cache"; invalidateDrawingCache(); } // If we're not caching, then: if our cache is invalid OR we have crossed a 1000-tick boundary since we last cached, cache an image if (!cachingNow_ && (!drawingCache_ || ((completedTicks / 1000) > (drawingCacheTick_ / 1000)))) { invalidateDrawingCache(); //qDebug() << "making new cache at tick " << community->Tick(); cachingNow_ = true; QPixmap *cache = new QPixmap(interiorRect.size()); cache->fill(Qt::transparent); // transparent so grid lines don't get overwritten by drawPixmap() QPainter cachePainter(cache); drawGraph(cachePainter, cache->rect()); drawingCache_ = cache; drawingCacheTick_ = completedTicks; cachingNow_ = false; } // Now draw our cache, if we have one if (drawingCache_) { //qDebug() << "drawing cache:" << drawingCache_->rect() << ", drawingCacheTick_ == " << drawingCacheTick_; painter.drawPixmap(interiorRect, *drawingCache_, drawingCache_->rect()); } // Draw the size history as a scatter plot; better suited to caching of the image for (Species *species : community->all_species_) { Population &pop = species->population_; bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); // First draw subpops, then draw the population size for (int iter = (showSubpops ? 0 : 1); iter <= 1; ++iter) { QColor speciesColor = controller_->qcolorForSpecies(species); QColor pointColor = (iter == 0) ? speciesColor.lighter(150) : speciesColor; for (auto history_record_iter : pop.subpop_size_histories_) { if (((iter == 0) && (history_record_iter.first != -1)) || ((iter == 1) && (history_record_iter.first == -1))) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; // If we're caching now, draw all points; otherwise, if we have a cache, draw only additional points slim_tick_t firstHistoryEntryToDraw = (cachingNow_ ? 0 : (drawingCache_ ? drawingCacheTick_ : 0)); for (slim_tick_t i = firstHistoryEntryToDraw; (i < historyLength) && (i < completedTicks); ++i) { slim_popsize_t historyEntry = history[i]; if (historyEntry != 0) { QPointF historyPoint(plotToDeviceX(i, interiorRect), plotToDeviceY(historyEntry, interiorRect)); painter.fillRect(QRectF(historyPoint.x() - 0.5, historyPoint.y() - 0.5, 1.0, 1.0), pointColor); } } } } } } } void QtSLiMGraphView_MultispeciesPopSizeOverTime::drawLineGraph(QPainter &painter, QRect interiorRect) { Community *community = controller_->community; for (Species *species : community->all_species_) { Population &pop = species->population_; slim_tick_t completedTicks = community->Tick() - 1; // Draw the size history as a line plot bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); // First draw subpops, then draw the population size for (int iter = (showSubpops ? 0 : 1); iter <= 1; ++iter) { QColor speciesColor = controller_->qcolorForSpecies(species); QColor lineColor = (iter == 0) ? speciesColor.lighter(150) : speciesColor; double lineWidth = (iter == 0) ? 1.0 : 1.5; for (auto history_record_iter : pop.subpop_size_histories_) { if (((iter == 0) && (history_record_iter.first != -1)) || ((iter == 1) && (history_record_iter.first == -1))) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; QPainterPath linePath; bool startedLine = false; for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) { slim_popsize_t historyEntry = history[i]; if (historyEntry == 0) { startedLine = false; } else { QPointF historyPoint(plotToDeviceX(i, interiorRect), plotToDeviceY(historyEntry, interiorRect)); if (startedLine) linePath.lineTo(historyPoint); else linePath.moveTo(historyPoint); startedLine = true; } } painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } } } } void QtSLiMGraphView_MultispeciesPopSizeOverTime::drawGraph(QPainter &painter, QRect interiorRect) { if (drawLines_) drawLineGraph(painter, interiorRect); else drawPointGraph(painter, interiorRect); } bool QtSLiMGraphView_MultispeciesPopSizeOverTime::providesStringForData(void) { return true; } void QtSLiMGraphView_MultispeciesPopSizeOverTime::appendStringForData(QString &string) { Community *community = controller_->community; slim_tick_t completedTicks = community->Tick() - 1; // Size history for (Species *species : community->all_species_) { QString speciesName = QString::fromStdString(species->name_); Population &pop = species->population_; bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); string.append(QString("\n\n# Size history (species %1):\n").arg(speciesName)); for (int iter = 0; iter <= (showSubpops ? 1 : 0); ++iter) { for (auto history_record_iter : pop.subpop_size_histories_) { if (((iter == 0) && (history_record_iter.first == -1)) || ((iter == 1) && (history_record_iter.first != -1))) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; if (iter == 1) string.append(QString("\n\n# Size history (subpopulation p%1):\n").arg(history_record_iter.first)); for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) string.append(QString("%1, ").arg(history[i])); string.append("\n"); } } } } } QtSLiMLegendSpec QtSLiMGraphView_MultispeciesPopSizeOverTime::legendKey(void) { Community *community = controller_->community; QtSLiMLegendSpec legend_key; for (Species *species : community->all_species_) { QString speciesName = QString::fromStdString(species->name_); QColor speciesColor = controller_->qcolorForSpecies(species); legend_key.emplace_back(speciesName, speciesColor); } return legend_key; } void QtSLiMGraphView_MultispeciesPopSizeOverTime::toggleShowSubpopulations(void) { showSubpopulations_ = !showSubpopulations_; invalidateDrawingCache(); update(); } void QtSLiMGraphView_MultispeciesPopSizeOverTime::toggleDrawLines(void) { drawLines_ = !drawLines_; invalidateDrawingCache(); update(); } void QtSLiMGraphView_MultispeciesPopSizeOverTime::subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent * /* event */) { contextMenu.addAction(showSubpopulations_ ? "Hide Subpopulations" : "Show Subpopulations", this, &QtSLiMGraphView_MultispeciesPopSizeOverTime::toggleShowSubpopulations); contextMenu.addAction(drawLines_ ? "Draw Points (Faster)" : "Draw Lines (Slower)", this, &QtSLiMGraphView_MultispeciesPopSizeOverTime::toggleDrawLines); } ================================================ FILE: QtSLiM/QtSLiMGraphView_MultispeciesPopSizeOverTime.h ================================================ // // QtSLiMGraphView_MultispeciesPopSizeOverTime.h // SLiM // // Created by Ben Haller on 4/25/2022. // Copyright (c) 2022-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_MULTISPECIESPOPSIZEOVERTIME_H #define QTSLIMGRAPHVIEW_MULTISPECIESPOPSIZEOVERTIME_H #include #include "QtSLiMGraphView.h" class QPixmap; class QtSLiMGraphView_MultispeciesPopSizeOverTime : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_MultispeciesPopSizeOverTime(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_MultispeciesPopSizeOverTime() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event) override; public slots: virtual void invalidateDrawingCache(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void toggleShowSubpopulations(void); void toggleDrawLines(void); protected: virtual QtSLiMLegendSpec legendKey(void) override; private: bool showSubpopulations_ = false; bool drawLines_ = false; QPixmap *drawingCache_ = nullptr; slim_tick_t drawingCacheTick_ = 0; void setDefaultYAxisRange(void); void drawPointGraph(QPainter &painter, QRect interiorRect); void drawLineGraph(QPainter &painter, QRect interiorRect); }; #endif // QTSLIMGRAPHVIEW_MULTISPECIESPOPSIZEOVERTIME_H ================================================ FILE: QtSLiM/QtSLiMGraphView_PopFitnessDist.cpp ================================================ // // QtSLiMGraphView_PopFitnessDist.cpp // SLiM // // Created by Ben Haller on 8/3/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_PopFitnessDist.h" #include "QtSLiMWindow.h" #include "species.h" #include "population.h" #include "subpopulation.h" #include "individual.h" #include #include QtSLiMGraphView_PopFitnessDist::QtSLiMGraphView_PopFitnessDist(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 50; allowBinCountRescale_ = true; original_x1_ = 2.0; x1_ = original_x1_; xAxisMax_ = x1_; xAxisMajorTickInterval_ = 1.0; xAxisMinorTickInterval_ = 0.2; xAxisMajorTickModulus_ = 5; xAxisTickValuePrecision_ = 1; xAxisLabel_ = "Fitness (rescaled)"; yAxisLabel_ = "Frequency"; allowXAxisUserRescale_ = true; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; } QtSLiMGraphView_PopFitnessDist::~QtSLiMGraphView_PopFitnessDist() { } QString QtSLiMGraphView_PopFitnessDist::graphTitle(void) { return "Population Fitness Distribution"; } QString QtSLiMGraphView_PopFitnessDist::aboutString(void) { return "The Population Fitness Distribution graph shows the distribution of fitness " "values across all individuals in the population, as a histogram. Fitness " "is 'rescaled' as explained in the Fitness ~ Time graph's about " "info. The number of histogram bins can be changed in the action menu. The " "Subpopulation Fitness Distributions graph provides an alternative that " "might also be useful."; } double *QtSLiMGraphView_PopFitnessDist::populationFitnessData(void) { int binCount = histogramBinCount_; static double *bins = nullptr; static size_t allocedBins = 0; if (!bins || (allocedBins < (size_t)binCount)) { allocedBins = binCount; bins = static_cast(realloc(bins, allocedBins * sizeof(double))); } for (int i = 0; i < binCount; ++i) bins[i] = 0.0; // bin fitness values from across the population Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; for (const std::pair &subpop_pair : pop.subpops_) { const Subpopulation *subpop = subpop_pair.second; for (const Individual *individual : subpop->parent_individuals_) { double fitness = individual->cached_unscaled_fitness_; int bin = (int)(((fitness - xAxisMin_) / (xAxisMax_ - xAxisMin_)) * binCount); if (bin < 0) bin = 0; if (bin >= binCount) bin = binCount - 1; bins[bin]++; } } // normalize the frequencies to a total of 1.0 double totalCount = 0.0; for (int i = 0; i < binCount; ++i) totalCount += bins[i]; if (totalCount == 0.0) totalCount = 1.0; // counts are all zero; prevent divide by zero below, get 0 instead for (int i = 0; i < binCount; ++i) bins[i] = bins[i] / totalCount; return bins; } void QtSLiMGraphView_PopFitnessDist::drawGraph(QPainter &painter, QRect interiorRect) { double *plotData = populationFitnessData(); int binCount = histogramBinCount_; // plot our histogram bars drawBarplot(painter, interiorRect, plotData, binCount, xAxisMin_, (xAxisMax_ - xAxisMin_) / binCount); } bool QtSLiMGraphView_PopFitnessDist::providesStringForData(void) { return true; } void QtSLiMGraphView_PopFitnessDist::appendStringForData(QString &string) { double *plotData = populationFitnessData(); int binCount = histogramBinCount_; for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(plotData[i], 0, 'f', 4)); string.append("\n"); } ================================================ FILE: QtSLiM/QtSLiMGraphView_PopFitnessDist.h ================================================ // // QtSLiMGraphView_PopFitnessDist.h // SLiM // // Created by Ben Haller on 8/3/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_POPFITNESSDIST_H #define QTSLIMGRAPHVIEW_POPFITNESSDIST_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_PopFitnessDist : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_PopFitnessDist(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_PopFitnessDist() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; private: double *populationFitnessData(void); }; #endif // QTSLIMGRAPHVIEW_POPFITNESSDIST_H ================================================ FILE: QtSLiM/QtSLiMGraphView_PopSizeOverTime.cpp ================================================ // // QtSLiMGraphView_PopSizeOverTime.cpp // SLiM // // Created by Ben Haller on 8/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_PopSizeOverTime.h" #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" QtSLiMGraphView_PopSizeOverTime::QtSLiMGraphView_PopSizeOverTime(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { //setXAxisRangeFromTick(); // the end tick is not yet known setDefaultYAxisRange(); xAxisLabel_ = "Tick"; yAxisLabel_ = "Number of individuals"; allowXAxisUserRescale_ = true; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; tweakXAxisTickLabelAlignment_ = true; showSubpopulations_ = true; drawLines_ = true; QtSLiMGraphView_PopSizeOverTime::updateAfterTick(); } void QtSLiMGraphView_PopSizeOverTime::setDefaultYAxisRange(void) { original_y0_ = 0.0; original_y1_ = 100.0; // dynamic y0_ = original_y0_; y1_ = original_y1_; yAxisMin_ = y0_; yAxisMax_ = y1_; yAxisMajorTickInterval_ = 50; yAxisMinorTickInterval_ = 10; yAxisMajorTickModulus_ = 5; yAxisTickValuePrecision_ = 0; } QtSLiMGraphView_PopSizeOverTime::~QtSLiMGraphView_PopSizeOverTime() { // We are responsible for our own destruction QtSLiMGraphView_PopSizeOverTime::invalidateDrawingCache(); } void QtSLiMGraphView_PopSizeOverTime::invalidateDrawingCache(void) { delete drawingCache_; drawingCache_ = nullptr; drawingCacheTick_ = 0; } void QtSLiMGraphView_PopSizeOverTime::controllerRecycled(void) { if (!controller_->invalidSimulation()) { if (!yAxisIsUserRescaled_) setDefaultYAxisRange(); //if (!xAxisIsUserRescaled_) // setXAxisRangeFromTick(); // the end tick is not yet known update(); } QtSLiMGraphView::controllerRecycled(); } QString QtSLiMGraphView_PopSizeOverTime::graphTitle(void) { return "Population Size ~ Time"; } QString QtSLiMGraphView_PopSizeOverTime::aboutString(void) { return "The Population Size ~ Time graph shows population (and subpopulation) size as a " "function of time. The size of the population is shown with a thick black line, " "while those of subpopulations are shown with thinner colored lines."; } void QtSLiMGraphView_PopSizeOverTime::updateAfterTick(void) { Species *graphSpecies = focalDisplaySpecies(); // BCH 3/20/2024: We set the x axis range each tick, because the end tick is now invalid until after initialize() callbacks if (!controller_->invalidSimulation() && graphSpecies && !xAxisIsUserRescaled_) setXAxisRangeFromTick(); if (!controller_->invalidSimulation() && graphSpecies && !yAxisIsUserRescaled_) { Population &pop = graphSpecies->population_; slim_popsize_t maxHistory = 0; bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); for (auto history_record_iter : pop.subpop_size_histories_) { if (showSubpops || (history_record_iter.first == -1)) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; // find the min and max history value for (int i = 0; i < historyLength; ++i) maxHistory = std::max(maxHistory, history[i]); } } // set axis range to encompass the data if (maxHistory > yAxisMax_) { if (maxHistory <= 1000) { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 100.0) * 100.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 200; yAxisMinorTickInterval_ = 100; yAxisMajorTickModulus_ = 2; } else if (maxHistory <= 10000) { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 1000.0) * 1000.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 2000; yAxisMinorTickInterval_ = 1000; yAxisMajorTickModulus_ = 2; } else if (maxHistory <= 100000) { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 10000.0) * 10000.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 20000; yAxisMinorTickInterval_ = 10000; yAxisMajorTickModulus_ = 2; } else { maxHistory = (slim_popsize_t)(std::ceil(maxHistory / 100000.0) * 100000.0); yAxisMax_ = maxHistory; original_y1_ = yAxisMax_; // the same as yAxisMax_, for base plots y1_ = original_y1_; yAxisMajorTickInterval_ = 200000; yAxisMinorTickInterval_ = 100000; yAxisMajorTickModulus_ = 2; } QtSLiMGraphView_PopSizeOverTime::invalidateDrawingCache(); } } QtSLiMGraphView::updateAfterTick(); } void QtSLiMGraphView_PopSizeOverTime::drawPointGraph(QPainter &painter, QRect interiorRect) { Community *community = controller_->community; Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; slim_tick_t completedTicks = community->Tick() - 1; // The tick counter can get set backwards, in which case our drawing cache is invalid – it contains drawing of things in the // future that may no longer happen. So we need to detect that case and invalidate our cache. if (!cachingNow_ && drawingCache_ && (drawingCacheTick_ > completedTicks)) { //qDebug() << "backward tick change detected, invalidating drawing cache"; invalidateDrawingCache(); } // If we're not caching, then: if our cache is invalid OR we have crossed a 1000-tick boundary since we last cached, cache an image if (!cachingNow_ && (!drawingCache_ || ((completedTicks / 1000) > (drawingCacheTick_ / 1000)))) { invalidateDrawingCache(); //qDebug() << "making new cache at tick " << community->Tick(); cachingNow_ = true; QPixmap *cache = new QPixmap(interiorRect.size()); cache->fill(Qt::transparent); // transparent so grid lines don't get overwritten by drawPixmap() QPainter cachePainter(cache); drawGraph(cachePainter, cache->rect()); drawingCache_ = cache; drawingCacheTick_ = completedTicks; cachingNow_ = false; } // Now draw our cache, if we have one if (drawingCache_) { //qDebug() << "drawing cache:" << drawingCache_->rect() << ", drawingCacheTick_ == " << drawingCacheTick_; painter.drawPixmap(interiorRect, *drawingCache_, drawingCache_->rect()); } // Draw the size history as a scatter plot; better suited to caching of the image bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); bool drawSubpopsGray = (showSubpops && (pop.subpop_size_histories_.size() > 8)); // 7 subpops + pop // First draw subpops, then draw the population size for (int iter = (showSubpops ? 0 : 1); iter <= 1; ++iter) { QColor pointColor = ((iter == 0) ? QtSLiMColorWithWhite(0.5, 1.0) : Qt::black); for (auto history_record_iter : pop.subpop_size_histories_) { if (((iter == 0) && (history_record_iter.first != -1)) || ((iter == 1) && (history_record_iter.first == -1))) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; if ((iter == 0) && !drawSubpopsGray) pointColor = controller_->whiteContrastingColorForIndex(history_record_iter.first); // If we're caching now, draw all points; otherwise, if we have a cache, draw only additional points slim_tick_t firstHistoryEntryToDraw = (cachingNow_ ? 0 : (drawingCache_ ? drawingCacheTick_ : 0)); for (slim_tick_t i = firstHistoryEntryToDraw; (i < historyLength) && (i < completedTicks); ++i) { slim_popsize_t historyEntry = history[i]; if (historyEntry != 0) { QPointF historyPoint(plotToDeviceX(i, interiorRect), plotToDeviceY(historyEntry, interiorRect)); painter.fillRect(QRectF(historyPoint.x() - 0.5, historyPoint.y() - 0.5, 1.0, 1.0), pointColor); } } } } } } void QtSLiMGraphView_PopSizeOverTime::drawLineGraph(QPainter &painter, QRect interiorRect) { Community *community = controller_->community; Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; slim_tick_t completedTicks = community->Tick() - 1; // Draw the size history as a line plot bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); bool drawSubpopsGray = (showSubpops && (pop.subpop_size_histories_.size() > 8)); // 7 subpops + pop // First draw subpops, then draw the population size for (int iter = (showSubpops ? 0 : 1); iter <= 1; ++iter) { QColor lineColor = (iter == 0) ? QtSLiMColorWithWhite(0.5, 1.0) : Qt::black; double lineWidth = (iter == 0) ? 1.0 : 1.5; for (auto history_record_iter : pop.subpop_size_histories_) { if (((iter == 0) && (history_record_iter.first != -1)) || ((iter == 1) && (history_record_iter.first == -1))) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; QPainterPath linePath; bool startedLine = false; for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) { slim_popsize_t historyEntry = history[i]; if (historyEntry == 0) { startedLine = false; } else { QPointF historyPoint(plotToDeviceX(i, interiorRect), plotToDeviceY(historyEntry, interiorRect)); if (startedLine) linePath.lineTo(historyPoint); else linePath.moveTo(historyPoint); startedLine = true; } } if ((iter == 0) && !drawSubpopsGray) lineColor = controller_->whiteContrastingColorForIndex(history_record_iter.first); painter.strokePath(linePath, QPen(lineColor, lineWidth)); } } } } void QtSLiMGraphView_PopSizeOverTime::drawGraph(QPainter &painter, QRect interiorRect) { if (drawLines_) drawLineGraph(painter, interiorRect); else drawPointGraph(painter, interiorRect); } bool QtSLiMGraphView_PopSizeOverTime::providesStringForData(void) { return true; } void QtSLiMGraphView_PopSizeOverTime::appendStringForData(QString &string) { Community *community = controller_->community; Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; slim_tick_t completedTicks = community->Tick() - 1; // Size history bool showSubpops = showSubpopulations_ && (pop.subpop_size_histories_.size() > 2); string.append("\n\n# Size history:\n"); for (int iter = 0; iter <= (showSubpops ? 1 : 0); ++iter) { for (auto history_record_iter : pop.subpop_size_histories_) { if (((iter == 0) && (history_record_iter.first == -1)) || ((iter == 1) && (history_record_iter.first != -1))) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; if (iter == 1) string.append(QString("\n\n# Size history (subpopulation p%1):\n").arg(history_record_iter.first)); for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) string.append(QString("%1, ").arg(history[i])); string.append("\n"); } } } } QtSLiMLegendSpec QtSLiMGraphView_PopSizeOverTime::legendKey(void) { if (!showSubpopulations_) return QtSLiMLegendSpec(); Species *graphSpecies = focalDisplaySpecies(); std::vector subpopsToDisplay; for (auto history_record_iter : graphSpecies->population_.subpop_size_histories_) subpopsToDisplay.emplace_back(history_record_iter.first); return subpopulationLegendKey(subpopsToDisplay, subpopsToDisplay.size() > 8); } void QtSLiMGraphView_PopSizeOverTime::toggleShowSubpopulations(void) { showSubpopulations_ = !showSubpopulations_; invalidateDrawingCache(); update(); } void QtSLiMGraphView_PopSizeOverTime::toggleDrawLines(void) { drawLines_ = !drawLines_; invalidateDrawingCache(); update(); } void QtSLiMGraphView_PopSizeOverTime::subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent * /* event */) { contextMenu.addAction(showSubpopulations_ ? "Hide Subpopulations" : "Show Subpopulations", this, &QtSLiMGraphView_PopSizeOverTime::toggleShowSubpopulations); contextMenu.addAction(drawLines_ ? "Draw Points (Faster)" : "Draw Lines (Slower)", this, &QtSLiMGraphView_PopSizeOverTime::toggleDrawLines); } ================================================ FILE: QtSLiM/QtSLiMGraphView_PopSizeOverTime.h ================================================ // // QtSLiMGraphView_PopSizeOverTime.h // SLiM // // Created by Ben Haller on 8/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_POPSIZEOVERTIME_H #define QTSLIMGRAPHVIEW_POPSIZEOVERTIME_H #include #include "QtSLiMGraphView.h" class QPixmap; class QtSLiMGraphView_PopSizeOverTime : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_PopSizeOverTime(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_PopSizeOverTime() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event) override; public slots: virtual void invalidateDrawingCache(void) override; virtual void controllerRecycled(void) override; virtual void updateAfterTick(void) override; void toggleShowSubpopulations(void); void toggleDrawLines(void); protected: virtual QtSLiMLegendSpec legendKey(void) override; private: bool showSubpopulations_ = false; bool drawLines_ = false; QPixmap *drawingCache_ = nullptr; slim_tick_t drawingCacheTick_ = 0; void setDefaultYAxisRange(void); void drawPointGraph(QPainter &painter, QRect interiorRect); void drawLineGraph(QPainter &painter, QRect interiorRect); }; #endif // QTSLIMGRAPHVIEW_POPSIZEOVERTIME_H ================================================ FILE: QtSLiM/QtSLiMGraphView_PopulationVisualization.cpp ================================================ // // QtSLiMGraphView_PopulationVisualization.cpp // SLiM // // Created by Ben Haller on 3/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_PopulationVisualization.h" #include #include #include #include #include #include #include #include #include "QtSLiMWindow.h" #include "subpopulation.h" // This define changes the visualization a little for use making Perry's icon; should be 0 otherwise #define PERRY_ICON 0 #if PERRY_ICON #warning PERRY_ICON should be 0! #endif QtSLiMGraphView_PopulationVisualization::QtSLiMGraphView_PopulationVisualization(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { showXAxis_ = false; showYAxis_ = false; } QtSLiMGraphView_PopulationVisualization::~QtSLiMGraphView_PopulationVisualization() { } QString QtSLiMGraphView_PopulationVisualization::graphTitle(void) { return "Population Visualization"; } QString QtSLiMGraphView_PopulationVisualization::aboutString(void) { return "The Population Visualization graph shows a visual depiction of the population structure of " "the model, at the current tick. Each subpopulation is shown as a circle, with size " "proportional to the number of individuals in the subpopulation, and color representing the " "mean fitness of the subpopulation. Arrows show migration between subpopulations, with " "the thickness of arrows representing the magnitude of migration."; } QRectF QtSLiMGraphView_PopulationVisualization::rectForSubpop(Subpopulation *subpop, QPointF center) { // figure out the right radius, clamped to reasonable limits slim_popsize_t subpopSize = subpop->parent_subpop_size_; slim_popsize_t clampedSubpopSize = subpopSize; if (clampedSubpopSize < 200) clampedSubpopSize = 200; if (clampedSubpopSize > 10000) clampedSubpopSize = 10000; double subpopRadius = sqrt(clampedSubpopSize) / 500; // size 10,000 has radius 0.2 if (subpop->gui_radius_scaling_from_user_) subpopRadius *= subpop->gui_radius_scaling_; return QRectF(center.x() - subpopRadius, center.y() - subpopRadius, 2 * subpopRadius, 2 * subpopRadius); } void QtSLiMGraphView_PopulationVisualization::drawSubpop(QPainter &painter, Subpopulation *subpop, slim_objectid_t subpopID, QPointF center) { // figure out the right radius, clamped to reasonable limits slim_popsize_t subpopSize = subpop->parent_subpop_size_; slim_popsize_t clampedSubpopSize = subpopSize; if (clampedSubpopSize < 200) clampedSubpopSize = 200; if (clampedSubpopSize > 10000) clampedSubpopSize = 10000; double subpopRadius = sqrt(clampedSubpopSize) / 500; // size 10,000 has radius 0.2 if (subpop->gui_radius_scaling_from_user_) subpopRadius *= subpop->gui_radius_scaling_; subpop->gui_radius_ = subpopRadius; // determine the color float colorRed = 0.0, colorGreen = 0.0, colorBlue = 0.0; if (subpop->gui_color_from_user_) { colorRed = subpop->gui_color_red_; colorGreen = subpop->gui_color_green_; colorBlue = subpop->gui_color_blue_; } else { // calculate the color from the mean fitness of the population // we normalize fitness values with subpopFitnessScaling so individual fitness, unscaled by subpopulation fitness, is used for coloring const double fitnessScalingFactor = 0.8; // used to be controller->fitnessColorScale; double fitness = ((subpopSize == 0) ? -10000.0 : subpop->parental_mean_unscaled_fitness_); RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, fitnessScalingFactor); } QColor color = QtSLiMColorWithRGB(static_cast(colorRed), static_cast(colorGreen), static_cast(colorBlue), 1.0); // draw the circle painter.setBrush(color); painter.setPen(QPen(Qt::black, 0.002)); painter.drawEllipse(center, subpopRadius, subpopRadius); // label it with the subpopulation ID #if !PERRY_ICON painter.setWorldMatrixEnabled(false); QString popString = QString("p%1").arg(subpopID); double brightness = static_cast(0.299f * colorRed + 0.587f * colorGreen + 0.114f * colorBlue); double scalingFromUser = (subpop->gui_radius_scaling_from_user_ ? subpop->gui_radius_scaling_ : 1.0); painter.setFont(labelFontOfPointSize(0.04 * scalingFactor_ * scalingFromUser)); painter.setPen((brightness > 0.5) ? Qt::black : Qt::white); painter.setBrush(Qt::NoBrush); QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, popString); QPointF drawPoint = painter.transform().map(center); drawPoint.setX(drawPoint.x() - labelBoundingRect.width() / 2.0 + 1.0); drawPoint.setY(drawPoint.y() + 0.008 * scalingFactor_ * scalingFromUser); painter.drawText(drawPoint, popString); painter.setWorldMatrixEnabled(true); #endif } void QtSLiMGraphView_PopulationVisualization::drawArrowFromSubpopToSubpop(QPainter &painter, Subpopulation *sourceSubpop, Subpopulation *destSubpop, double migrantFraction) { double destCenterX = destSubpop->gui_center_x_; double destCenterY = destSubpop->gui_center_y_; double sourceCenterX = sourceSubpop->gui_center_x_; double sourceCenterY = sourceSubpop->gui_center_y_; // we want to draw an arrow connecting these two subpops; first, we need to figure out the endpoints // they start and end a small fixed distance outside of the source/dest subpop circles double vectorDX = (destCenterX - sourceCenterX); double vectorDY = (destCenterY - sourceCenterY); double vectorLength = sqrt(vectorDX * vectorDX + vectorDY * vectorDY); double sourceEndWeight = (0.01 + sourceSubpop->gui_radius_) / vectorLength; double sourceEndX = sourceCenterX + (destCenterX - sourceCenterX) * sourceEndWeight; double sourceEndY = sourceCenterY + (destCenterY - sourceCenterY) * sourceEndWeight; double destEndWeight = (0.01 + destSubpop->gui_radius_) / vectorLength; double destEndX = destCenterX + (sourceCenterX - destCenterX) * destEndWeight; double destEndY = destCenterY + (sourceCenterY - destCenterY) * destEndWeight; // now, using those endpoints, we have a "partial vector" that goes from just outside the source subpop circle to // just outside the dest subpop circle; this partial vector will be the basis for the bezier that we draw, but we // need to calculate control points to make the bezier curve outward, using a perpendicular vector double partVecDX = destEndX - sourceEndX; // dx/dy for the partial vector from source to dest that we have just calculated double partVecDY = destEndY - sourceEndY; double partVecLength = sqrt(partVecDX * partVecDX + partVecDY * partVecDY); // the length of that partial vector double perpendicularFromSourceDX = partVecDY; // a vector perpendicular to that partial vector, by clockwise rotation double perpendicularFromSourceDY = -partVecDX; double controlPoint1X = sourceEndX + partVecDX * 0.3 + perpendicularFromSourceDX * 0.1; double controlPoint1Y = sourceEndY + partVecDY * 0.3 + perpendicularFromSourceDY * 0.1; double controlPoint2X = destEndX - partVecDX * 0.3 + perpendicularFromSourceDX * 0.1; double controlPoint2Y = destEndY - partVecDY * 0.3 + perpendicularFromSourceDY * 0.1; // now we figure out our line width, and we calculate a spatial translation of the bezier to shift in slightly off of // the midline, based on the line width, so that incoming and outgoing vectors do not overlap at the start/end points double lineWidth = 0.001 * (sqrt(migrantFraction) / 0.03); // non-linear line width scale #if PERRY_ICON double finalShiftMagnitude = 0.0; #else double finalShiftMagnitude = std::max(lineWidth * 0.75, 0.010); #endif double finalShiftX = perpendicularFromSourceDX * finalShiftMagnitude / partVecLength; double finalShiftY = perpendicularFromSourceDY * finalShiftMagnitude / partVecLength; double arrowheadSize = std::max(lineWidth * 1.5, 0.008); // we have to use a clipping path to cut back the destination end of the vector, to make room for the arrowhead painter.save(); double clipRadius = vectorLength - (destSubpop->gui_radius_ + arrowheadSize + 0.01); QRectF clipCircle = QRectF(sourceCenterX - clipRadius, sourceCenterY - clipRadius, clipRadius * 2, clipRadius * 2); QPainterPath clipBezier; clipBezier.addEllipse(clipCircle); painter.setClipPath(clipBezier, Qt::IntersectClip); // now draw the curved line connecting the subpops QPainterPath bezierLines; double shiftedSourceEndX = sourceEndX + finalShiftX, shiftedSourceEndY = sourceEndY + finalShiftY; double shiftedDestEndX = destEndX + finalShiftX, shiftedDestEndY = destEndY + finalShiftY; double shiftedControl1X = controlPoint1X + finalShiftX, shiftedControl1Y = controlPoint1Y + finalShiftY; double shiftedControl2X = controlPoint2X + finalShiftX, shiftedControl2Y = controlPoint2Y + finalShiftY; #if PERRY_ICON bezierLines.moveTo(QPointF(shiftedSourceEndX, shiftedSourceEndY)); bezierLines.lineTo(QPointF(shiftedDestEndX, shiftedDestEndY)); #else bezierLines.moveTo(QPointF(shiftedSourceEndX, shiftedSourceEndY)); bezierLines.cubicTo(QPointF(shiftedControl1X, shiftedControl1Y), QPointF(shiftedControl2X, shiftedControl2Y), QPointF(shiftedDestEndX, shiftedDestEndY)); #endif painter.strokePath(bezierLines, QPen(Qt::black, lineWidth)); // restore the clipping path painter.restore(); #if !PERRY_ICON // draw the arrowhead; this is oriented along the line from (shiftedDestEndX, shiftedDestEndY) to (shiftedControl2X, shiftedControl2Y), // of length partVecLength, and is calculated using a perpendicular off of that vector QPainterPath bezierArrowheads; double angleCorrectionFactor = (arrowheadSize / partVecLength) * 0.5; // large arrowheads need to be tilted closer to the source-dest pop line double headInsideX = shiftedControl2X * (1 - angleCorrectionFactor) + shiftedSourceEndX * angleCorrectionFactor; double headInsideY = shiftedControl2Y * (1 - angleCorrectionFactor) + shiftedSourceEndY * angleCorrectionFactor; double headMidlineDX = headInsideX - shiftedDestEndX, headMidlineDY = headInsideY - shiftedDestEndY; double headMidlineLength = sqrt(headMidlineDX * headMidlineDX + headMidlineDY * headMidlineDY); double headMidlineNormDX = (headMidlineDX / headMidlineLength) * arrowheadSize; // normalized to have length arrowheadSize double headMidlineNormDY = (headMidlineDY / headMidlineLength) * arrowheadSize; double headPerpendicular1DX = headMidlineNormDY, headPerpendicular1DY = -headMidlineNormDX; // perpendicular to the normalized midline double headPerpendicular2DX = -headMidlineNormDY, headPerpendicular2DY = headMidlineNormDX; // just the negation of perpendicular 1 bezierArrowheads.moveTo(shiftedDestEndX, shiftedDestEndY); bezierArrowheads.lineTo(shiftedDestEndX + headMidlineNormDX * 1.75 + headPerpendicular1DX * 0.9, shiftedDestEndY + headMidlineNormDY * 1.75 + headPerpendicular1DY * 0.9); bezierArrowheads.lineTo(shiftedDestEndX + headMidlineNormDX * 1.2, shiftedDestEndY + headMidlineNormDY * 1.2); bezierArrowheads.lineTo(shiftedDestEndX + headMidlineNormDX * 1.75 + headPerpendicular2DX * 0.9, shiftedDestEndY + headMidlineNormDY * 1.75 + headPerpendicular2DY * 0.9); bezierArrowheads.closeSubpath(); painter.fillPath(bezierArrowheads, Qt::black); #endif } static bool is_line_intersection(double p0_x, double p0_y, double p1_x, double p1_y, double p2_x, double p2_y, double p3_x, double p3_y) { double s02_x, s02_y, s10_x, s10_y, s32_x, s32_y, s_numer, t_numer, denom; s10_x = p1_x - p0_x; s10_y = p1_y - p0_y; s32_x = p3_x - p2_x; s32_y = p3_y - p2_y; denom = s10_x * s32_y - s32_x * s10_y; if (fabs(denom - 0) < 0.00001) return false; // Collinear bool denomPositive = denom > 0; s02_x = p0_x - p2_x; s02_y = p0_y - p2_y; s_numer = s10_x * s02_y - s10_y * s02_x; if ((s_numer < 0) == denomPositive) return false; // No collision t_numer = s32_x * s02_y - s32_y * s02_x; if ((t_numer < 0) == denomPositive) return false; // No collision if (((s_numer > denom) == denomPositive) || ((t_numer > denom) == denomPositive)) return false; // No collision return true; } double QtSLiMGraphView_PopulationVisualization::scorePositions(double *center_x, double *center_y, bool *connected, size_t subpopCount) { double score = 0.0; double meanEdge = 0.0; int edgeCount = 0; double minx = std::numeric_limits::infinity(), maxy = -std::numeric_limits::infinity(); // First we calculate the mean edge length; we will consider this the optimum length for (size_t subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { double xc = center_x[subpopIndex]; double yc = center_y[subpopIndex]; // If any node has a NaN value, that is an immediate disqualifier; I'm not sure how it happens, but it occasionally does if (std::isnan(xc) || std::isnan(yc)) return -100000000; if (xc < minx) minx = xc; if (yc > maxy) maxy = yc; for (size_t sourceIndex = subpopIndex + 1; sourceIndex < subpopCount; ++sourceIndex) { if (connected[subpopIndex * subpopCount + sourceIndex]) { double dx = xc - center_x[sourceIndex]; double dy = yc - center_y[sourceIndex]; double distanceSquared = dx * dx + dy * dy; double distance = sqrt(distanceSquared); meanEdge += distance; edgeCount++; } } } meanEdge /= edgeCount; // Add a little score if the first subpop is near the upper left if ((fabs(center_x[0] - minx) < 0.05) && (fabs(center_y[0] - maxy) < 0.05)) { score += 0.01; // Add a little more score if the second subpop is to its right in roughly the same row if ((center_x[1] - center_x[0] > meanEdge/2) && (fabs(center_y[0] - center_y[1]) < 0.05)) score += 0.01; } // Score distances and crossings for (size_t subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { double xc = center_x[subpopIndex]; double yc = center_y[subpopIndex]; for (size_t sourceIndex = subpopIndex + 1; sourceIndex < subpopCount; ++sourceIndex) { double dx = xc - center_x[sourceIndex]; double dy = yc - center_y[sourceIndex]; double distanceSquared = dx * dx + dy * dy; double distance = sqrt(distanceSquared); // being closer than k invokes a penalty if (distance < meanEdge) score -= (meanEdge - distance); // on the other hand, distance between connected subpops is very bad; this is above all what we want to optimize if (connected[subpopIndex * subpopCount + sourceIndex]) { if (distance > meanEdge) score -= (distance - meanEdge); // Detect crossings for (size_t secondSubpop = subpopIndex + 1; secondSubpop < subpopCount; ++secondSubpop) for (size_t secondSource = secondSubpop + 1; secondSource < subpopCount; ++secondSource) if (connected[secondSubpop * subpopCount + secondSource]) { double x0 = xc, x1 = center_x[sourceIndex], x2 = center_x[secondSubpop], x3 = center_x[secondSource]; double y0 = yc, y1 = center_y[sourceIndex], y2 = center_y[secondSubpop], y3 = center_y[secondSource]; // I test intersection with slightly shortened line segments, because I don't want endpoints that touch to be marked as intersections if (is_line_intersection(x0*0.99 + x1*0.01, y0*0.99 + y1*0.01, x0*0.01 + x1*0.99, y0*0.01 + y1*0.99, x2*0.99 + x3*0.01, y2*0.99 + y3*0.01, x2*0.01 + x3*0.99, y2*0.01 + y3*0.99)) score -= 100; } } } } return score; } // This is a simple implementation of the algorithm of Fruchterman and Reingold 1991; // there are better algorithms out there, but this one is simple... void QtSLiMGraphView_PopulationVisualization::optimizePositions(void) { Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; size_t subpopCount = pop.subpops_.size(); if (subpopCount == 0) return; double layout_width = 0.58, layout_length = 0.58; // allows for the radii of the vertices at max subpop size double area = layout_width * layout_length; double k = sqrt(area / subpopCount); double kSquared = k * k; bool *connected; connected = static_cast(calloc(subpopCount * subpopCount, sizeof(bool))); // We start by figuring out connectivity auto subpopIter = pop.subpops_.begin(); for (size_t subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; for (const std::pair &fractions_pair : subpop->migrant_fractions_) { slim_objectid_t migrant_source_id = fractions_pair.first; // We need to get from the source ID to the index of the source subpop in the pop array auto sourceIter = pop.subpops_.begin(); size_t sourceIndex; for (sourceIndex = 0; sourceIndex < subpopCount; ++sourceIndex) { if ((*sourceIter).first == migrant_source_id) break; ++sourceIter; } if (sourceIndex == subpopCount) { free(connected); return; } // Mark the connection bidirectionally connected[subpopIndex * subpopCount + sourceIndex] = true; connected[sourceIndex * subpopCount + subpopIndex] = true; } ++subpopIter; } double *pos_x, *pos_y; // vertex positions double *disp_x, *disp_y; // vertex forces/displacements double *best_x, *best_y; // best vertex positions from multiple runs double best_score = -std::numeric_limits::infinity(); pos_x = static_cast(malloc(sizeof(double) * subpopCount)); pos_y = static_cast(malloc(sizeof(double) * subpopCount)); disp_x = static_cast(malloc(sizeof(double) * subpopCount)); disp_y = static_cast(malloc(sizeof(double) * subpopCount)); best_x = static_cast(malloc(sizeof(double) * subpopCount)); best_y = static_cast(malloc(sizeof(double) * subpopCount)); // We do multiple separate runs from different starting configurations, to try to find the optimal solution for (int trialIteration = 0; trialIteration < 50; ++trialIteration) { double temperature = layout_width / 5.0; // initialize positions; this is basically the G := (V,E) step of the Fruchterman & Reingold algorithm for (size_t subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { pos_x[subpopIndex] = (random() / static_cast(INT32_MAX)) * layout_width - layout_width/2; pos_y[subpopIndex] = (random() / static_cast(INT32_MAX)) * layout_length - layout_length/2; } // Then we do the core loop of the Fruchterman & Reingold algorithm, which calculates forces and displacements for (int optimizeIteration = 1; optimizeIteration < 1000; ++optimizeIteration) { // Calculate repulsive forces for (size_t v = 0; v < subpopCount; ++v) { disp_x[v] = 0.0; disp_y[v] = 0.0; for (size_t u = 0; u < subpopCount; ++u) { if (u != v) { double delta_x = pos_x[v] - pos_x[u]; double delta_y = pos_y[v] - pos_y[u]; double delta_magnitude_squared = delta_x * delta_x + delta_y * delta_y; double multiplier = kSquared / delta_magnitude_squared; // This is a speed-optimized version of the pseudocode version commented out below disp_x[v] += delta_x * multiplier; disp_y[v] += delta_y * multiplier; //double delta_magnitude = sqrt(delta_magnitude_squared); //disp_x[v] += (delta_x / delta_magnitude) * (kSquared / delta_magnitude); //disp_y[v] += (delta_y / delta_magnitude) * (kSquared / delta_magnitude); } } } // Calculate attractive forces for (size_t v = 0; v < subpopCount; ++v) { for (size_t u = v + 1; u < subpopCount; ++u) { if (connected[v * subpopCount + u]) { // There is an edge between u and v double delta_x = pos_x[v] - pos_x[u]; double delta_y = pos_y[v] - pos_y[u]; double delta_magnitude_squared = delta_x * delta_x + delta_y * delta_y; double delta_magnitude = sqrt(delta_magnitude_squared); double multiplier = (delta_magnitude_squared / k) / delta_magnitude; double delta_multiplier_x = delta_x * multiplier; double delta_multiplier_y = delta_y * multiplier; disp_x[v] -= delta_multiplier_x; disp_y[v] -= delta_multiplier_y; disp_x[u] += delta_multiplier_x; disp_y[u] += delta_multiplier_y; } } } // Limit max displacement to temperature t and prevent displacement outside frame for (size_t v = 0; v < subpopCount; ++v) { double delta_x = disp_x[v]; double delta_y = disp_y[v]; double delta_magnitude_squared = delta_x * delta_x + delta_y * delta_y; double delta_magnitude = sqrt(delta_magnitude_squared); if (delta_magnitude < temperature) { pos_x[v] += disp_x[v]; pos_y[v] += disp_y[v]; } else { pos_x[v] += (disp_x[v] / delta_magnitude) * temperature; pos_y[v] += (disp_y[v] / delta_magnitude) * temperature; } if (pos_x[v] < -layout_width/2) pos_x[v] = -layout_width/2; if (pos_y[v] < -layout_length/2) pos_y[v] = -layout_length/2; if (pos_x[v] > layout_width/2) pos_x[v] = layout_width/2; if (pos_y[v] > layout_length/2) pos_y[v] = layout_length/2; } // reduce the temperature as the layout approaches a better configuration // Fruchterman & Reingold are vague about exactly what they did here, but there is a rapid cooling phase (quenching) // and then a constant low-temperature phase (simmering); I've taken a guess at what that might look like temperature = temperature * 0.95; if (temperature < 0.002) temperature = 0.002; } // Test the final candidate and keep the best candidate double candidate_score = scorePositions(pos_x, pos_y, connected, subpopCount); if (candidate_score > best_score) { for (size_t v = 0; v < subpopCount; ++v) { best_x[v] = pos_x[v]; best_y[v] = pos_y[v]; } best_score = candidate_score; //NSLog(@"better candidate, new score == %f", best_score); } } // Finally, we set the positions we have arrived at back into the subpops subpopIter = pop.subpops_.begin(); for (size_t subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; subpop->gui_center_x_ = best_x[subpopIndex] + 0.5; subpop->gui_center_y_ = best_y[subpopIndex] + 0.5; subpop->gui_center_from_user_ = false; // optimization overrides previously set display settings ++subpopIter; } free(pos_x); free(pos_y); free(disp_x); free(disp_y); free(connected); free(best_x); free(best_y); } void QtSLiMGraphView_PopulationVisualization::drawGraph(QPainter &painter, QRect interiorRect) { Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; int subpopCount = static_cast(pop.subpops_.size()); Community &community = graphSpecies->community_; if (subpopCount == 0) { // this is an ugly hack that assumes things about QtSLiMGraphView's implementation // we restore() twice to get back to the original coordinate system for drawMessage() // then we save() twice so that the expected number of pops are still available painter.restore(); painter.restore(); drawMessage(painter, "no subpopulations", rect()); painter.save(); painter.save(); return; } // First, we transform our coordinate system so that a square of size (1,1) fits maximally and centered painter.save(); QTransform transform; if (interiorRect.width() > interiorRect.height()) { transform.translate(interiorRect.x(), interiorRect.y()); transform.translate(SLIM_SCREEN_ROUND((interiorRect.width() - interiorRect.height()) / 2.0), 0); scalingFactor_ = interiorRect.height(); } else { transform.translate(interiorRect.x(), interiorRect.y()); transform.translate(0, SLIM_SCREEN_ROUND((interiorRect.height() - interiorRect.width()) / 2.0)); scalingFactor_ = interiorRect.width(); } transform.scale(scalingFactor_, scalingFactor_); painter.setWorldTransform(transform, true); // test frame //painter.setBrush(Qt::NoBrush); //painter.setPen(QPen(Qt::black, 0.002)); //painter.drawRect(QRect(0, 0, 1, 1)); if (subpopCount == 1) { auto subpopIter = pop.subpops_.begin(); // a single subpop is shown as a circle at the center drawSubpop(painter, (*subpopIter).second, (*subpopIter).first, QPointF(0.5, 0.5)); } else if (subpopCount > 1) { // first we distribute our subpops in a ring bool allUserConfigured = true; { auto subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; double theta = (M_PI * 2.0 / subpopCount) * subpopIndex + M_PI_2; if (!subpop->gui_center_from_user_) { subpop->gui_center_x_ = 0.5 - cos(theta) * 0.29; subpop->gui_center_y_ = 0.5 + sin(theta) * 0.29; allUserConfigured = false; } ++subpopIter; } } // if position optimization is on, we do that to optimize the positions of the subpops if ((community.ModelType() == SLiMModelType::kModelTypeWF) && optimizePositions_ && (subpopCount > 2)) optimizePositions(); if (!allUserConfigured) { // then do some sizing, to figure out the maximum extent of our subpops QRectF boundingBox = QRectF(); { auto subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; QPointF center(subpop->gui_center_x_, subpop->gui_center_y_); QRectF subpopRect = rectForSubpop(subpop, center); boundingBox = ((subpopIndex == 0) ? subpopRect : boundingBox.united(subpopRect)); ++subpopIter; } } // then we translate our coordinate system so that the subpops are centered within our (0, 0, 1, 1) box double offsetX = ((1.0 - boundingBox.width()) / 2.0) - boundingBox.x(); double offsetY = ((1.0 - boundingBox.height()) / 2.0) - boundingBox.y(); QTransform offsetTransform; offsetTransform.translate(offsetX, offsetY); painter.setWorldTransform(offsetTransform, true); } // then we draw the subpops { auto subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; slim_objectid_t subpopID = (*subpopIter).first; QPointF center(subpop->gui_center_x_, subpop->gui_center_y_); drawSubpop(painter, subpop, subpopID, center); ++subpopIter; } } // in the multipop case, we need to draw migration arrows, too { for (auto destSubpopIter : pop.subpops_) { Subpopulation *destSubpop = destSubpopIter.second; std::map &destMigrants = (community.ModelType() == SLiMModelType::kModelTypeWF) ? destSubpop->migrant_fractions_ : destSubpop->gui_migrants_; for (auto sourceSubpopIter : destMigrants) { slim_objectid_t sourceSubpopID = sourceSubpopIter.first; Subpopulation *sourceSubpop = graphSpecies->SubpopulationWithID(sourceSubpopID); if (sourceSubpop) { double migrantFraction = sourceSubpopIter.second; // The gui_migrants_ map is raw migration counts, which need to be converted to a fraction of the sourceSubpop pre-migration size if (community.ModelType() == SLiMModelType::kModelTypeNonWF) { if (sourceSubpop->gui_premigration_size_ <= 0) continue; migrantFraction /= sourceSubpop->gui_premigration_size_; if (migrantFraction < 0.0) migrantFraction = 0.0; if (migrantFraction > 1.0) migrantFraction = 1.0; } if (migrationArrows_ == 1) drawArrowFromSubpopToSubpop(painter, sourceSubpop, destSubpop, 0.002); // thin arrows; 0.002 is arbitrary but seems reasonable else if (migrationArrows_ == 2) drawArrowFromSubpopToSubpop(painter, sourceSubpop, destSubpop, migrantFraction); // migration-scaled arrow thickness } } } } } // We're done with our transformed coordinate system painter.restore(); } void QtSLiMGraphView_PopulationVisualization::toggleOptimizedPositions(void) { optimizePositions_ = !optimizePositions_; update(); } void QtSLiMGraphView_PopulationVisualization::subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent * /* event */) { QAction *menuItem = contextMenu.addAction(optimizePositions_ ? "Standard Positions" : "Optimized Positions", this, &QtSLiMGraphView_PopulationVisualization::toggleOptimizedPositions); Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; // If any subpop has a user-defined center, disable position optimization; it doesn't know how to // handle those, and there's no way to revert back after it messes things up, and so forth bool userDefinedCenter = false; for (auto subpopIter : pop.subpops_) if (subpopIter.second->gui_center_from_user_) { userDefinedCenter = true; break; } menuItem->setEnabled(!userDefinedCenter); // Offer a choice regarding the migration arrow style, since they can clutter things up a lot contextMenu.addSeparator(); QAction *noMigAction = contextMenu.addAction("No Migration Arrows", this, [this]() { migrationArrows_ = 0; update(); }); QAction *thinMigAction = contextMenu.addAction("Thin Migration Arrows", this, [this]() { migrationArrows_ = 1; update(); }); QAction *fullMigAction = contextMenu.addAction("Full Migration Arrows", this, [this]() { migrationArrows_ = 2; update(); }); noMigAction->setCheckable(true); thinMigAction->setCheckable(true); fullMigAction->setCheckable(true); noMigAction->setChecked(migrationArrows_ == 0); thinMigAction->setChecked(migrationArrows_ == 1); fullMigAction->setChecked(migrationArrows_ == 2); } void QtSLiMGraphView_PopulationVisualization::appendStringForData(QString & /* string */) { // No data string } ================================================ FILE: QtSLiM/QtSLiMGraphView_PopulationVisualization.h ================================================ // // QtSLiMGraphView_PopulationVisualization.h // SLiM // // Created by Ben Haller on 3/30/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_POPULATIONVISUALIZATION_H #define QTSLIMGRAPHVIEW_POPULATIONVISUALIZATION_H #include #include "QtSLiMGraphView.h" class QtSLiMGraphView_PopulationVisualization : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_PopulationVisualization(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_PopulationVisualization() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual void subclassAddItemsToMenu(QMenu &contextMenu, QContextMenuEvent *p_event) override; virtual void appendStringForData(QString &string) override; public slots: void toggleOptimizedPositions(void); private: double scalingFactor_; bool optimizePositions_ = false; double scorePositions(double *center_x, double *center_y, bool *connected, size_t subpopCount); void optimizePositions(void); int migrationArrows_ = 2; // 0 == none, 1 == thin, 2 == full QRectF rectForSubpop(Subpopulation *subpop, QPointF center); void drawSubpop(QPainter &painter, Subpopulation *subpop, slim_objectid_t subpopID, QPointF center); void drawArrowFromSubpopToSubpop(QPainter &painter, Subpopulation *sourceSubpop, Subpopulation *destSubpop, double migrantFraction); }; #endif // QTSLIMGRAPHVIEW_POPULATIONVISUALIZATION_H ================================================ FILE: QtSLiM/QtSLiMGraphView_SubpopFitnessDists.cpp ================================================ // // QtSLiMGraphView_SubpopFitnessDists.cpp // SLiM // // Created by Ben Haller on 8/8/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMGraphView_SubpopFitnessDists.h" #include "QtSLiMWindow.h" #include "species.h" #include "population.h" #include "subpopulation.h" #include "individual.h" #include #include #include QtSLiMGraphView_SubpopFitnessDists::QtSLiMGraphView_SubpopFitnessDists(QWidget *p_parent, QtSLiMWindow *controller) : QtSLiMGraphView(p_parent, controller) { histogramBinCount_ = 50; allowBinCountRescale_ = true; original_x1_ = 2.0; x1_ = original_x1_; xAxisMax_ = x1_; xAxisMajorTickInterval_ = 1.0; xAxisMinorTickInterval_ = 0.2; xAxisMajorTickModulus_ = 5; xAxisTickValuePrecision_ = 1; xAxisLabel_ = "Fitness (rescaled)"; yAxisLabel_ = "Frequency"; allowXAxisUserRescale_ = true; allowYAxisUserRescale_ = true; showHorizontalGridLines_ = true; } QtSLiMGraphView_SubpopFitnessDists::~QtSLiMGraphView_SubpopFitnessDists() { } QString QtSLiMGraphView_SubpopFitnessDists::graphTitle(void) { return "Subpopulation Fitness Distributions"; } QString QtSLiMGraphView_SubpopFitnessDists::aboutString(void) { return "The Subpopulation Fitness Distributions graph shows the distribution of fitness " "values for each subpopulation as a separate line. The primary purpose of this " "visualization is to allow the fitness distributions of many subpopulations " "to be compared visually. Fitness is 'rescaled' as explained in the " "Fitness ~ Time graph's about info. The number of histogram bins can be changed " "in the action menu. The Population Fitness Distribution graph provides an " "alternative that might also be useful."; } double *QtSLiMGraphView_SubpopFitnessDists::subpopulationFitnessData(const Subpopulation *requestedSubpop) { int binCount = histogramBinCount_; static double *bins = nullptr; static size_t allocedBins = 0; if (!bins || (allocedBins < (size_t)binCount)) { allocedBins = binCount; bins = static_cast(realloc(bins, allocedBins * sizeof(double))); } for (int i = 0; i < binCount; ++i) bins[i] = 0.0; // bin fitness values from one subpop or from across the population Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; for (const std::pair &subpop_pair : pop.subpops_) { const Subpopulation *subpop = subpop_pair.second; if (requestedSubpop && (subpop != requestedSubpop)) continue; for (const Individual *individual : subpop->parent_individuals_) { double fitness = individual->cached_unscaled_fitness_; int bin = (int)(((fitness - xAxisMin_) / (xAxisMax_ - xAxisMin_)) * binCount); if (bin < 0) bin = 0; if (bin >= binCount) bin = binCount - 1; bins[bin]++; } } // normalize the frequencies to a total of 1.0 double totalCount = 0.0; for (int i = 0; i < binCount; ++i) totalCount += bins[i]; if (totalCount == 0) totalCount = 1; // if counts are all zero, avoid a divide by zero below and just end up with 0 instead for (int i = 0; i < binCount; ++i) bins[i] = bins[i] / totalCount; return bins; } void QtSLiMGraphView_SubpopFitnessDists::drawGraph(QPainter &painter, QRect interiorRect) { Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; bool showSubpops = true; bool drawSubpopsGray = (showSubpops && (pop.subpops_.size() > 8)); // 7 subpops + pop int binCount = histogramBinCount_; // First draw subpop fitness distributions if (showSubpops) { for (const std::pair &subpop_pair : pop.subpops_) { const Subpopulation *subpop = subpop_pair.second; double *plotData = subpopulationFitnessData(subpop); QPainterPath linePath; bool startedLine = false; for (int i = 0; i < binCount; ++i) { QPointF historyPoint(plotToDeviceX(xAxisMin_ + (xAxisMax_ - xAxisMin_) * (i + 0.5) / binCount, interiorRect), plotToDeviceY(plotData[i], interiorRect)); if (startedLine) linePath.lineTo(historyPoint); else linePath.moveTo(historyPoint); startedLine = true; } if (drawSubpopsGray) painter.strokePath(linePath, QPen(QtSLiMColorWithWhite(0.5, 1.0), 1.0)); else painter.strokePath(linePath, QPen(controller_->whiteContrastingColorForIndex(subpop->subpopulation_id_), 1.0)); } } // Then draw the population fitness distribution double *plotData = subpopulationFitnessData(nullptr); QPainterPath linePath; bool startedLine = false; for (int i = 0; i < binCount; ++i) { QPointF historyPoint(plotToDeviceX(xAxisMin_ + (xAxisMax_ - xAxisMin_) * (i + 0.5) / binCount, interiorRect), plotToDeviceY(plotData[i], interiorRect)); if (startedLine) linePath.lineTo(historyPoint); else linePath.moveTo(historyPoint); startedLine = true; } painter.strokePath(linePath, QPen(Qt::black, 1.5)); } bool QtSLiMGraphView_SubpopFitnessDists::providesStringForData(void) { return true; } void QtSLiMGraphView_SubpopFitnessDists::appendStringForData(QString &string) { Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; bool showSubpops = true; int binCount = histogramBinCount_; // First add subpop fitness distributions if (showSubpops) { for (const std::pair &subpop_pair : pop.subpops_) { const Subpopulation *subpop = subpop_pair.second; double *plotData = subpopulationFitnessData(subpop); string.append(QString("# Fitness distribution (subpopulation p%1):\n").arg(subpop->subpopulation_id_)); for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(plotData[i], 0, 'f', 4)); string.append("\n\n"); } } // Then add the population fitness distribution double *plotData = subpopulationFitnessData(nullptr); string.append("# Fitness distribution (population):\n"); for (int i = 0; i < binCount; ++i) string.append(QString("%1, ").arg(plotData[i], 0, 'f', 4)); string.append("\n"); } QtSLiMLegendSpec QtSLiMGraphView_SubpopFitnessDists::legendKey(void) { Species *graphSpecies = focalDisplaySpecies(); Population &pop = graphSpecies->population_; bool showSubpops = true; bool drawSubpopsGray = (showSubpops && (pop.subpops_.size() > 8)); // 7 subpops + pop if (!showSubpops) return QtSLiMLegendSpec(); QtSLiMLegendSpec legend_key; legend_key.emplace_back("All", Qt::black); if (drawSubpopsGray) { legend_key.emplace_back("pX", QtSLiMColorWithWhite(0.5, 1.0)); } else { for (const std::pair &subpop_pair : pop.subpops_) { slim_objectid_t subpop_id = subpop_pair.second->subpopulation_id_; QString labelString = QString("p%1").arg(subpop_id); legend_key.emplace_back(labelString, controller_->whiteContrastingColorForIndex(subpop_id)); } } return legend_key; } ================================================ FILE: QtSLiM/QtSLiMGraphView_SubpopFitnessDists.h ================================================ // // QtSLiMGraphView_SubpopFitnessDists.h // SLiM // // Created by Ben Haller on 8/3/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMGRAPHVIEW_SUBPOPFITNESSDISTS_H #define QTSLIMGRAPHVIEW_SUBPOPFITNESSDISTS_H #include #include "QtSLiMGraphView.h" class Subpopulation; class QtSLiMGraphView_SubpopFitnessDists : public QtSLiMGraphView { Q_OBJECT public: QtSLiMGraphView_SubpopFitnessDists(QWidget *p_parent, QtSLiMWindow *controller); virtual ~QtSLiMGraphView_SubpopFitnessDists() override; virtual QString graphTitle(void) override; virtual QString aboutString(void) override; virtual void drawGraph(QPainter &painter, QRect interiorRect) override; virtual bool providesStringForData(void) override; virtual void appendStringForData(QString &string) override; protected: virtual QtSLiMLegendSpec legendKey(void) override; private: double *subpopulationFitnessData(const Subpopulation *requestedSubpop); }; #endif // QTSLIMGRAPHVIEW_SUBPOPFITNESSDISTS_H ================================================ FILE: QtSLiM/QtSLiMHaplotypeManager.cpp ================================================ // // QtSLiMHaplotypeManager.h // SLiM // // Created by Ben Haller on 4/3/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMHaplotypeManager.h" #include "QtSLiMChromosomeWidget.h" #include "QtSLiMHaplotypeOptions.h" #include "QtSLiMHaplotypeProgress.h" #include "QtSLiMPreferences.h" #include "QtSLiMExtras.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "eidos_globals.h" #include "subpopulation.h" #include "species.h" const int QtSLiM_SubpopulationStripWidth = 5; // This class method runs a plot options dialog, and then produces a haplotype plot with a progress panel as it is being constructed void QtSLiMHaplotypeManager::CreateHaplotypePlot(QtSLiMChromosomeWidgetController *controller, Chromosome *focalChromosome) { QtSLiMWindow *slimWindow = controller->slimWindow(); if (!slimWindow) return; Species *displaySpecies = controller->focalDisplaySpecies(); if (!displaySpecies) { QMessageBox messageBox(slimWindow); messageBox.setText("Haplotype Plot"); messageBox.setInformativeText("A single species must be chosen to create a haplotype plot; the plot will be based upon the selected species."); messageBox.setIcon(QMessageBox::Warning); // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::ApplicationModal // seems necessary so floating windows can't be on top of the message box messageBox.setWindowModality(Qt::ApplicationModal); messageBox.exec(); return; } // This method can create a haplotype plot for one chromosome or many. Many is the // base case, of which one is a special case. Chromosomes assort independently, so // each per-chromosome plot is independent, but it is useful to see them together. std::vector chromosomes; if (focalChromosome) chromosomes.push_back(focalChromosome); else chromosomes = displaySpecies->Chromosomes(); if (chromosomes.size() == 0) return; // should never happen; menu items etc. should be disabled in this case slim_position_t longestLength = 0; for (Chromosome *chromosome : chromosomes) longestLength = std::max(longestLength, chromosome->last_position_ + 1); // Run the options panel QtSLiMHaplotypeOptions optionsPanel(controller->slimWindow()); int result = optionsPanel.exec(); if (result != QDialog::Accepted) return; QtSLiMHaplotypeManager::ClusteringMethod clusteringMethod = optionsPanel.clusteringMethod(); QtSLiMHaplotypeManager::ClusteringOptimization clusteringOptimization = optionsPanel.clusteringOptimization(); size_t haplosomeSampleSize = optionsPanel.haplosomeSampleSize(); // Make a new window to show the graph QWidget *window = new QWidget(controller->slimWindow(), Qt::Window | Qt::Tool); // the graph window has us as a parent, but is still a standalone window window->setMinimumSize(400, 200); window->resize(500, 400); #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect window->setWindowIcon(QIcon()); #endif // Set up a top-level view, topViewWidget, that contains all of the haplotype views (empty for now) QVBoxLayout *topLayout = new QVBoxLayout; window->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(0); QtSLiMHaplotypeTopView *topViewWidget = new QtSLiMHaplotypeTopView(nullptr); QHBoxLayout *allViewsLayout = new QHBoxLayout; allViewsLayout->setContentsMargins(5, 5, 5, 5); allViewsLayout->setSpacing(5); topViewWidget->setLayout(allViewsLayout); topLayout->addWidget(topViewWidget); if (chromosomes.size() > 1) { topViewWidget->setShowChromosomeSymbols(true); allViewsLayout->setContentsMargins(5, 20, 5, 5); } std::vector haplotypeViews; for (Chromosome *chromosome : chromosomes) { QtSLiMHaplotypeView *haplotypeView = new QtSLiMHaplotypeView(nullptr); QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Expanding); sizePolicy1.setHorizontalStretch(std::max(3, (int)std::round(255 * ((chromosome->last_position_ + 1) / (double)longestLength)))); sizePolicy1.setVerticalStretch(0); haplotypeView->setSizePolicy(sizePolicy1); haplotypeView->chromosomeSymbol_ = chromosome->Symbol(); allViewsLayout->addWidget(haplotypeView); haplotypeViews.push_back(haplotypeView); } // Add a horizontal layout at the bottom, for the action button, and maybe other cruft, over time // The first haplotype view runs the action buttion, and forwards all state changes along the chain QHBoxLayout *buttonLayout = nullptr; { buttonLayout = new QHBoxLayout; buttonLayout->setContentsMargins(5, 5, 5, 5); buttonLayout->setSpacing(5); topLayout->addLayout(buttonLayout); if (controller->community()->all_species_.size() > 1) { // make our species avatar badge QLabel *speciesLabel = new QLabel(); speciesLabel->setText(QString::fromStdString(displaySpecies->avatar_)); buttonLayout->addWidget(speciesLabel); } QSpacerItem *leftSpacer = new QSpacerItem(16, 5, QSizePolicy::Expanding, QSizePolicy::Minimum); buttonLayout->addItem(leftSpacer); if (chromosomes.size() > 1) { // draw a little warning, in gray italic small, since users might think the chromosome plots are somehow correlated QLabel *warningLabel = new QLabel(); warningLabel->setText("note: each chromosome is sampled independently"); warningLabel->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); QFont labelFont(warningLabel->font()); labelFont.setItalic(true); labelFont.setPointSize(labelFont.pointSize() - 2); warningLabel->setFont(labelFont); QPalette labelPalette(warningLabel->palette()); labelPalette.setColor(QPalette::WindowText, QtSLiMColorWithWhite(0.5, 1.0)); warningLabel->setPalette(labelPalette); buttonLayout->addWidget(warningLabel); } QSpacerItem *rightSpacer = new QSpacerItem(16, 5, QSizePolicy::Expanding, QSizePolicy::Minimum); buttonLayout->addItem(rightSpacer); // this code is based on the creation of executeScriptButton in ui_QtSLiMEidosConsole.h QtSLiMPushButton *actionButton = new QtSLiMPushButton(window); actionButton->setObjectName(QString::fromUtf8("actionButton")); actionButton->setMinimumSize(QSize(20, 20)); actionButton->setMaximumSize(QSize(20, 20)); actionButton->setFocusPolicy(Qt::NoFocus); QIcon icon4; icon4.addFile(QtSLiMImagePath("action", false), QSize(), QIcon::Normal, QIcon::Off); icon4.addFile(QtSLiMImagePath("action", true), QSize(), QIcon::Normal, QIcon::On); actionButton->setIcon(icon4); actionButton->setIconSize(QSize(20, 20)); actionButton->qtslimSetBaseName("action"); actionButton->setCheckable(true); actionButton->setFlat(true); #if QT_CONFIG(tooltip) actionButton->setToolTip("

configure plot

"); #endif // QT_CONFIG(tooltip) buttonLayout->addWidget(actionButton); connect(actionButton, &QPushButton::pressed, topViewWidget, [actionButton, topViewWidget]() { actionButton->qtslimSetHighlight(true); topViewWidget->actionButtonRunMenu(actionButton); }); connect(actionButton, &QPushButton::released, topViewWidget, [actionButton]() { actionButton->qtslimSetHighlight(false); }); actionButton->setEnabled(true); } // make window actions for all global menu items // we do NOT need to do this, because we use Qt::Tool; Qt will use our parent winodw's shortcuts //qtSLiMAppDelegate->addActionsForGlobalMenuItems(window); // If we have more than one chromosome to do, show the window so the user can see partial results there if (chromosomes.size() > 1) QtSLiMMakeWindowVisibleAndExposed(window); // then create and install the haplotype managers, one by one; each will display once it is completed for (int index = 0; index < (int)chromosomes.size(); ++index) { Chromosome *chromosome = chromosomes[index]; QtSLiMHaplotypeView *haplotypeView = haplotypeViews[index]; // First generate the haplotype plot data, with a progress panel QtSLiMHaplotypeManager *haplotypeManager = new QtSLiMHaplotypeManager(nullptr, clusteringMethod, clusteringOptimization, controller, displaySpecies, chromosome, QtSLiMRange(0,0), haplosomeSampleSize, true, index + 1, chromosomes.size()); if (haplotypeManager->valid_) { // this will be called for each chromosome, but the titles should all be the same, so it's fine window->setWindowTitle(QString("Haplotype snapshot (%1)").arg(haplotypeManager->titleStringWithoutChromosome)); // The haplotype manager is owned by the graph view, as a delegate object haplotypeView->setDelegate(haplotypeManager); } } // If we have just one chromosome to do, show the window now that it's done if (chromosomes.size() <= 1) QtSLiMMakeWindowVisibleAndExposed(window); } QtSLiMHaplotypeManager::QtSLiMHaplotypeManager(QObject *p_parent, ClusteringMethod clusteringMethod, ClusteringOptimization optimizationMethod, QtSLiMChromosomeWidgetController *controller, Species *displaySpecies, Chromosome *chromosome, QtSLiMRange displayedRange, size_t sampleSize, bool showProgress, int progressChromIndex, int progressChromTotal) : QObject(p_parent) { controller_ = controller; focalSpeciesName_ = displaySpecies->name_; Community *community = controller_->community(); Species *graphSpecies = focalDisplaySpecies(); Population &population = graphSpecies->population_; clusterMethod = clusteringMethod; clusterOptimization = optimizationMethod; // Figure out which subpops are selected (or if none are, consider all to be); we will display only the selected subpops std::vector selected_subpops; for (auto subpop_pair : population.subpops_) if (subpop_pair.second->gui_selected_) selected_subpops.emplace_back(subpop_pair.second); if (selected_subpops.size() == 0) for (auto subpop_pair : population.subpops_) selected_subpops.emplace_back(subpop_pair.second); // Figure out whether we're analyzing / displaying a subrange usingSubrange = (displayedRange.length == 0) ? false : true; subrangeFirstBase = displayedRange.location; subrangeLastBase = displayedRange.location + displayedRange.length - 1; // Also dig to find out whether we're displaying all mutation types or just a subset; if a subset, each MutationType has a display flag displayingMuttypeSubset = (controller_->displayMuttypes_.size() != 0); // Set our window title from the controller's state QString title; if (selected_subpops.size() == 0) { // If there are no subpops (which can happen at the very start of running a model, for example), use a dash title = "–"; } else { bool first_subpop = true; for (Subpopulation *subpop : selected_subpops) { if (!first_subpop) title.append(" "); title.append(QString("p%1").arg(subpop->subpopulation_id_)); first_subpop = false; } } if (usingSubrange) title.append(QString(", positions %1:%2").arg(subrangeFirstBase).arg(subrangeLastBase)); title.append(QString(", tick %1").arg(community->Tick())); titleStringWithoutChromosome = title; if (displaySpecies->Chromosomes().size() > 1) title.append(QString(", chromosome '%2'").arg(QString::fromStdString(chromosome->Symbol()))); titleString = title; subpopCount = static_cast(selected_subpops.size()); // Fetch haplosomes and figure out what we're going to plot; note that we plot only non-null haplosomes slim_chromosome_index_t chromosome_index = chromosome->Index(); int first_haplosome_index = graphSpecies->FirstHaplosomeIndices()[chromosome_index]; int last_haplosome_index = graphSpecies->LastHaplosomeIndices()[chromosome_index]; for (Subpopulation *subpop : selected_subpops) { for (Individual *ind : subpop->parent_individuals_) { Haplosome **ind_haplosomes = ind->haplosomes_; for (int haplosome_index = first_haplosome_index; haplosome_index <= last_haplosome_index; haplosome_index++) { Haplosome *haplosome = ind_haplosomes[haplosome_index]; if (!haplosome->IsNull()) haplosomes.emplace_back(haplosome); } } } // If a sample is requested, select that now; sampleSize <= 0 means no sampling if ((sampleSize > 0) && (haplosomes.size() > sampleSize)) { Eidos_random_unique(haplosomes.begin(), haplosomes.end(), sampleSize); haplosomes.resize(sampleSize); } // Cache all the information about the mutations that we're going to need configureMutationInfoBuffer(chromosome); // Keep track of the range of subpop IDs we reference, even if not represented by any haplosomes here maxSubpopID = 0; minSubpopID = SLIM_MAX_ID_VALUE; for (Subpopulation *subpop : selected_subpops) { slim_objectid_t subpop_id = subpop->subpopulation_id_; minSubpopID = std::min(minSubpopID, subpop_id); maxSubpopID = std::max(maxSubpopID, subpop_id); } // Show a progress panel if requested if (showProgress) { int progressSteps = (clusterOptimization == QtSLiMHaplotypeManager::ClusterOptimizeWith2opt) ? 3 : 2; progressPanel_ = new QtSLiMHaplotypeProgress(controller_->slimWindow()); progressPanel_->runProgressWithHaplosomeCount(haplosomes.size(), progressSteps, progressChromIndex, progressChromTotal); } // Do the clustering analysis synchronously, updating the progress panel as we go finishClusteringAnalysis(); // Hide the progress panel if (progressPanel_) { progressPanel_->hide(); delete progressPanel_; progressPanel_ = nullptr; } } QtSLiMHaplotypeManager::~QtSLiMHaplotypeManager(void) { if (mutationInfo) { free(mutationInfo); mutationInfo = nullptr; } if (mutationPositions) { free(mutationPositions); mutationPositions = nullptr; } if (displayList) { delete displayList; displayList = nullptr; } } Species *QtSLiMHaplotypeManager::focalDisplaySpecies(void) { // We look up our focal species object by name every time, since keeping a pointer to it would be unsafe // Before initialize() is done species have not been created, so we return nullptr in that case if (controller_ && controller_->community() && (controller_->community()->Tick() >= 1)) return controller_->community()->SpeciesWithName(focalSpeciesName_); return nullptr; } void QtSLiMHaplotypeManager::finishClusteringAnalysis(void) { // Work out an approximate best sort order sortHaplosomes(); if (valid_ && progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) valid_ = false; if (valid_) { // Remember the subpop ID for each haplosome for (Haplosome *haplosome : haplosomes) haplosomeSubpopIDs.emplace_back(haplosome->individual_->subpopulation_->subpopulation_id_); // Build our plotting data vectors. Because we are a snapshot, we can't rely on our controller's data // at all after this method returns; we have to remember everything we need to create our display list. configureDisplayBuffers(); } // Now we are done with the haplosomes vector; clear it haplosomes.clear(); haplosomes.resize(0); } void QtSLiMHaplotypeManager::configureMutationInfoBuffer(Chromosome *chromosome) { Species *graphSpecies = focalDisplaySpecies(); if (!graphSpecies) return; Population &population = graphSpecies->population_; double scalingFactor = 0.8; // used to be controller->selectionColorScale; int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); const MutationIndex *reg_end_ptr = registry + registry_size; MutationIndex biggest_index = 0; // First, find the biggest index presently in use; that's how many entries we need // BCH 12/25/2024: With multiple chromosomes, this is rather wasteful; I think this class // could be redesigned to capture just the subset of mutations that are live for a given // chromosome, essentially re-indexing the mutations, but it's not clear this matters // to performance; we just waste a bit of memory here, but it's not a big deal. for (const MutationIndex *reg_ptr = registry; reg_ptr != reg_end_ptr; ++reg_ptr) { MutationIndex mut_index = *reg_ptr; if (mut_index > biggest_index) biggest_index = mut_index; } // Allocate our mutationInfo buffer with entries for every MutationIndex in use mutationIndexCount = static_cast(biggest_index + 1); mutationInfo = static_cast(malloc(sizeof(HaploMutation) * mutationIndexCount)); mutationPositions = static_cast(malloc(sizeof(slim_position_t) * mutationIndexCount)); // Copy the information we need on each mutation in use Mutation *mut_block_ptr = gSLiM_Mutation_Block; for (const MutationIndex *reg_ptr = registry; reg_ptr != reg_end_ptr; ++reg_ptr) { MutationIndex mut_index = *reg_ptr; const Mutation *mut = mut_block_ptr + mut_index; slim_position_t mut_position = mut->position_; const MutationType *mut_type = mut->mutation_type_ptr_; HaploMutation *haplo_mut = mutationInfo + mut_index; haplo_mut->position_ = mut_position; *(mutationPositions + mut_index) = mut_position; if (!mut_type->color_.empty()) { haplo_mut->red_ = mut_type->color_red_; haplo_mut->green_ = mut_type->color_green_; haplo_mut->blue_ = mut_type->color_blue_; } else { RGBForSelectionCoeff(static_cast(mut->selection_coeff_), &haplo_mut->red_, &haplo_mut->green_, &haplo_mut->blue_, scalingFactor); } haplo_mut->neutral_ = (mut->selection_coeff_ == 0.0f); haplo_mut->display_ = mut_type->mutation_type_displayed_; } // Remember the chromosome length mutationLastPosition = chromosome->last_position_; } void QtSLiMHaplotypeManager::sortHaplosomes(void) { size_t haplosome_count = haplosomes.size(); if (haplosome_count == 0) return; std::vector original_haplosomes = haplosomes; // copy the vector because we will need to reorder it below std::vector final_path; // first get our distance matrix; these are inter-city distances int64_t *distances; if (displayingMuttypeSubset) { if (usingSubrange) distances = buildDistanceArrayForSubrangeAndSubtypes(); else distances = buildDistanceArrayForSubtypes(); } else { if (usingSubrange) distances = buildDistanceArrayForSubrange(); else distances = buildDistanceArray(); } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) goto cancelledExit; switch (clusterMethod) { case ClusterNearestNeighbor: nearestNeighborSolve(distances, haplosome_count, final_path); break; case ClusterGreedy: greedySolve(distances, haplosome_count, final_path); break; } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) goto cancelledExit; checkPath(final_path, haplosome_count); if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) goto cancelledExit; if (clusterOptimization != ClusterNoOptimization) { switch (clusterOptimization) { case ClusterNoOptimization: break; case ClusterOptimizeWith2opt: do2optOptimizationOfSolution(final_path, distances, haplosome_count); break; } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) goto cancelledExit; checkPath(final_path, haplosome_count); } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) goto cancelledExit; // reorder the haplosomes vector according to the path we found for (size_t haplosome_index = 0; haplosome_index < haplosome_count; ++haplosome_index) haplosomes[haplosome_index] = original_haplosomes[static_cast(final_path[haplosome_index])]; cancelledExit: free(distances); } void QtSLiMHaplotypeManager::configureDisplayBuffers(void) { size_t haplosome_count = haplosomes.size(); // Allocate our display list and size it so it has one std::vector per haplosome displayList = new std::vector>; displayList->resize(haplosome_count); // Then save off the information for each haplosome into the display list for (size_t haplosome_index = 0; haplosome_index < haplosome_count; ++haplosome_index) { Haplosome &haplosome = *haplosomes[haplosome_index]; std::vector &haplosome_display = (*displayList)[haplosome_index]; if (!usingSubrange) { // Size our display list to fit the number of mutations in the haplosome size_t mut_count = static_cast(haplosome.mutation_count()); haplosome_display.reserve(mut_count); // Loop through mutations to get the mutation indices int mutrun_count = haplosome.mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome.mutruns_[run_index]; const MutationIndex *mut_start_ptr = mutrun->begin_pointer_const(); const MutationIndex *mut_end_ptr = mutrun->end_pointer_const(); if (displayingMuttypeSubset) { // displaying a subset of mutation types, need to check for (const MutationIndex *mut_ptr = mut_start_ptr; mut_ptr < mut_end_ptr; ++mut_ptr) { MutationIndex mut_index = *mut_ptr; if ((mutationInfo + mut_index)->display_) haplosome_display.emplace_back(*mut_ptr); } } else { // displaying all mutation types, no need to check for (const MutationIndex *mut_ptr = mut_start_ptr; mut_ptr < mut_end_ptr; ++mut_ptr) haplosome_display.emplace_back(*mut_ptr); } } } else { // We are using a subrange, so we need to check the position of each mutation before adding it int mutrun_count = haplosome.mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome.mutruns_[run_index]; const MutationIndex *mut_start_ptr = mutrun->begin_pointer_const(); const MutationIndex *mut_end_ptr = mutrun->end_pointer_const(); if (displayingMuttypeSubset) { // displaying a subset of mutation types, need to check for (const MutationIndex *mut_ptr = mut_start_ptr; mut_ptr < mut_end_ptr; ++mut_ptr) { MutationIndex mut_index = *mut_ptr; slim_position_t mut_position = *(mutationPositions + mut_index); if ((mut_position >= subrangeFirstBase) && (mut_position <= subrangeLastBase)) if ((mutationInfo + mut_index)->display_) haplosome_display.emplace_back(mut_index); } } else { // displaying all mutation types, no need to check for (const MutationIndex *mut_ptr = mut_start_ptr; mut_ptr < mut_end_ptr; ++mut_ptr) { MutationIndex mut_index = *mut_ptr; slim_position_t mut_position = *(mutationPositions + mut_index); if ((mut_position >= subrangeFirstBase) && (mut_position <= subrangeLastBase)) haplosome_display.emplace_back(mut_index); } } } } } } void QtSLiMHaplotypeManager::tallyBincounts(int64_t *bincounts, std::vector &haplosomeList) { EIDOS_BZERO(bincounts, 1024 * sizeof(int64_t)); for (MutationIndex mut_index : haplosomeList) bincounts[mutationInfo[mut_index].position_ % 1024]++; } int64_t QtSLiMHaplotypeManager::distanceForBincounts(int64_t *bincounts1, int64_t *bincounts2) { int64_t distance = 0; for (int i = 0; i < 1024; ++i) distance += abs(bincounts1[i] - bincounts2[i]); return distance; } #ifndef SLIM_NO_OPENGL void QtSLiMHaplotypeManager::glDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground) { // Erase the background to either black or white, depending on displayBW if (eraseBackground) { if (displayBW) glColor3f(1.0f, 1.0f, 1.0f); else glColor3f(0.0f, 0.0f, 0.0f); glRecti(interior.x(), interior.y(), interior.x() + interior.width(), interior.y() + interior.height()); } // Draw subpopulation strips if requested if (showSubpopStrips) { QRect subpopStripRect = interior; subpopStripRect.setWidth(QtSLiM_SubpopulationStripWidth); glDrawSubpopStripsInRect(subpopStripRect); interior.adjust(QtSLiM_SubpopulationStripWidth, 0, 0, 0); } // Draw the haplotypes in the remaining portion of the interior glDrawDisplayListInRect(interior, displayBW); } #endif void QtSLiMHaplotypeManager::qtDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground, QPainter &painter) { // Erase the background to either black or white, depending on displayBW if (eraseBackground) { painter.fillRect(interior, displayBW ? Qt::white : Qt::black); } // Draw subpopulation strips if requested if (showSubpopStrips) { QRect subpopStripRect = interior; subpopStripRect.setWidth(QtSLiM_SubpopulationStripWidth); qtDrawSubpopStripsInRect(subpopStripRect, painter); interior.adjust(QtSLiM_SubpopulationStripWidth, 0, 0, 0); } // Draw the haplotypes in the remaining portion of the interior qtDrawDisplayListInRect(interior, displayBW, painter); } // Traveling Salesman Problem code // // We have a set of haplosomes, each of which may be defined as being a particular distance from each other haplosome (defined here // as the number of differences in the mutations contained). We want to sort the haplosomes into an order that groups similar // haplosomes together, minimizing the overall distance through "haplosome space" traveled from top to bottom of our display. This // is exactly the Traveling Salesman Problem, without returning to the starting "city". This is a very intensively studied // problem, is NP-hard, and would take an enormously long time to solve exactly for even a relatively small number of haplosomes, // whereas we will routinely have thousands of haplosomes. We will find an approximate solution using a fast heuristic algorithm, // because we are not greatly concerned with the quality of the solution and we are extremely concerned with runtime. The // nearest-neighbor method is the fastest heuristic, and is O(N^2) in the number of cities; the Greedy algorithm is slower but // produces significantly better results. We can refine our initial solution using the 2-opt method. #pragma mark Traveling salesman problem // This allocates and builds an array of distances between haplosomes. The returned array is owned by the caller. This is where // we spend the large majority of our time, at present; this algorithm is O(N^2), but has a large constant (because really also // it depends on the length of the chromosome, the configuration of mutation runs, etc.). This method runs prior to the actual // Traveling Salesman Problem; here we're just figuring out the distances between our "cities". We have four versions of this // method, for speed; this is the base version, and separate versions are below that handle a chromosome subrange and/or a // subset of all of the mutation types. int64_t *QtSLiMHaplotypeManager::buildDistanceArray(void) { size_t haplosome_count = haplosomes.size(); int64_t *distances = static_cast(malloc(haplosome_count * haplosome_count * sizeof(int64_t))); uint8_t *mutation_seen = static_cast(calloc(mutationIndexCount, sizeof(uint8_t))); uint8_t seen_marker = 1; for (size_t i = 0; i < haplosome_count; ++i) { Haplosome *haplosome1 = haplosomes[i]; int64_t *distance_column = distances + i; int64_t *distance_row = distances + i * haplosome_count; int mutrun_count = haplosome1->mutrun_count_; const MutationRun **haplosome1_mutruns = haplosome1->mutruns_; distance_row[i] = 0; for (size_t j = i + 1; j < haplosome_count; ++j) { Haplosome *haplosome2 = haplosomes[j]; const MutationRun **haplosome2_mutruns = haplosome2->mutruns_; int64_t distance = 0; for (int mutrun_index = 0; mutrun_index < mutrun_count; ++mutrun_index) { const MutationRun *haplosome1_mutrun = haplosome1_mutruns[mutrun_index]; const MutationRun *haplosome2_mutrun = haplosome2_mutruns[mutrun_index]; int haplosome1_mutcount = haplosome1_mutrun->size(); int haplosome2_mutcount = haplosome2_mutrun->size(); if (haplosome1_mutrun == haplosome2_mutrun) ; // identical runs have no differences else if (haplosome1_mutcount == 0) distance += haplosome2_mutcount; else if (haplosome2_mutcount == 0) distance += haplosome1_mutcount; else { // We use a radix strategy to count the number of mismatches; assume up front that all mutations are mismatched, // and then subtract two for each mutation that turns out to be shared, using a uint8_t buffer to track usage. distance += haplosome1_mutcount + haplosome2_mutcount; const MutationIndex *mutrun1_end = haplosome1_mutrun->end_pointer_const(); for (const MutationIndex *mutrun1_ptr = haplosome1_mutrun->begin_pointer_const(); mutrun1_ptr != mutrun1_end; ++mutrun1_ptr) mutation_seen[*mutrun1_ptr] = seen_marker; const MutationIndex *mutrun2_end = haplosome2_mutrun->end_pointer_const(); for (const MutationIndex *mutrun2_ptr = haplosome2_mutrun->begin_pointer_const(); mutrun2_ptr != mutrun2_end; ++mutrun2_ptr) if (mutation_seen[*mutrun2_ptr] == seen_marker) distance -= 2; // To avoid having to clear the usage buffer every time, we play an additional trick: we use an incrementing // marker value to indicate usage, and clear the buffer only when it reaches 255. Makes about a 10% difference! seen_marker++; if (seen_marker == 0) { EIDOS_BZERO(mutation_seen, mutationIndexCount); seen_marker = 1; } } } // set the distance at both mirrored locations in the distance buffer *(distance_column + j * haplosome_count) = distance; *(distance_row + j) = distance; } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) break; if (progressPanel_) progressPanel_->setHaplotypeProgress(i + 1, 0); } free(mutation_seen); return distances; } // This does the same thing as buildDistanceArrayForHaplosomes:, but uses the chosen subrange of each haplosome int64_t *QtSLiMHaplotypeManager::buildDistanceArrayForSubrange(void) { slim_position_t firstBase = subrangeFirstBase, lastBase = subrangeLastBase; size_t haplosome_count = haplosomes.size(); int64_t *distances = static_cast(malloc(haplosome_count * haplosome_count * sizeof(int64_t))); uint8_t *mutation_seen = static_cast(calloc(mutationIndexCount, sizeof(uint8_t))); uint8_t seen_marker = 1; for (size_t i = 0; i < haplosome_count; ++i) { Haplosome *haplosome1 = haplosomes[i]; int64_t *distance_column = distances + i; int64_t *distance_row = distances + i * haplosome_count; slim_position_t mutrun_length = haplosome1->mutrun_length_; int mutrun_count = haplosome1->mutrun_count_; const MutationRun **haplosome1_mutruns = haplosome1->mutruns_; distance_row[i] = 0; for (size_t j = i + 1; j < haplosome_count; ++j) { Haplosome *haplosome2 = haplosomes[j]; const MutationRun **haplosome2_mutruns = haplosome2->mutruns_; int64_t distance = 0; for (int mutrun_index = 0; mutrun_index < mutrun_count; ++mutrun_index) { // Skip mutation runs outside of the subrange we're focused on if ((mutrun_length * mutrun_index > lastBase) || (mutrun_length * mutrun_index + mutrun_length - 1 < firstBase)) continue; // OK, this mutrun intersects with our chosen subrange; proceed const MutationRun *haplosome1_mutrun = haplosome1_mutruns[mutrun_index]; const MutationRun *haplosome2_mutrun = haplosome2_mutruns[mutrun_index]; if (haplosome1_mutrun == haplosome2_mutrun) ; // identical runs have no differences else { // We use a radix strategy to count the number of mismatches. Note this is done a bit differently than in // buildDistanceArrayForHaplosomes:; here we do not add the total and then subtract matches. const MutationIndex *mutrun1_end = haplosome1_mutrun->end_pointer_const(); for (const MutationIndex *mutrun1_ptr = haplosome1_mutrun->begin_pointer_const(); mutrun1_ptr != mutrun1_end; ++mutrun1_ptr) { MutationIndex mut1_index = *mutrun1_ptr; slim_position_t mut1_position = mutationPositions[mut1_index]; if ((mut1_position >= firstBase) && (mut1_position <= lastBase)) { mutation_seen[mut1_index] = seen_marker; distance++; // assume unmatched } } const MutationIndex *mutrun2_end = haplosome2_mutrun->end_pointer_const(); for (const MutationIndex *mutrun2_ptr = haplosome2_mutrun->begin_pointer_const(); mutrun2_ptr != mutrun2_end; ++mutrun2_ptr) { MutationIndex mut2_index = *mutrun2_ptr; slim_position_t mut2_position = mutationPositions[mut2_index]; if ((mut2_position >= firstBase) && (mut2_position <= lastBase)) { if (mutation_seen[mut2_index] == seen_marker) distance -= 1; // matched, so decrement to compensate for the assumption of non-match above else distance++; // not matched, so increment } } // To avoid having to clear the usage buffer every time, we play an additional trick: we use an incrementing // marker value to indicate usage, and clear the buffer only when it reaches 255. Makes about a 10% difference! seen_marker++; if (seen_marker == 0) { EIDOS_BZERO(mutation_seen, mutationIndexCount); seen_marker = 1; } } } // set the distance at both mirrored locations in the distance buffer *(distance_column + j * haplosome_count) = distance; *(distance_row + j) = distance; } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) break; if (progressPanel_) progressPanel_->setHaplotypeProgress(i + 1, 0); } free(mutation_seen); return distances; } // This does the same thing as buildDistanceArrayForHaplosomes:, but uses only mutations of a mutation type that is chosen for display int64_t *QtSLiMHaplotypeManager::buildDistanceArrayForSubtypes(void) { size_t haplosome_count = haplosomes.size(); int64_t *distances = static_cast(malloc(haplosome_count * haplosome_count * sizeof(int64_t))); uint8_t *mutation_seen = static_cast(calloc(mutationIndexCount, sizeof(uint8_t))); uint8_t seen_marker = 1; for (size_t i = 0; i < haplosome_count; ++i) { Haplosome *haplosome1 = haplosomes[i]; int64_t *distance_column = distances + i; int64_t *distance_row = distances + i * haplosome_count; int mutrun_count = haplosome1->mutrun_count_; const MutationRun **haplosome1_mutruns = haplosome1->mutruns_; distance_row[i] = 0; for (size_t j = i + 1; j < haplosome_count; ++j) { Haplosome *haplosome2 = haplosomes[j]; const MutationRun **haplosome2_mutruns = haplosome2->mutruns_; int64_t distance = 0; for (int mutrun_index = 0; mutrun_index < mutrun_count; ++mutrun_index) { const MutationRun *haplosome1_mutrun = haplosome1_mutruns[mutrun_index]; const MutationRun *haplosome2_mutrun = haplosome2_mutruns[mutrun_index]; if (haplosome1_mutrun == haplosome2_mutrun) ; // identical runs have no differences else { // We use a radix strategy to count the number of mismatches. Note this is done a bit differently than in // buildDistanceArrayForHaplosomes:; here we do not add the total and then subtract matches. const MutationIndex *mutrun1_end = haplosome1_mutrun->end_pointer_const(); for (const MutationIndex *mutrun1_ptr = haplosome1_mutrun->begin_pointer_const(); mutrun1_ptr != mutrun1_end; ++mutrun1_ptr) { MutationIndex mut1_index = *mutrun1_ptr; if (mutationInfo[mut1_index].display_) { mutation_seen[mut1_index] = seen_marker; distance++; // assume unmatched } } const MutationIndex *mutrun2_end = haplosome2_mutrun->end_pointer_const(); for (const MutationIndex *mutrun2_ptr = haplosome2_mutrun->begin_pointer_const(); mutrun2_ptr != mutrun2_end; ++mutrun2_ptr) { MutationIndex mut2_index = *mutrun2_ptr; if (mutationInfo[mut2_index].display_) { if (mutation_seen[mut2_index] == seen_marker) distance -= 1; // matched, so decrement to compensate for the assumption of non-match above else distance++; // not matched, so increment } } // To avoid having to clear the usage buffer every time, we play an additional trick: we use an incrementing // marker value to indicate usage, and clear the buffer only when it reaches 255. Makes about a 10% difference! seen_marker++; if (seen_marker == 0) { EIDOS_BZERO(mutation_seen, mutationIndexCount); seen_marker = 1; } } } // set the distance at both mirrored locations in the distance buffer *(distance_column + j * haplosome_count) = distance; *(distance_row + j) = distance; } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) break; if (progressPanel_) progressPanel_->setHaplotypeProgress(i + 1, 0); } free(mutation_seen); return distances; } // This does the same thing as buildDistanceArrayForHaplosomes:, but uses the chosen subrange of each haplosome, and only mutations of mutation types being displayed int64_t *QtSLiMHaplotypeManager::buildDistanceArrayForSubrangeAndSubtypes(void) { slim_position_t firstBase = subrangeFirstBase, lastBase = subrangeLastBase; size_t haplosome_count = haplosomes.size(); int64_t *distances = static_cast(malloc(haplosome_count * haplosome_count * sizeof(int64_t))); uint8_t *mutation_seen = static_cast(calloc(mutationIndexCount, sizeof(uint8_t))); uint8_t seen_marker = 1; for (size_t i = 0; i < haplosome_count; ++i) { Haplosome *haplosome1 = haplosomes[i]; int64_t *distance_column = distances + i; int64_t *distance_row = distances + i * haplosome_count; slim_position_t mutrun_length = haplosome1->mutrun_length_; int mutrun_count = haplosome1->mutrun_count_; const MutationRun **haplosome1_mutruns = haplosome1->mutruns_; distance_row[i] = 0; for (size_t j = i + 1; j < haplosome_count; ++j) { Haplosome *haplosome2 = haplosomes[j]; const MutationRun **haplosome2_mutruns = haplosome2->mutruns_; int64_t distance = 0; for (int mutrun_index = 0; mutrun_index < mutrun_count; ++mutrun_index) { // Skip mutation runs outside of the subrange we're focused on if ((mutrun_length * mutrun_index > lastBase) || (mutrun_length * mutrun_index + mutrun_length - 1 < firstBase)) continue; // OK, this mutrun intersects with our chosen subrange; proceed const MutationRun *haplosome1_mutrun = haplosome1_mutruns[mutrun_index]; const MutationRun *haplosome2_mutrun = haplosome2_mutruns[mutrun_index]; if (haplosome1_mutrun == haplosome2_mutrun) ; // identical runs have no differences else { // We use a radix strategy to count the number of mismatches. Note this is done a bit differently than in // buildDistanceArrayForHaplosomes:; here we do not add the total and then subtract matches. const MutationIndex *mutrun1_end = haplosome1_mutrun->end_pointer_const(); for (const MutationIndex *mutrun1_ptr = haplosome1_mutrun->begin_pointer_const(); mutrun1_ptr != mutrun1_end; ++mutrun1_ptr) { MutationIndex mut1_index = *mutrun1_ptr; slim_position_t mut1_position = mutationPositions[mut1_index]; if ((mut1_position >= firstBase) && (mut1_position <= lastBase)) { if (mutationInfo[mut1_index].display_) { mutation_seen[mut1_index] = seen_marker; distance++; // assume unmatched } } } const MutationIndex *mutrun2_end = haplosome2_mutrun->end_pointer_const(); for (const MutationIndex *mutrun2_ptr = haplosome2_mutrun->begin_pointer_const(); mutrun2_ptr != mutrun2_end; ++mutrun2_ptr) { MutationIndex mut2_index = *mutrun2_ptr; slim_position_t mut2_position = mutationPositions[mut2_index]; if ((mut2_position >= firstBase) && (mut2_position <= lastBase)) { if (mutationInfo[mut2_index].display_) { if (mutation_seen[mut2_index] == seen_marker) distance -= 1; // matched, so decrement to compensate for the assumption of non-match above else distance++; // not matched, so increment } } } // To avoid having to clear the usage buffer every time, we play an additional trick: we use an incrementing // marker value to indicate usage, and clear the buffer only when it reaches 255. Makes about a 10% difference! seen_marker++; if (seen_marker == 0) { EIDOS_BZERO(mutation_seen, mutationIndexCount); seen_marker = 1; } } } // set the distance at both mirrored locations in the distance buffer *(distance_column + j * haplosome_count) = distance; *(distance_row + j) = distance; } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) break; if (progressPanel_) progressPanel_->setHaplotypeProgress(i + 1, 0); } free(mutation_seen); return distances; } // Since we want to solve the Traveling Salesman Problem without returning to the original city, the choice of the initial city // may be quite important to the solution we get. It seems reasonable to start at the city that is the most isolated, i.e. has // the largest distance from itself to any other city. By starting with this city, we avoid having to have two edges connecting // to it, both of which would be relatively long. However, this is just a guess, and might be modified by refinement later. int QtSLiMHaplotypeManager::indexOfMostIsolatedHaplosomeWithDistances(int64_t *distances, size_t haplosome_count) { int64_t greatest_isolation = -1; int greatest_isolation_index = -1; for (size_t i = 0; i < haplosome_count; ++i) { int64_t isolation = INT64_MAX; int64_t *row_ptr = distances + i * haplosome_count; for (size_t j = 0; j < haplosome_count; ++j) { int64_t distance = row_ptr[j]; // distances of 0 don't count for isolation estimation; we really want the most isolated identical cluster of haplosomes // this also serves to take care of the j == i case for us without special-casing, which is nice... if (distance == 0) continue; if (distance < isolation) isolation = distance; } if (isolation > greatest_isolation) { greatest_isolation = isolation; greatest_isolation_index = static_cast(i); } } return greatest_isolation_index; } // The nearest-neighbor method provides an initial solution for the Traveling Salesman Problem by beginning with a chosen city // (see indexOfMostIsolatedHaplosomeWithDistances:size: above) and adding successive cities according to which is closest to the // city we have reached thus far. This is quite simple to implement, and runs in O(N^2) time. However, the greedy algorithm // below runs only a little more slowly, and produces significantly better results, so unless speed is essential it is better. void QtSLiMHaplotypeManager::nearestNeighborSolve(int64_t *distances, size_t haplosome_count, std::vector &solution) { size_t haplosomes_left = haplosome_count; solution.reserve(haplosome_count); // we have to make a copy of the distances matrix, as we modify it internally int64_t *distances_copy = static_cast(malloc(haplosome_count * haplosome_count * sizeof(int64_t))); memcpy(distances_copy, distances, haplosome_count * haplosome_count * sizeof(int64_t)); // find the haplosome that is farthest from any other haplosome; this will be our starting point, for now int last_path_index = indexOfMostIsolatedHaplosomeWithDistances(distances_copy, haplosome_count); do { // add the chosen haplosome to our path solution.emplace_back(last_path_index); if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) break; if (progressPanel_) progressPanel_->setHaplotypeProgress(haplosome_count - haplosomes_left + 1, 1); // if we just added the last haplosome, we're done if (--haplosomes_left == 0) break; // otherwise, mark the chosen haplosome as unavailable by setting distances to it to INT64_MAX int64_t *column_ptr = distances_copy + last_path_index; for (size_t i = 0; i < haplosome_count; ++i) { *column_ptr = INT64_MAX; column_ptr += haplosome_count; } // now we need to find the next city, which will be the nearest neighbor of the last city int64_t *row_ptr = distances_copy + last_path_index * static_cast(haplosome_count); int64_t nearest_neighbor_distance = INT64_MAX; int nearest_neighbor_index = -1; for (size_t i = 0; i < haplosome_count; ++i) { int64_t distance = row_ptr[i]; if (distance < nearest_neighbor_distance) { nearest_neighbor_distance = distance; nearest_neighbor_index = static_cast(i); } } // found the next city; add it to the path by looping back to the top last_path_index = nearest_neighbor_index; } while (true); free(distances_copy); } // The greedy method provides an initial solution for the Traveling Salesman Problem by sorting all possible edges, // and then iteratively adding the shortest legal edge to the path until the full path has been constructed. This // is a little more complex than nearest neighbor, and runs a bit more slowly, but gives a somewhat better result. typedef struct { int i, k; int64_t d; } greedy_edge; static bool operator<(const greedy_edge &i, const greedy_edge &j) __attribute__((unused)); static bool operator>(const greedy_edge &i, const greedy_edge &j) __attribute__((unused)); static bool operator==(const greedy_edge &i, const greedy_edge &j) __attribute__((unused)); static bool operator!=(const greedy_edge &i, const greedy_edge &j) __attribute__((unused)); static bool operator<(const greedy_edge &i, const greedy_edge &j) { return (i.d < j.d); } static bool operator>(const greedy_edge &i, const greedy_edge &j) { return (i.d > j.d); } static bool operator==(const greedy_edge &i, const greedy_edge &j) { return (i.d == j.d); } static bool operator!=(const greedy_edge &i, const greedy_edge &j) { return (i.d != j.d); } void QtSLiMHaplotypeManager::greedySolve(int64_t *distances, size_t haplosome_count, std::vector &solution) { // The first thing we need to do is sort all possible edges in ascending order by length; // we don't need to differentiate a->b versus b->a since our distances are symmetric std::vector edge_buf; size_t edge_count = (haplosome_count * (haplosome_count - 1)) / 2; edge_buf.reserve(edge_count); // one of the two factors is even so /2 is safe for (size_t i = 0; i < haplosome_count - 1; ++i) { for (size_t k = i + 1; k < haplosome_count; ++k) edge_buf.emplace_back(greedy_edge{static_cast(i), static_cast(k), *(distances + i + k * haplosome_count)}); } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) return; if (progressPanel_) { // We have a progress panel, so we do an incremental sort BareBoneIIQS sorter(edge_buf.data(), edge_count); for (size_t i = 0; i < haplosome_count - 1; ++i) { for (size_t k = i + 1; k < haplosome_count; ++k) sorter.next(); if (progressPanel_->haplotypeProgressIsCancelled()) return; progressPanel_->setHaplotypeProgress(i, 1); } } else { // If we're not running with a progress panel, we have no progress indicator so we can just use std::sort() std::sort(edge_buf.begin(), edge_buf.end()); } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) return; // Now we take take the first legal edge from the top of edge_buf and add it to our path. "Legal" means it // doesn't increase the degree of either participating node above 2, and doesn't create a cycle. We check // the first condition by keeping a vector of the degrees of all nodes, so that's easy. We check the second // condition by keeping a vector of "group" tags for each participating node; an edge that joins two nodes // in the same group creates a cycle and is thus illegal (maybe there's a better way to detect cycles but I // haven't thought of it yet :->). std::vector path_components; uint8_t *node_degrees = static_cast(calloc(sizeof(uint8_t), haplosome_count)); int *node_groups = static_cast(calloc(sizeof(int), haplosome_count)); int next_node_group = 1; path_components.reserve(haplosome_count); for (size_t edge_index = 0; edge_index < edge_count; ++edge_index) { greedy_edge &candidate_edge = edge_buf[edge_index]; // Get the participating nodes and check that they still have a free end int i = candidate_edge.i; if (node_degrees[i] == 2) continue; int k = candidate_edge.k; if (node_degrees[k] == 2) continue; // Check whether they are in the same group (and not 0), in which case this edge would create a cycle int group_i = node_groups[i]; int group_k = node_groups[k]; if ((group_i != 0) && (group_i == group_k)) continue; // OK, the edge is legal. Add it to our path, and maintain the group tags path_components.emplace_back(candidate_edge); node_degrees[i]++; node_degrees[k]++; if ((group_i == 0) && (group_k == 0)) { // making a new group node_groups[i] = next_node_group; node_groups[k] = next_node_group; next_node_group++; } else if (group_i == 0) { // adding node i to an existing group node_groups[i] = group_k; } else if (group_k == 0) { // adding node k to an existing group node_groups[k] = group_i; } else { // joining two groups; one gets assimilated // the assimilation could probably be done more efficiently but this overhead won't matter for (size_t node_index = 0; node_index < haplosome_count; ++node_index) if (node_groups[node_index] == group_k) node_groups[node_index] = group_i; } if (path_components.size() == haplosome_count - 1) // no return edge break; if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) goto cancelExit; } // Check our work { int degree1_count = 0, degree2_count = 0, universal_group = node_groups[0]; for (size_t node_index = 0; node_index < haplosome_count; ++node_index) { if (node_degrees[node_index] == 1) ++degree1_count; else if (node_degrees[node_index] == 2) ++degree2_count; else qDebug() << "node of degree other than 1 or 2 seen (degree" << node_degrees[node_index] << ")"; if (node_groups[node_index] != universal_group) qDebug() << "node of non-matching group seen (group" << node_groups[node_index] << ")"; } // suppress "variable set but not used" warnings, since we may want these bookkeeping variables at some point... (void)degree1_count; (void)degree2_count; } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) goto cancelExit; // Finally, we have a jumble of edges that are in no order, and we need to make a coherent path from them. // We start at the first degree-1 node we find, which is one of the two ends; doesn't matter which. { size_t remaining_edge_count = haplosome_count - 1; size_t last_index; for (last_index = 0; last_index < haplosome_count; ++last_index) if (node_degrees[last_index] == 1) break; solution.emplace_back(static_cast(last_index)); do { // look for an edge involving last_index that we haven't used yet (there should be only one) size_t next_edge_index; int next_index = INT_MAX; // get rid of the unitialized var warning, and cause a crash if we have a bug for (next_edge_index = 0; next_edge_index < remaining_edge_count; ++next_edge_index) { greedy_edge &candidate_edge = path_components[next_edge_index]; if (candidate_edge.i == static_cast(last_index)) { next_index = candidate_edge.k; break; } else if (candidate_edge.k == static_cast(last_index)) { next_index = candidate_edge.i; break; } } if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) break; // found it; assimilate it into the path and remove it from path_components solution.emplace_back(next_index); last_index = static_cast(next_index); path_components[next_edge_index] = path_components[--remaining_edge_count]; } while (remaining_edge_count > 0); } cancelExit: free(node_degrees); free(node_groups); } // check that a given path visits every city exactly once bool QtSLiMHaplotypeManager::checkPath(std::vector &path, size_t haplosome_count) { uint8_t *visits = static_cast(calloc(sizeof(uint8_t), haplosome_count)); if (path.size() != haplosome_count) { qDebug() << "checkPath:size: path is wrong length"; free(visits); return false; } for (size_t i = 0; i < haplosome_count; ++i) { int city_index = path[i]; visits[city_index]++; } for (size_t i = 0; i < haplosome_count; ++i) if (visits[i] != 1) { qDebug() << "checkPath:size: city visited wrong count (" << visits[i] << ")"; free(visits); return false; } free(visits); return true; } // calculate the length of a given path int64_t QtSLiMHaplotypeManager::lengthOfPath(std::vector &path, int64_t *distances, size_t haplosome_count) { int64_t length = 0; int current_city = path[0]; for (size_t city_index = 1; city_index < haplosome_count; city_index++) { int next_city = path[city_index]; length += *(distances + static_cast(current_city) * haplosome_count + next_city); current_city = next_city; } return length; } // Do "2-opt" optimization of a given path, which involves inverting ranges of the path that lead to a better solution. // This is quite time-consuming and improves the result only marginally, so we do not want it to be the default, but it // might be useful to provide as an option. This method always takes the first optimization it sees that moves in a // positive direction; I tried taking the best optimization available at each step, instead, and it ran half as fast // and achieved results that were no better on average, so I didn't even keep that code. void QtSLiMHaplotypeManager::do2optOptimizationOfSolution(std::vector &path, int64_t *distances, size_t haplosome_count) { // Figure out the length of the current path int64_t original_distance = lengthOfPath(path, distances, haplosome_count); int64_t best_distance = original_distance; //NSLog(@"2-opt initial length: %lld", (long long int)best_distance); // Iterate until we can find no 2-opt improvement; this algorithm courtesy of https://en.wikipedia.org/wiki/2-opt size_t farthest_i = 0; // for our progress bar startAgain: for (size_t i = 0; i < haplosome_count - 1; i++) { for (size_t k = i + 1; k < haplosome_count; ++k) { // First, try the proposed path without actually constructing it; we just need to subtract the lengths of the // edges being removed and add the lengths of the edges being added, rather than constructing the whole new // path and measuring its length. If we have a path 1:9 and are inverting i=3 to k=5, it looks like: // // 1 2 3 4 5 6 7 8 9 // (i k) // // 1 2 (5 4 3) 6 7 8 9 // // So the 2-3 edge and the 5-6 edge were subtracted, and the 2-5 edge and the 3-6 edge were added. Note that // we can only get away with juggling the distances this way because our problem is symmetric; the length of // 3-4-5 is guaranteed the same as the length of the reversed segment 5-4-3. If the reversed segment is at // one or the other end of the path, we only need to patch up one edge; we don't return to the start city. // Note also that i and k are not haplosome indexes; they are indexes into our current path, which provides us // with the relevant haplosomes indexes. int64_t new_distance = best_distance; size_t index_i = static_cast(path[i]); size_t index_k = static_cast(path[k]); if (i > 0) { size_t index_i_minus_1 = static_cast(path[i - 1]); new_distance -= *(distances + index_i_minus_1 + index_i * haplosome_count); // remove edge (i-1)-(i) new_distance += *(distances + index_i_minus_1 + index_k * haplosome_count); // add edge (i-1)-(k) } if (k < haplosome_count - 1) { size_t index_k_plus_1 = static_cast(path[k + 1]); new_distance -= *(distances + index_k + index_k_plus_1 * haplosome_count); // remove edge (k)-(k+1) new_distance += *(distances + index_i + index_k_plus_1 * haplosome_count); // add edge (i)-(k+1) } if (new_distance < best_distance) { // OK, the new path is an improvement, so let's take it. We construct it by inverting the sequence // from i to k in our path vector, by swapping elements until we reach the center. for (size_t inversion_length = 0; ; inversion_length++) { size_t swap1 = i + inversion_length; size_t swap2 = k - inversion_length; if (swap1 >= swap2) break; std::swap(path[swap1], path[swap2]); } best_distance = new_distance; //NSLog(@"Improved path length: %lld (inverted from %d to %d)", (long long int)best_distance, i, k); //NSLog(@" checkback: new path length is %lld", (long long int)[self lengthOfPath:path withDistances:distances size:haplosome_count]); goto startAgain; } } // We update our progress bar according to the furthest we have ever gotten in the outer loop; we keep having to start // over again, and there's no way to know how many times we're going to do that, so this seems like the best estimator. farthest_i = std::max(farthest_i, i + 1); if (progressPanel_) progressPanel_->setHaplotypeProgress(farthest_i, 2); if (progressPanel_ && progressPanel_->haplotypeProgressIsCancelled()) break; } //NSLog(@"Distance changed from %lld to %lld (%.3f%% improvement)", (long long int)original_distance, (long long int)best_distance, ((original_distance - best_distance) / (double)original_distance) * 100.0); } // // QtSLiMHaplotypeView // // This class is private to QtSLiMHaplotypeManager, but is declared here so MOC gets it automatically // It displays a haplotype view for one chromosome; QtSLiMHaplotypeTopView may contain one or more // #pragma mark QtSLiMHaplotypeView QtSLiMHaplotypeView::QtSLiMHaplotypeView(QWidget *p_parent, Qt::WindowFlags f) #ifndef SLIM_NO_OPENGL : QOpenGLWidget(p_parent, f) #else : QWidget(p_parent, f) #endif { // We support both OpenGL and non-OpenGL display, because some platforms seem // to have problems with OpenGL (https://github.com/MesserLab/SLiM/issues/462) QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::useOpenGLPrefChanged, this, [this]() { update(); }); } QtSLiMHaplotypeView::~QtSLiMHaplotypeView(void) { delegate_ = nullptr; } #ifndef SLIM_NO_OPENGL void QtSLiMHaplotypeView::initializeGL() { initializeOpenGLFunctions(); glClearColor(1.0f, 0.0f, 0.0f, 1.0f); } void QtSLiMHaplotypeView::resizeGL(int w, int h) { glViewport(0, 0, w, h); // Update the projection glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } #endif #ifndef SLIM_NO_OPENGL void QtSLiMHaplotypeView::paintGL() #else void QtSLiMHaplotypeView::paintEvent(QPaintEvent * /* p_paint_event */) #endif { QPainter painter(this); QRect interior = rect(); // frame with gray #ifndef SLIM_NO_OPENGL if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) { painter.beginNativePainting(); glColor3f(0.5f, 0.5f, 0.5f); glRecti(interior.x(), interior.y(), interior.x() + interior.width(), interior.y() + interior.height()); } else #endif { painter.fillRect(interior, QtSLiMColorWithWhite(0.5, 1.0)); } interior.adjust(1, 1, -1, -1); if (delegate_) { #ifndef SLIM_NO_OPENGL if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) delegate_->glDrawHaplotypes(interior, displayBlackAndWhite_, showSubpopulationStrips_, true); else #endif delegate_->qtDrawHaplotypes(interior, displayBlackAndWhite_, showSubpopulationStrips_, true, painter); } else { #ifndef SLIM_NO_OPENGL if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) { painter.beginNativePainting(); glColor3f(0.95f, 0.95f, 0.95f); glRecti(interior.x(), interior.y(), interior.x() + interior.width(), interior.y() + interior.height()); } else #endif { painter.fillRect(interior, QtSLiMColorWithWhite(0.9, 1.0)); } } #ifndef SLIM_NO_OPENGL if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) painter.endNativePainting(); #endif } void QtSLiMHaplotypeView::setDisplayBlackAndWhite(bool flag) { displayBlackAndWhite_ = flag; update(); } void QtSLiMHaplotypeView::setDisplaySubpopulationStrips(bool flag) { showSubpopulationStrips_ = flag; update(); } // // QtSLiMHaplotypeTopView // // This class is private to QtSLiMHaplotypeManager, but is declared here so MOC gets it automatically // This contains a set of QtSLiMHaplotypeViews to display a set of haplotype plots for chromosomes // #pragma mark QtSLiMHaplotypeTopView QtSLiMHaplotypeTopView::QtSLiMHaplotypeTopView(QWidget *p_parent, Qt::WindowFlags f) : QWidget(p_parent, f) { } QtSLiMHaplotypeTopView::~QtSLiMHaplotypeTopView(void) { } void QtSLiMHaplotypeTopView::paintEvent(QPaintEvent * /* p_paint_event */) { QPainter painter(this); QRect interior = rect(); painter.fillRect(interior, Qt::white); if (showChromosomeSymbols_) { // We draw the text labels for our child views, since we are a regular QWidget and they might be QOpenGLWidgets // This is the main motivation for the existence of this class at all; taking a snapshot of a mixed-drawing // widget with render() does not work well for some reason, so we need the QPainter drawing done in the parent static QFont *tickFont = nullptr; if (!tickFont) { tickFont = new QFont(); #ifdef __linux__ tickFont->setPointSize(8); #else tickFont->setPointSize(11); #endif } painter.setFont(*tickFont); painter.setPen(Qt::black); QFontMetricsF fontMetrics(*tickFont); const QObjectList &child_objects = children(); for (QObject *child_object : child_objects) { QtSLiMHaplotypeView *child_widget = qobject_cast(child_object); if (child_widget && child_widget->isVisible()) { QString chromosomeSymbol = QString::fromStdString(child_widget->chromosomeSymbol_); if (chromosomeSymbol.length()) { #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) double symbolLabelWidth = fontMetrics.width(chromosomeSymbol); // deprecated in 5.11 #else double symbolLabelWidth = fontMetrics.horizontalAdvance(chromosomeSymbol); // added in Qt 5.11 #endif QRect viewFrame = child_widget->geometry(); QRect labelFrame = QRect(viewFrame.left(), 0, viewFrame.width(), viewFrame.top()); if (symbolLabelWidth <= viewFrame.width()) painter.drawText(labelFrame, Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignVCenter | Qt::AlignHCenter, chromosomeSymbol); } } } } } void QtSLiMHaplotypeTopView::actionButtonRunMenu(QtSLiMPushButton *p_actionButton) { QMenu contextMenu("graph_menu", this); QAction *bwColorToggle = contextMenu.addAction(displayBlackAndWhite_ ? "Display Colors" : "Display Black && White"); QAction *subpopStripsToggle = contextMenu.addAction(showSubpopulationStrips_ ? "Hide Subpopulation Strips" : "Show Subpopulation Strips"); contextMenu.addSeparator(); QAction *copyPlot = contextMenu.addAction("Copy Plot"); QAction *exportPlot = contextMenu.addAction("Export Plot..."); // Run the context menu synchronously QPoint menuPos = QCursor::pos(); QAction *action = contextMenu.exec(menuPos); // Act upon the chosen action; we just do it right here instead of dealing with slots if (action) { if (action == bwColorToggle) { displayBlackAndWhite_ = !displayBlackAndWhite_; // We tell all of our child QtSLiMHaplotypeViews about this configuration change const QObjectList &child_objects = children(); for (QObject *child_object : child_objects) { QtSLiMHaplotypeView *child_widget = qobject_cast(child_object); if (child_widget && child_widget->isVisible()) child_widget->setDisplayBlackAndWhite(displayBlackAndWhite_); } } if (action == subpopStripsToggle) { showSubpopulationStrips_ = !showSubpopulationStrips_; // We tell all of our child QtSLiMHaplotypeViews about this configuration change const QObjectList &child_objects = children(); for (QObject *child_object : child_objects) { QtSLiMHaplotypeView *child_widget = qobject_cast(child_object); if (child_widget && child_widget->isVisible()) child_widget->setDisplaySubpopulationStrips(showSubpopulationStrips_); } } else if (action == copyPlot) { QPixmap pixmap(size()); render(&pixmap); QImage snap = pixmap.toImage(); QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setImage(snap); } else if (action == exportPlot) { // FIXME maybe this should use QtSLiMDefaultSaveDirectory? see QtSLiMWindow::saveAs() QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); QFileInfo fileInfo(QDir(desktopPath), "haplotypes.png"); QString path = fileInfo.absoluteFilePath(); QString fileName = QFileDialog::getSaveFileName(this, "Export Graph", path); if (!fileName.isEmpty()) { QPixmap pixmap(size()); render(&pixmap); QImage snap = pixmap.toImage(); snap.save(fileName, "PNG", 100); // JPG does not come out well; colors washed out } } } // This is not called by Qt, for some reason (nested tracking loops?), so we call it explicitly p_actionButton->qtslimSetHighlight(false); } ================================================ FILE: QtSLiM/QtSLiMHaplotypeManager.h ================================================ // // QtSLiMHaplotypeManager.h // SLiM // // Created by Ben Haller on 4/3/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMHAPLOTYPEMANAGER_H #define QTSLIMHAPLOTYPEMANAGER_H // Silence deprecated OpenGL warnings on macOS #define GL_SILENCE_DEPRECATION #include #include #include #include #ifndef SLIM_NO_OPENGL #include #include #endif #include #include "slim_globals.h" #include "mutation.h" #include "QtSLiMChromosomeWidget.h" class QtSLiMChromosomeWidgetController; class Species; class Haplosome; class QtSLiMHaplotypeProgress; class QtSLiMPushButton; class QtSLiMHaplotypeManager : public QObject { Q_OBJECT public: enum ClusteringMethod { ClusterNearestNeighbor, ClusterGreedy }; enum ClusteringOptimization { ClusterNoOptimization, ClusterOptimizeWith2opt }; // This class method runs a plot options dialog, and then produces a haplotype plot with a progress panel as it is being constructed static void CreateHaplotypePlot(QtSLiMChromosomeWidgetController *controller, Chromosome *focalChromosome); // Constructing a QtSLiMHaplotypeManager directly is also allowing, if you don't want options or progress QtSLiMHaplotypeManager(QObject *p_parent, ClusteringMethod clusteringMethod, ClusteringOptimization optimizationMethod, QtSLiMChromosomeWidgetController *controller, Species *displaySpecies, Chromosome *chromosome, QtSLiMRange displayedRange, size_t sampleSize, bool showProgress, int progressChromIndex, int progressChromTotal); ~QtSLiMHaplotypeManager(void); #ifndef SLIM_NO_OPENGL void glDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground); #endif void qtDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground, QPainter &painter); // Public properties QString titleString; QString titleStringWithoutChromosome; int subpopCount = 0; bool valid_ = true; // set to false if the user cancels the progress panel private: QtSLiMChromosomeWidgetController *controller_ = nullptr; std::string focalSpeciesName_; // we keep the name of our focal species, since a pointer would be unsafe Species *focalDisplaySpecies(void); QtSLiMHaplotypeProgress *progressPanel_ = nullptr; ClusteringMethod clusterMethod; ClusteringOptimization clusterOptimization; // Display list data structures. We map every Mutation in the registry to a struct we define that keeps the necessary information // to display that mutation: position and color. We use MutationIndex to index into a vector of those structs, using the same // index values used by the registry for simplicity. Each haplosome is then turned into a vector of MutationIndex that lets // us plot the mutations for that haplosome. struct HaploMutation { slim_position_t position_; float red_, green_, blue_; bool neutral_; // selection_coeff_ == 0.0, used to display neutral mutations under selected mutations bool display_; // from the mutation type's mutation_type_displayed_ flag }; // Haplosomes: note that this vector points back into SLiM's data structures, so using it is not safe in general. It is used // by this class only while building the display list below; after that stage, we clear this vector. The work to build the // display list gets done on a background thread, but the SLiMgui window is blocked by the progress panel during that time. std::vector haplosomes; // Display list HaploMutation *mutationInfo = nullptr; // a buffer of SLiMHaploMutation providing display information per mutation slim_position_t *mutationPositions = nullptr; // the same info as in mutationInfo, but in a single buffer for access efficiency slim_position_t mutationLastPosition = 0; // from the chromosome size_t mutationIndexCount = 0; // the number of MutationIndex values in use std::vector> *displayList = nullptr; // a vector of haplosome information, where each haplosome is a vector of MutationIndex // Subpopulation information std::vector haplosomeSubpopIDs; // the subpop ID for each haplosome, corresponding to the display list order slim_objectid_t maxSubpopID = 0; slim_objectid_t minSubpopID = 0; // Chromosome subrange information bool usingSubrange = false; slim_position_t subrangeFirstBase = 0, subrangeLastBase = 0; // Mutation type display information bool displayingMuttypeSubset = false; void finishClusteringAnalysis(void); void configureMutationInfoBuffer(Chromosome *chromosome); void sortHaplosomes(void); void configureDisplayBuffers(void); void allocateGLBuffers(void); void tallyBincounts(int64_t *bincounts, std::vector &haplosomeList); int64_t distanceForBincounts(int64_t *bincounts1, int64_t *bincounts2); // OpenGL drawing; this is the primary drawing code #ifndef SLIM_NO_OPENGL void glDrawSubpopStripsInRect(QRect interior); void glDrawDisplayListInRect(QRect interior, bool displayBW); #endif // Qt-based drawing, provided as a backup if OpenGL has problems on a given platform void qtDrawSubpopStripsInRect(QRect interior, QPainter &painter); void qtDrawDisplayListInRect(QRect interior, bool displayBW, QPainter &painter); int64_t *buildDistanceArray(void); int64_t *buildDistanceArrayForSubrange(void); int64_t *buildDistanceArrayForSubtypes(void); int64_t *buildDistanceArrayForSubrangeAndSubtypes(void); int indexOfMostIsolatedHaplosomeWithDistances(int64_t *distances, size_t haplosome_count); void nearestNeighborSolve(int64_t *distances, size_t haplosome_count, std::vector &solution); void greedySolve(int64_t *distances, size_t haplosome_count, std::vector &solution); bool checkPath(std::vector &path, size_t haplosome_count); int64_t lengthOfPath(std::vector &path, int64_t *distances, size_t haplosome_count); void do2optOptimizationOfSolution(std::vector &path, int64_t *distances, size_t haplosome_count); }; // // QtSLiMHaplotypeView // // This class is private to QtSLiMHaplotypeManager, but is declared here so MOC gets it automatically // It displays a haplotype view for one chromosome; QtSLiMHaplotypeTopView may contain one or more // #ifndef SLIM_NO_OPENGL class QtSLiMHaplotypeView : public QOpenGLWidget, protected QOpenGLFunctions #else class QtSLiMHaplotypeView : public QWidget #endif { Q_OBJECT public: std::string chromosomeSymbol_; explicit QtSLiMHaplotypeView(QWidget *p_parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); virtual ~QtSLiMHaplotypeView(void) override; void setDelegate(QtSLiMHaplotypeManager *delegate) { delegate_ = delegate; delegate_->setParent(this); update(); } // state changes from the action button; called by QtSLiMHaplotypeTopView void setDisplayBlackAndWhite(bool flag); void setDisplaySubpopulationStrips(bool flag); private: QtSLiMHaplotypeManager *delegate_ = nullptr; bool displayBlackAndWhite_ = false; bool showSubpopulationStrips_ = false; #ifndef SLIM_NO_OPENGL virtual void initializeGL() override; virtual void resizeGL(int w, int h) override; virtual void paintGL() override; #else virtual void paintEvent(QPaintEvent *event) override; #endif }; // // QtSLiMHaplotypeTopView // // This class is private to QtSLiMHaplotypeManager, but is declared here so MOC gets it automatically // This contains a set of QtSLiMHaplotypeViews to display a set of haplotype plots for chromosomes // class QtSLiMHaplotypeTopView : public QWidget { Q_OBJECT public: explicit QtSLiMHaplotypeTopView(QWidget *p_parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); virtual ~QtSLiMHaplotypeTopView(void) override; public slots: void actionButtonRunMenu(QtSLiMPushButton *actionButton); void setShowChromosomeSymbols(bool flag) { showChromosomeSymbols_ = flag; } private: bool displayBlackAndWhite_ = false; bool showSubpopulationStrips_ = false; bool showChromosomeSymbols_ = false; virtual void paintEvent(QPaintEvent *event) override; }; #endif // QTSLIMHAPLOTYPEMANAGER_H ================================================ FILE: QtSLiM/QtSLiMHaplotypeManager_GL.cpp ================================================ // // QtSLiMHaplotypeManager_GL.h // SLiM // // Created by Ben Haller on 8/26/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef SLIM_NO_OPENGL #include "QtSLiMHaplotypeManager.h" #include "QtSLiMExtras.h" #include "QtSLiMOpenGL.h" #include #include #include #include void QtSLiMHaplotypeManager::glDrawSubpopStripsInRect(QRect interior) { // Set up to draw rects SLIM_GL_PREPARE(); // Loop through the haplosomes and draw them; we do this in two passes, neutral mutations underneath selected mutations size_t haplosome_index = 0, haplosome_count = haplosomeSubpopIDs.size(); float height_divisor = haplosome_count; float left = static_cast(interior.x()); float right = static_cast(interior.x() + interior.width()); for (slim_objectid_t haplosome_subpop_id : haplosomeSubpopIDs) { float top = interior.y() + (haplosome_index / height_divisor) * interior.height(); float bottom = interior.y() + ((haplosome_index + 1) / height_divisor) * interior.height(); if (bottom - top > 1.0f) { // If the range spans a width of more than one pixel, then use the maximal pixel range top = floorf(top); bottom = ceilf(bottom); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary top = floorf(top); bottom = top + 1; } SLIM_GL_PUSHRECT(); float colorRed, colorGreen, colorBlue, colorAlpha; double hue = (haplosome_subpop_id - minSubpopID) / static_cast(maxSubpopID - minSubpopID + 1); QColor hsbColor = QtSLiMColorWithHSV(hue, 1.0, 1.0, 1.0); QColor rgbColor = hsbColor.toRgb(); colorRed = static_cast(rgbColor.redF()); colorGreen = static_cast(rgbColor.greenF()); colorBlue = static_cast(rgbColor.blueF()); colorAlpha = 1.0; SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); haplosome_index++; } // Draw any leftovers SLIM_GL_FINISH(); } void QtSLiMHaplotypeManager::glDrawDisplayListInRect(QRect interior, bool displayBW) { // Set up to draw rects SLIM_GL_PREPARE(); // decide whether to plot in ascending order or descending order; we do this based on // which end has higher mutational density, to try to maximize visual continuity size_t haplosome_count = displayList->size(); bool ascending = true; if (haplosome_count > 1) { std::vector &first_haplosome_list = (*displayList)[0]; std::vector &last_haplosome_list = (*displayList)[haplosome_count - 1]; ascending = (first_haplosome_list.size() < last_haplosome_list.size()); } // Loop through the haplosomes and draw them; we do this in two passes, neutral mutations underneath selected mutations for (int pass_count = 0; pass_count <= 1; ++pass_count) { bool plotting_neutral = (pass_count == 0); float height_divisor = haplosome_count; float width_subtractor = (usingSubrange ? subrangeFirstBase : 0); float width_divisor = (usingSubrange ? (subrangeLastBase - subrangeFirstBase + 1) : (mutationLastPosition + 1)); for (size_t haplosome_index = 0; haplosome_index < haplosome_count; ++haplosome_index) { std::vector &haplosome_list = (ascending ? (*displayList)[haplosome_index] : (*displayList)[(haplosome_count - 1) - haplosome_index]); float top = interior.y() + (haplosome_index / height_divisor) * interior.height(); float bottom = interior.y() + ((haplosome_index + 1) / height_divisor) * interior.height(); if (bottom - top > 1.0f) { // If the range spans a width of more than one pixel, then use the maximal pixel range top = floorf(top); bottom = ceilf(bottom); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary top = floorf(top); bottom = top + 1; } for (MutationIndex mut_index : haplosome_list) { HaploMutation &mut_info = mutationInfo[mut_index]; if (mut_info.neutral_ == plotting_neutral) { slim_position_t mut_position = mut_info.position_; float left = interior.x() + ((mut_position - width_subtractor) / width_divisor) * interior.width(); float right = interior.x() + ((mut_position - width_subtractor + 1) / width_divisor) * interior.width(); if (right - left > 1.0f) { // If the range spans a width of more than one pixel, then use the maximal pixel range left = floorf(left); right = ceilf(right); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary left = floorf(left); right = left + 1; } SLIM_GL_PUSHRECT(); float colorRed, colorGreen, colorBlue, colorAlpha = 1.0; if (displayBW) { colorRed = 0; colorGreen = 0; colorBlue = 0; } else { colorRed = mut_info.red_; colorGreen = mut_info.green_; colorBlue = mut_info.blue_; } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } } // Draw any leftovers SLIM_GL_FINISH(); } #endif ================================================ FILE: QtSLiM/QtSLiMHaplotypeManager_QT.cpp ================================================ // // QtSLiMHaplotypeManager_QT.h // SLiM // // Created by Ben Haller on 8/26/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMHaplotypeManager.h" #include "QtSLiMExtras.h" #include "QtSLiMOpenGL_Emulation.h" #include #include #include #include void QtSLiMHaplotypeManager::qtDrawSubpopStripsInRect(QRect interior, QPainter &painter) { // Set up to draw rects SLIM_GL_PREPARE(); // Loop through the haplosomes and draw them; we do this in two passes, neutral mutations underneath selected mutations size_t haplosome_index = 0, haplosome_count = haplosomeSubpopIDs.size(); float height_divisor = haplosome_count; float left = static_cast(interior.x()); float right = static_cast(interior.x() + interior.width()); for (slim_objectid_t haplosome_subpop_id : haplosomeSubpopIDs) { float top = interior.y() + (haplosome_index / height_divisor) * interior.height(); float bottom = interior.y() + ((haplosome_index + 1) / height_divisor) * interior.height(); if (bottom - top > 1.0f) { // If the range spans a width of more than one pixel, then use the maximal pixel range top = floorf(top); bottom = ceilf(bottom); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary top = floorf(top); bottom = top + 1; } SLIM_GL_PUSHRECT(); float colorRed, colorGreen, colorBlue, colorAlpha; double hue = (haplosome_subpop_id - minSubpopID) / static_cast(maxSubpopID - minSubpopID + 1); QColor hsbColor = QtSLiMColorWithHSV(hue, 1.0, 1.0, 1.0); QColor rgbColor = hsbColor.toRgb(); colorRed = static_cast(rgbColor.redF()); colorGreen = static_cast(rgbColor.greenF()); colorBlue = static_cast(rgbColor.blueF()); colorAlpha = 1.0; SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); haplosome_index++; } // Draw any leftovers SLIM_GL_FINISH(); } void QtSLiMHaplotypeManager::qtDrawDisplayListInRect(QRect interior, bool displayBW, QPainter &painter) { // Set up to draw rects SLIM_GL_PREPARE(); // decide whether to plot in ascending order or descending order; we do this based on // which end has higher mutational density, to try to maximize visual continuity size_t haplosome_count = displayList->size(); bool ascending = true; if (haplosome_count > 1) { std::vector &first_haplosome_list = (*displayList)[0]; std::vector &last_haplosome_list = (*displayList)[haplosome_count - 1]; ascending = (first_haplosome_list.size() < last_haplosome_list.size()); } // Loop through the haplosomes and draw them; we do this in two passes, neutral mutations underneath selected mutations for (int pass_count = 0; pass_count <= 1; ++pass_count) { bool plotting_neutral = (pass_count == 0); float height_divisor = haplosome_count; float width_subtractor = (usingSubrange ? subrangeFirstBase : 0); float width_divisor = (usingSubrange ? (subrangeLastBase - subrangeFirstBase + 1) : (mutationLastPosition + 1)); for (size_t haplosome_index = 0; haplosome_index < haplosome_count; ++haplosome_index) { std::vector &haplosome_list = (ascending ? (*displayList)[haplosome_index] : (*displayList)[(haplosome_count - 1) - haplosome_index]); float top = interior.y() + (haplosome_index / height_divisor) * interior.height(); float bottom = interior.y() + ((haplosome_index + 1) / height_divisor) * interior.height(); if (bottom - top > 1.0f) { // If the range spans a width of more than one pixel, then use the maximal pixel range top = floorf(top); bottom = ceilf(bottom); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary top = floorf(top); bottom = top + 1; } for (MutationIndex mut_index : haplosome_list) { HaploMutation &mut_info = mutationInfo[mut_index]; if (mut_info.neutral_ == plotting_neutral) { slim_position_t mut_position = mut_info.position_; float left = interior.x() + ((mut_position - width_subtractor) / width_divisor) * interior.width(); float right = interior.x() + ((mut_position - width_subtractor + 1) / width_divisor) * interior.width(); if (right - left > 1.0f) { // If the range spans a width of more than one pixel, then use the maximal pixel range left = floorf(left); right = ceilf(right); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary left = floorf(left); right = left + 1; } SLIM_GL_PUSHRECT(); float colorRed, colorGreen, colorBlue, colorAlpha = 1.0; if (displayBW) { colorRed = 0; colorGreen = 0; colorBlue = 0; } else { colorRed = mut_info.red_; colorGreen = mut_info.green_; colorBlue = mut_info.blue_; } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); } } } } // Draw any leftovers SLIM_GL_FINISH(); } ================================================ FILE: QtSLiM/QtSLiMHaplotypeOptions.cpp ================================================ // // QtSLiMHaplotypeOptions.cpp // SLiM // // Created by Ben Haller on 4/4/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMHaplotypeOptions.h" #include "ui_QtSLiMHaplotypeOptions.h" #include #include "QtSLiMAppDelegate.h" QtSLiMHaplotypeOptions::QtSLiMHaplotypeOptions(QWidget *p_parent) : QDialog(p_parent), ui(new Ui::QtSLiMHaplotypeOptions) { ui->setupUi(this); // no window icon #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect setWindowIcon(QIcon()); #endif // change the app icon to our multi-size app icon for best results ui->appIconButton->setIcon(qtSLiMAppDelegate->applicationIcon()); // fix sizing setFixedSize(sizeHint()); setSizeGripEnabled(false); // enabled/disable the sample size lineEdit connect(ui->haplosomesSampleRadio, &QAbstractButton::toggled, this, [this]() { ui->sampleSizeLineEdit->setEnabled(ui->haplosomesSampleRadio->isChecked()); }); } QtSLiMHaplotypeOptions::~QtSLiMHaplotypeOptions() { delete ui; } void QtSLiMHaplotypeOptions::done(int r) { // do validation; see https://www.qtcentre.org/threads/8048-Validate-Data-in-QDialog bool usingSampleSize = ui->haplosomesSampleRadio->isChecked(); if ((QDialog::Accepted == r) && usingSampleSize) // ok was pressed and the sample size field is being used { QString sampleSizeText = ui->sampleSizeLineEdit->text(); size_t sampleSizeInt = sampleSizeText.toULong(); QString validatedText = QString("%1").arg(sampleSizeInt); if ((sampleSizeText == validatedText) && (sampleSizeInt > 1)) { QDialog::done(r); return; } else { qApp->beep(); return; } } else // cancel, close or exc was pressed { QDialog::done(r); return; } } size_t QtSLiMHaplotypeOptions::haplosomeSampleSize(void) { bool usingSampleSize = ui->haplosomesSampleRadio->isChecked(); if (!usingSampleSize) return 0; return ui->sampleSizeLineEdit->text().toULong(); } QtSLiMHaplotypeManager::ClusteringMethod QtSLiMHaplotypeOptions::clusteringMethod(void) { if (ui->clusterNearestRadio->isChecked()) return QtSLiMHaplotypeManager::ClusterNearestNeighbor; if (ui->clusterGreedyRadio->isChecked()) return QtSLiMHaplotypeManager::ClusterGreedy; if (ui->clusterGreedyOpt2Radio->isChecked()) return QtSLiMHaplotypeManager::ClusterGreedy; return QtSLiMHaplotypeManager::ClusterNearestNeighbor; } QtSLiMHaplotypeManager::ClusteringOptimization QtSLiMHaplotypeOptions::clusteringOptimization(void) { if (ui->clusterNearestRadio->isChecked()) return QtSLiMHaplotypeManager::ClusterNoOptimization; if (ui->clusterGreedyRadio->isChecked()) return QtSLiMHaplotypeManager::ClusterNoOptimization; if (ui->clusterGreedyOpt2Radio->isChecked()) return QtSLiMHaplotypeManager::ClusterOptimizeWith2opt; return QtSLiMHaplotypeManager::ClusterNoOptimization; } ================================================ FILE: QtSLiM/QtSLiMHaplotypeOptions.h ================================================ // // QtSLiMHaplotypeOptions.h // SLiM // // Created by Ben Haller on 4/4/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMHAPLOTYPEOPTIONS_H #define QTSLIMHAPLOTYPEOPTIONS_H #include #include "QtSLiMHaplotypeManager.h" namespace Ui { class QtSLiMHaplotypeOptions; } class QtSLiMHaplotypeOptions : public QDialog { Q_OBJECT public: explicit QtSLiMHaplotypeOptions(QWidget *p_parent = nullptr); virtual ~QtSLiMHaplotypeOptions() override; size_t haplosomeSampleSize(void); // 0 indicates "all haplosomes" QtSLiMHaplotypeManager::ClusteringMethod clusteringMethod(void); QtSLiMHaplotypeManager::ClusteringOptimization clusteringOptimization(void); private: Ui::QtSLiMHaplotypeOptions *ui; virtual void done(int r) override; }; #endif // QTSLIMHAPLOTYPEOPTIONS_H ================================================ FILE: QtSLiM/QtSLiMHaplotypeOptions.ui ================================================ QtSLiMHaplotypeOptions 0 0 479 437 Haplotype Plot Options true 0 0 48 48 48 48 :/icons/AppIcon128.png:/icons/AppIcon128.png 48 48 true Qt::Vertical 20 40 0 75 true Haplotype Plot Options Qt::Vertical QSizePolicy::Fixed 20 5 Choose options to be used in generating the haplotype plot. Note that some choices may cause plot generation to take a very long time! true Qt::Vertical QSizePolicy::Fixed 20 12 Qt::Horizontal QSizePolicy::Fixed 12 20 0 0 Haplosomes to display: true 8 12 12 10 All haplosomes in the selected subpopulation false A sample from those haplosomes of size: true Qt::Horizontal QSizePolicy::Fixed 8 5 65 0 65 16777215 1000 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Horizontal 40 5 0 0 Clustering algorithm: true 8 QLayout::SetDefaultConstraint 12 24 Nearest neighbor (faster for large samples) true Greedy (a little slower, but higher quality) Greedy + 2-opt (very slow, highest quality) Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok QtSLiMIconView QPushButton
QtSLiMExtras.h
buttonBox accepted() QtSLiMHaplotypeOptions accept() 248 254 157 274 buttonBox rejected() QtSLiMHaplotypeOptions reject() 316 260 286 274
================================================ FILE: QtSLiM/QtSLiMHaplotypeProgress.cpp ================================================ // // QtSLiMHaplotypeProgress.cpp // SLiM // // Created by Ben Haller on 4/4/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMHaplotypeProgress.h" #include "ui_QtSLiMHaplotypeProgress.h" #include "QtSLiMAppDelegate.h" QtSLiMHaplotypeProgress::QtSLiMHaplotypeProgress(QWidget *p_parent) : QDialog(p_parent), ui(new Ui::QtSLiMHaplotypeProgress) { ui->setupUi(this); // no window icon #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect setWindowIcon(QIcon()); #endif // change the app icon to our multi-size app icon for best results ui->appIconButton->setIcon(qtSLiMAppDelegate->applicationIcon()); // wire up cancel button connect(ui->cancelButton, &QPushButton::clicked, this, [this]() { cancelled_ = true; }); } QtSLiMHaplotypeProgress::~QtSLiMHaplotypeProgress() { delete ui; } void QtSLiMHaplotypeProgress::runProgressWithHaplosomeCount(size_t haplosome_count, int stepCount, int progressChromIndex, int progressChromTotal) { // set up initial state taskDistances_Value_ = 0; taskClustering_Value_ = 0; taskOptimization_Value_ = 0; ui->chromosomeIndexLabel->setText(QString("Chromosome %1 of %2:").arg(progressChromIndex).arg(progressChromTotal)); ui->step1ProgressBar->setRange(0, static_cast(haplosome_count)); ui->step2ProgressBar->setRange(0, static_cast(haplosome_count)); ui->step3ProgressBar->setRange(0, static_cast(haplosome_count)); ui->step1ProgressBar->setValue(0); ui->step2ProgressBar->setValue(0); ui->step3ProgressBar->setValue(0); // if we're not doing an optimization step, remove those controls if (stepCount == 2) { ui->barBoxLayout->removeWidget(ui->step3Box); // remove from layout ui->step3Box->setParent(nullptr); // remove from parent widget delete ui->step3Box; // also deletes ui->step3ProgressBar ui->step3Box = nullptr; ui->step3ProgressBar = nullptr; } // fix sizing setFixedSize(sizeHint()); setSizeGripEnabled(false); // make the progress window visible/active setModal(true); show(); } bool QtSLiMHaplotypeProgress::haplotypeProgressIsCancelled(void) { // spin the event loop for the panel, so the user can click "Cancel" if (!cancelled_) QCoreApplication::processEvents(); // return the cancelled state return cancelled_; } void QtSLiMHaplotypeProgress::setHaplotypeProgress(size_t progress, int stage) { switch (stage) { case 0: taskDistances_Value_ = static_cast(progress); break; case 1: taskClustering_Value_ = static_cast(progress); break; case 2: taskOptimization_Value_ = static_cast(progress); break; } ui->step1ProgressBar->setValue(taskDistances_Value_); ui->step2ProgressBar->setValue(taskClustering_Value_); if (ui->step3ProgressBar) ui->step3ProgressBar->setValue(taskOptimization_Value_); } ================================================ FILE: QtSLiM/QtSLiMHaplotypeProgress.h ================================================ // // QtSLiMHaplotypeProgress.h // SLiM // // Created by Ben Haller on 4/4/2020. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMHAPLOTYPEPROGRESS_H #define QTSLIMHAPLOTYPEPROGRESS_H #include namespace Ui { class QtSLiMHaplotypeProgress; } class QtSLiMHaplotypeProgress : public QDialog { Q_OBJECT public: explicit QtSLiMHaplotypeProgress(QWidget *p_parent = nullptr); virtual ~QtSLiMHaplotypeProgress() override; void runProgressWithHaplosomeCount(size_t haplosome_count, int stepCount, int progressChromIndex, int progressChromTotal); bool haplotypeProgressIsCancelled(void); void setHaplotypeProgress(size_t progress, int stage); private: Ui::QtSLiMHaplotypeProgress *ui; int taskDistances_Value_; int taskClustering_Value_; int taskOptimization_Value_; bool cancelled_ = false; }; #endif // QTSLIMHAPLOTYPEPROGRESS_H ================================================ FILE: QtSLiM/QtSLiMHaplotypeProgress.ui ================================================ QtSLiMHaplotypeProgress 0 0 327 330 Haplotype Plot Progress true 0 0 48 48 48 48 :/icons/AppIcon128.png:/icons/AppIcon128.png 48 48 true Qt::Orientation::Vertical 20 40 0 true Haplotype Plot Progress Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 20 5 Please wait for the haplotype analysis to complete... true Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 20 16 Chromosome X of Y: Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 20 12 Qt::Orientation::Horizontal QSizePolicy::Policy::Fixed 12 20 0 0 Calculating genetic distances: true 8 12 12 10 24 0 0 Clustering haplosomes: true 8 QLayout::SizeConstraint::SetDefaultConstraint 12 10 24 Optimizing clustering: true 8 16 24 0 0 0 Cancel QtSLiMIconView QPushButton
QtSLiMExtras.h
================================================ FILE: QtSLiM/QtSLiMHelpWindow.cpp ================================================ // // QtSLiMHelpWindow.cpp // SLiM // // Created by Ben Haller on 11/19/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMHelpWindow.h" #include "ui_QtSLiMHelpWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "community.h" #include "QtSLiMExtras.h" #include "QtSLiMAppDelegate.h" #include #include #include #include // // This is our custom outline item class, which can hold a QTextDocumentFragment // QtSLiMHelpItem::~QtSLiMHelpItem() { } QVariant QtSLiMHelpItem::data(int column, int role) const { if (is_top_level && (role == Qt::ForegroundRole)) { bool inDarkMode = QtSLiMInDarkMode(); if (inDarkMode) return QBrush(QtSLiMColorWithWhite(0.8, 1.0)); else return QBrush(QtSLiMColorWithWhite(0.4, 1.0)); } return QTreeWidgetItem::data(column, role); } // // This subclass of QStyledItemDelegate provides custom drawing for the outline view. // QtSLiMHelpOutlineDelegate::~QtSLiMHelpOutlineDelegate(void) { } void QtSLiMHelpOutlineDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QString itemString = index.data().toString(); itemString = itemString.replace(QChar::Nbsp, ' '); // standardize down to regular spaces bool topLevel = !index.parent().isValid(); QRect fullRect = option.rect; fullRect.setLeft(0); // we are not clipped; we will draw outside our rect to occupy the full width of the view // if we're a top-level item, wash a background color over our row; we use alpha so the disclosure triangle remains visible if (topLevel) { bool inDarkMode = QtSLiMInDarkMode(); bool isEidos = index.data().toString().startsWith("Eidos "); if (isEidos) painter->fillRect(fullRect, QBrush(inDarkMode ? QtSLiMColorWithRGB(0.0, 1.0, 0.0, 0.10) : QtSLiMColorWithRGB(0.0, 1.0, 0.0, 0.04))); // pale green background for Eidos docs else painter->fillRect(fullRect, QBrush(inDarkMode ? QtSLiMColorWithRGB(0.0, 0.0, 1.0, 0.15) : QtSLiMColorWithRGB(0.0, 0.0, 1.0, 0.04))); // pale blue background for SLiM docs } // On Ubuntu, items get shown as having "focus" even when they're not selectable, which I dislike; this disables that appearance // See https://stackoverflow.com/a/2061871/2752221 QStyleOptionViewItem modifiedOption(option); if (modifiedOption.state & QStyle::State_HasFocus) modifiedOption.state = modifiedOption.state ^ QStyle::State_HasFocus; // then let super draw QStyledItemDelegate::paint(painter, modifiedOption, index); // custom overdraw if (topLevel) { // if an item is top-level, we want to frame it to look heavier, like a "group item" on macOS bool inDarkMode = QtSLiMInDarkMode(); painter->fillRect(QRect(fullRect.left(), fullRect.top(), fullRect.width(), 1), QtSLiMColorWithWhite(inDarkMode ? 0.15 : 0.85, 1.0)); // top edge in light gray painter->fillRect(QRect(fullRect.left(), fullRect.top() + fullRect.height() - 1, fullRect.width(), 1), QtSLiMColorWithWhite(inDarkMode ? 0.05 : 0.65, 1.0)); // bottom edge in medium gray } else { // otherwise, add a color box on the right for the items that need them static QStringList *stringsWF = nullptr; static QStringList *stringsNonWF = nullptr; static QStringList *stringsNucmut = nullptr; if (!stringsWF) stringsWF = new QStringList({"– addSubpopSplit()", "– registerMateChoiceCallback()", "cloningRate =>", "immigrantSubpopFractions =>", "immigrantSubpopIDs =>", "selfingRate =>", "sexRatio =>", "– setCloningRate()", "– setMigrationRates()", "– setSelfingRate()", "– setSexRatio()", "– setSubpopulationSize()", "mateChoice() callbacks" }); if (!stringsNonWF) stringsNonWF = new QStringList({"initializeSLiMModelType()", "age =>", "modelType =>", "– registerReproductionCallback()", "– registerSurvivalCallback()", "– addCloned()", "– addCrossed()", "– addEmpty()", "– addMultiRecombinant()", "– addRecombinant()", "– addSelfed()", "– removeSubpopulation()", "– killIndividuals()", "– takeMigrants()", "reproduction() callbacks", "survival() callbacks" }); if (!stringsNucmut) stringsNucmut = new QStringList({"initializeAncestralNucleotides()", "initializeMutationTypeNuc()", "initializeHotspotMap()", "codonsToAminoAcids()", "randomNucleotides()", "mm16To256()", "mmJukesCantor()", "mmKimura()", "nucleotideCounts()", "nucleotideFrequencies()", "nucleotidesToCodons()", "codonsToNucleotides()", "nucleotideBased =>", "nucleotide <–>", "nucleotideValue <–>", "nucleotide =>", "nucleotideValue =>", "mutationMatrix =>", "– setMutationMatrix()", "– ancestralNucleotides()", "– setAncestralNucleotides()", "– nucleotides()", "hotspotEndPositions =>", "hotspotEndPositionsF =>", "hotspotEndPositionsM =>", "hotspotMultipliers =>", "hotspotMultipliersF =>", "hotspotMultipliersM =>", "– setHotspotMap()" }); bool draw_WF_box = stringsWF->contains(itemString); bool draw_nonWF_box = stringsNonWF->contains(itemString); bool draw_nucmut_box = stringsNucmut->contains(itemString); if (draw_WF_box || draw_nonWF_box || draw_nucmut_box) { QRect boxRect = QRect(fullRect.left() + fullRect.width() - 14, fullRect.top() + 4, 8, 8); QColor boxColor; if (draw_WF_box) boxColor = QColor(66, 255, 53); // WF-only color (green) else if (draw_nonWF_box) boxColor = QColor(88, 148, 255); // nonWF-only color (blue) else //if (draw_nucmut_box) boxColor = QColor(228, 118, 255); // nucmut color (purple) painter->fillRect(boxRect, boxColor); QtSLiMFrameRect(boxRect, Qt::black, *painter); } } } // // This is the QWidget subclass for the help window, which does the heavy lifting of building the doc outline from HTML files // QtSLiMHelpWindow &QtSLiMHelpWindow::instance(void) { static QtSLiMHelpWindow *inst = nullptr; if (!inst) inst = new QtSLiMHelpWindow(nullptr); return *inst; } QtSLiMHelpWindow::QtSLiMHelpWindow(QWidget *p_parent) : QWidget(p_parent, Qt::Window), ui(new Ui::QtSLiMHelpWindow) { ui->setupUi(this); interpolateSplitters(); // no window icon #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect setWindowIcon(QIcon()); #endif // prevent this window from keeping the app running when all main windows are closed setAttribute(Qt::WA_QuitOnClose, false); // Configure the search field to look like a search field ui->searchField->setClearButtonEnabled(true); ui->searchField->setPlaceholderText("Search..."); connect(ui->searchField, &QLineEdit::returnPressed, this, &QtSLiMHelpWindow::searchFieldChanged); connect(ui->searchScopeButton, &QPushButton::clicked, this, &QtSLiMHelpWindow::searchScopeToggled); // Change the search scope button's appearance based on the app palette connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, &QtSLiMHelpWindow::applicationPaletteChanged); applicationPaletteChanged(); // Configure the outline view to behave as we wish connect(ui->topicOutlineView, &QTreeWidget::itemSelectionChanged, this, &QtSLiMHelpWindow::outlineSelectionChanged); connect(ui->topicOutlineView, &QTreeWidget::itemClicked, this, &QtSLiMHelpWindow::itemClicked); connect(ui->topicOutlineView, &QTreeWidget::itemCollapsed, this, &QtSLiMHelpWindow::itemCollapsed); connect(ui->topicOutlineView, &QTreeWidget::itemExpanded, this, &QtSLiMHelpWindow::itemExpanded); QAbstractItemDelegate *outlineDelegate = new QtSLiMHelpOutlineDelegate(ui->topicOutlineView); ui->topicOutlineView->setItemDelegate(outlineDelegate); // tweak appearance on Linux; the form is adjusted for macOS #if defined(__linux__) { // use a smaller font for the outline QFont outlineFont(ui->topicOutlineView->font()); outlineFont.setPointSizeF(outlineFont.pointSizeF() - 1); ui->topicOutlineView->setFont(outlineFont); // the headers/content button needs somewhat different metrics ui->searchScopeButton->setMinimumWidth(75); ui->searchScopeButton->setMaximumWidth(75); } #endif // Restore the saved window position; see https://doc.qt.io/qt-5/qsettings.html#details QSettings settings; settings.beginGroup("QtSLiMHelpWindow"); resize(settings.value("size", QSize(550, 400)).toSize()); move(settings.value("pos", QPoint(25, 45)).toPoint()); settings.endGroup(); // Add Eidos topics std::vector builtin_properties = EidosClass::RegisteredClassProperties(true, false); std::vector builtin_methods = EidosClass::RegisteredClassMethods(true, false); addTopicsFromRTFFile("EidosHelpFunctions", "Eidos Functions", &EidosInterpreter::BuiltInFunctions(), nullptr, nullptr); addTopicsFromRTFFile("EidosHelpClasses", "Eidos Classes", &EidosInterpreter::BuiltInFunctions(), &builtin_methods, &builtin_properties); // constructors are in BuiltInFunctions() addTopicsFromRTFFile("EidosHelpOperators", "Eidos Operators", nullptr, nullptr, nullptr); addTopicsFromRTFFile("EidosHelpStatements", "Eidos Statements", nullptr, nullptr, nullptr); addTopicsFromRTFFile("EidosHelpTypes", "Eidos Types", nullptr, nullptr, nullptr); // Check for completeness of the Eidos documentation checkDocumentationOfFunctions(&EidosInterpreter::BuiltInFunctions()); for (EidosClass *class_object : EidosClass::RegisteredClasses(true, false)) { const std::string &element_type = class_object->ClassName(); if (!Eidos_string_hasPrefix(element_type, "_") && (element_type != "DictionaryRetained")) // internal classes are undocumented { checkDocumentationOfClass(class_object); addSuperclassItemForClass(class_object); } } // Add SLiM topics std::vector context_properties = EidosClass::RegisteredClassProperties(false, true); std::vector context_methods = EidosClass::RegisteredClassMethods(false, true); const std::vector *zg_functions = Community::ZeroTickFunctionSignatures(); const std::vector *slim_functions = Community::SLiMFunctionSignatures(); std::vector all_slim_functions; all_slim_functions.insert(all_slim_functions.end(), zg_functions->begin(), zg_functions->end()); all_slim_functions.insert(all_slim_functions.end(), slim_functions->begin(), slim_functions->end()); addTopicsFromRTFFile("SLiMHelpFunctions", "SLiM Functions", &all_slim_functions, nullptr, nullptr); addTopicsFromRTFFile("SLiMHelpClasses", "SLiM Classes", nullptr, &context_methods, &context_properties); addTopicsFromRTFFile("SLiMHelpCallbacks", "SLiM Events and Callbacks", nullptr, nullptr, nullptr); // Check for completeness of the SLiM documentation checkDocumentationOfFunctions(&all_slim_functions); for (EidosClass *class_object : EidosClass::RegisteredClasses(false, true)) { const std::string &element_type = class_object->ClassName(); if (!Eidos_string_hasPrefix(element_type, "_")) // internal classes are undocumented { checkDocumentationOfClass(class_object); addSuperclassItemForClass(class_object); } } // make window actions for all global menu items qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); } void QtSLiMHelpWindow::applicationPaletteChanged(void) { bool inDarkMode = QtSLiMInDarkMode(); // Custom colors for the search mode button for dark mode vs. light mode; note that this completely overrides the style sheet in the .ui file! if (inDarkMode) ui->searchScopeButton->setStyleSheet("QPushButton { border: 1px solid #888; border-radius: 20px; border-style: outset; margin: 0px; padding: 2px; background: rgb(125, 125, 125); } QPushButton:pressed { background: rgb(105, 105, 105); }"); else ui->searchScopeButton->setStyleSheet("QPushButton { border: 1px solid #888; border-radius: 20px; border-style: outset; margin: 0px; padding: 2px; background: rgb(245, 245, 245); } QPushButton:pressed { background: rgb(195, 195, 195); }"); } QtSLiMHelpWindow::~QtSLiMHelpWindow() { delete ui; } void QtSLiMHelpWindow::interpolateSplitters(void) { #if 1 // add a top-level horizontal splitter QLayout *parentLayout = ui->horizontalLayout; QWidget *firstWidget = ui->topicOutlineView; QWidget *secondWidget = ui->descriptionTextEdit; // force geometry calculation, which is lazy setAttribute(Qt::WA_DontShowOnScreen, true); show(); hide(); setAttribute(Qt::WA_DontShowOnScreen, false); // change fixed-size views to be flexible, so they cooperate with the splitter firstWidget->setMinimumWidth(200); firstWidget->setMaximumWidth(400); // empty out parentLayout QLayoutItem *child; while ((child = parentLayout->takeAt(0)) != nullptr); // make the QSplitter between the left and right and add the subsidiary widgets to it splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(firstWidget); splitter->addWidget(secondWidget); splitter->setHandleWidth(splitter->handleWidth() + 3); splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 2); // initially, give 2/3 of the width to the description textedit splitter->setCollapsible(0, true); splitter->setCollapsible(1, false); // and finally, add the splitter to the parent layout parentLayout->addWidget(splitter); parentLayout->setContentsMargins(0, 0, 0, 0); #endif } bool QtSLiMHelpWindow::findItemsMatchingSearchString(QTreeWidgetItem *root, const QString searchString, bool titlesOnly, std::vector &matchKeys, std::vector &expandItems) { bool anyChildMatches = false; for (int child_index = 0; child_index < root->childCount(); ++child_index) { QTreeWidgetItem *childItem = root->child(child_index); if (childItem->childCount() > 0) { // Recurse through the child's children bool result = findItemsMatchingSearchString(childItem, searchString, titlesOnly, matchKeys, expandItems); if (result) anyChildMatches = true; } else if (childItem->childIndicatorPolicy() == QTreeWidgetItem::DontShowIndicatorWhenChildless) { // If the item has no children, and is not showing an indicator, it is a leaf and should be searched bool isMatch = false; const QString itemText = childItem->text(0); if (itemText.contains(searchString, Qt::CaseInsensitive)) isMatch = true; if (!titlesOnly) { QtSLiMHelpItem *helpItem = dynamic_cast(childItem); if (helpItem) { QTextDocumentFragment *helpFragment = helpItem->doc_fragment; if (helpFragment) { const QString helpText = helpFragment->toPlainText(); if (helpText.contains(searchString, Qt::CaseInsensitive)) isMatch = true; } } } if (isMatch) { matchKeys.emplace_back(childItem); anyChildMatches = true; } } } if (anyChildMatches) expandItems.emplace_back(root); return anyChildMatches; } void QtSLiMHelpWindow::expandToShowItems(const std::vector &expandItems, const std::vector &matchKeys) { // Coalesce the selection change to avoid obsessively re-generating the documentation textedit doingProgrammaticSelection = true; doingProgrammaticCollapseExpand = true; // Deselect and collapse everything, as an initial state ui->topicOutlineView->setCurrentItem(nullptr, 0, QItemSelectionModel::Clear); recursiveCollapse(ui->topicOutlineView->invisibleRootItem()); // collapseAll() only collapses items that are visible! // Expand all nodes that have a search hit; reverse order so parents expand before their children for (auto iter = expandItems.rbegin(); iter != expandItems.rend(); ++iter) ui->topicOutlineView->expandItem(*iter); // Select all of the items that matched for (auto item : matchKeys) ui->topicOutlineView->setCurrentItem(item, 0, QItemSelectionModel::Select); // Finish coalescing selection changes doingProgrammaticCollapseExpand = false; doingProgrammaticSelection = false; outlineSelectionChanged(); } void QtSLiMHelpWindow::searchFieldChanged(void) { QString searchString = ui->searchField->text(); ui->searchField->selectAll(); if (searchString.length()) { // Do a depth-first search under the topic root that matches the search pattern, and gather tasks to perform std::vector matchKeys; std::vector expandItems; findItemsMatchingSearchString(ui->topicOutlineView->invisibleRootItem(), searchString, (searchType == 0), matchKeys, expandItems); if (matchKeys.size()) expandToShowItems(expandItems, matchKeys); else qApp->beep(); } } void QtSLiMHelpWindow::searchScopeToggled(void) { searchType = 1 - searchType; if (searchType == 0) ui->searchScopeButton->setText("🔍 headers"); else if (searchType == 1) ui->searchScopeButton->setText("🔍 content"); searchFieldChanged(); } void QtSLiMHelpWindow::enterSearchForString(QString searchString, bool titlesOnly) { // Show our window and bring it front QtSLiMMakeWindowVisibleAndExposed(this); // Set the search string per the request ui->searchField->setText(searchString); // Set the search type per the request int desiredSearchType = titlesOnly ? 0 : 1; if (searchType != desiredSearchType) searchScopeToggled(); // re-runs the search as a side effect else searchFieldChanged(); // re-run explicitly } void QtSLiMHelpWindow::closeEvent(QCloseEvent *p_event) { // Save the window position; see https://doc.qt.io/qt-5/qsettings.html#details QSettings settings; settings.beginGroup("QtSLiMHelpWindow"); settings.setValue("size", size()); settings.setValue("pos", pos()); settings.endGroup(); // use super's default behavior QWidget::closeEvent(p_event); } // This is a helper method for addTopicsFromRTFFile:... that finds the right parent item to insert a given section index under. // This method makes a lot of assumptions about the layout of the RTF file, such as that section number proceeds in sorted order. QTreeWidgetItem *QtSLiMHelpWindow::parentItemForSection(const QString §ionString, QtSLiMTopicMap &topics, QtSLiMHelpItem *topItem) { QStringList sectionComponents = sectionString.split('.'); int sectionCount = sectionComponents.size(); if (sectionCount <= 1) { // With an empty section string, or a whole-number section like "3", the parent is the top item return topItem; } else { // We have a section string like "3.1" or "3.1.2"; we want to look for a parent to add it to – like "3" or "3.1", respectively sectionComponents.pop_back(); QString parentSectionString = sectionComponents.join('.'); auto parentTopicDictIter = topics.find(parentSectionString); if (parentTopicDictIter != topics.end()) return parentTopicDictIter.value(); // Found a parent to add to else return topItem; // Couldn't find a parent to add to, so the parent is the top item } } // This is a helper method for addTopicsFromRTFFile:... that creates a new QTreeWidgetItem under which items will be placed, and finds the right parent // item to insert it under. This method makes a lot of assumptions about the layout of the RTF file, such as that section number proceeds in sorted order. QtSLiMHelpItem *QtSLiMHelpWindow::createItemForSection(const QString §ionString, QString title, QtSLiMTopicMap &topics, QtSLiMHelpItem *topItem) { static const QString functions(" functions"); if (title.endsWith(functions)) title.chop(functions.length()); QStringList sectionComponents = sectionString.split('.'); QTreeWidgetItem *parentItem = parentItemForSection(sectionString, topics, topItem); QtSLiMHelpItem *newItem = new QtSLiMHelpItem(parentItem); newItem->setText(0, title); newItem->setFlags(Qt::ItemIsEnabled); newItem->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); topics.insert(sectionString, newItem); return newItem; } // This is the main RTF doc file reading method; it finds an RTF file of a given name in the main bundle, reads it into an attributed string, and then scans // that string for topic headings, function/method/property signature lines, etc., and creates a hierarchy of help topics from the results. This process // assumes that the RTF doc file is laid out in a standard way that fits the regex patterns used here; it is designed to work directly with content copied // and pasted out of our Word documentation files into RTF in TextEdit. void QtSLiMHelpWindow::addTopicsFromRTFFile(const QString &htmlFile, const QString &topLevelHeading, const std::vector *functionList, const std::vector *methodList, const std::vector *propertyList) { QString topicFilePath = QString(":/help/") + htmlFile + QString(".html"); QTextDocument topicFileTextDocument; // Read the HTML resource in and create a QTextDocument { QFile topicFile(topicFilePath); QString topicFileData; if (!topicFile.open(QIODevice::ReadOnly | QIODevice::Text)) { // QIODevice::Text converts line endings to \n, making Windows happy; harmless qDebug() << "QtSLiMHelpWindow::addTopicsFromRTFFile(): could not find HTML file " << htmlFile; return; } topicFileData = topicFile.readAll(); topicFile.close(); // OK, so this is gross and probably fragile. Our HTML content has colors set on the text in some places: sometimes for syntax coloring // of code snippets, but also just setting text explicitly to black for no apparent reason. This doesn't translate well to dark mode; // the syntax coloring uses colors that are fairly illegible against black, and the black text of course ends up black-on-black. We thus // need to strip off all color information. Ideally, we'd do this on the QTextDocument, but I don't know how to do that (see question at // https://stackoverflow.com/questions/65779196/how-to-remove-all-text-color-attributes-from-a-qtextdocument). So instead, here we scan // the HTML source code for color attributes and remove them, with a regex. (What could possibly go wrong???) // BCH 5/7/2021: This regex worked well, but a better solution appears to be the code below. We'll see whether it causes any problems. // BCH 12/12/2021: Well, that code below did cause problems: it caused the formatting to be lost from "SLiM Events and Callbacks" and // "Eidos Statements" for no apparent reason. So, reverting to this regex, which has had no observed problems thus far. static const QRegularExpression htmlColorRegex("(;? ?color: ?#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])"); topicFileData.replace(htmlColorRegex, ""); // FIXME while on the topic of dark mode, there is a subtle bug in the help window's handling of it. We call ColorizeCallSignature() and // ColorizePropertySignature() below to colorize property/function/method signatures. Those produce colorized strings tailored to the // mode we're currently in: light mode or dark mode. If the user then switches modes, those strings are incorrectly colored. This is // not a disaster - the color schemes are not that different - but it's a bug. Ideally, we'd either do the colorizing dynamically at // display time, or we'd reload the whole help document when the display mode switches. Both are rather difficult to do, however. // Create the QTextDocument from the HTML topicFileTextDocument.setHtml(topicFileData); // Get rid of foreground and background colors set on the text; the original solution was the regex commented out above, but this seems // to work just as well, and is less icky/scary. This solution is from https://stackoverflow.com/a/67384128/2752221. // BCH 12/12/2021: This solution caused loss of text formatting in some cases; reverting to the regex solution above. Oh well. /*QTextCharFormat defaultFormat = QTextCharFormat(); QTextCursor tc(&topicFileTextDocument); tc.select(QTextCursor::Document); QTextCharFormat docFormat = tc.charFormat(); docFormat.setBackground(defaultFormat.background()); docFormat.setForeground(defaultFormat.foreground()); tc.mergeCharFormat(docFormat);*/ } // Create the topic map for the section being parsed; this keeps track of numbered sections so we can find where children go QtSLiMTopicMap topics; // keys are strings like 3.1 or 3.1.2 or whatever // Create the top-level item for the section we're parsing; note that QtSLiMHelpOutlineDelegate does additional display customization QtSLiMHelpItem *topItem = new QtSLiMHelpItem(ui->topicOutlineView); topItem->setText(0, topLevelHeading); topItem->setFlags(Qt::ItemIsEnabled); topItem->setForeground(0, QBrush(QtSLiMColorWithWhite(0.4, 1.0))); topItem->setSizeHint(0, QSize(20.0, 20.0)); topItem->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); QFont itemFont(topItem->font(0)); itemFont.setBold(true); topItem->setFont(0, itemFont); topItem->is_top_level = true; // Set up the current topic item that we are appending content into QtSLiMHelpItem *currentTopicItem = topItem; QString topicItemKey; QTextCursor *topicItemCursor = nullptr; // Make regular expressions that we will use below static const QRegularExpression topicHeaderRegex("^((?:[0-9]+\\.)*[0-9]+)\\.?[\u00A0 ] (.+)$", QRegularExpression::CaseInsensitiveOption); static const QRegularExpression topicGenericItemRegex("^((?:[0-9]+\\.)*[0-9]+)\\.?[\u00A0 ] ITEM: ((?:[0-9]+\\.? )?)(.+)$", QRegularExpression::CaseInsensitiveOption); static const QRegularExpression topicFunctionRegex("^\\([a-zA-Z<>\\*+$]+\\)([a-zA-Z_0-9]+)\\(.+$", QRegularExpression::CaseInsensitiveOption); static const QRegularExpression topicMethodRegex("^([-–+])[\u00A0 ]\\([a-zA-Z<>\\*+$]+\\)([a-zA-Z_0-9]+)\\(.+$", QRegularExpression::CaseInsensitiveOption); static const QRegularExpression topicPropertyRegex("^([a-zA-Z_0-9]+)[\u00A0 ]((?:<[-–]>)|(?:=>)) \\([a-zA-Z<>\\*+$]+\\)$", QRegularExpression::CaseInsensitiveOption); if (!topicHeaderRegex.isValid() || !topicGenericItemRegex.isValid() || !topicFunctionRegex.isValid() || !topicMethodRegex.isValid() || !topicPropertyRegex.isValid()) qDebug() << "QtSLiMHelpWindow::addTopicsFromRTFFile(): invalid regex"; // Scan through the file one line at a time, parsing out topic headers QString topicFileString = topicFileTextDocument.toRawText(); QStringList topicFileLineArray = topicFileString.split(QChar::ParagraphSeparator); // this is what Qt's HTML rendering uses between blocks int lineCount = topicFileLineArray.size(); int lineStartIndex = 0; // the character index of the current line in topicFileAttrString for (int lineIndex = 0; lineIndex < lineCount; ++lineIndex) { const QString &line = topicFileLineArray.at(lineIndex); int lineLength = line.length(); QTextCursor lineCursor(&topicFileTextDocument); lineCursor.setPosition(lineStartIndex); lineCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, lineLength); // figure out what kind of line we have and handle it QRegularExpressionMatch match_topicHeaderRegex = topicHeaderRegex.match(line); QRegularExpressionMatch match_topicGenericItemRegex = topicGenericItemRegex.match(line); QRegularExpressionMatch match_topicFunctionRegex = topicFunctionRegex.match(line); QRegularExpressionMatch match_topicMethodRegex = topicMethodRegex.match(line); QRegularExpressionMatch match_topicPropertyRegex = topicPropertyRegex.match(line); int isTopicHeaderLine = match_topicHeaderRegex.hasMatch(); int isTopicGenericItemLine = match_topicGenericItemRegex.hasMatch(); int isTopicFunctionLine = match_topicFunctionRegex.hasMatch(); int isTopicMethodLine = match_topicMethodRegex.hasMatch(); int isTopicPropertyLine = match_topicPropertyRegex.hasMatch(); int matchCount = isTopicHeaderLine + isTopicFunctionLine + isTopicMethodLine + isTopicPropertyLine; // excludes isTopicGenericItemLine, which is a subtype of isTopicHeaderLine if (matchCount > 1) { qDebug() << "*** line matched more than one regex type: %@" << line; return; } if (matchCount == 0) { if (lineLength) { // If we have a topic, this is a content line, to be appended to the current topic item's content if (topicItemCursor) topicItemCursor->movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, lineLength + 1); // 1 for the QChar::ParagraphSeparator else if (line.trimmed().length()) qDebug() << "orphan line while reading for top level heading " << topLevelHeading << ": " << line; } } if ((matchCount == 1) || ((matchCount == 0) && (lineIndex == lineCount - 1))) { // This line starts a new header or item or ends the file, so we need to terminate the current item if (topicItemCursor && topicItemKey.length()) { if (!currentTopicItem) { qDebug() << "no current topic item for text to be placed into"; return; } QtSLiMHelpItem *childItem = new QtSLiMHelpItem(currentTopicItem); QTextDocumentFragment *topicFragment = new QTextDocumentFragment(topicItemCursor->selection()); childItem->setText(0, topicItemKey); childItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); childItem->doc_fragment = topicFragment; delete topicItemCursor; topicItemCursor = nullptr; topicItemKey.clear(); } } if (isTopicHeaderLine) { // We have hit a new topic header. This might be a subtopic of the current topic, or a sibling, or a sibling of one of our ancestors QString sectionString = match_topicHeaderRegex.captured(1); QString titleString = match_topicHeaderRegex.captured(2); //qDebug() << "topic header section " << sectionString << ", title " << titleString << ", line: " << line; if (isTopicGenericItemLine) { // This line plays two roles: it is both a header (with a period-separated section index at the beginning) and a // topic item starter like function/method/property lines, with item content following it immediately. First we // use the header-line section string to find the right parent section to place it. currentTopicItem = dynamic_cast(parentItemForSection(sectionString, topics, topItem)); // Then we extract the item name and create a new item under the parent dict. //QString itemOrder = match_topicGenericItemRegex.captured(2); QString itemName = match_topicGenericItemRegex.captured(3); int itemName_pos = match_topicGenericItemRegex.capturedStart(3); int itemName_len = match_topicGenericItemRegex.capturedLength(3); topicItemCursor = new QTextCursor(&topicFileTextDocument); topicItemCursor->setPosition(lineStartIndex + itemName_pos); topicItemCursor->movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, itemName_len); topicItemKey = itemName; } else { // This header line is not also an item line, so we want to create a new expandable category and await items currentTopicItem = createItemForSection(sectionString, titleString, topics, topItem); } } else if (isTopicFunctionLine) { // This topic item is a function declaration QString callName = match_topicFunctionRegex.captured(1); //qDebug() << "topic function name: " << callName << ", line: " << line; // Check for a built-in function signature that matches and substitute it in if (functionList) { std::string function_name = callName.toStdString(); const EidosFunctionSignature *function_signature = nullptr; for (const auto &signature_iter : *functionList) if (signature_iter->call_name_.compare(function_name) == 0) { function_signature = signature_iter.get(); break; } if (function_signature) ColorizeCallSignature(function_signature, 11.0, lineCursor); else qDebug() << "*** no function signature found for function name " << callName; } topicItemKey = callName + "()"; topicItemCursor = new QTextCursor(lineCursor); } else if (isTopicMethodLine) { // This topic item is a method declaration QString classMethodString = match_topicMethodRegex.captured(1); QString callName = match_topicMethodRegex.captured(2); //qDebug() << "topic method name: " << callName << ", line: " << line; // Check for a built-in method signature that matches and substitute it in // BCH 3 April 2022: I don't think there's any reason why we can't have more than one method with the same name, // but with different signatures, as long as they are not in the same class; we can't handle overloading, but // method lookup is within-class. So this code could be generalized as the property lookup code below was; I just // haven't bothered to do so. if (methodList) { std::string method_name(callName.toStdString()); const EidosMethodSignature *method_signature = nullptr; for (const auto &signature_iter : *methodList) if (signature_iter->call_name_.compare(method_name) == 0) { method_signature = signature_iter.get(); break; } if (method_signature) ColorizeCallSignature(method_signature, 11.0, lineCursor); else qDebug() << "*** no method signature found for method name " << callName; } topicItemKey = classMethodString + "\u00A0" + callName + "()"; topicItemCursor = new QTextCursor(lineCursor); } else if (isTopicPropertyLine) { // This topic item is a method declaration QString callName = match_topicPropertyRegex.captured(1); QString readOnlyName = match_topicPropertyRegex.captured(2); //qDebug() << "topic property name: " << callName << ", line: " << line; // Check for a built-in property signature that matches and substitute it in. Note that we accept a match from any property in any class // API as long as the signature matches; we do not rigorously check that the API within a given class matches between signature and doc. // This is mostly not a problem because it is quite rare for the same property name to be used with more than one signature. if (propertyList) { std::string property_name(callName.toStdString()); bool found_match = false, found_mismatch = false; QString oldSignatureString, newSignatureString; for (const auto &signature_iter : *propertyList) if (signature_iter->property_name_.compare(property_name) == 0) { const EidosPropertySignature *property_signature = signature_iter.get(); oldSignatureString = lineCursor.selectedText(); std::ostringstream ss; ss << *property_signature; newSignatureString = QString::fromStdString(ss.str()); if (newSignatureString == oldSignatureString) { //qDebug() << "signature match for method" << callName; // Replace the signature line with the syntax-colored version ColorizePropertySignature(property_signature, 11.0, lineCursor); found_match = true; break; } else { // If we find a mismatched signature but no matching signature, that's probably an error in either the doc or // the signature, unless we find a match later on with a different signature for the same property name. found_mismatch = true; } } if (found_mismatch && !found_match) qDebug() << "*** property signature mismatch:\nold: " << oldSignatureString << "\nnew: " << newSignatureString; else if (!found_match) qDebug() << "*** no property signature found for property name" << callName; } topicItemKey = callName + "\u00A0" + readOnlyName; topicItemCursor = new QTextCursor(lineCursor); } lineStartIndex += (lineLength + 1); // +1 to jump over the newline } } void QtSLiMHelpWindow::checkDocumentationOfFunctions(const std::vector *functions) { for (const EidosFunctionSignature_CSP &functionSignature : *functions) { QString functionNameString = QString::fromStdString(functionSignature->call_name_); if (!functionNameString.startsWith("_")) if (!findObjectForKeyEqualTo(functionNameString + "()", ui->topicOutlineView->invisibleRootItem())) qDebug() << "*** no documentation found for function " << functionNameString << "()"; } } void QtSLiMHelpWindow::checkDocumentationOfClass(EidosClass *classObject) { const EidosClass *superclassObject = classObject->Superclass(); // We're hiding DictionaryRetained, so DictionaryRetained subclasses pretend their superclass is Dictionary if (superclassObject == gEidosDictionaryRetained_Class) superclassObject = gEidosDictionaryUnretained_Class; const QString className = QString::fromStdString(classObject->ClassName()); const QString classKey = "Class " + className; QtSLiMHelpItem *classDocumentation = findObjectWithKeySuffix(classKey, ui->topicOutlineView->invisibleRootItem()); int classDocChildCount = classDocumentation ? classDocumentation->childCount() : 0; if (classDocumentation && (classDocumentation->doc_fragment == nullptr) && (classDocChildCount > 0)) { QString propertiesKey = /*QString("1. ") +*/ className + QString(" properties"); QString methodsKey = /*QString("2. ") +*/ className + QString(" methods"); QtSLiMHelpItem *classPropertyItem = findObjectForKeyEqualTo(propertiesKey, classDocumentation); QtSLiMHelpItem *classMethodsItem = findObjectForKeyEqualTo(methodsKey, classDocumentation); if ((classDocChildCount == 2) || (classDocChildCount == 3)) // 3 if there is a constructor function, which we don't presently check { // Check for complete documentation of all properties defined by the class { const std::vector *classProperties = classObject->Properties(); const std::vector *superclassProperties = superclassObject ? superclassObject->Properties() : nullptr; QStringList docProperties; if (classPropertyItem) for (int child_index = 0; child_index < classPropertyItem->childCount(); ++child_index) docProperties.push_back(classPropertyItem->child(child_index)->text(0)); for (const EidosPropertySignature_CSP &propertySignature : *classProperties) { const std::string &&connector_string = propertySignature->PropertySymbol(); const std::string &property_name_string = propertySignature->property_name_; QString property_string = QString::fromStdString(property_name_string) + QString("\u00A0") + QString::fromStdString(connector_string); int docIndex = docProperties.indexOf(property_string); if (docIndex != -1) { // If the property is defined in this class doc, consider it documented docProperties.removeAt(docIndex); } else { // If the property is not defined in this class doc, then that is an error unless it is a superclass property bool isSuperclassProperty = superclassProperties && (std::find(superclassProperties->begin(), superclassProperties->end(), propertySignature) != superclassProperties->end()); if (!isSuperclassProperty) qDebug() << "*** no documentation found for class " << className << " property " << property_string; } } if (docProperties.size()) qDebug() << "*** excess documentation found for class " << className << " properties " << docProperties; } // Check for complete documentation of all methods defined by the class { const std::vector *classMethods = classObject->Methods(); const std::vector *superclassMethods = superclassObject ? superclassObject->Methods() : nullptr; QStringList docMethods; if (classMethodsItem) for (int child_index = 0; child_index < classMethodsItem->childCount(); ++child_index) docMethods.push_back(classMethodsItem->child(child_index)->text(0)); for (const EidosMethodSignature_CSP &methodSignature : *classMethods) { const std::string &method_name_string = methodSignature->call_name_; if ((method_name_string.length() == 0) || (method_name_string[0] != '_')) { const std::string &&prefix_string = methodSignature->CallPrefix(); QString method_string = QString::fromStdString(prefix_string) + QString::fromStdString(method_name_string) + QString("()"); int docIndex = docMethods.indexOf(method_string); if (docIndex != -1) { // If the method is defined in this class doc, consider it documented docMethods.removeAt(docIndex); } else { // If the method is not defined in this class doc, then that is an error unless it is a superclass method bool isSuperclassMethod = superclassMethods && (std::find(superclassMethods->begin(), superclassMethods->end(), methodSignature) != superclassMethods->end()); if (!isSuperclassMethod) qDebug() << "*** no documentation found for class " << className << " method " << method_string; } } } if (docMethods.size()) qDebug() << "*** excess documentation found for class " << className << " methods " << docMethods; } } else { qDebug() << "*** documentation for class " << className << " in unexpected format"; } } else { qDebug() << "*** no documentation found for class " << className; } } void QtSLiMHelpWindow::addSuperclassItemForClass(EidosClass *classObject) { const EidosClass *superclassObject = classObject->Superclass(); // We're hiding DictionaryRetained, so DictionaryRetained subclasses pretend their superclass is Dictionary if (superclassObject == gEidosDictionaryRetained_Class) superclassObject = gEidosDictionaryUnretained_Class; const QString className = QString::fromStdString(classObject->ClassName()); const QString classKey = "Class " + className; QtSLiMHelpItem *classDocumentation = findObjectWithKeySuffix(classKey, ui->topicOutlineView->invisibleRootItem()); int classDocChildCount = classDocumentation ? classDocumentation->childCount() : 0; if (classDocumentation && (classDocumentation->doc_fragment == nullptr) && (classDocChildCount > 0)) { QString superclassName = superclassObject ? QString::fromStdString(superclassObject->ClassName()) : QString("none"); bool inDarkMode = QtSLiMInDarkMode(); QtSLiMHelpItem *superclassItem = new QtSLiMHelpItem((QTreeWidgetItem *)nullptr); superclassItem->setText(0, QString("Superclass: " + superclassName)); superclassItem->setFlags(Qt::ItemIsEnabled); if (superclassObject) { // Hyperlink appearance, with blue underlined text superclassItem->setForeground(0, QBrush(inDarkMode ? QtSLiMColorWithHSV(0.65, 0.6, 1.0, 1.0) : QtSLiMColorWithHSV(0.65, 1.0, 0.8, 1.0))); QFont itemFont(superclassItem->font(0)); itemFont.setUnderline(true); superclassItem->setFont(0, itemFont); } else { // Dimmed appearance superclassItem->setForeground(0, QBrush(inDarkMode ? QtSLiMColorWithWhite(0.5, 1.0) : QtSLiMColorWithWhite(0.3, 1.0))); } classDocumentation->insertChild(0, superclassItem); // takes ownership } } void QtSLiMHelpWindow::outlineSelectionChanged(void) { if (!doingProgrammaticSelection) { QList &&selection = ui->topicOutlineView->selectedItems(); QPlainTextEdit *textedit = ui->descriptionTextEdit; QTextDocument *textdoc = textedit->document(); bool firstItem = true; textedit->clear(); QTextCursor insertion(textdoc); insertion.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); QTextBlockFormat defaultBlockFormat; QTextBlockFormat hrBlockFormat; hrBlockFormat.setTopMargin(10.0); hrBlockFormat.setBottomMargin(10.0); hrBlockFormat.setAlignment(Qt::AlignHCenter); for (QTreeWidgetItem *selwidget : selection) { if (!firstItem) { insertion.insertBlock(hrBlockFormat); // inserting an HR doesn't work properly, but the bug I filed got a useful response if I ever want to revisit this: https://bugreports.qt.io/browse/QTBUG-80473 // insertion.insertHtml("
"); insertion.insertText("––––––––––––––––––––"); insertion.insertBlock(defaultBlockFormat); } firstItem = false; QtSLiMHelpItem *helpItem = dynamic_cast(selwidget); if (helpItem && helpItem->doc_fragment) insertion.insertFragment(*helpItem->doc_fragment); } //qDebug() << textdoc->toHtml(); } } void QtSLiMHelpWindow::recursiveExpand(QTreeWidgetItem *item) { // Expand pre-order; I don't think this matters, but it seems safer if (!item->isExpanded()) ui->topicOutlineView->expandItem(item); for (int child_index = 0; child_index < item->childCount(); child_index++) recursiveExpand(item->child(child_index)); } void QtSLiMHelpWindow::recursiveCollapse(QTreeWidgetItem *item) { // Collapse post-order; I don't think this matters, but it seems safer for (int child_index = 0; child_index < item->childCount(); child_index++) recursiveCollapse(item->child(child_index)); if (item->isExpanded()) ui->topicOutlineView->collapseItem(item); } void QtSLiMHelpWindow::itemClicked(QTreeWidgetItem *item, int __attribute__((__unused__)) column) { if ((item->childCount() == 0) && (item->childIndicatorPolicy() == QTreeWidgetItem::DontShowIndicatorWhenChildless)) { // If the item has no children, and is not showing an indicator, it is a leaf and might be a hyperlink item QString itemText = item->text(0); if (itemText.startsWith("Superclass: ")) { QString superclassName = itemText.right(itemText.length() - QString("Superclass: ").length()); if (superclassName != "none") { QString sectionTitle = "Class " + superclassName; // Open disclosure triangles to show the section std::vector matchKeys; std::vector expandItems; QTreeWidgetItem *classDocumentation = findObjectWithKeySuffix(sectionTitle, ui->topicOutlineView->invisibleRootItem()); while (classDocumentation) { expandItems.emplace_back(classDocumentation); classDocumentation = classDocumentation->parent(); } if (expandItems.size()) expandToShowItems(expandItems, matchKeys); else qApp->beep(); } } } else { // Otherwise, it has a disclosure triangle and needs to expand/collapse bool optionPressed = QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier); doingProgrammaticCollapseExpand = true; if (optionPressed) { // recursively expand/collapse items below this item if (item->isExpanded()) recursiveCollapse(item); else recursiveExpand(item); } else { // expand/collapse just this item if (item->isExpanded()) ui->topicOutlineView->collapseItem(item); else ui->topicOutlineView->expandItem(item); } doingProgrammaticCollapseExpand = false; } } void QtSLiMHelpWindow::itemCollapsed(QTreeWidgetItem *item) { // If the user has collapsed an item by clicking, we want to implement option-clicking on top of that if (!doingProgrammaticCollapseExpand) { bool optionPressed = QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier); if (optionPressed) { doingProgrammaticCollapseExpand = true; recursiveCollapse(item); doingProgrammaticCollapseExpand = false; } } } void QtSLiMHelpWindow::itemExpanded(QTreeWidgetItem *item) { // If the user has expanded an item by clicking, we want to implement option-clicking on top of that if (!doingProgrammaticCollapseExpand) { bool optionPressed = QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier); if (optionPressed) { doingProgrammaticCollapseExpand = true; recursiveExpand(item); doingProgrammaticCollapseExpand = false; } } } QtSLiMHelpItem *QtSLiMHelpWindow::findObjectWithKeySuffix(const QString searchKeySuffix, QTreeWidgetItem *searchItem) { QtSLiMHelpItem *value = nullptr; int childCount = searchItem->childCount(); for (int childIndex = 0; childIndex < childCount; ++childIndex) { QTreeWidgetItem *child = searchItem->child(childIndex); QtSLiMHelpItem *our_child = dynamic_cast(child); if (our_child) { QString childTitle = child->text(0); // Search by substring matching; we have to be careful to use this method to search only for unique keys if (childTitle.endsWith(searchKeySuffix)) value = our_child; else if (our_child->childCount()) value = findObjectWithKeySuffix(searchKeySuffix, our_child); if (value) break; } } return value; } QtSLiMHelpItem *QtSLiMHelpWindow::findObjectForKeyEqualTo(const QString searchKey, QTreeWidgetItem *searchItem) { QtSLiMHelpItem *value = nullptr; int childCount = searchItem->childCount(); for (int childIndex = 0; childIndex < childCount; ++childIndex) { QTreeWidgetItem *child = searchItem->child(childIndex); QtSLiMHelpItem *our_child = dynamic_cast(child); if (our_child) { QString childTitle = child->text(0); // Search using string equality; we have to be careful to use this method to search only for unique keys if (childTitle == searchKey) value = our_child; else if (our_child->childCount()) value = findObjectForKeyEqualTo(searchKey, our_child); if (value) break; } } return value; } ================================================ FILE: QtSLiM/QtSLiMHelpWindow.h ================================================ // // QtSLiMHelpWindow.h // SLiM // // Created by Ben Haller on 11/19/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMHELPWINDOW_H #define QTSLIMHELPWINDOW_H #include #include #include #include #include #include #include "eidos_call_signature.h" #include "eidos_property_signature.h" class QCloseEvent; class QSplitter; class EidosClass; // SLiMgui uses an NSDictionary-based design to hold the documentation tree data structure. Here we // instead leverage the fact that QTreeWidgetItem already represents a tree structure, and simply // use it directly to represent the documentation tree. We use a custom subclass so we can keep // associated doc, which is held as a QTextDocumentFragment; this is used only by leaves class QtSLiMHelpItem : public QTreeWidgetItem { // no Q_OBJECT; QTreeWidgetItem is not a QObject subclass! public: QTextDocumentFragment *doc_fragment = nullptr; bool is_top_level = false; explicit QtSLiMHelpItem(QTreeWidget *p_parent) : QTreeWidgetItem(p_parent) {} explicit QtSLiMHelpItem(QTreeWidgetItem *p_parent) : QTreeWidgetItem(p_parent) {} virtual ~QtSLiMHelpItem() override; virtual QVariant data(int column, int role) const override; }; // This subclass of QStyledItemDelegate provides custom drawing for the outline view. class QtSLiMHelpOutlineDelegate : public QStyledItemDelegate { Q_OBJECT public: QtSLiMHelpOutlineDelegate(QObject *p_parent = nullptr) : QStyledItemDelegate(p_parent) {} virtual ~QtSLiMHelpOutlineDelegate(void) override; virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; // We keep a QMap of topics in the currently building hierarchy so we can find the right parent // for each new item. This is a temporary data structure used only during the build of each section. typedef QMap QtSLiMTopicMap; // This class provides a singleton help window namespace Ui { class QtSLiMHelpWindow; } class QtSLiMHelpWindow : public QWidget { Q_OBJECT public: static QtSLiMHelpWindow &instance(void); void enterSearchForString(QString searchString, bool titlesOnly = true); protected slots: void applicationPaletteChanged(void); private: // singleton pattern explicit QtSLiMHelpWindow(QWidget *p_parent = nullptr); QtSLiMHelpWindow(void) = delete; virtual ~QtSLiMHelpWindow(void) override; QtSLiMHelpWindow(const QtSLiMHelpWindow&) = delete; QtSLiMHelpWindow& operator=(const QtSLiMHelpWindow&) = delete; // Add topics and items drawn from a specially formatted HTML file, under a designated top-level heading. // The signatures found for functions, methods, and prototypes will be checked against the supplied lists, // if they are not NULL, and if matches are found, colorized versions will be substituted. void addTopicsFromRTFFile(const QString &htmlFile, const QString &topLevelHeading, const std::vector *functionList, const std::vector *methodList, const std::vector *propertyList); // Searching bool findItemsMatchingSearchString(QTreeWidgetItem *root, const QString searchString, bool titlesOnly, std::vector &matchKeys, std::vector &expandItems); void expandToShowItems(const std::vector &expandItems, const std::vector &matchKeys); void searchFieldChanged(void); void searchScopeToggled(void); // Smart expand/contract, with the option key making it apply to all children as well void recursiveExpand(QTreeWidgetItem *item); void recursiveCollapse(QTreeWidgetItem *item); void itemClicked(QTreeWidgetItem *item, int column); void itemCollapsed(QTreeWidgetItem *item); void itemExpanded(QTreeWidgetItem *item); // Check for complete documentation QtSLiMHelpItem *findObjectWithKeySuffix(const QString searchKeySuffix, QTreeWidgetItem *searchItem); QtSLiMHelpItem *findObjectForKeyEqualTo(const QString searchKey, QTreeWidgetItem *searchItem); void checkDocumentationOfFunctions(const std::vector *functions); void checkDocumentationOfClass(EidosClass *classObject); void addSuperclassItemForClass(EidosClass *classObject); // responding to events virtual void closeEvent(QCloseEvent *e) override; void outlineSelectionChanged(void); // Internals QTreeWidgetItem *parentItemForSection(const QString §ionString, QtSLiMTopicMap &topics, QtSLiMHelpItem *topItem); QtSLiMHelpItem *createItemForSection(const QString §ionString, QString title, QtSLiMTopicMap &topics, QtSLiMHelpItem *topItem); private: int searchType = 0; // 0 == Title, 1 == Content; equal to the tags on the search type menu items bool doingProgrammaticCollapseExpand = false; // used to distinguish user actions from programmatic ones bool doingProgrammaticSelection = false; // used to distinguish user actions from programmatic ones void interpolateSplitters(void); QSplitter *splitter = nullptr; Ui::QtSLiMHelpWindow *ui; }; #endif // QTSLIMHELPWINDOW_H ================================================ FILE: QtSLiM/QtSLiMHelpWindow.ui ================================================ QtSLiMHelpWindow 0 0 700 400 Scripting Help 4 4 4 4 4 4 0 0 60 0 60 16777215 9 Qt::NoFocus QPushButton { border: 1px solid #888; border-radius: 20px; border-style: outset; margin: 0px; padding: 2px; background: rgb(245, 245, 245); } QPushButton:pressed { background: rgb(195, 195, 195); } 🔍 headers false 4 0 0 280 200 280 16777215 11 Qt::ClickFocus QAbstractItemView::NoEditTriggers QAbstractItemView::ExtendedSelection 13 true false 1 250 0 Qt::ClickFocus false true ================================================ FILE: QtSLiM/QtSLiMIndividualsWidget.cpp ================================================ // // QtSLiMIndividualsWidget.cpp // SLiM // // Created by Ben Haller on 7/30/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMIndividualsWidget.h" #include "QtSLiMWindow.h" #include "QtSLiMPreferences.h" #include #include #include #include #include #include #include #include #include #include QtSLiMIndividualsWidget::QtSLiMIndividualsWidget(QWidget *p_parent, Qt::WindowFlags f) #ifndef SLIM_NO_OPENGL : QOpenGLWidget(p_parent, f) #else : QWidget(p_parent, f) #endif { preferredDisplayMode = PopulationViewDisplayMode::kDisplaySpatialSeparate; // prefer spatial display when possible, fall back to individuals // We support both OpenGL and non-OpenGL display, because some platforms seem // to have problems with OpenGL (https://github.com/MesserLab/SLiM/issues/462) QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::useOpenGLPrefChanged, this, [this]() { update(); }); } QtSLiMIndividualsWidget::~QtSLiMIndividualsWidget() { } #ifndef SLIM_NO_OPENGL void QtSLiMIndividualsWidget::initializeGL() { initializeOpenGLFunctions(); glClearColor(1.0f, 0.0f, 0.0f, 1.0f); } void QtSLiMIndividualsWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); // Update the projection glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } #endif #ifndef SLIM_NO_OPENGL void QtSLiMIndividualsWidget::paintGL() #else void QtSLiMIndividualsWidget::paintEvent(QPaintEvent * /* p_paint_event */) #endif { QPainter painter(this); bool inDarkMode = QtSLiMInDarkMode(); //painter.eraseRect(rect()); // erase to background color, which is not guaranteed painter.fillRect(rect(), Qt::red); // // NOTE this code is parallel to code in tileSubpopulations() and both should be maintained! // QRect bounds = rect(); QtSLiMWindow *controller = dynamic_cast(window()); std::vector selectedSubpopulations = controller->selectedSubpopulations(); int selectedSubpopCount = static_cast(selectedSubpopulations.size()); bool displayingUnified = ((preferredDisplayMode == PopulationViewDisplayMode::kDisplaySpatialUnified) && canDisplayUnified(selectedSubpopulations)); // In SLiMgui this has to be called in advance, to swap in/out the error view, but here we // can simply call it before each update to pre-plan, making for a simpler design tileSubpopulations(selectedSubpopulations); if ((selectedSubpopCount == 0) || !canDisplayAllIndividuals) { // clear to a shade of gray if (inDarkMode) painter.fillRect(QRect(0, 0, bounds.width(), bounds.height()), QtSLiMColorWithWhite(0.118, 1.0)); else painter.fillRect(QRect(0, 0, bounds.width(), bounds.height()), QtSLiMColorWithWhite(0.9, 1.0)); // display a message if we have too many subpops to show if (!canDisplayAllIndividuals) { painter.setPen(Qt::darkGray); painter.drawText(bounds, Qt::AlignCenter, "too many subpops\nor individuals\nto display – try\nresizing to make\nmore space"); } // Frame our view qtDrawViewFrameInBounds(bounds, painter); } else { // Clear to background gray; we always do this now, because the tile title bars are on the window background painter.fillRect(rect(), palette().color(QPalette::Window)); // Show title bars above each subpop tile static QFont *titleFont = nullptr; static QIcon actionIcon_LIGHT, actionIcon_DARK; if (!titleFont) { titleFont = new QFont(); #ifdef __linux__ // font sizes are calibrated for macOS; on Linux they need to be a little smaller titleFont->setPointSize(titleFont->pointSize() * 0.75); #endif actionIcon_LIGHT.addFile(":/buttons/action.png", QSize(), QIcon::Normal, QIcon::Off); actionIcon_LIGHT.addFile(":/buttons/action_H.png", QSize(), QIcon::Normal, QIcon::On); actionIcon_DARK.addFile(":/buttons_DARK/action_DARK.png", QSize(), QIcon::Normal, QIcon::Off); actionIcon_DARK.addFile(":/buttons_DARK/action_H_DARK.png", QSize(), QIcon::Normal, QIcon::On); } QIcon *actionIcon = (inDarkMode ? &actionIcon_DARK : &actionIcon_LIGHT); painter.save(); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.setPen(inDarkMode ? Qt::white : Qt::black); painter.setFont(*titleFont); for (Subpopulation *subpop : selectedSubpopulations) { auto tileIter = subpopTiles.find(subpop->subpopulation_id_); if (tileIter != subpopTiles.end()) { QRect tileBounds = tileIter->second; QRect buttonBounds(tileBounds.left(), tileBounds.top(), 20, 20); if (subpop->subpopulation_id_ == actionButtonHighlightSubpopID_) actionIcon->paint(&painter, buttonBounds, Qt::AlignCenter, QIcon::Normal, QIcon::On); else actionIcon->paint(&painter, buttonBounds, Qt::AlignCenter, QIcon::Normal, QIcon::Off); int titleX = tileBounds.left() + 23; int titleY = tileBounds.top() + 17; int textFlags = (Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignBottom | Qt::AlignLeft); QString title; if (displayingUnified) { title = "Unified (all subpopulations)"; } else { title = QString("p%1").arg(subpop->subpopulation_id_); if (controller->community->all_species_.size() > 1) title.append(" ").append(QString::fromStdString(subpop->species_.avatar_)); } painter.drawText(QRect(titleX, titleY, 0, 0), textFlags, title); } if (displayingUnified) break; } painter.restore(); // find a consensus square size for non-spatial display, for consistency int squareSize = 20; for (Subpopulation *subpop : selectedSubpopulations) { PopulationViewDisplayMode displayMode = (displayingUnified ? PopulationViewDisplayMode::kDisplaySpatialUnified : displayModeForSubpopulation(subpop)); if (displayMode == PopulationViewDisplayMode::kDisplayIndividuals) { auto tileIter = subpopTiles.find(subpop->subpopulation_id_); if (tileIter != subpopTiles.end()) { QRect tileBounds = tileIter->second; // remove a margin at the top for the title bar tileBounds.setTop(tileBounds.top() + 22); int thisSquareSize = squareSizeForSubpopulationInArea(subpop, tileBounds); if ((thisSquareSize < squareSize) && (thisSquareSize >= 1)) squareSize = thisSquareSize; } } } // And now draw the tiles themselves bool useGL = QtSLiMPreferencesNotifier::instance().useOpenGLPref(); if (useGL) painter.beginNativePainting(); bool clearBackground = true; // used for display mode 2 to prevent repeated clearing for (Subpopulation *subpop : selectedSubpopulations) { Species *displaySpecies = &subpop->species_; PopulationViewDisplayMode displayMode = (displayingUnified ? PopulationViewDisplayMode::kDisplaySpatialUnified : displayModeForSubpopulation(subpop)); auto tileIter = subpopTiles.find(subpop->subpopulation_id_); if (tileIter != subpopTiles.end()) { QRect tileBounds = tileIter->second; // remove a margin at the top for the title bar tileBounds.setTop(tileBounds.top() + 22); if ((displayMode == PopulationViewDisplayMode::kDisplaySpatialSeparate) || (displayMode == PopulationViewDisplayMode::kDisplaySpatialUnified)) { QRect spatialDisplayBounds = spatialDisplayBoundsForSubpopulation(subpop, tileBounds); QRect frameBounds = spatialDisplayBounds.adjusted(-1, -1, 1, 1); if (clearBackground) { if (frameBounds != tileBounds) { // If we have inset the tileBounds because of aspect ratio considerations // in spatialDisplayBoundsForSubpopulation() (which only happens in 2D), // clear to a shade of gray and frame the overall tileBounds #ifndef SLIM_NO_OPENGL if (useGL) { glColor3f(0.9f, 0.9f, 0.9f); glRecti(tileBounds.left(), tileBounds.top(), (tileBounds.left() + tileBounds.width()), (tileBounds.top() + tileBounds.height())); glDrawViewFrameInBounds(tileBounds); } else #endif { painter.fillRect(tileBounds, QtSLiMColorWithWhite(0.9, 1.0)); qtDrawViewFrameInBounds(tileBounds, painter); } } #ifndef SLIM_NO_OPENGL if (useGL) glDrawSpatialBackgroundInBoundsForSubpopulation(spatialDisplayBounds, subpop, displaySpecies->spatial_dimensionality_); else #endif qtDrawSpatialBackgroundInBoundsForSubpopulation(spatialDisplayBounds, subpop, displaySpecies->spatial_dimensionality_, painter); } float forceRGB[4]; float *forceColor = nullptr; if ((displayMode == PopulationViewDisplayMode::kDisplaySpatialUnified) && (controller->focalSpeciesName.compare("all") == 0)) { controller->colorForSpecies(displaySpecies, forceRGB + 0, forceRGB + 1, forceRGB + 2,forceRGB + 3); forceColor = forceRGB; } #ifndef SLIM_NO_OPENGL if (useGL) { glDrawSpatialIndividualsFromSubpopulationInArea(subpop, spatialDisplayBounds, displaySpecies->spatial_dimensionality_, forceColor); glDrawViewFrameInBounds(frameBounds); // framed more than once in displayMode 2, which is OK } else #endif { qtDrawSpatialIndividualsFromSubpopulationInArea(subpop, spatialDisplayBounds, displaySpecies->spatial_dimensionality_, forceColor, painter); qtDrawViewFrameInBounds(frameBounds, painter); // framed more than once in displayMode 2, which is OK } if (displayMode == 2) clearBackground = false; } else // displayMode == PopulationViewDisplayMode::kDisplayIndividuals { auto backgroundIter = subviewSettings.find(subpop->subpopulation_id_); PopulationViewSettings background; chooseDefaultBackgroundSettingsForSubpopulation(&background, nullptr, subpop); if ((backgroundIter != subviewSettings.end()) && (backgroundIter->second.backgroundType <= 2)) background = backgroundIter->second; int backgroundColor = background.backgroundType; float bgGray = 0.0; if (backgroundColor == 0) bgGray = 0.0; else if (backgroundColor == 1) bgGray = 0.3f; else if (backgroundColor == 2) bgGray = 1.0; #ifndef SLIM_NO_OPENGL if (useGL) { glColor3f(bgGray, bgGray, bgGray); glRecti(tileBounds.left(), tileBounds.top(), (tileBounds.left() + tileBounds.width()), (tileBounds.top() + tileBounds.height())); glDrawViewFrameInBounds(tileBounds); glDrawIndividualsFromSubpopulationInArea(subpop, tileBounds, squareSize); } else #endif { painter.fillRect(tileBounds, QtSLiMColorWithWhite(bgGray, 1.0)); qtDrawViewFrameInBounds(tileBounds, painter); qtDrawIndividualsFromSubpopulationInArea(subpop, tileBounds, squareSize, painter); } } } } if (useGL) painter.endNativePainting(); } } bool QtSLiMIndividualsWidget::canDisplayUnified(std::vector &selectedSubpopulations) { QtSLiMWindow *controller = dynamic_cast(window()); Community *community = controller->community; int selectedSubpopCount = static_cast(selectedSubpopulations.size()); if (community->simulation_valid_ && (community->Tick() >= 1)) { if (selectedSubpopCount <= 1) return false; // unified display requires more than one subpop // we need all the subpops to have the same spatial bounds and dimensionality, so their coordinate systems match up // we presently allow periodicity to not match; not sure about that one way or the other double x0 = 0, x1 = 0, y0 = 0, y1 = 0, z0 = 0, z1 = 0; // suppress unused warnings int dimensionality = 0; bool first = true; for (Subpopulation *subpop : selectedSubpopulations) { Species *subpop_species = &subpop->species_; if (subpop_species->spatial_dimensionality_ == 0) return false; if (first) { x0 = subpop->bounds_x0_; x1 = subpop->bounds_x1_; y0 = subpop->bounds_y0_; y1 = subpop->bounds_y1_; z0 = subpop->bounds_z0_; z1 = subpop->bounds_z1_; dimensionality = subpop_species->spatial_dimensionality_; first = false; } else { if ((x0 != subpop->bounds_x0_) || (x1 != subpop->bounds_x1_) || (y0 != subpop->bounds_y0_) || (y1 != subpop->bounds_y1_) || (z0 != subpop->bounds_z0_) || (z1 != subpop->bounds_z1_) || (dimensionality != subpop_species->spatial_dimensionality_)) return false; } } return true; } return true; // allow unified to be chosen as long as we have no information to the contrary } PopulationViewDisplayMode QtSLiMIndividualsWidget::displayModeForSubpopulation(Subpopulation *subpopulation) { // the decision regarding unified display is made external to this method, so we don't worry about it here // We just need to choose between individual versus spatial display if (preferredDisplayMode == PopulationViewDisplayMode::kDisplayIndividuals) return PopulationViewDisplayMode::kDisplayIndividuals; if (subpopulation->species_.spatial_dimensionality_ == 0) return PopulationViewDisplayMode::kDisplayIndividuals; return PopulationViewDisplayMode::kDisplaySpatialSeparate; } void QtSLiMIndividualsWidget::tileSubpopulations(std::vector &selectedSubpopulations) { // // NOTE this code is parallel to code in paintGL() and both should be maintained! // // We will decide upon new tiles for our subpopulations here, so start out empty subpopTiles.clear(); QRect bounds = rect(); int selectedSubpopCount = static_cast(selectedSubpopulations.size()); bool displayingUnified = ((preferredDisplayMode == PopulationViewDisplayMode::kDisplaySpatialUnified) && canDisplayUnified(selectedSubpopulations)); if (selectedSubpopCount == 0) { canDisplayAllIndividuals = true; } else if (displayingUnified) { // set all tiles to be the full bounds for overlay mode for (int subpopIndex = 0; subpopIndex < selectedSubpopCount; subpopIndex++) { Subpopulation *subpop = selectedSubpopulations[static_cast(subpopIndex)]; subpopTiles.emplace(subpop->subpopulation_id_, bounds); } canDisplayAllIndividuals = true; } else if (selectedSubpopCount == 1) { Subpopulation *selectedSubpop = selectedSubpopulations[0]; PopulationViewDisplayMode displayMode = displayModeForSubpopulation(selectedSubpop); subpopTiles.emplace(selectedSubpop->subpopulation_id_, bounds); if (displayMode == PopulationViewDisplayMode::kDisplaySpatialSeparate) { canDisplayAllIndividuals = true; } else { bounds.setTop(bounds.top() + 22); // take out title bar space canDisplayAllIndividuals = canDisplayIndividualsFromSubpopulationInArea(selectedSubpop, bounds); } } else // not unified, more than one subpop { // adaptively finds the layout that maximizes the pixel area covered; fails if no layout is satisfactory QtSLiMWindow *controller = dynamic_cast(window()); int minBoxWidth = ((controller->community->all_species_.size() > 1) ? 70 : 50); // room for avatars int64_t bestTotalExtent = 0; canDisplayAllIndividuals = false; for (int rowCount = 1; rowCount <= selectedSubpopCount; ++rowCount) { int columnCount = static_cast(ceil(selectedSubpopCount / static_cast(rowCount))); int interBoxSpace = 5; int totalInterboxHeight = interBoxSpace * (rowCount - 1); int totalInterboxWidth = interBoxSpace * (columnCount - 1); double boxWidth = (bounds.width() - totalInterboxWidth) / static_cast(columnCount); double boxHeight = (bounds.height() - totalInterboxHeight) / static_cast(rowCount); std::map candidateTiles; int64_t totalExtent = 0; // Round the box width down, for consistency, and calculate an offset to center the tiles // So the visual width of the individuals view is quantized in such a way as to evenly subdivide // We don't do this with the height since the effect of height variation is less visible, and // having the visual height of the view not match the neighboring views would look weird boxWidth = floor(boxWidth); int leftOffset = floor(bounds.width() - (boxWidth * columnCount + totalInterboxWidth)) / 2; for (int subpopIndex = 0; subpopIndex < selectedSubpopCount; subpopIndex++) { int columnIndex = subpopIndex % columnCount; int rowIndex = subpopIndex / columnCount; int boxLeft = qRound(bounds.x() + leftOffset + columnIndex * (interBoxSpace + boxWidth)); int boxRight = qRound(bounds.x() + leftOffset + columnIndex * (interBoxSpace + boxWidth) + boxWidth); int boxTop = qRound(bounds.y() + rowIndex * (interBoxSpace + boxHeight)); int boxBottom = qRound(bounds.y() + rowIndex * (interBoxSpace + boxHeight) + boxHeight); QRect boxBounds(boxLeft, boxTop, boxRight - boxLeft, boxBottom - boxTop); Subpopulation *subpop = selectedSubpopulations[static_cast(subpopIndex)]; PopulationViewDisplayMode displayMode = displayModeForSubpopulation(subpop); // Too narrow or short a box size (figuring in 22 pixels for the title bar) is not allowed if ((boxWidth < minBoxWidth) || (boxHeight < ((displayMode == PopulationViewDisplayMode::kDisplaySpatialSeparate) ? 72 : 42))) goto layoutRejected; //qDebug() << "boxWidth ==" << boxWidth << "for rowCount ==" << rowCount; candidateTiles.emplace(subpop->subpopulation_id_, boxBounds); // find out what pixel area actually gets used by this box, and use that to choose the optimal layout boxBounds.setTop(boxBounds.top() + 22); // take out title bar space if (displayMode == PopulationViewDisplayMode::kDisplaySpatialSeparate) { // for spatial display, squeeze to the spatial aspect ratio boxBounds = spatialDisplayBoundsForSubpopulation(subpop, boxBounds); } else { // for non-spatial display, check that the individuals will fit in the allotted area if (!canDisplayIndividualsFromSubpopulationInArea(subpop, boxBounds)) { totalExtent = 0; break; } } int64_t extent = static_cast(boxBounds.width()) * static_cast(boxBounds.height()); totalExtent += extent; } if (totalExtent > bestTotalExtent) { bestTotalExtent = totalExtent; std::swap(subpopTiles, candidateTiles); canDisplayAllIndividuals = true; } layoutRejected: ; } } } bool QtSLiMIndividualsWidget::canDisplayIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds) { // // NOTE this code is parallel to the code in drawIndividualsFromSubpopulation:inArea: and should be maintained in parallel // slim_popsize_t subpopSize = subpop->parent_subpop_size_; int squareSize, viewColumns = 0, viewRows = 0; // first figure out the biggest square size that will allow us to display the whole subpopulation for (squareSize = 20; squareSize > 1; --squareSize) { viewColumns = static_cast(floor((bounds.width() - 3) / squareSize)); viewRows = static_cast(floor((bounds.height() - 3) / squareSize)); if (viewColumns * viewRows > subpopSize) { // If we have an empty row at the bottom, then break for sure; this allows us to look nice and symmetrical if ((subpopSize - 1) / viewColumns < viewRows - 1) break; // Otherwise, break only if we are getting uncomfortably small; otherwise, let's drop down one square size to allow symmetry if (squareSize <= 5) break; } } return (squareSize > 1); } QRect QtSLiMIndividualsWidget::spatialDisplayBoundsForSubpopulation(Subpopulation *subpop, QRect tileBounds) { // Determine a subframe for drawing spatial information inside. The subframe we use is the maximal subframe // with integer boundaries that preserves, as closely as possible, the aspect ratio of the subpop's bounds. // If sim->spatial_dimensionality_ is 1, there are no aspect ratio considerations so we just inset. QRect spatialDisplayBounds = tileBounds.adjusted(1, 1, -1, -1); if (subpop->species_.spatial_dimensionality_ > 1) { double displayAspect = spatialDisplayBounds.width() / static_cast(spatialDisplayBounds.height()); double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; double subpopAspect = bounds_x_size / bounds_y_size; if (subpopAspect > displayAspect) { // The display bounds will need to shrink vertically to match the subpop int idealSize = qRound(spatialDisplayBounds.width() / subpopAspect); int roundedOffset = qRound((spatialDisplayBounds.height() - idealSize) / 2.0); spatialDisplayBounds.setY(spatialDisplayBounds.y() + roundedOffset); spatialDisplayBounds.setHeight(idealSize); } else if (subpopAspect < displayAspect) { // The display bounds will need to shrink horizontally to match the subpop int idealSize = qRound(spatialDisplayBounds.height() * subpopAspect); int roundedOffset = qRound((spatialDisplayBounds.width() - idealSize) / 2.0); spatialDisplayBounds.setX(spatialDisplayBounds.x() + roundedOffset); spatialDisplayBounds.setWidth(idealSize); } } return spatialDisplayBounds; } int QtSLiMIndividualsWidget::squareSizeForSubpopulationInArea(Subpopulation *subpop, QRect bounds) { slim_popsize_t subpopSize = subpop->parent_subpop_size_; int squareSize, viewColumns = 0, viewRows = 0; // first figure out the biggest square size that will allow us to display the whole subpopulation for (squareSize = 20; squareSize > 1; --squareSize) { viewColumns = static_cast(floor((bounds.width() - 3) / squareSize)); viewRows = static_cast(floor((bounds.height() - 3) / squareSize)); if (viewColumns * viewRows > subpopSize) { // If we have an empty row at the bottom, then break for sure; this allows us to look nice and symmetrical if ((subpopSize - 1) / viewColumns < viewRows - 1) break; // Otherwise, break only if we are getting uncomfortably small; otherwise, let's drop down one square size to allow symmetry if (squareSize <= 5) break; } } return squareSize; } void QtSLiMIndividualsWidget::cacheDisplayBufferForMapForSubpopulation(SpatialMap *background_map, Subpopulation *subpop, bool flipped) { // Cache a display buffer for the given background map. This method should be called only in the 2D "xy" // case; in the 1D case we can't know the maximum width ahead of time, so we just draw rects without caching, // and in the 3D "xyz" case we don't know which z-slice to use so we can't display the spatial map. // The window might be too narrow to display a full-size map right now, but we want to premake a full-size map. // The sizing logic here is taken from drawRect:, assuming that we are not constrained in width. // By the way, it may occur to the reader to wonder why we keep the buffer as uint8_t values, given that we // convert to and from uint_8 for the display code that uses float RGB components. My rationale is that // this drastically cuts the amount of memory that has to be accessed, and that the display code, in particular, // is probably memory-bound since most of the work is done in the GPU. I haven't done any speed tests to // confirm that hunch, though. In any case, it's plenty fast and I don't see significant display artifacts. QRect full_bounds = rect().adjusted(1, 1, -1, -1); int max_height = full_bounds.height(); double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; double subpopAspect = bounds_x_size / bounds_y_size; int max_width = qRound(max_height * subpopAspect); // If we have a display buffer of the wrong size, free it. This should only happen when the user changes // the Subpopulation's spatialBounds after it has displayed; it should not happen with a window resize. // The user has no way to change the map or the colormap except to set a whole new map, which will also // result in the old one being freed, so we're already safe in those circumstances. if (background_map->display_buffer_ && ((background_map->buffer_width_ != max_width) || (background_map->buffer_height_ != max_height) || (background_map->buffer_flipped_ != flipped))) { free(background_map->display_buffer_); background_map->display_buffer_ = nullptr; } // Now allocate and validate the display buffer if we haven't already done so. if (!background_map->display_buffer_) { uint8_t *display_buf = static_cast(malloc(static_cast(max_width * max_height * 3) * sizeof(uint8_t))); background_map->display_buffer_ = display_buf; background_map->buffer_width_ = max_width; background_map->buffer_height_ = max_height; background_map->buffer_flipped_ = flipped; background_map->FillRGBBuffer(display_buf, max_width, max_height, flipped, /* no_interpolation */ false); } } void QtSLiMIndividualsWidget::chooseDefaultBackgroundSettingsForSubpopulation(PopulationViewSettings *background, SpatialMap **returnMap, Subpopulation *subpop) { PopulationViewDisplayMode displayMode = displayModeForSubpopulation(subpop); bool inDarkMode = QtSLiMInDarkMode(); if (displayMode == PopulationViewDisplayMode::kDisplayIndividuals) { // black or white following the dark mode setting, by default if (inDarkMode) background->backgroundType = 0; else background->backgroundType = 2; } else { // black by default background->backgroundType = 0; // if there are spatial maps defined, we try to choose one, requiring "x" or "y" or "xy", and requiring // a color map to be defined, and preferring 2D over 1D, providing the same default behavior as SLiM 2.x SpatialMapMap &spatial_maps = subpop->spatial_maps_; SpatialMap *background_map = nullptr; std::string background_map_name; for (const auto &map_pair : spatial_maps) { SpatialMap *map = map_pair.second; // a map must be "x", "y", or "xy", and must have a defined color map, for us to choose it as a default at all if (((map->spatiality_string_ == "x") || (map->spatiality_string_ == "y") || (map->spatiality_string_ == "xy")) && (map->n_colors_ > 0)) { // the map is usable, so now we check whether it's better than the map we previously found, if any if ((!background_map) || (map->spatiality_ > background_map->spatiality_)) { background_map = map; background_map_name = map_pair.first; } } } if (background_map) { background->backgroundType = 3; background->spatialMapName = background_map_name; background->showGridPoints = false; *returnMap = background_map; } } } void QtSLiMIndividualsWidget::runContextMenuAtPoint(QPoint globalPoint, Subpopulation *subpopForEvent) { QtSLiMWindow *controller = dynamic_cast(window()); Community *community = controller->community; bool disableAll = false; // When the simulation is not valid and initialized, the context menu is disabled if (!community || !community->simulation_valid_ || (community->Tick() < 1)) disableAll = true; QMenu contextMenu("population_menu", this); QAction *titleAction1 = contextMenu.addAction("Global preferred display mode:"); QFont titleFont = titleAction1->font(); titleFont.setBold(true); titleFont.setItalic(true); titleAction1->setFont(titleFont); titleAction1->setEnabled(false); QAction *displayNonSpatial = contextMenu.addAction("Display Non-spatial"); displayNonSpatial->setData(0); displayNonSpatial->setCheckable(true); displayNonSpatial->setEnabled(!disableAll); QAction *displaySpatial = contextMenu.addAction("Display Spatial (separate)"); displaySpatial->setData(1); displaySpatial->setCheckable(true); displaySpatial->setEnabled(!disableAll); QAction *displayUnified = contextMenu.addAction("Display Spatial (unified)"); displayUnified->setData(2); displayUnified->setCheckable(true); displayUnified->setEnabled(!disableAll); // Check the item corresponding to our current display preference, if any if (!disableAll) { if (preferredDisplayMode == PopulationViewDisplayMode::kDisplayIndividuals) displayNonSpatial->setChecked(true); if (preferredDisplayMode == PopulationViewDisplayMode::kDisplaySpatialSeparate) displaySpatial->setChecked(true); if (preferredDisplayMode == PopulationViewDisplayMode::kDisplaySpatialUnified) displayUnified->setChecked(true); } QActionGroup *displayGroup = new QActionGroup(this); // On Linux this provides a radio-button-group appearance displayGroup->addAction(displayNonSpatial); displayGroup->addAction(displaySpatial); displayGroup->addAction(displayUnified); // Provide background options (colors, spatial maps for spatial subpops) if (subpopForEvent && !disableAll) { contextMenu.addSeparator(); QAction *titleAction2 = contextMenu.addAction("For this subview:"); titleAction2->setFont(titleFont); titleAction2->setEnabled(false); QAction *headerAction = contextMenu.addAction(QString("Background for p%1:").arg(subpopForEvent->subpopulation_id_)); headerAction->setData(-1); headerAction->setEnabled(false); // check the menu item for the preferred display option; if we're in auto mode, don't check anything (could put a dash by the currently chosen style?) auto backgroundIter = subviewSettings.find(subpopForEvent->subpopulation_id_); PopulationViewSettings *background = ((backgroundIter == subviewSettings.end()) ? nullptr : &backgroundIter->second); int backgroundType = (background ? background->backgroundType : -1); bool showGrid = (background ? background->showGridPoints : false); QAction *blackAction = contextMenu.addAction("Black Background"); blackAction->setData(10); blackAction->setCheckable(true); blackAction->setChecked(backgroundType == 0); blackAction->setEnabled(!disableAll); QAction *grayAction = contextMenu.addAction("Gray Background"); grayAction->setData(11); grayAction->setCheckable(true); grayAction->setChecked(backgroundType == 1); grayAction->setEnabled(!disableAll); QAction *whiteAction = contextMenu.addAction("White Background"); whiteAction->setData(12); whiteAction->setCheckable(true); whiteAction->setChecked(backgroundType == 2); whiteAction->setEnabled(!disableAll); QActionGroup *backgroundGroup = new QActionGroup(this); // On Linux this provides a radio-button-group appearance backgroundGroup->addAction(blackAction); backgroundGroup->addAction(grayAction); backgroundGroup->addAction(whiteAction); if (preferredDisplayMode > 0) { // look for spatial maps to offer as choices; need to scan the defined maps for the ones we can use SpatialMapMap &spatial_maps = subpopForEvent->spatial_maps_; for (const auto &map_pair : spatial_maps) { SpatialMap *map = map_pair.second; // We used to display only maps with a color scale; now we just make up a color scale if none is given. Only // "x", "y", and "xy" maps are considered displayable; we can't display a z coordinate, and we can't display // even the x or y portion of "xz", "yz", and "xyz" maps. bool displayable = ((map->spatiality_string_ == "x") || (map->spatiality_string_ == "y") || (map->spatiality_string_ == "xy")); QString mapName = QString::fromStdString(map_pair.first); QString spatialityName = QString::fromStdString(map->spatiality_string_); QString menuItemTitle; if (map->spatiality_ == 1) menuItemTitle = QString("Spatial Map \"%1\" (\"%2\", %3)").arg(mapName).arg(spatialityName).arg(map->grid_size_[0]); else if (map->spatiality_ == 2) menuItemTitle = QString("Spatial Map \"%1\" (\"%2\", %3×%4)").arg(mapName).arg(spatialityName).arg(map->grid_size_[0]).arg(map->grid_size_[1]); else // (map->spatiality_ == 3) menuItemTitle = QString("Spatial Map \"%1\" (\"%2\", %3×%4×%5)").arg(mapName).arg(spatialityName).arg(map->grid_size_[0]).arg(map->grid_size_[1]).arg(map->grid_size_[2]); QAction *mapAction1 = contextMenu.addAction(menuItemTitle); mapAction1->setData(mapName); mapAction1->setCheckable(true); mapAction1->setChecked((backgroundType == 3) && (mapName == QString::fromStdString(background->spatialMapName) && !showGrid)); mapAction1->setEnabled(!disableAll && displayable); backgroundGroup->addAction(mapAction1); // 9/29/2023: now we support displaying spatial maps with a display of the underlying grid, too // There is a second menu item for each map, with "with grid" added to the title and "__WITH_GRID" added to the data menuItemTitle.append(" with grid"); QString mapDataName = mapName; mapDataName.append("__WITH_GRID"); QAction *mapAction2 = contextMenu.addAction(menuItemTitle); mapAction2->setData(mapDataName); mapAction2->setCheckable(true); mapAction2->setChecked((backgroundType == 3) && (mapName == QString::fromStdString(background->spatialMapName) && showGrid)); mapAction2->setEnabled(!disableAll && displayable); backgroundGroup->addAction(mapAction2); } } } // Run the context menu synchronously QAction *action = contextMenu.exec(globalPoint); // Act upon the chosen action; we just do it right here instead of dealing with slots if (action) { if ((action == displayNonSpatial) || (action == displaySpatial) || (action == displayUnified)) { // - (IBAction)setDisplayStyle:(id)sender PopulationViewDisplayMode newDisplayMode = (PopulationViewDisplayMode)action->data().toInt(); if (newDisplayMode != preferredDisplayMode) { preferredDisplayMode = newDisplayMode; update(); } } else if (subpopForEvent) { // - (IBAction)setDisplayBackground:(id)sender int newDisplayBackground; bool newShowGrid = false; auto backgroundIter = subviewSettings.find(subpopForEvent->subpopulation_id_); PopulationViewSettings *background = ((backgroundIter == subviewSettings.end()) ? nullptr : &backgroundIter->second); std::string mapName; // If the user has selected a spatial map, extract its name #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (static_cast(action->data().type()) == QMetaType::QString) // for some reason this method's return type is apparently misdeclared; see the doc #else if (action->data().typeId() == QMetaType::QString) #endif { QString qMapName = action->data().toString(); // detect the "with grid" ending if present if (qMapName.endsWith("__WITH_GRID")) { qMapName.chop(11); newShowGrid = true; } mapName = qMapName.toStdString(); if (mapName.length() == 0) return; newDisplayBackground = 3; } else { newDisplayBackground = action->data().toInt() - 10; newShowGrid = false; } // Update the existing background entry, or make a new entry if (background) { background->backgroundType = newDisplayBackground; background->spatialMapName = mapName; background->showGridPoints = newShowGrid; update(); } else { subviewSettings.emplace(subpopForEvent->subpopulation_id_, PopulationViewSettings{newDisplayBackground, mapName, newShowGrid}); update(); } } } } void QtSLiMIndividualsWidget::contextMenuEvent(QContextMenuEvent *p_event) { QtSLiMWindow *controller = dynamic_cast(window()); Community *community = controller->community; bool disableAll = false; // When the simulation is not valid and initialized, the context menu is disabled if (!community || !community->simulation_valid_ || (community->Tick() < 1)) disableAll = true; // Find the subpop that was clicked in; in "unified" display mode, this is the first selected subpop Subpopulation *subpopForEvent = nullptr; if (!disableAll) { std::vector selectedSubpopulations = controller->selectedSubpopulations(); QPoint viewPoint = p_event->pos(); // our tile coordinates are in the OpenGL coordinate system, which has the origin at top left //viewPoint.y = [self bounds].size.height - viewPoint.y; for (Subpopulation *subpop : selectedSubpopulations) { slim_objectid_t subpop_id = subpop->subpopulation_id_; auto tileIter = subpopTiles.find(subpop_id); if (tileIter != subpopTiles.end()) { QRect tileRect = tileIter->second; if (tileRect.contains(viewPoint)) { subpopForEvent = subpop; break; } } } } runContextMenuAtPoint(p_event->globalPos(), subpopForEvent); } void QtSLiMIndividualsWidget::mousePressEvent(QMouseEvent *p_event) { QtSLiMWindow *controller = dynamic_cast(window()); Community *community = controller->community; // When the simulation is not valid and initialized, the context menu is disabled if (!community || !community->simulation_valid_ || (community->Tick() < 1)) return; std::vector selectedSubpopulations = controller->selectedSubpopulations(); int selectedSubpopCount = static_cast(selectedSubpopulations.size()); if ((selectedSubpopCount == 0) || !canDisplayAllIndividuals) return; QPoint mousePos = p_event->pos(); Subpopulation *subpopForEvent = nullptr; for (Subpopulation *subpop : selectedSubpopulations) { auto tileIter = subpopTiles.find(subpop->subpopulation_id_); if (tileIter != subpopTiles.end()) { QRect tileBounds = tileIter->second; QRect buttonBounds(tileBounds.left(), tileBounds.top(), 20, 20); double xd = (mousePos.x() - buttonBounds.left()) / (double)buttonBounds.width() - 0.5; double yd = (mousePos.y() - buttonBounds.top()) / (double)buttonBounds.height() - 0.5; double distance = std::sqrt(xd * xd + yd * yd); if (buttonBounds.contains(mousePos) && (distance <= 0.51)) { actionButtonHighlightSubpopID_ = subpop->subpopulation_id_; update(); subpopForEvent = subpop; break; } } } if (subpopForEvent) #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) runContextMenuAtPoint(p_event->globalPos(), subpopForEvent); #else runContextMenuAtPoint(p_event->globalPosition().toPoint(), subpopForEvent); #endif // redraw to get rid of action button highlight actionButtonHighlightSubpopID_ = -1; update(); } ================================================ FILE: QtSLiM/QtSLiMIndividualsWidget.h ================================================ // // QtSLiMIndividualsWidget.h // SLiM // // Created by Ben Haller on 7/30/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMINDIVIDUALSWIDGET_H #define QTSLIMINDIVIDUALSWIDGET_H // Silence deprecated OpenGL warnings on macOS #define GL_SILENCE_DEPRECATION #include #ifndef SLIM_NO_OPENGL #include #include #endif #include "slim_globals.h" #include "subpopulation.h" #include class QContextMenuEvent; typedef struct { int backgroundType; // 0 == black, 1 == gray, 2 == white, 3 == named spatial map; if no preference has been set, no entry will exist std::string spatialMapName; // the name of the spatial map chosen, for backgroundType == 3 bool showGridPoints; // if true show spatial grid points, for backgroundType == 3 } PopulationViewSettings; typedef enum { kDisplayIndividuals = 0, kDisplaySpatialSeparate, kDisplaySpatialUnified } PopulationViewDisplayMode; #ifndef SLIM_NO_OPENGL class QtSLiMIndividualsWidget : public QOpenGLWidget, protected QOpenGLFunctions #else class QtSLiMIndividualsWidget : public QWidget #endif { Q_OBJECT // display mode: this is now just a suggestion; each subpop will fall back to what it is able to do PopulationViewDisplayMode preferredDisplayMode = kDisplaySpatialSeparate; // per-subview display preferences, kept indexed by subpopulation id std::map subviewSettings; // subview tiling, kept indexed by subpopulation id std::map subpopTiles; bool canDisplayAllIndividuals = false; // action button tracking support slim_objectid_t actionButtonHighlightSubpopID_ = -1; public: explicit QtSLiMIndividualsWidget(QWidget *p_parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); virtual ~QtSLiMIndividualsWidget() override; void tileSubpopulations(std::vector &selectedSubpopulations); void runContextMenuAtPoint(QPoint globalPoint, Subpopulation *subpopForEvent); protected: #ifndef SLIM_NO_OPENGL virtual void initializeGL() override; virtual void resizeGL(int w, int h) override; virtual void paintGL() override; #else virtual void paintEvent(QPaintEvent *event) override; #endif bool canDisplayUnified(std::vector &selectedSubpopulations); PopulationViewDisplayMode displayModeForSubpopulation(Subpopulation *subpopulation); bool canDisplayIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds); QRect spatialDisplayBoundsForSubpopulation(Subpopulation *subpop, QRect tileBounds); int squareSizeForSubpopulationInArea(Subpopulation *subpop, QRect bounds); void cacheDisplayBufferForMapForSubpopulation(SpatialMap *background_map, Subpopulation *subpop, bool flipped); void chooseDefaultBackgroundSettingsForSubpopulation(PopulationViewSettings *settings, SpatialMap **returnMap, Subpopulation *subpop); // OpenGL drawing; this is the primary drawing code #ifndef SLIM_NO_OPENGL void glDrawViewFrameInBounds(QRect bounds); void glDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize); void _glDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints); void glDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int dimensionality); void glDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor); #endif // Qt-based drawing, provided as a backup if OpenGL has problems on a given platform void qtDrawViewFrameInBounds(QRect bounds, QPainter &painter); void qtDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize, QPainter &painter); void _qtDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints, QPainter &painter); void qtDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int dimensionality, QPainter &painter); void qtDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor, QPainter &painter); virtual void contextMenuEvent(QContextMenuEvent *p_event) override; virtual void mousePressEvent(QMouseEvent *p_event) override; }; #endif // QTSLIMINDIVIDUALSWIDGET_H ================================================ FILE: QtSLiM/QtSLiMIndividualsWidget_GL.cpp ================================================ // // QtSLiMIndividualsWidget_GL.cpp // SLiM // // Created by Ben Haller on 8/25/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef SLIM_NO_OPENGL #include "QtSLiMIndividualsWidget.h" #include "QtSLiMWindow.h" #include "QtSLiMOpenGL.h" #include #include #include #include #include // // OpenGL-based drawing; maintain this in parallel with the Qt-based drawing! // void QtSLiMIndividualsWidget::glDrawViewFrameInBounds(QRect bounds) { int ox = bounds.left(), oy = bounds.top(); bool inDarkMode = QtSLiMInDarkMode(); if (inDarkMode) glColor3f(0.067f, 0.067f, 0.067f); else glColor3f(0.77f, 0.77f, 0.77f); glRecti(ox, oy, ox + 1, oy + bounds.height()); glRecti(ox + 1, oy, ox + bounds.width() - 1, oy + 1); glRecti(ox + bounds.width() - 1, oy, ox + bounds.width(), oy + bounds.height()); glRecti(ox + 1, oy + bounds.height() - 1, ox + bounds.width() - 1, oy + bounds.height()); } void QtSLiMIndividualsWidget::glDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize) { // // NOTE this code is parallel to the code in canDisplayIndividualsFromSubpopulation:inArea: and should be maintained in parallel // //QtSLiMWindow *controller = dynamic_cast(window()); double scalingFactor = 0.8; // used to be controller->fitnessColorScale; slim_popsize_t subpopSize = subpop->parent_subpop_size_; int viewColumns = 0, viewRows = 0; // our square size is given from above (a consensus based on squareSizeForSubpopulationInArea(); calculate metrics from it viewColumns = static_cast(floor((bounds.width() - 3) / squareSize)); viewRows = static_cast(floor((bounds.height() - 3) / squareSize)); if (viewColumns * viewRows < subpopSize) squareSize = 1; if (squareSize > 1) { int squareSpacing = 0; // Convert square area to space between squares if possible if (squareSize > 2) { --squareSize; ++squareSpacing; } if (squareSize > 5) { --squareSize; ++squareSpacing; } double excessSpaceX = bounds.width() - ((squareSize + squareSpacing) * viewColumns - squareSpacing); double excessSpaceY = bounds.height() - ((squareSize + squareSpacing) * viewRows - squareSpacing); int offsetX = static_cast(floor(excessSpaceX / 2.0)); int offsetY = static_cast(floor(excessSpaceY / 2.0)); // If we have an empty row at the bottom, then we can use the same value for offsetY as for offsetX, for symmetry if ((subpopSize - 1) / viewColumns < viewRows - 1) offsetY = offsetX; QRect individualArea(bounds.left() + offsetX, bounds.top() + offsetY, bounds.width() - offsetX, bounds.height() - offsetY); int individualArrayIndex; // Set up to draw rects SLIM_GL_PREPARE(); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... float left = static_cast(individualArea.left() + (individualArrayIndex % viewColumns) * (squareSize + squareSpacing)); float top = static_cast(individualArea.top() + (individualArrayIndex / viewColumns) * (squareSize + squareSpacing)); float right = left + squareSize; float bottom = top + squareSize; SLIM_GL_PUSHRECT(); // dark gray default, for a fitness of NaN; should never happen float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; if (Individual::s_any_individual_color_set_ && individual.color_set_) { colorRed = individual.colorR_ / 255.0F; colorGreen = individual.colorG_ / 255.0F; colorBlue = individual.colorB_ / 255.0F; } else { // use individual trait values to determine color; we use fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring double fitness = individual.cached_unscaled_fitness_; if (!std::isnan(fitness)) RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } // Draw any leftovers SLIM_GL_FINISH(); } else { // This is what we do if we cannot display a subpopulation because there are too many individuals in it to display glColor3f(0.9f, 0.9f, 1.0f); int ox = bounds.left(), oy = bounds.top(); glRecti(ox + 1, oy + 1, ox + bounds.width() - 1, oy + bounds.height() - 1); } } void QtSLiMIndividualsWidget::_glDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints) { // We have a spatial map with a color map, so use it to draw the background int bounds_x1 = bounds.x(); int bounds_y1 = bounds.y(); int bounds_x2 = bounds.x() + bounds.width(); int bounds_y2 = bounds.y() + bounds.height(); //glColor3f(0.0, 0.0, 0.0); //glRecti(bounds_x1, bounds_y1, bounds_x2, bounds_y2); { // Set up to draw rects SLIM_GL_PREPARE(); if (background_map->spatiality_ == 1) { // This is the spatiality "x" and "y" cases; they are the only 1D spatiality values for which SLiMgui will draw // In the 1D case we can't cache a display buffer, since we don't know what aspect ratio to use, so we just // draw rects. Whether those rects are horizontal or vertical will depend on the spatiality of the map. Most // of the code is identical, though, because of the way we handle dimensions, so we share the two cases here. bool spatiality_is_x = (background_map->spatiality_string_ == "x"); int64_t xsize = background_map->grid_size_[0]; double *values = background_map->values_; if (background_map->interpolate_) { // Interpolation, so we need to draw every line individually int min_coord = (spatiality_is_x ? bounds_x1 : bounds_y1); int max_coord = (spatiality_is_x ? bounds_x2 : bounds_y2); for (int xc = min_coord; xc < max_coord; ++xc) { double x_fraction = (xc + 0.5 - min_coord) / (max_coord - min_coord); // values evaluated at pixel centers double x_map = x_fraction * (xsize - 1); int x1_map = static_cast(floor(x_map)); int x2_map = static_cast(ceil(x_map)); double fraction_x2 = x_map - x1_map; double fraction_x1 = 1.0 - fraction_x2; double value_x1 = values[x1_map] * fraction_x1; double value_x2 = values[x2_map] * fraction_x2; double value = value_x1 + value_x2; float left, right, top, bottom; if (spatiality_is_x) { left = xc; right = xc + 1; top = bounds_y1; bottom = bounds_y2; } else { top = (max_coord - 1) - xc + min_coord; // flip for y, to use Cartesian coordinates bottom = top + 1; left = bounds_x1; right = bounds_x2; } float rgb[3]; background_map->ColorForValue(value, rgb); float colorRed = rgb[0]; float colorGreen = rgb[1]; float colorBlue = rgb[2]; float colorAlpha = 1.0; //glColor3f(colorRed, colorGreen, colorBlue); //glRecti(x1, y1, x2, y2); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } else { // No interpolation, so we can draw whole grid blocks for (int xc = 0; xc < xsize; xc++) { double value = (spatiality_is_x ? values[xc] : values[(xsize - 1) - xc]); // flip for y, to use Cartesian coordinates float left, right, top, bottom; if (spatiality_is_x) { left = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); right = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); if (left < bounds_x1) left = bounds_x1; if (right > bounds_x2) right = bounds_x2; top = bounds_y1; bottom = bounds_y2; } else { top = qRound(((xc - 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); bottom = qRound(((xc + 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); if (top < bounds_y1) top = bounds_y1; if (bottom > bounds_y2) bottom = bounds_y2; left = bounds_x1; right = bounds_x2; } float rgb[3]; background_map->ColorForValue(value, rgb); float colorRed = rgb[0]; float colorGreen = rgb[1]; float colorBlue = rgb[2]; float colorAlpha = 1.0; //glColor3f(colorRed, colorGreen, colorBlue); //glRecti(x1, y1, x2, y2); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else // if (background_map->spatiality_ == 2) { // This is the spatiality "xy" case; it is the only 2D spatiality for which SLiMgui will draw // First, cache the display buffer if needed. If this succeeds, we'll use it. // It should always succeed, so the tile-drawing code below is dead code, kept for parallelism with the 1D case. cacheDisplayBufferForMapForSubpopulation(background_map, subpop, /* flipped */ false); uint8_t *display_buf = background_map->display_buffer_; if (display_buf) { // Use a cached display buffer to draw. // FIXME I think there is a bug here somewhere, the boundaries of the pixels fluctuate oddly when the // individuals pane is resized, even if the actual area the map is displaying in doesn't change size. // Maybe try using GL_POINTS? int buf_width = background_map->buffer_width_; int buf_height = background_map->buffer_height_; bool display_full_size = ((bounds.width() == buf_width) && (bounds.height() == buf_height)); float scale_x = bounds.width() / static_cast(buf_width); float scale_y = bounds.height() / static_cast(buf_height); // Then run through the pixels in the display buffer and draw them; this could be done // with some sort of OpenGL image-drawing method instead, but it's actually already // remarkably fast, at least on my machine, and drawing an image with OpenGL seems very // gross, and I tried it once before and couldn't get it to work well... for (int yc = 0; yc < buf_height; yc++) { // We flip the buffer vertically; it's the simplest way to get it into the right coordinate space uint8_t *buf_ptr = display_buf + ((buf_height - 1) - yc) * buf_width * 3; for (int xc = 0; xc < buf_width; xc++) { float colorRed = *(buf_ptr++) / 255.0f; float colorGreen = *(buf_ptr++) / 255.0f; float colorBlue = *(buf_ptr++) / 255.0f; float colorAlpha = 1.0; float left, right, top, bottom; if (display_full_size) { left = bounds_x1 + xc; right = left + 1.0f; top = bounds_y1 + yc; bottom = top + 1.0f; } else { left = bounds_x1 + xc * scale_x; right = bounds_x1 + (xc + 1) * scale_x; top = bounds_y1 + yc * scale_y; bottom = bounds_y1 + (yc + 1) * scale_y; } SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } else { // Draw rects for each map tile, without caching. Not as slow as you might expect, // but for really big maps it does get cumbersome. This is dead code now, overridden // by the buffer-drawing code above, which also handles interpolation correctly. int64_t xsize = background_map->grid_size_[0]; int64_t ysize = background_map->grid_size_[1]; double *values = background_map->values_; int n_colors = background_map->n_colors_; for (int yc = 0; yc < ysize; yc++) { float top = qRound(((yc - 0.5) / (ysize - 1)) * bounds.height() + bounds.y()); float bottom = qRound(((yc + 0.5) / (ysize - 1)) * bounds.height() + bounds.y()); if (top < bounds_y1) top = bounds_y1; if (bottom > bounds_y2) bottom = bounds_y2; // Flip our display, since our coordinate system is flipped relative to our buffer double *values_row = values + ((ysize - 1) - yc) * xsize; for (int xc = 0; xc < xsize; xc++) { double value = *(values_row + xc); float left = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); float right = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); if (left < bounds_x1) left = bounds_x1; if (right > bounds_x2) right = bounds_x2; float value_fraction = (background_map->colors_min_ < background_map->colors_max_) ? static_cast((value - background_map->colors_min_) / (background_map->colors_max_ - background_map->colors_min_)) : 0.0f; float color_index = value_fraction * (n_colors - 1); int color_index_1 = static_cast(floorf(color_index)); int color_index_2 = static_cast(ceilf(color_index)); if (color_index_1 < 0) color_index_1 = 0; if (color_index_1 >= n_colors) color_index_1 = n_colors - 1; if (color_index_2 < 0) color_index_2 = 0; if (color_index_2 >= n_colors) color_index_2 = n_colors - 1; float color_2_weight = color_index - color_index_1; float color_1_weight = 1.0f - color_2_weight; float red1 = background_map->red_components_[color_index_1]; float green1 = background_map->green_components_[color_index_1]; float blue1 = background_map->blue_components_[color_index_1]; float red2 = background_map->red_components_[color_index_2]; float green2 = background_map->green_components_[color_index_2]; float blue2 = background_map->blue_components_[color_index_2]; float colorRed = red1 * color_1_weight + red2 * color_2_weight; float colorGreen = green1 * color_1_weight + green2 * color_2_weight; float colorBlue = blue1 * color_1_weight + blue2 * color_2_weight; float colorAlpha = 1.0; //glColor3f(red, green, blue); //glRecti(x1, y1, x2, y2); SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); //std::cout << "x = " << x << ", y = " << y << ", value = " << value << ": color_index = " << color_index << ", color_index_1 = " << color_index_1 << ", color_index_2 = " << color_index_2 << ", color_1_weight = " << color_1_weight << ", color_2_weight = " << color_2_weight << ", red = " << red << std::endl; } } } } // Draw any leftovers SLIM_GL_FINISH(); } if (showGridPoints) { // BCH 9/29/2023 new feature: draw boxes showing where the grid nodes are, since that is rather confusing! float margin_outer = 5.5f; float margin_inner = 3.5f; float spacing = 10.0f; int64_t xsize = background_map->grid_size_[0]; int64_t ysize = background_map->grid_size_[1]; double *values = background_map->values_; // require that there is sufficient space that we're not just showing a packed grid of squares // downsize to small and smaller depictions as needed if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) { margin_outer = 4.5f; margin_inner = 2.5f; spacing = 8.0; } if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) { margin_outer = 3.5f; margin_inner = 1.5f; spacing = 6.0; } if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) { margin_outer = 1.0f; margin_inner = 0.0f; spacing = 2.0; } if (((xsize - 1) * (margin_outer * 2.0 + spacing) <= bounds_x2) && ((ysize - 1) * (margin_outer * 2.0 + spacing) <= bounds_y2)) { // Set up to draw rects SLIM_GL_PREPARE(); // first pass we draw squares to make outlines, second pass we draw the interiors in color for (int pass = 0; pass <= 1; ++pass) { const float margin = ((pass == 0) ? margin_outer : margin_inner); if (margin == 0.0) continue; for (int x = 0; x < xsize; ++x) { for (int y = 0; y < ysize; ++y) { float position_x = x / (float)(xsize - 1); // 0 to 1 float position_y = y / (float)(ysize - 1); // 0 to 1 float centerX = (float)(bounds_x1 + round(position_x * bounds.width())); float centerY = (float)(bounds_y1 + bounds.height() - round(position_y * bounds.height())); float left = centerX - margin; float top = centerY - margin; float right = centerX + margin; float bottom = centerY + margin; if (left < bounds_x1) left = bounds_x1; if (top < bounds_y1) top = bounds_y1; if (right > bounds_x2) right = bounds_x2; if (bottom > bounds_y2) bottom = bounds_y2; SLIM_GL_PUSHRECT(); float colorRed; float colorGreen; float colorBlue; float colorAlpha; if (pass == 0) { colorRed = 1.0; colorGreen = 0.25; colorBlue = 0.25; colorAlpha = 1.0; } else { // look up the map's color at this grid point float rgb[3]; double value = values[x + y * xsize]; background_map->ColorForValue(value, rgb); colorRed = rgb[0]; colorGreen = rgb[1]; colorBlue = rgb[2]; colorAlpha = 1.0; } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } } } // Draw any leftovers SLIM_GL_FINISH(); } } } void QtSLiMIndividualsWidget::glDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int /* dimensionality */) { auto backgroundIter = subviewSettings.find(subpop->subpopulation_id_); PopulationViewSettings background; SpatialMap *background_map = nullptr; if (backgroundIter == subviewSettings.end()) { // The user has not made a choice, so choose a temporary default. We don't want this choice to "stick", // so that we can, e.g., begin as black and then change to a spatial map if one is defined. chooseDefaultBackgroundSettingsForSubpopulation(&background, &background_map, subpop); } else { // The user has made a choice; verify that it is acceptable, and then use it. background = backgroundIter->second; if (background.backgroundType == 3) { SpatialMapMap &spatial_maps = subpop->spatial_maps_; auto map_iter = spatial_maps.find(background.spatialMapName); if (map_iter != spatial_maps.end()) { background_map = map_iter->second; // if the user somehow managed to choose a map that is not of an acceptable dimensionality, reject it here if ((background_map->spatiality_string_ != "x") && (background_map->spatiality_string_ != "y") && (background_map->spatiality_string_ != "xy")) background_map = nullptr; } } // if we're supposed to use a background map but we couldn't find it, or it's unacceptable, revert to black if ((background.backgroundType == 3) && !background_map) background.backgroundType = 0; } if ((background.backgroundType == 3) && background_map) { _glDrawBackgroundSpatialMap(background_map, bounds, subpop, background.showGridPoints); } else { // No background map, so just clear to the preferred background color int backgroundColor = background.backgroundType; if (backgroundColor == 0) glColor3f(0.0, 0.0, 0.0); else if (backgroundColor == 1) glColor3f(0.3f, 0.3f, 0.3f); else if (backgroundColor == 2) glColor3f(1.0, 1.0, 1.0); else glColor3f(0.0, 0.0, 0.0); glRecti(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height()); } } void QtSLiMIndividualsWidget::glDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor) { QtSLiMWindow *controller = dynamic_cast(window()); double scalingFactor = 0.8; // used to be controller->fitnessColorScale; slim_popsize_t subpopSize = subpop->parent_subpop_size_; double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; QRect individualArea(bounds.x(), bounds.y(), bounds.width() - 1, bounds.height() - 1); int individualArrayIndex; // Set up to draw rects SLIM_GL_PREPARE(); // First we outline all individuals if (dimensionality == 1) srandom(static_cast(controller->community->Tick())); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; float position_x, position_y; if (dimensionality == 1) { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast(random() / static_cast(INT32_MAX)); if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds continue; } else { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds continue; } float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); float left = centerX - 2.5f; float top = centerY - 2.5f; float right = centerX + 2.5f; float bottom = centerY + 2.5f; if (left < individualArea.x()) left = static_cast(individualArea.x()); if (top < individualArea.y()) top = static_cast(individualArea.y()); if (right > individualArea.x() + individualArea.width() + 1) right = static_cast(individualArea.x() + individualArea.width() + 1); if (bottom > individualArea.y() + individualArea.height() + 1) bottom = static_cast(individualArea.y() + individualArea.height() + 1); float colorRed = 0.25; float colorGreen = 0.25; float colorBlue = 0.25; float colorAlpha = 1.0; SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } // Then we draw all individuals if (dimensionality == 1) srandom(static_cast(controller->community->Tick())); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; float position_x, position_y; if (dimensionality == 1) { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast(random() / static_cast(INT32_MAX)); if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds continue; } else { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds continue; } float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); float left = centerX - 1.5f; float top = centerY - 1.5f; float right = centerX + 1.5f; float bottom = centerY + 1.5f; // clipping deliberately not done here; because individual rects are 3x3, they will fall at most one pixel // outside our drawing area, and thus the flaw will be covered by the view frame when it overdraws SLIM_GL_PUSHRECT(); // dark gray default, for a fitness of NaN; should never happen float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; if (Individual::s_any_individual_color_set_ && individual.color_set_) { colorRed = individual.colorR_ / 255.0F; colorGreen = individual.colorG_ / 255.0F; colorBlue = individual.colorB_ / 255.0F; } else if (forceColor) { // forceColor is used to make each species draw with a distinctive color in multispecies models in unified display mode colorRed = forceColor[0]; colorGreen = forceColor[1]; colorBlue = forceColor[2]; } else { // use individual trait values to determine color; we used fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring double fitness = individual.cached_unscaled_fitness_; if (!std::isnan(fitness)) RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS(); } // Draw any leftovers SLIM_GL_FINISH(); } #endif ================================================ FILE: QtSLiM/QtSLiMIndividualsWidget_QT.cpp ================================================ // // QtSLiMIndividualsWidget_QT.cpp // SLiM // // Created by Ben Haller on 8/25/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMIndividualsWidget.h" #include "QtSLiMWindow.h" #include "QtSLiMExtras.h" #include "QtSLiMOpenGL_Emulation.h" #include #include #include #include #include // // Qt-based drawing; maintain this in parallel with the OpenGL-based drawing! // void QtSLiMIndividualsWidget::qtDrawViewFrameInBounds(QRect bounds, QPainter &painter) { bool inDarkMode = QtSLiMInDarkMode(); if (inDarkMode) QtSLiMFrameRect(bounds, QtSLiMColorWithWhite(0.067, 1.0), painter); else QtSLiMFrameRect(bounds, QtSLiMColorWithWhite(0.77, 1.0), painter); } void QtSLiMIndividualsWidget::qtDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize, QPainter &painter) { // // NOTE this code is parallel to the code in canDisplayIndividualsFromSubpopulation:inArea: and should be maintained in parallel // //QtSLiMWindow *controller = dynamic_cast(window()); double scalingFactor = 0.8; // used to be controller->fitnessColorScale; slim_popsize_t subpopSize = subpop->parent_subpop_size_; int viewColumns = 0, viewRows = 0; // our square size is given from above (a consensus based on squareSizeForSubpopulationInArea(); calculate metrics from it viewColumns = static_cast(floor((bounds.width() - 3) / squareSize)); viewRows = static_cast(floor((bounds.height() - 3) / squareSize)); if (viewColumns * viewRows < subpopSize) squareSize = 1; if (squareSize > 1) { int squareSpacing = 0; // Convert square area to space between squares if possible if (squareSize > 2) { --squareSize; ++squareSpacing; } if (squareSize > 5) { --squareSize; ++squareSpacing; } double excessSpaceX = bounds.width() - ((squareSize + squareSpacing) * viewColumns - squareSpacing); double excessSpaceY = bounds.height() - ((squareSize + squareSpacing) * viewRows - squareSpacing); int offsetX = static_cast(floor(excessSpaceX / 2.0)); int offsetY = static_cast(floor(excessSpaceY / 2.0)); // If we have an empty row at the bottom, then we can use the same value for offsetY as for offsetX, for symmetry if ((subpopSize - 1) / viewColumns < viewRows - 1) offsetY = offsetX; QRect individualArea(bounds.left() + offsetX, bounds.top() + offsetY, bounds.width() - offsetX, bounds.height() - offsetY); int individualArrayIndex; // Set up to draw rects SLIM_GL_PREPARE(); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... float left = static_cast(individualArea.left() + (individualArrayIndex % viewColumns) * (squareSize + squareSpacing)); float top = static_cast(individualArea.top() + (individualArrayIndex / viewColumns) * (squareSize + squareSpacing)); float right = left + squareSize; float bottom = top + squareSize; SLIM_GL_PUSHRECT(); // dark gray default, for a fitness of NaN; should never happen float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; if (Individual::s_any_individual_color_set_ && individual.color_set_) { colorRed = individual.colorR_ / 255.0F; colorGreen = individual.colorG_ / 255.0F; colorBlue = individual.colorB_ / 255.0F; } else { // use individual trait values to determine color; we use fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring double fitness = individual.cached_unscaled_fitness_; if (!std::isnan(fitness)) RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); } // Draw any leftovers SLIM_GL_FINISH(); } else { // This is what we do if we cannot display a subpopulation because there are too many individuals in it to display QRect insetBounds = bounds.adjusted(1, 1, -1, -1); painter.fillRect(insetBounds, QtSLiMColorWithRGB(0.9, 0.9, 1.0, 1.0)); } } void QtSLiMIndividualsWidget::_qtDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints, QPainter &painter) { // We have a spatial map with a color map, so use it to draw the background int bounds_x1 = bounds.x(); int bounds_y1 = bounds.y(); int bounds_x2 = bounds.x() + bounds.width(); int bounds_y2 = bounds.y() + bounds.height(); { // Set up to draw rects SLIM_GL_PREPARE(); if (background_map->spatiality_ == 1) { // This is the spatiality "x" and "y" cases; they are the only 1D spatiality values for which SLiMgui will draw // In the 1D case we can't cache a display buffer, since we don't know what aspect ratio to use, so we just // draw rects. Whether those rects are horizontal or vertical will depend on the spatiality of the map. Most // of the code is identical, though, because of the way we handle dimensions, so we share the two cases here. bool spatiality_is_x = (background_map->spatiality_string_ == "x"); int64_t xsize = background_map->grid_size_[0]; double *values = background_map->values_; if (background_map->interpolate_) { // Interpolation, so we need to draw every line individually int min_coord = (spatiality_is_x ? bounds_x1 : bounds_y1); int max_coord = (spatiality_is_x ? bounds_x2 : bounds_y2); for (int xc = min_coord; xc < max_coord; ++xc) { double x_fraction = (xc + 0.5 - min_coord) / (max_coord - min_coord); // values evaluated at pixel centers double x_map = x_fraction * (xsize - 1); int x1_map = static_cast(floor(x_map)); int x2_map = static_cast(ceil(x_map)); double fraction_x2 = x_map - x1_map; double fraction_x1 = 1.0 - fraction_x2; double value_x1 = values[x1_map] * fraction_x1; double value_x2 = values[x2_map] * fraction_x2; double value = value_x1 + value_x2; float left, right, top, bottom; if (spatiality_is_x) { left = xc; right = xc + 1; top = bounds_y1; bottom = bounds_y2; } else { top = (max_coord - 1) - xc + min_coord; // flip for y, to use Cartesian coordinates bottom = top + 1; left = bounds_x1; right = bounds_x2; } float rgb[3]; background_map->ColorForValue(value, rgb); float colorRed = rgb[0]; float colorGreen = rgb[1]; float colorBlue = rgb[2]; float colorAlpha = 1.0; SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); } } else { // No interpolation, so we can draw whole grid blocks for (int xc = 0; xc < xsize; xc++) { double value = (spatiality_is_x ? values[xc] : values[(xsize - 1) - xc]); // flip for y, to use Cartesian coordinates float left, right, top, bottom; if (spatiality_is_x) { left = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); right = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); if (left < bounds_x1) left = bounds_x1; if (right > bounds_x2) right = bounds_x2; top = bounds_y1; bottom = bounds_y2; } else { top = qRound(((xc - 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); bottom = qRound(((xc + 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); if (top < bounds_y1) top = bounds_y1; if (bottom > bounds_y2) bottom = bounds_y2; left = bounds_x1; right = bounds_x2; } float rgb[3]; background_map->ColorForValue(value, rgb); float colorRed = rgb[0]; float colorGreen = rgb[1]; float colorBlue = rgb[2]; float colorAlpha = 1.0; SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); } } } else // if (background_map->spatiality_ == 2) { // This is the spatiality "xy" case; it is the only 2D spatiality for which SLiMgui will draw // First, cache the display buffer if needed. If this succeeds, we'll use it. // It should always succeed, so the tile-drawing code below is dead code, kept for parallelism with the 1D case. cacheDisplayBufferForMapForSubpopulation(background_map, subpop, /* flipped */ true); const uint8_t *display_buf = background_map->display_buffer_; if (display_buf) { // Use a cached display buffer to draw. int buf_width = background_map->buffer_width_; int buf_height = background_map->buffer_height_; QImage bufferImage(display_buf, buf_width, buf_height, buf_width * 3, QImage::Format_RGB888); painter.drawImage(bounds, bufferImage); } } // Draw any leftovers SLIM_GL_FINISH(); } if (showGridPoints) { // BCH 9/29/2023 new feature: draw boxes showing where the grid nodes are, since that is rather confusing! float margin_outer = 5.5f; float margin_inner = 3.5f; float spacing = 10.0f; int64_t xsize = background_map->grid_size_[0]; int64_t ysize = background_map->grid_size_[1]; double *values = background_map->values_; // require that there is sufficient space that we're not just showing a packed grid of squares // downsize to small and smaller depictions as needed if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) { margin_outer = 4.5f; margin_inner = 2.5f; spacing = 8.0; } if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) { margin_outer = 3.5f; margin_inner = 1.5f; spacing = 6.0; } if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) { margin_outer = 1.0f; margin_inner = 0.0f; spacing = 2.0; } if (((xsize - 1) * (margin_outer * 2.0 + spacing) <= bounds_x2) && ((ysize - 1) * (margin_outer * 2.0 + spacing) <= bounds_y2)) { // Set up to draw rects SLIM_GL_PREPARE(); // first pass we draw squares to make outlines, second pass we draw the interiors in color for (int pass = 0; pass <= 1; ++pass) { const float margin = ((pass == 0) ? margin_outer : margin_inner); if (margin == 0.0) continue; for (int x = 0; x < xsize; ++x) { for (int y = 0; y < ysize; ++y) { float position_x = x / (float)(xsize - 1); // 0 to 1 float position_y = y / (float)(ysize - 1); // 0 to 1 float centerX = (float)(bounds_x1 + round(position_x * bounds.width())); float centerY = (float)(bounds_y1 + bounds.height() - round(position_y * bounds.height())); float left = centerX - margin; float top = centerY - margin; float right = centerX + margin; float bottom = centerY + margin; if (left < bounds_x1) left = bounds_x1; if (top < bounds_y1) top = bounds_y1; if (right > bounds_x2) right = bounds_x2; if (bottom > bounds_y2) bottom = bounds_y2; SLIM_GL_PUSHRECT(); float colorRed; float colorGreen; float colorBlue; float colorAlpha; if (pass == 0) { colorRed = 1.0; colorGreen = 0.25; colorBlue = 0.25; colorAlpha = 1.0; } else { // look up the map's color at this grid point float rgb[3]; double value = values[x + y * xsize]; background_map->ColorForValue(value, rgb); colorRed = rgb[0]; colorGreen = rgb[1]; colorBlue = rgb[2]; colorAlpha = 1.0; } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); } } } // Draw any leftovers SLIM_GL_FINISH(); } } } void QtSLiMIndividualsWidget::qtDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int /* dimensionality */, QPainter &painter) { auto backgroundIter = subviewSettings.find(subpop->subpopulation_id_); PopulationViewSettings background; SpatialMap *background_map = nullptr; if (backgroundIter == subviewSettings.end()) { // The user has not made a choice, so choose a temporary default. We don't want this choice to "stick", // so that we can, e.g., begin as black and then change to a spatial map if one is defined. chooseDefaultBackgroundSettingsForSubpopulation(&background, &background_map, subpop); } else { // The user has made a choice; verify that it is acceptable, and then use it. background = backgroundIter->second; if (background.backgroundType == 3) { SpatialMapMap &spatial_maps = subpop->spatial_maps_; auto map_iter = spatial_maps.find(background.spatialMapName); if (map_iter != spatial_maps.end()) { background_map = map_iter->second; // if the user somehow managed to choose a map that is not of an acceptable dimensionality, reject it here if ((background_map->spatiality_string_ != "x") && (background_map->spatiality_string_ != "y") && (background_map->spatiality_string_ != "xy")) background_map = nullptr; } } // if we're supposed to use a background map but we couldn't find it, or it's unacceptable, revert to black if ((background.backgroundType == 3) && !background_map) background.backgroundType = 0; } if ((background.backgroundType == 3) && background_map) { _qtDrawBackgroundSpatialMap(background_map, bounds, subpop, background.showGridPoints, painter); } else { // No background map, so just clear to the preferred background color int backgroundColor = background.backgroundType; if (backgroundColor == 0) painter.fillRect(bounds, Qt::black); else if (backgroundColor == 1) painter.fillRect(bounds, QtSLiMColorWithWhite(0.3, 1.0)); else if (backgroundColor == 2) painter.fillRect(bounds, Qt::white); else painter.fillRect(bounds, Qt::black); } } void QtSLiMIndividualsWidget::qtDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor, QPainter &painter) { QtSLiMWindow *controller = dynamic_cast(window()); double scalingFactor = 0.8; // used to be controller->fitnessColorScale; slim_popsize_t subpopSize = subpop->parent_subpop_size_; double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; QRect individualArea(bounds.x(), bounds.y(), bounds.width() - 1, bounds.height() - 1); int individualArrayIndex; // Set up to draw rects SLIM_GL_PREPARE(); // First we outline all individuals if (dimensionality == 1) srandom(static_cast(controller->community->Tick())); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; float position_x, position_y; if (dimensionality == 1) { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast(random() / static_cast(INT32_MAX)); if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds continue; } else { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds continue; } float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); float left = centerX - 2.5f; float top = centerY - 2.5f; float right = centerX + 2.5f; float bottom = centerY + 2.5f; if (left < individualArea.x()) left = static_cast(individualArea.x()); if (top < individualArea.y()) top = static_cast(individualArea.y()); if (right > individualArea.x() + individualArea.width() + 1) right = static_cast(individualArea.x() + individualArea.width() + 1); if (bottom > individualArea.y() + individualArea.height() + 1) bottom = static_cast(individualArea.y() + individualArea.height() + 1); float colorRed = 0.25; float colorGreen = 0.25; float colorBlue = 0.25; float colorAlpha = 1.0; SLIM_GL_PUSHRECT(); SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); } // Then we draw all individuals if (dimensionality == 1) srandom(static_cast(controller->community->Tick())); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; float position_x, position_y; if (dimensionality == 1) { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast(random() / static_cast(INT32_MAX)); if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds continue; } else { position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds continue; } float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); float left = centerX - 1.5f; float top = centerY - 1.5f; float right = centerX + 1.5f; float bottom = centerY + 1.5f; // clipping deliberately not done here; because individual rects are 3x3, they will fall at most one pixel // outside our drawing area, and thus the flaw will be covered by the view frame when it overdraws SLIM_GL_PUSHRECT(); // dark gray default, for a fitness of NaN; should never happen float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; if (Individual::s_any_individual_color_set_ && individual.color_set_) { colorRed = individual.colorR_ / 255.0F; colorGreen = individual.colorG_ / 255.0F; colorBlue = individual.colorB_ / 255.0F; } else if (forceColor) { // forceColor is used to make each species draw with a distinctive color in multispecies models in unified display mode colorRed = forceColor[0]; colorGreen = forceColor[1]; colorBlue = forceColor[2]; } else { // use individual trait values to determine color; we used fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring double fitness = individual.cached_unscaled_fitness_; if (!std::isnan(fitness)) RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); } SLIM_GL_PUSHRECT_COLORS(); SLIM_GL_CHECKBUFFERS_NORECT(); } // Draw any leftovers SLIM_GL_FINISH(); } ================================================ FILE: QtSLiM/QtSLiMOpenGL.cpp ================================================ // // QtSLiMOpenGL.cpp // SLiM // // Created by Ben Haller on 8/25/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMOpenGL.h" #include float *glArrayVertices = nullptr; float *glArrayColors = nullptr; void QtSLiM_AllocateGLBuffers(void) { if (!glArrayVertices) glArrayVertices = static_cast(malloc(kMaxVertices * 2 * sizeof(float))); // 2 floats per vertex, kMaxVertices vertices if (!glArrayColors) glArrayColors = static_cast(malloc(kMaxVertices * 4 * sizeof(float))); // 4 floats per color, kMaxVertices colors } void QtSLiM_FreeGLBuffers(void) { if (glArrayVertices) { free(glArrayVertices); glArrayVertices = nullptr; } if (glArrayColors) { free(glArrayColors); glArrayColors = nullptr; } } ================================================ FILE: QtSLiM/QtSLiMOpenGL.h ================================================ // // QtSLiMOpenGL.h // SLiM // // Created by Ben Haller on 8/25/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMOPENGL_H #define QTSLIMOPENGL_H /* * This header defines utility macros and functions for drawing with OpenGL. * This should be included only locally within OpenGL-specific rendering code. */ // OpenGL buffers for rendering; shared across all rendering code // These buffers are allocated by QtSLiMAppDelegate at launch #define kMaxGLRects 4000 // 4000 rects #define kMaxVertices (kMaxGLRects * 4) // 4 vertices each extern float *glArrayVertices; extern float *glArrayColors; extern void QtSLiM_AllocateGLBuffers(void); extern void QtSLiM_FreeGLBuffers(void); #define SLIM_GL_PREPARE() \ int displayListIndex = 0; \ float *vertices = glArrayVertices, *colors = glArrayColors; \ \ glEnableClientState(GL_VERTEX_ARRAY); \ glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); \ glEnableClientState(GL_COLOR_ARRAY); \ glColorPointer(4, GL_FLOAT, 0, glArrayColors); #define SLIM_GL_DEFCOORDS(rect) \ float left = static_cast(rect.left()); \ float top = static_cast(rect.top()); \ float right = left + static_cast(rect.width()); \ float bottom = top + static_cast(rect.height()); #define SLIM_GL_PUSHRECT() \ *(vertices++) = left; \ *(vertices++) = top; \ *(vertices++) = left; \ *(vertices++) = bottom; \ *(vertices++) = right; \ *(vertices++) = bottom; \ *(vertices++) = right; \ *(vertices++) = top; #define SLIM_GL_PUSHRECT_COLORS() \ for (int j = 0; j < 4; ++j) \ { \ *(colors++) = colorRed; \ *(colors++) = colorGreen; \ *(colors++) = colorBlue; \ *(colors++) = colorAlpha; \ } #define SLIM_GL_CHECKBUFFERS() \ displayListIndex++; \ \ if (displayListIndex == kMaxGLRects) \ { \ glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); \ \ vertices = glArrayVertices; \ colors = glArrayColors; \ displayListIndex = 0; \ } #define SLIM_GL_FINISH() \ if (displayListIndex) \ glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); \ \ glDisableClientState(GL_VERTEX_ARRAY); \ glDisableClientState(GL_COLOR_ARRAY); #endif // QTSLIMOPENGL_H ================================================ FILE: QtSLiM/QtSLiMOpenGL_Emulation.h ================================================ // // QtSLiMOpenGL_Emulation.h // SLiM // // Created by Ben Haller on 8/25/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMOPENGL_EMULATION_H #define QTSLIMOPENGL_EMULATION_H /* * This header defines utility macros and functions for drawing with OpenGL, but they are emulated with Qt. * This should be included only locally within Qt-specific rendering code. See also QtSLiMOpenGL.h. */ #define SLIM_GL_PREPARE() #define SLIM_GL_DEFCOORDS(rect) \ QRect &RECT_TO_DRAW = rect; #define SLIM_GL_PUSHRECT() #define SLIM_GL_PUSHRECT_COLORS() #define SLIM_GL_CHECKBUFFERS() \ { \ QColor COLOR_TO_DRAW; \ COLOR_TO_DRAW.setRgbF(colorRed, colorGreen, colorBlue, colorAlpha); \ painter.fillRect(RECT_TO_DRAW, COLOR_TO_DRAW); \ } #define SLIM_GL_CHECKBUFFERS_NORECT() \ { \ QColor COLOR_TO_DRAW; \ COLOR_TO_DRAW.setRgbF(colorRed, colorGreen, colorBlue, colorAlpha); \ QRect RECT_TO_DRAW(left, top, right-left, bottom-top); \ painter.fillRect(RECT_TO_DRAW, COLOR_TO_DRAW); \ } #define SLIM_GL_FINISH() #endif // QTSLIMOPENGL_EMULATION_H ================================================ FILE: QtSLiM/QtSLiMPopulationTable.cpp ================================================ // // QtSLiMPopulationTable.cpp // SLiM // // Created by Ben Haller on 7/30/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMPopulationTable.h" #include "QtSLiMWindow.h" #include "QtSLiMAppDelegate.h" #include "subpopulation.h" #include #include #include QtSLiMPopulationTableModel::QtSLiMPopulationTableModel(QObject *p_parent) : QAbstractTableModel(p_parent) { // p_parent must be a pointer to QtSLiMWindow, which holds our model information if (dynamic_cast(p_parent) == nullptr) throw p_parent; } QtSLiMPopulationTableModel::~QtSLiMPopulationTableModel() { } int QtSLiMPopulationTableModel::rowCount(const QModelIndex & /* parent */) const { return static_cast(displaySubpops.size()); } int QtSLiMPopulationTableModel::columnCount(const QModelIndex & /* parent */) const { return 6; } QVariant QtSLiMPopulationTableModel::data(const QModelIndex &p_index, int role) const { if (!p_index.isValid()) return QVariant(); QtSLiMWindow *controller = static_cast(parent()); Community *community = controller->community; int subpopCount = static_cast(displaySubpops.size()); if (subpopCount == 0) return QVariant(); if (role == Qt::DisplayRole) { if (p_index.row() < subpopCount) { auto popIter = displaySubpops.begin(); std::advance(popIter, p_index.row()); Subpopulation *subpop = *popIter; Species *species = &(subpop->species_); // BCH 3/21/2024: This check is a debugging leftover that should stay permanently. The display list // for the population table was out of date and contained subpopulations that had been deallocated, // leading to a crash. This was a very hard bug to find, so it's worth keeping this code here. The // bug was fixed with needsUpdateForDisplaySubpops() in QtSLiMPopulationTableModel. if (species == nullptr) { qDebug() << "INVALID SUBPOPULATION in QtSLiMPopulationTableModel::data()!"; qApp->beep(); } if (p_index.column() == 0) { QString idString = QString("p%1").arg(subpop->subpopulation_id_); if (community->all_species_.size() > 1) idString.append(" ").append(QString::fromStdString(species->avatar_)); return QVariant(idString); } else if (p_index.column() == 1) { return QVariant(QString("%1").arg(subpop->parent_subpop_size_)); } else if (community->ModelType() == SLiMModelType::kModelTypeNonWF) { // in nonWF models selfing/cloning/sex rates/ratios are emergent, calculated from collected metrics double total_offspring = subpop->gui_offspring_cloned_M_ + subpop->gui_offspring_crossed_ + subpop->gui_offspring_empty_ + subpop->gui_offspring_selfed_; if (subpop->sex_enabled_) total_offspring += subpop->gui_offspring_cloned_F_; // avoid double-counting clones when we are modeling hermaphrodites if (p_index.column() == 2) { if (!subpop->sex_enabled_ && (total_offspring > 0)) return QVariant(QString("%1").arg(subpop->gui_offspring_selfed_ / total_offspring, 0, 'f', 2)); } else if (p_index.column() == 3) { if (total_offspring > 0) return QVariant(QString("%1").arg(subpop->gui_offspring_cloned_F_ / total_offspring, 0, 'f', 2)); } else if (p_index.column() == 4) { if (total_offspring > 0) return QVariant(QString("%1").arg(subpop->gui_offspring_cloned_M_ / total_offspring, 0, 'f', 2)); } else if (p_index.column() == 5) { if (subpop->sex_enabled_ && (subpop->parent_subpop_size_ > 0)) return QVariant(QString("%1").arg(1.0 - subpop->parent_first_male_index_ / static_cast(subpop->parent_subpop_size_), 0, 'f', 2)); } return QVariant("—"); } else // sim->ModelType() == SLiMModelType::kModelTypeWF { if (p_index.column() == 2) { if (subpop->sex_enabled_) return QVariant("—"); else return QVariant(QString("%1").arg(subpop->selfing_fraction_, 0, 'f', 2)); } else if (p_index.column() == 3) { return QVariant(QString("%1").arg(subpop->female_clone_fraction_, 0, 'f', 2)); } else if (p_index.column() == 4) { return QVariant(QString("%1").arg(subpop->male_clone_fraction_, 0, 'f', 2)); } else if (p_index.column() == 5) { if (subpop->sex_enabled_) return QVariant(QString("%1").arg(subpop->parent_sex_ratio_, 0, 'f', 2)); else return QVariant("—"); } } } } else if (role == Qt::TextAlignmentRole) { switch (p_index.column()) { case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter); case 1: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 2: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 3: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 4: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 5: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); } } return QVariant(); } QVariant QtSLiMPopulationTableModel::headerData(int section, Qt::Orientation /* p_orientation */, int role) const { if (role == Qt::DisplayRole) { switch (section) { case 0: return QVariant("ID"); case 1: return QVariant("N"); //case 2: return QVariant("self"); //case 3: return QVariant("clF"); //case 4: return QVariant("clM"); //case 5: return QVariant("SR"); default: return QVariant(""); } } else if (role == Qt::ToolTipRole) { switch (section) { case 0: return QVariant("the Eidos identifier for the subpopulation"); case 1: return QVariant("the subpopulation size"); case 2: return QVariant("the selfing rate of the subpopulation"); case 3: return QVariant("the cloning rate of the subpopulation, for females"); case 4: return QVariant("the cloning rate of the subpopulation, for males"); case 5: return QVariant("the sex ratio of the subpopulation, M:(M+F)"); } } else if (role == Qt::TextAlignmentRole) { switch (section) { case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter); case 1: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 2: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 3: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 4: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); case 5: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); } } // else if (role == Qt::DecorationRole) // { // switch (section) // { // case 2: return QVariant::fromValue(QIcon(QtSLiMImagePath("Qt_selfing_rate", false))); // case 3: return QVariant::fromValue(QIcon(QtSLiMImagePath("Qt_female_symbol", false))); // case 4: return QVariant::fromValue(QIcon(QtSLiMImagePath("Qt_male_symbol", false))); // case 5: return QVariant::fromValue(QIcon(QtSLiMImagePath("Qt_sex_ratio", false))); // } // } return QVariant(); } bool QtSLiMPopulationTableModel::needsUpdateForDisplaySubpops(std::vector &newDisplayList) { // Checks whether our cached display list is out of date; if it is, a reload needs to be forced. return (displaySubpops != newDisplayList); } void QtSLiMPopulationTableModel::reloadTable(std::vector &newDisplayList) { beginResetModel(); // recache the list of subpopulations we display std::swap(displaySubpops, newDisplayList); newDisplayList.clear(); endResetModel(); } QtSLiMPopulationTableHeaderView::QtSLiMPopulationTableHeaderView(Qt::Orientation p_orientation, QWidget *p_parent) : QHeaderView(p_orientation, p_parent) { cacheIcons(); // Recache our icons if the light mode / dark mode setting changes connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, [this]() { freeCachedIcons(); cacheIcons(); }); } void QtSLiMPopulationTableHeaderView::freeCachedIcons(void) { if (icon_cloning_rate) { delete icon_cloning_rate; icon_cloning_rate = nullptr; } if (icon_selfing_rate) { delete icon_selfing_rate; icon_selfing_rate = nullptr; } if (icon_sex_ratio) { delete icon_sex_ratio; icon_sex_ratio = nullptr; } if (icon_female_symbol) { delete icon_female_symbol; icon_female_symbol = nullptr; } if (icon_male_symbol) { delete icon_male_symbol; icon_male_symbol = nullptr; } } void QtSLiMPopulationTableHeaderView::cacheIcons(void) { // Note that this caches the icons for the current light mode / dark mode setting; they will be recached if the mode changes icon_cloning_rate = new QIcon(QtSLiMImagePath("Qt_cloning_rate", false)); icon_selfing_rate = new QIcon(QtSLiMImagePath("Qt_selfing_rate", false)); icon_sex_ratio = new QIcon(QtSLiMImagePath("Qt_sex_ratio", false)); icon_female_symbol = new QIcon(QtSLiMImagePath("Qt_female_symbol", false)); icon_male_symbol = new QIcon(QtSLiMImagePath("Qt_male_symbol", false)); } QtSLiMPopulationTableHeaderView::~QtSLiMPopulationTableHeaderView() { freeCachedIcons(); } void QtSLiMPopulationTableHeaderView::paintSection(QPainter *painter, const QRect &p_rect, int p_logicalIndex) const { painter->save(); QHeaderView::paintSection(painter, p_rect, p_logicalIndex); painter->restore(); painter->save(); painter->setRenderHint(QPainter::SmoothPixmapTransform); switch (p_logicalIndex) { case 2: case 5: { QIcon *icon = (p_logicalIndex == 2 ? icon_selfing_rate : icon_sex_ratio); QPoint center = p_rect.center(); icon->paint(painter, center.x() - 5, center.y() - 6, 12, 12); break; } case 3: { QIcon *icon1 = icon_cloning_rate; QIcon *icon2 = icon_female_symbol; QPoint center = p_rect.center(); icon1->paint(painter, center.x() - 11, center.y() - 6, 12, 12); icon2->paint(painter, center.x() + 1, center.y() - 6, 12, 12); break; } case 4: { QIcon *icon1 = icon_cloning_rate; QIcon *icon2 = icon_male_symbol; QPoint center = p_rect.center(); icon1->paint(painter, center.x() - 13, center.y() - 6, 12, 12); icon2->paint(painter, center.x() + 1, center.y() - 6, 12, 12); break; } default: break; } painter->restore(); } ================================================ FILE: QtSLiM/QtSLiMPopulationTable.h ================================================ // // QtSLiMPopulationTable.h // SLiM // // Created by Ben Haller on 7/30/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMPOPULATIONTABLE_H #define QTSLIMPOPULATIONTABLE_H #include #include #include class QPainter; class Subpopulation; class QtSLiMPopulationTableModel : public QAbstractTableModel { Q_OBJECT public: QtSLiMPopulationTableModel(QObject *p_parent = nullptr); virtual ~QtSLiMPopulationTableModel() override; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual QVariant headerData(int section, Qt::Orientation p_orientation, int role = Qt::DisplayRole) const override; bool needsUpdateForDisplaySubpops(std::vector &newDisplayList); void reloadTable(std::vector &newDisplayList); Subpopulation *subpopAtIndex(int i) const { return displaySubpops[i]; } protected: // We cache a list of the subpopulations we are displaying, for more efficient display std::vector displaySubpops; }; class QtSLiMPopulationTableHeaderView : public QHeaderView { Q_OBJECT QIcon *icon_cloning_rate = nullptr; QIcon *icon_selfing_rate = nullptr; QIcon *icon_sex_ratio = nullptr; QIcon *icon_female_symbol = nullptr; QIcon *icon_male_symbol = nullptr; public: QtSLiMPopulationTableHeaderView(Qt::Orientation p_orientation, QWidget *p_parent = nullptr); virtual ~QtSLiMPopulationTableHeaderView() override; virtual void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override; protected: void freeCachedIcons(void); void cacheIcons(void); }; #endif // QTSLIMPOPULATIONTABLE_H ================================================ FILE: QtSLiM/QtSLiMPreferences.cpp ================================================ // // QtSLiMPreferences.cpp // SLiM // // Created by Ben Haller on 8/3/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMPreferences.h" #include "ui_QtSLiMPreferences.h" #include #include #include #include "QtSLiMAppDelegate.h" // // QSettings keys for the prefs we control; these are private // static const char *QtSLiMAppStartupAction = "QtSLiMAppStartupAction"; static const char *QtSLiMForceDarkMode = "QtSLiMForceDarkMode"; static const char *QtSLiMForceFusionStyle = "QtSLiMForceFusionStyle"; static const char *QtSLiMUseOpenGL = "QtSLiMUseOpenGL"; static const char *QtSLiMDisplayFontFamily = "QtSLiMDisplayFontFamily"; static const char *QtSLiMDisplayFontSize = "QtSLiMDisplayFontSize"; static const char *QtSLiMSyntaxHighlightScript = "QtSLiMSyntaxHighlightScript"; static const char *QtSLiMSyntaxHighlightOutput = "QtSLiMSyntaxHighlightOutput"; static const char *QtSLiMShowLineNumbers = "QtSLiMShowLineNumbers"; static const char *QtSLiMShowPageGuide = "QtSLiMShowPageGuide"; static const char *QtSLiMPageGuideColumn = "QtSLiMPageGuideColumn"; static const char *QtSLiMHighlightCurrentLine = "QtSLiMHighlightCurrentLine"; static const char *QtSLiMAutosaveOnRecycle = "QtSLiMAutosaveOnRecycle"; static const char *QtSLiMShowSaveInUntitled = "QtSLiMShowSaveInUntitled"; static const char *QtSLiMReloadOnSafeExternalEdits = "QtSLiMReloadOnSafeExternalEdits"; static QFont &defaultDisplayFont(void) { // This function determines the default font chosen when the user has expressed no preference. // It depends upon font availability, so it can't be hard-coded. static QFont *defaultFont = nullptr; if (!defaultFont) { #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QFontDatabase fontdb; QStringList families = fontdb.families(); #else QStringList families = QFontDatabase::families(); #endif // Use filter() to look for matches, since the foundry can be appended after the name (why isn't this easier??) if (families.filter("Consola").size() > 0) // good on Windows defaultFont = new QFont("Consola", 13); else if (families.filter("Courier New").size() > 0) // good on Mac defaultFont = new QFont("Courier New", 13); else if (families.filter("Menlo").size() > 0) // good on Mac defaultFont = new QFont("Menlo", 12); else if (families.filter("Ubuntu Mono").size() > 0) // good on Ubuntu defaultFont = new QFont("Ubuntu Mono", 11); else if (families.filter("DejaVu Sans Mono").size() > 0) // good on Ubuntu defaultFont = new QFont("DejaVu Sans Mono", 9); else defaultFont = new QFont("Courier", 10); // a reasonable default that should be omnipresent } return *defaultFont; } // // QtSLiMPreferencesNotifier: the pref supplier and notifier // QtSLiMPreferencesNotifier &QtSLiMPreferencesNotifier::instance(void) { static QtSLiMPreferencesNotifier *inst = nullptr; if (!inst) inst = new QtSLiMPreferencesNotifier(); return *inst; } // pref value fetching int QtSLiMPreferencesNotifier::appStartupPref(void) const { QSettings settings; return settings.value(QtSLiMAppStartupAction, QVariant(1)).toInt(); } bool QtSLiMPreferencesNotifier::forceDarkModePref(void) { #ifdef __APPLE__ // On macOS this pref is always considered to be false return false; #endif QSettings settings; return settings.value(QtSLiMForceDarkMode, QVariant(false)).toBool(); } bool QtSLiMPreferencesNotifier::forceFusionStylePref(void) { #ifdef __APPLE__ // On macOS this pref is always considered to be false return false; #endif QSettings settings; return settings.value(QtSLiMForceFusionStyle, QVariant(false)).toBool(); } bool QtSLiMPreferencesNotifier::useOpenGLPref(void) { #ifndef SLIM_NO_OPENGL QSettings settings; #ifdef _WIN32 // BCH 3/23/2025: Too many people are getting bitten by OpenGL not working properly on // Windows, for reasons that have not been diagnosed. Turning this off on Windows. // It runs a little slower, but there will be a lot less confusion. return settings.value(QtSLiMUseOpenGL, QVariant(false)).toBool(); #else return settings.value(QtSLiMUseOpenGL, QVariant(true)).toBool(); #endif #else return false; #endif } QFont QtSLiMPreferencesNotifier::displayFontPref(double *tabWidth) const { QFont &defaultFont = defaultDisplayFont(); QString defaultFamily = defaultFont.family(); int defaultSize = defaultFont.pointSize(); QSettings settings; QString fontFamily = settings.value(QtSLiMDisplayFontFamily, QVariant(defaultFamily)).toString(); int fontSize = settings.value(QtSLiMDisplayFontSize, QVariant(defaultSize)).toInt(); QFont font(fontFamily, fontSize); font.setFixedPitch(true); // I think this is a hint to help QFont match to similar fonts? if (tabWidth) { QFontMetricsF fm(font); #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) *tabWidth = fm.width(" "); // deprecated in 5.11 #else *tabWidth = fm.horizontalAdvance(" "); // added in Qt 5.11 #endif } return font; } bool QtSLiMPreferencesNotifier::scriptSyntaxHighlightPref(void) const { QSettings settings; return settings.value(QtSLiMSyntaxHighlightScript, QVariant(true)).toBool(); } bool QtSLiMPreferencesNotifier::outputSyntaxHighlightPref(void) const { QSettings settings; return settings.value(QtSLiMSyntaxHighlightOutput, QVariant(true)).toBool(); } bool QtSLiMPreferencesNotifier::showLineNumbersPref(void) const { QSettings settings; return settings.value(QtSLiMShowLineNumbers, QVariant(true)).toBool(); } bool QtSLiMPreferencesNotifier::highlightCurrentLinePref(void) const { QSettings settings; return settings.value(QtSLiMHighlightCurrentLine, QVariant(true)).toBool(); } bool QtSLiMPreferencesNotifier::showPageGuidePref(void) const { QSettings settings; return settings.value(QtSLiMShowPageGuide, QVariant(false)).toBool(); } int QtSLiMPreferencesNotifier::pageGuideColumnPref(void) const { QSettings settings; return settings.value(QtSLiMPageGuideColumn, QVariant(80)).toInt(); } bool QtSLiMPreferencesNotifier::autosaveOnRecyclePref(void) const { QSettings settings; return settings.value(QtSLiMAutosaveOnRecycle, QVariant(false)).toBool(); } bool QtSLiMPreferencesNotifier::showSaveIfUntitledPref(void) const { QSettings settings; return settings.value(QtSLiMShowSaveInUntitled, QVariant(false)).toBool(); } bool QtSLiMPreferencesNotifier::reloadOnSafeExternalEditsPref(void) const { QSettings settings; return settings.value(QtSLiMReloadOnSafeExternalEdits, QVariant(false)).toBool(); } void QtSLiMPreferencesNotifier::displayFontBigger(void) { QFont &defaultFont = defaultDisplayFont(); int defaultSize = defaultFont.pointSize(); QSettings settings; int fontSize = settings.value(QtSLiMDisplayFontSize, QVariant(defaultSize)).toInt(); if (fontSize < 50) // matches value in QtSLiMPreferences.ui { // if the prefs window exists, we need to tell it to adjust itself // if not, we send ourselves the message it would have sent us QtSLiMPreferences *prefsWindow = QtSLiMPreferences::instanceForcingAllocation(false); if (prefsWindow) prefsWindow->ui->fontSizeSpinBox->setValue(fontSize + 1); else fontSizeChanged(fontSize + 1); } else qApp->beep(); } void QtSLiMPreferencesNotifier::displayFontSmaller(void) { QFont &defaultFont = defaultDisplayFont(); int defaultSize = defaultFont.pointSize(); QSettings settings; int fontSize = settings.value(QtSLiMDisplayFontSize, QVariant(defaultSize)).toInt(); if (fontSize > 6) // matches value in QtSLiMPreferences.ui { // if the prefs window exists, we need to tell it to adjust itself // if not, we send ourselves the message it would have sent us QtSLiMPreferences *prefsWindow = QtSLiMPreferences::instanceForcingAllocation(false); if (prefsWindow) prefsWindow->ui->fontSizeSpinBox->setValue(fontSize - 1); else fontSizeChanged(fontSize - 1); } else qApp->beep(); } // slots; these update the settings and then emit new signals void QtSLiMPreferencesNotifier::startupRadioChanged() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; if (prefsUI.ui->startupRadioCreateNew->isChecked()) settings.setValue(QtSLiMAppStartupAction, QVariant(1)); else if (prefsUI.ui->startupRadioOpenFile->isChecked()) settings.setValue(QtSLiMAppStartupAction, QVariant(2)); emit appStartupPrefChanged(); } void QtSLiMPreferencesNotifier::forceDarkModeToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMForceDarkMode, QVariant(prefsUI.ui->forceDarkMode->isChecked())); // no signal is emitted for this pref; it takes effect on the next restart of the app //emit forceDarkModePrefChanged(); } void QtSLiMPreferencesNotifier::forceFusionStyleToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMForceFusionStyle, QVariant(prefsUI.ui->forceFusionStyle->isChecked())); // no signal is emitted for this pref; it takes effect on the next restart of the app //emit forceFusionStylePrefChanged(); } void QtSLiMPreferencesNotifier::useOpenGLToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMUseOpenGL, QVariant(prefsUI.ui->useOpenGL->isChecked())); emit useOpenGLPrefChanged(); } void QtSLiMPreferencesNotifier::fontChanged(const QFont &newFont) { QString fontFamily = newFont.family(); QSettings settings; settings.setValue(QtSLiMDisplayFontFamily, QVariant(fontFamily)); emit displayFontPrefChanged(); } void QtSLiMPreferencesNotifier::fontSizeChanged(int newSize) { QSettings settings; settings.setValue(QtSLiMDisplayFontSize, QVariant(newSize)); emit displayFontPrefChanged(); } void QtSLiMPreferencesNotifier::syntaxHighlightScriptToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMSyntaxHighlightScript, QVariant(prefsUI.ui->syntaxHighlightScript->isChecked())); emit scriptSyntaxHighlightPrefChanged(); } void QtSLiMPreferencesNotifier::syntaxHighlightOutputToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMSyntaxHighlightOutput, QVariant(prefsUI.ui->syntaxHighlightOutput->isChecked())); emit outputSyntaxHighlightPrefChanged(); } void QtSLiMPreferencesNotifier::showLineNumbersToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMShowLineNumbers, QVariant(prefsUI.ui->showLineNumbers->isChecked())); emit showLineNumbersPrefChanged(); } void QtSLiMPreferencesNotifier::highlightCurrentLineToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMHighlightCurrentLine, QVariant(prefsUI.ui->highlightCurrentLine->isChecked())); emit highlightCurrentLinePrefChanged(); } void QtSLiMPreferencesNotifier::showPageGuideToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMShowPageGuide, QVariant(prefsUI.ui->showPageGuide->isChecked())); emit pageGuidePrefsChanged(); } void QtSLiMPreferencesNotifier::pageGuideColumnChanged(int newColumn) { QSettings settings; settings.setValue(QtSLiMPageGuideColumn, QVariant(newColumn)); emit pageGuidePrefsChanged(); } void QtSLiMPreferencesNotifier::autosaveOnRecycleToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMAutosaveOnRecycle, QVariant(prefsUI.ui->autosaveOnRecycle->isChecked())); emit autosaveOnRecyclePrefChanged(); } void QtSLiMPreferencesNotifier::showSaveIfUntitledToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMShowSaveInUntitled, QVariant(prefsUI.ui->showSaveIfUntitled->isChecked())); emit showSaveIfUntitledPrefChanged(); } void QtSLiMPreferencesNotifier::reloadOnSafeExternalEditsToggled() { QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); QSettings settings; settings.setValue(QtSLiMReloadOnSafeExternalEdits, QVariant(prefsUI.ui->reloadOnSafeExternalEdits->isChecked())); emit reloadOnSafeExternalEditsChanged(); } void QtSLiMPreferencesNotifier::resetSuppressedClicked() { // All "do not show this again" settings should be removed here // There is no signal rebroadcast for this; nobody should cache these flags QSettings settings; settings.remove("QtSLiMSuppressScriptCheckSuccessPanel"); } // // QtSLiMPreferences: the actual UI class // QtSLiMPreferences *QtSLiMPreferences::instanceForcingAllocation(bool force_allocation) { static QtSLiMPreferences *inst = nullptr; if (!inst && force_allocation) inst = new QtSLiMPreferences(nullptr); return inst; } QtSLiMPreferences &QtSLiMPreferences::instance(void) { return *QtSLiMPreferences::instanceForcingAllocation(true); } QtSLiMPreferences::QtSLiMPreferences(QWidget *p_parent) : QDialog(p_parent), ui(new Ui::QtSLiMPreferences) { ui->setupUi(this); // no window icon #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect setWindowIcon(QIcon()); #endif // prevent this window from keeping the app running when all main windows are closed setAttribute(Qt::WA_QuitOnClose, false); // set the initial state of the UI elements from QtSLiMPreferencesNotifier QtSLiMPreferencesNotifier *notifier = &QtSLiMPreferencesNotifier::instance(); ui->startupRadioCreateNew->setChecked(notifier->appStartupPref() == 1); ui->startupRadioOpenFile->setChecked(notifier->appStartupPref() == 2); ui->fontComboBox->setCurrentFont(notifier->displayFontPref()); ui->fontSizeSpinBox->setValue(notifier->displayFontPref().pointSize()); ui->syntaxHighlightScript->setChecked(notifier->scriptSyntaxHighlightPref()); ui->syntaxHighlightOutput->setChecked(notifier->outputSyntaxHighlightPref()); ui->showLineNumbers->setChecked(notifier->showLineNumbersPref()); ui->highlightCurrentLine->setChecked(notifier->highlightCurrentLinePref()); // the presence of this hidden widget fixes a padding bug; see https://forum.qt.io/topic/10757/unwanted-padding-around-qhboxlayout ui->pageGuideNoPadWidget->hide(); ui->showPageGuide->setChecked(notifier->showPageGuidePref()); ui->pageGuideSpinBox->setValue(notifier->pageGuideColumnPref()); ui->autosaveOnRecycle->setChecked(notifier->autosaveOnRecyclePref()); ui->showSaveIfUntitled->setChecked(notifier->showSaveIfUntitledPref()); ui->showSaveIfUntitled->setEnabled(notifier->autosaveOnRecyclePref()); ui->reloadOnSafeExternalEdits->setChecked(notifier->reloadOnSafeExternalEditsPref()); // connect the UI elements to QtSLiMPreferencesNotifier connect(ui->startupRadioOpenFile, &QRadioButton::toggled, notifier, &QtSLiMPreferencesNotifier::startupRadioChanged); connect(ui->startupRadioCreateNew, &QRadioButton::toggled, notifier, &QtSLiMPreferencesNotifier::startupRadioChanged); connect(ui->fontComboBox, &QFontComboBox::currentFontChanged, notifier, &QtSLiMPreferencesNotifier::fontChanged); connect(ui->fontSizeSpinBox, QOverload::of(&QSpinBox::valueChanged), notifier, &QtSLiMPreferencesNotifier::fontSizeChanged); connect(ui->syntaxHighlightScript, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::syntaxHighlightScriptToggled); connect(ui->syntaxHighlightOutput, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::syntaxHighlightOutputToggled); connect(ui->showLineNumbers, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::showLineNumbersToggled); connect(ui->highlightCurrentLine, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::highlightCurrentLineToggled); connect(ui->showPageGuide, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::showPageGuideToggled); connect(ui->pageGuideSpinBox, QOverload::of(&QSpinBox::valueChanged), notifier, &QtSLiMPreferencesNotifier::pageGuideColumnChanged); connect(ui->autosaveOnRecycle, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::autosaveOnRecycleToggled); connect(ui->showSaveIfUntitled, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::showSaveIfUntitledToggled); connect(notifier, &QtSLiMPreferencesNotifier::autosaveOnRecyclePrefChanged, this, [this, notifier]() { ui->showSaveIfUntitled->setEnabled(notifier->autosaveOnRecyclePref()); }); connect(ui->reloadOnSafeExternalEdits, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::reloadOnSafeExternalEditsToggled); connect(ui->resetSuppressedButton, &QPushButton::clicked, notifier, &QtSLiMPreferencesNotifier::resetSuppressedClicked); // handle the user interface display prefs, which are hidden and disconnected on macOS ui->useOpenGL->setChecked(notifier->useOpenGLPref()); connect(ui->useOpenGL, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::useOpenGLToggled); #ifdef __APPLE__ // This old code hid the UI prefs entirely on macOS // ui->uiAppearanceGroup->setHidden(true); // ui->verticalSpacer_uiAppearance->changeSize(0, 0); // ui->verticalSpacer_uiAppearance->invalidate(); // ui->verticalLayout->invalidate(); // This new code leaves the "Use OpenGL for speed" checkbox visible and hides the rest ui->requireRelaunchLabel->setHidden(true); ui->forceDarkMode->setHidden(true); ui->forceFusionStyle->setHidden(true); ui->verticalSpacer_requireRelaunch->changeSize(0, 0); ui->verticalSpacer_requireRelaunch->invalidate(); ui->verticalLayout->invalidate(); #else ui->forceDarkMode->setChecked(notifier->forceDarkModePref()); ui->forceFusionStyle->setChecked(notifier->forceFusionStylePref()); connect(ui->forceDarkMode, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::forceDarkModeToggled); connect(ui->forceFusionStyle, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::forceFusionStyleToggled); #endif // make window actions for all global menu items qtSLiMAppDelegate->addActionsForGlobalMenuItems(this); } QtSLiMPreferences::~QtSLiMPreferences() { delete ui; } ================================================ FILE: QtSLiM/QtSLiMPreferences.h ================================================ // // QtSLiMPreferences.h // SLiM // // Created by Ben Haller on 8/3/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMPREFERENCES_H #define QTSLIMPREFERENCES_H #include // This class provides a singleton object that interested parties can connect to // This separated design allows clients to connect before the prefs panel exists class QtSLiMPreferencesNotifier : public QObject { Q_OBJECT public: static QtSLiMPreferencesNotifier &instance(void); // Get the current pref values, falling back on defaults int appStartupPref(void) const; // 0 == do nothing, 1 == create a new window, 2 == run an open panel bool forceDarkModePref(void); bool forceFusionStylePref(void); bool useOpenGLPref(void); QFont displayFontPref(double *tabWidth = nullptr) const; bool scriptSyntaxHighlightPref(void) const; bool outputSyntaxHighlightPref(void) const; bool showLineNumbersPref(void) const; bool showPageGuidePref(void) const; int pageGuideColumnPref(void) const; bool highlightCurrentLinePref(void) const; bool autosaveOnRecyclePref(void) const; bool reloadOnSafeExternalEditsPref(void) const; bool showSaveIfUntitledPref(void) const; // Change preferences values in ways other than the Preferences panel itself void displayFontBigger(void); void displayFontSmaller(void); signals: // Get notified when a pref value changes void appStartupPrefChanged(void); void useOpenGLPrefChanged(void); void displayFontPrefChanged(void); void scriptSyntaxHighlightPrefChanged(void); void outputSyntaxHighlightPrefChanged(void); void showLineNumbersPrefChanged(void); void pageGuidePrefsChanged(void); void highlightCurrentLinePrefChanged(void); void autosaveOnRecyclePrefChanged(void); void reloadOnSafeExternalEditsChanged(void); void showSaveIfUntitledPrefChanged(void); private: // singleton pattern QtSLiMPreferencesNotifier() = default; ~QtSLiMPreferencesNotifier() = default; QtSLiMPreferencesNotifier(const QtSLiMPreferencesNotifier&) = delete; QtSLiMPreferencesNotifier& operator=(const QtSLiMPreferencesNotifier&) = delete; private slots: void startupRadioChanged(); void forceDarkModeToggled(); void forceFusionStyleToggled(); void useOpenGLToggled(); void fontChanged(const QFont &font); void fontSizeChanged(int newSize); void syntaxHighlightScriptToggled(); void syntaxHighlightOutputToggled(); void showLineNumbersToggled(); void showPageGuideToggled(); void pageGuideColumnChanged(int newColumn); void highlightCurrentLineToggled(); void autosaveOnRecycleToggled(); void reloadOnSafeExternalEditsToggled(); void showSaveIfUntitledToggled(); void resetSuppressedClicked(); friend class QtSLiMPreferences; }; // This is the actual UI stuff namespace Ui { class QtSLiMPreferences; } class QtSLiMPreferences : public QDialog { Q_OBJECT public: static QtSLiMPreferences *instanceForcingAllocation(bool force_allocation); static QtSLiMPreferences &instance(void); private: // singleton pattern explicit QtSLiMPreferences(QWidget *p_parent = nullptr); QtSLiMPreferences() = default; ~QtSLiMPreferences(); QtSLiMPreferences(const QtSLiMPreferencesNotifier&) = delete; QtSLiMPreferences& operator=(const QtSLiMPreferencesNotifier&) = delete; private: Ui::QtSLiMPreferences *ui; friend class QtSLiMPreferencesNotifier; }; #endif // QTSLIMPREFERENCES_H ================================================ FILE: QtSLiM/QtSLiMPreferences.ui ================================================ QtSLiMPreferences 0 0 303 780 SLiMgui Preferences 6 QLayout::SetFixedSize 12 Qt::Vertical QSizePolicy::Fixed 20 8 0 0 QGroupBox { font-weight: bold; } When SLiMgui starts: 6 6 6 6 6 a new untitled window with a default WF model will be created on startup Create a new simulation window true the "open file" panel will be run at startup Ask for a script file to open Qt::Vertical QSizePolicy::Fixed 20 8 0 0 QGroupBox { font-weight: bold; } User interface appearance: 6 6 6 6 6 <html><head/><body><p>use OpenGL for fast display; uncheck this if you experience display problems with the Individuals and Chromosome views</p></body></html> Use OpenGL for fast display Qt::Vertical QSizePolicy::Fixed 20 8 <html><head/><body><p><span style=" font-size:11pt; font-style:italic;">Changes below require a relaunch to take effect.</span></p></body></html> force a "dark mode" color palette for the user interface appearance Force a "dark mode" palette <html><head/><body><p>force the use of Qt's Fusion user interface style, which can make the &quot;dark mode&quot; appearance more consistent</p></body></html> Force the Fusion UI style Qt::Vertical QSizePolicy::Fixed 20 8 QGroupBox { font-weight: bold; } Display font: 6 6 6 6 6 Qt::NoFocus the font to use for script and output views false QFontDatabase::Latin QFontComboBox::MonospacedFonts|QFontComboBox::ScalableFonts 0 0 50 0 50 16777215 Qt::StrongFocus the font size to use for script and output views Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 6 50 12 0 0 points Qt::Vertical QSizePolicy::Fixed 20 8 0 0 QGroupBox { font-weight: bold; } Enable syntax highlighting: 6 6 6 6 6 enabled syntax highlighting in script views; this may cause poor performance with long scripts In the script area true enabled syntax highlighting in output views; this may cause poor performance with large amounts of output In the output area true Qt::Vertical QSizePolicy::Fixed 20 8 0 0 QGroupBox { font-weight: bold; } Script view appearance: 6 6 6 6 6 show line numbers in a gutter on the left of script views Show line numbers highlight the current editing line in script views Highlight current line Page guide at column: 55 0 Qt::StrongFocus Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 999 80 Qt::Horizontal 5 5 Qt::Vertical QSizePolicy::Fixed 20 8 0 0 QGroupBox { font-weight: bold; } Autosave: 6 6 6 6 6 save to disk automatically whenever the model is recycled Autosave on every recycle if autosave is enabled, run a "save file" panel if the model has not yet been saved Show save panel if untitled Qt::Vertical QSizePolicy::Fixed 20 8 0 0 QGroupBox { font-weight: bold; } External editing: 6 6 6 6 6 reload externally edited files without user confirmation, if no unsaved changes in SLiMgui would be overwritten Reload changes automatically <html><head/><body><p><span style=" font-size:11pt; font-style:italic;">(only if there are no unsaved changes)</span></p></body></html> Qt::Horizontal Qt::Vertical QSizePolicy::Fixed 20 8 Reset suppressed alert panels false 0 0 <html><head/><body><p align="center"><span style=" font-size:11pt; font-style:italic;">This resets all of the &quot;do not show this<br/>message again&quot; flags across SLiMgui.</span></p></body></html> ================================================ FILE: QtSLiM/QtSLiMScriptTextEdit.cpp ================================================ // // QtSLiMScriptTextEdit.cpp // SLiM // // Created by Ben Haller on 11/24/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMScriptTextEdit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "QtSLiMPreferences.h" #include "QtSLiMEidosPrettyprinter.h" #include "QtSLiMSyntaxHighlighting.h" #include "QtSLiMEidosConsole.h" #include "QtSLiMHelpWindow.h" #include "QtSLiMAppDelegate.h" #include "QtSLiMWindow.h" #include "QtSLiMExtras.h" #include "QtSLiM_SLiMgui.h" #include "eidos_script.h" #include "eidos_token.h" #include "slim_eidos_block.h" #include "subpopulation.h" #include "eidos_interpreter.h" #include "interaction_type.h" #include "eidos_sorting.h" // // QtSLiMTextEdit // QtSLiMTextEdit::QtSLiMTextEdit(const QString &text, QWidget *p_parent) : QPlainTextEdit(text, p_parent) { selfInit(); } QtSLiMTextEdit::QtSLiMTextEdit(QWidget *p_parent) : QPlainTextEdit(p_parent) { selfInit(); } void QtSLiMTextEdit::selfInit(void) { // track changes to undo/redo availability connect(this, &QPlainTextEdit::undoAvailable, this, [this](bool b) { undoAvailable_ = b; }); connect(this, &QPlainTextEdit::redoAvailable, this, [this](bool b) { redoAvailable_ = b; }); connect(this, &QPlainTextEdit::copyAvailable, this, [this](bool b) { copyAvailable_ = b; }); // clear the custom error background color whenever the selection changes connect(this, &QPlainTextEdit::selectionChanged, this, [this]() { setPalette(qtslimStandardPalette()); }); connect(this, &QPlainTextEdit::cursorPositionChanged, this, [this]() { setPalette(qtslimStandardPalette()); }); // because we mess with the palette, we have to reset it on dark mode changes; this resets the error // highlighting if it is set up, which is a bug, but not one worth worrying about I suppose... connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, [this]() { setPalette(qtslimStandardPalette()); }); // clear the status bar on a selection change connect(this, &QPlainTextEdit::selectionChanged, this, &QtSLiMTextEdit::updateStatusFieldFromSelection); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &QtSLiMTextEdit::updateStatusFieldFromSelection); // BCH 28 August 2025: get rid of the annoying insertion point remnant shown briefly when the selection changes // this is a dangerous change since it could, if there is a bug somewhere, make the insertion point disappear! connect(this, &QPlainTextEdit::selectionChanged, this, [this]() { if (textCursor().hasSelection()) setCursorWidth(0); else setCursorWidth(1); }); connect(this, &QPlainTextEdit::cursorPositionChanged, this, [this]() { if (textCursor().hasSelection()) setCursorWidth(0); else setCursorWidth(1); }); // Wire up to change the font when the display font pref changes QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::displayFontPrefChanged, this, &QtSLiMTextEdit::displayFontPrefChanged); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::scriptSyntaxHighlightPrefChanged, this, &QtSLiMTextEdit::scriptSyntaxHighlightPrefChanged); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::outputSyntaxHighlightPrefChanged, this, &QtSLiMTextEdit::outputSyntaxHighlightPrefChanged); // Get notified of modifier key changes, so we can change our cursor connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::modifiersChanged, this, &QtSLiMTextEdit::modifiersChanged); // set up tab stops based on the display font QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); double tabWidth = 0; QFont scriptFont = prefs.displayFontPref(&tabWidth); setFont(scriptFont); #if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) setTabStopWidth((int)floor(tabWidth)); // deprecated in 5.10 #else setTabStopDistance(tabWidth); // added in 5.10 #endif } QtSLiMTextEdit::~QtSLiMTextEdit() { } void QtSLiMTextEdit::setScriptType(ScriptType type) { // Configure our script type; this should be called once, early scriptType = type; } void QtSLiMTextEdit::setSyntaxHighlightType(ScriptHighlightingType type) { // Configure our syntax highlighting; this should be called once, early syntaxHighlightingType = type; scriptSyntaxHighlightPrefChanged(); // create a highlighter if needed outputSyntaxHighlightPrefChanged(); // create a highlighter if needed } void QtSLiMTextEdit::setOptionClickEnabled(bool enabled) { optionClickEnabled = enabled; optionClickIntercepted = false; } void QtSLiMTextEdit::displayFontPrefChanged() { QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); double tabWidth = 0; QFont displayFont = prefs.displayFontPref(&tabWidth); setFont(displayFont); #if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) setTabStopWidth((int)floor(tabWidth)); // deprecated in 5.10 #else setTabStopDistance(tabWidth); // added in 5.10 #endif } void QtSLiMTextEdit::scriptSyntaxHighlightPrefChanged() { if (syntaxHighlightingType == QtSLiMTextEdit::ScriptHighlighting) { QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); bool highlightPref = prefs.scriptSyntaxHighlightPref(); if (highlightPref && !scriptHighlighter) { scriptHighlighter = new QtSLiMScriptHighlighter(document()); } else if (!highlightPref && scriptHighlighter) { scriptHighlighter->setDocument(nullptr); scriptHighlighter->setParent(nullptr); delete scriptHighlighter; scriptHighlighter = nullptr; } } } void QtSLiMTextEdit::outputSyntaxHighlightPrefChanged() { if (syntaxHighlightingType == QtSLiMTextEdit::OutputHighlighting) { QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); bool highlightPref = prefs.outputSyntaxHighlightPref(); if (highlightPref && !outputHighlighter) { outputHighlighter = new QtSLiMOutputHighlighter(document()); } else if (!highlightPref && outputHighlighter) { outputHighlighter->setDocument(nullptr); outputHighlighter->setParent(nullptr); delete outputHighlighter; outputHighlighter = nullptr; } } } void QtSLiMTextEdit::highlightError(int startPosition, int endPosition) { QTextCursor highlight_cursor(document()); highlight_cursor.setPosition(startPosition); highlight_cursor.setPosition(endPosition, QTextCursor::KeepAnchor); setTextCursor(highlight_cursor); centerCursor(); setPalette(qtslimErrorPalette()); // note that this custom selection color is cleared by a connection to QPlainTextEdit::selectionChanged() } void QtSLiMTextEdit::selectErrorRange(EidosErrorContext &errorContext) { // If there is error-tracking information set, and the error is attributed to the user script, // then we can highlight the error range if ((!gEidosErrorContext.currentScript || (gEidosErrorContext.currentScript->UserScriptUTF16Offset() == 0)) && (errorContext.errorPosition.characterStartOfErrorUTF16 >= 0) && (errorContext.errorPosition.characterEndOfErrorUTF16 >= errorContext.errorPosition.characterStartOfErrorUTF16)) { highlightError(errorContext.errorPosition.characterStartOfErrorUTF16, errorContext.errorPosition.characterEndOfErrorUTF16 + 1); } } QPalette QtSLiMTextEdit::qtslimStandardPalette(void) { // Returns the standard palette for QtSLiMTextEdit, which could depend on platform and dark mode return qApp->palette(this); } QPalette QtSLiMTextEdit::qtslimErrorPalette(void) { // Returns a palette for QtSLiMTextEdit for highlighting errors, which could depend on platform and dark mode // Note that this is based on the current palette, and derives only the highlight colors QPalette p = palette(); p.setColor(QPalette::Highlight, QColor(QColor(Qt::red).lighter(120))); p.setColor(QPalette::HighlightedText, QColor(Qt::black)); return p; } QStatusBar *QtSLiMTextEdit::statusBarForWindow(void) { // This is a bit of a hack because the console window is not a MainWindow subclass, and makes its own status bar QWidget *ourWindow = window(); QMainWindow *mainWindow = dynamic_cast(ourWindow); QtSLiMEidosConsole *consoleWindow = dynamic_cast(ourWindow); QStatusBar *statusBar = nullptr; if (mainWindow) statusBar = mainWindow->statusBar(); else if (consoleWindow) statusBar = consoleWindow->statusBar(); return statusBar; } QtSLiMWindow *QtSLiMTextEdit::slimControllerForWindow(void) { QtSLiMWindow *windowSLiMController = dynamic_cast(window()); if (windowSLiMController) return windowSLiMController; QtSLiMEidosConsole *windowEidosConsole = dynamic_cast(window()); if (windowEidosConsole) return windowEidosConsole->parentSLiMWindow; return nullptr; } QtSLiMEidosConsole *QtSLiMTextEdit::slimEidosConsoleForWindow(void) { QtSLiMEidosConsole *windowEidosConsole = dynamic_cast(window()); if (windowEidosConsole) return windowEidosConsole; return nullptr; } bool QtSLiMTextEdit::checkScriptSuppressSuccessResponse(bool suppressSuccessResponse) { // Note this does *not* check out scriptString, which represents the state of the script when the Community object was created // Instead, it checks the current script in the script TextView – which is not used for anything until the recycle button is clicked. QByteArray utf8bytes = toPlainText().toUtf8(); const char *cstr = utf8bytes.constData(); std::string errorDiagnostic; if (!cstr) { errorDiagnostic = "The script string could not be read, possibly due to an encoding problem."; } else { if (scriptType == EidosScriptType) { EidosScript script(cstr); try { script.Tokenize(); script.ParseInterpreterBlockToAST(true); } catch (...) { errorDiagnostic = Eidos_GetTrimmedRaiseMessage(); } } else if (scriptType == SLiMScriptType) { SLiMEidosScript script(cstr); try { script.Tokenize(); script.ParseSLiMFileToAST(); } catch (...) { errorDiagnostic = Eidos_GetTrimmedRaiseMessage(); } } else { qDebug() << "checkScriptSuppressSuccessResponse() called with no script type set"; } } bool checkDidSucceed = !(errorDiagnostic.length()); if (!checkDidSucceed || !suppressSuccessResponse) { if (!checkDidSucceed) { // On failure, we show an alert describing the error, and highlight the relevant script line qApp->beep(); EidosErrorContext errorContext = gEidosErrorContext; ClearErrorContext(); selectErrorRange(errorContext); QString q_errorDiagnostic = QString::fromStdString(errorDiagnostic); QMessageBox messageBox(this); messageBox.setText("Script error"); messageBox.setInformativeText(q_errorDiagnostic); messageBox.setIcon(QMessageBox::Warning); // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::ApplicationModal // seems necessary so floating windows can't be on top of the message box messageBox.setWindowModality(Qt::ApplicationModal); messageBox.setFixedWidth(700); // seems to be ignored messageBox.exec(); // Show the error in the status bar also QStatusBar *statusBar = statusBarForWindow(); if (statusBar) statusBar->showMessage("" + q_errorDiagnostic.trimmed().toHtmlEscaped() + ""); } else { QSettings settings; if (!settings.value("QtSLiMSuppressScriptCheckSuccessPanel", false).toBool()) { // In SLiMgui we play a "success" sound too, but doing anything besides beeping is apparently difficult with Qt... QMessageBox messageBox(this); messageBox.setText("No script errors"); messageBox.setInformativeText("No errors found."); messageBox.setIcon(QMessageBox::Information); // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::ApplicationModal // seems necessary so floating windows can't be on top of the message box messageBox.setWindowModality(Qt::ApplicationModal); messageBox.setCheckBox(new QCheckBox("Do not show this message again", nullptr)); messageBox.exec(); if (messageBox.checkBox()->isChecked()) settings.setValue("QtSLiMSuppressScriptCheckSuccessPanel", true); } } } return checkDidSucceed; } void QtSLiMTextEdit::checkScript(void) { checkScriptSuppressSuccessResponse(false); } void QtSLiMTextEdit::_prettyprint_reformat(bool p_reformat) { if (isEnabled()) { if (checkScriptSuppressSuccessResponse(true)) { // We know the script is syntactically correct, so we can tokenize and parse it without worries QByteArray utf8bytes = toPlainText().toUtf8(); const char *cstr = utf8bytes.constData(); EidosScript script(cstr); script.Tokenize(false, true); // get whitespace and comment tokens // Then generate a new script string that is prettyprinted const std::vector &tokens = script.Tokens(); std::string pretty; bool success = false; if (p_reformat) success = Eidos_reformatTokensFromScript(tokens, script, pretty); else success = Eidos_prettyprintTokensFromScript(tokens, script, pretty); if (success) { // We want to replace our text in a way that is undoable; to do this, we use the text cursor QString replacementString = QString::fromStdString(pretty); QTextCursor &&replacement_cursor = textCursor(); replacement_cursor.beginEditBlock(); replacement_cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); replacement_cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); replacement_cursor.insertText(replacementString); replacement_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); replacement_cursor.endEditBlock(); setTextCursor(replacement_cursor); } else qApp->beep(); } } else { qApp->beep(); } } void QtSLiMTextEdit::prettyprint(void) { _prettyprint_reformat(false); } void QtSLiMTextEdit::reformat(void) { _prettyprint_reformat(true); } void QtSLiMTextEdit::prettyprintClicked(void) { // Get the option-key state; if it is pressed, we do a full reformat bool optionPressed = QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier); if (optionPressed) reformat(); else prettyprint(); } void QtSLiMTextEdit::scriptHelpOptionClick(QString searchString) { QtSLiMHelpWindow &helpWindow = QtSLiMHelpWindow::instance(); // A few Eidos substitutions to improve the search if (searchString == ":") searchString = "operator :"; else if (searchString == "(") searchString = "operator ()"; else if (searchString == ")") searchString = "operator ()"; else if (searchString == ",") searchString = "calls: operator ()"; else if (searchString == "[") searchString = "operator []"; else if (searchString == "]") searchString = "operator []"; else if (searchString == "{") searchString = "compound statements"; else if (searchString == "}") searchString = "compound statements"; else if (searchString == ".") searchString = "operator ."; else if (searchString == "=") searchString = "operator ="; else if (searchString == "+") searchString = "Arithmetic operators"; else if (searchString == "-") searchString = "Arithmetic operators"; else if (searchString == "*") searchString = "Arithmetic operators"; else if (searchString == "/") searchString = "Arithmetic operators"; else if (searchString == "%") searchString = "Arithmetic operators"; else if (searchString == "^") searchString = "Arithmetic operators"; else if (searchString == "|") searchString = "Logical operators"; else if (searchString == "&") searchString = "Logical operators"; else if (searchString == "!") searchString = "Logical operators"; else if (searchString == "==") searchString = "Comparative operators"; else if (searchString == "!=") searchString = "Comparative operators"; else if (searchString == "<=") searchString = "Comparative operators"; else if (searchString == ">=") searchString = "Comparative operators"; else if (searchString == "<") searchString = "Comparative operators"; else if (searchString == ">") searchString = "Comparative operators"; else if (searchString == "'") searchString = "type string"; else if (searchString == "\"") searchString = "type string"; else if (searchString == ";") searchString = "null statements"; else if (searchString == "//") searchString = "comments"; else if (searchString == "if") searchString = "if and if–else statements"; else if (searchString == "else") searchString = "if and if–else statements"; else if (searchString == "do") searchString = "do–while statements"; else if (searchString == "while") searchString = "while statements"; // this brings up both while and do-while statements, correctly else if (searchString == "for") searchString = "for statements"; else if (searchString == "in") searchString = "for statements"; else if (searchString == "next") searchString = "next statements"; else if (searchString == "break") searchString = "break statements"; else if (searchString == "return") searchString = "return statements"; else if (searchString == "function") searchString = "user-defined functions"; // and SLiM substitutions; "initialize" is deliberately omitted here so that the initialize...() methods also come up else if (searchString == "first") searchString = "Eidos events"; else if (searchString == "early") searchString = "Eidos events"; else if (searchString == "late") searchString = "Eidos events"; else if (searchString == "mutationEffect") searchString = "mutationEffect() callbacks"; else if (searchString == "fitnessEffect") searchString = "fitnessEffect() callbacks"; else if (searchString == "interaction") searchString = "interaction() callbacks"; else if (searchString == "mateChoice") searchString = "mateChoice() callbacks"; else if (searchString == "modifyChild") searchString = "modifyChild() callbacks"; else if (searchString == "recombination") searchString = "recombination() callbacks"; else if (searchString == "mutation") searchString = "mutation() callbacks"; else if (searchString == "survival") searchString = "survival() callbacks"; else if (searchString == "reproduction") searchString = "reproduction() callbacks"; // now send the search string on to the help window helpWindow.enterSearchForString(searchString, true); } void QtSLiMTextEdit::mousePressEvent(QMouseEvent *p_event) { bool optionPressed = (optionClickEnabled && QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier)); if (optionPressed) { // option-click gets intercepted to bring up help optionClickIntercepted = true; // get the position of the character clicked on; note that cursorForPosition() // returns the closest cursor position *between* characters, not which character // was actually clicked on, so we try to compensate here by fudging the position // leftward by half a character width; we used to use hitTest() for this purpose, // but for QPlainTextEdit hitTest() always returns -1 for some reason. const QFont &displayFont = QtSLiMPreferencesNotifier::instance().displayFontPref(nullptr); QFontMetricsF fm(displayFont); int fudgeFactor; #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) fudgeFactor = std::round(fm.width(" ") / 2.0) + 1; // deprecated in 5.11 #else fudgeFactor = std::round(fm.horizontalAdvance(" ") / 2.0) + 1; // added in Qt 5.11 #endif #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPoint localPos = p_event->localPos().toPoint(); #else QPoint localPos = p_event->position().toPoint(); #endif QPoint fudgedPoint(std::max(0, localPos.x() - fudgeFactor), localPos.y()); int characterPositionClicked = cursorForPosition(fudgedPoint).position(); //qDebug() << "localPos ==" << localPos << ", characterPositionClicked ==" << characterPositionClicked; if (characterPositionClicked == -1) // occurs if you click between lines of text return; QTextCursor charCursor(document()); charCursor.setPosition(characterPositionClicked, QTextCursor::MoveAnchor); charCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); QString characterString = charCursor.selectedText(); if (characterString.length() != 1) // not sure if this ever happens, being safe return; QChar character = characterString.at(0); if (character.isSpace()) // no help on whitespace return; //qDebug() << characterPositionClicked << ": " << charCursor.anchor() << "," << charCursor.position() << "," << charCursor.selectedText(); // if the character is a letter or number, we want to select the word it // is contained by and use that as the symbol for lookup; otherwise, // it is symbolic, and we want to try to match the right symbol in the code QTextCursor symbolCursor(charCursor); if (character.isLetterOrNumber()) { // start at the anchor and find the encompassing word symbolCursor.setPosition(symbolCursor.anchor(), QTextCursor::MoveAnchor); symbolCursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); symbolCursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); } else if ((character == '/') || (character == '=') || (character == '<') || (character == '>') || (character == '!')) { // the character clicked might be part of a multicharacter symbol: // == <= >= != // we will look at two-character groups anchored in the clicked character to test this QTextCursor leftPairCursor(document()), rightPairCursor(document()); leftPairCursor.setPosition(characterPositionClicked - 1, QTextCursor::MoveAnchor); leftPairCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 2); rightPairCursor.setPosition(characterPositionClicked, QTextCursor::MoveAnchor); rightPairCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 2); QString leftPairString = leftPairCursor.selectedText(), rightPairString = rightPairCursor.selectedText(); if ((leftPairString == "//") || (leftPairString == "==") || (leftPairString == "<=") || (leftPairString == ">=") || (leftPairString == "!=")) symbolCursor = leftPairCursor; else if ((rightPairString == "//") || (rightPairString == "==") || (rightPairString == "<=") || (rightPairString == ">=") || (rightPairString == "!=")) symbolCursor = rightPairCursor; // else we drop through and search for the one-character symbol } else { // the character clicked is a one-character symbol; we just drop through } // select the symbol and trigger a lookup QString symbol = symbolCursor.selectedText(); if (symbol.length()) { setTextCursor(symbolCursor); scriptHelpOptionClick(symbol); } } else { // all other cases go to super optionClickIntercepted = false; QPlainTextEdit::mousePressEvent(p_event); } } void QtSLiMTextEdit::mouseMoveEvent(QMouseEvent *p_event) { // forward to super, as long as we did not intercept this mouse event if (!optionClickIntercepted) QPlainTextEdit::mouseMoveEvent(p_event); } void QtSLiMTextEdit::mouseReleaseEvent(QMouseEvent *p_event) { // forward to super, as long as we did not intercept this mouse event if (!optionClickIntercepted) QPlainTextEdit::mouseReleaseEvent(p_event); optionClickIntercepted = false; } void QtSLiMTextEdit::fixMouseCursor(void) { if (optionClickEnabled) { // we want a pointing hand cursor when option is pressed; if the cursor is wrong, fix it // note the cursor for QPlainTextEdit is apparently controlled by its viewport bool optionPressed = QGuiApplication::queryKeyboardModifiers().testFlag(Qt::AltModifier); QWidget *vp = viewport(); if (optionPressed && (vp->cursor().shape() != Qt::PointingHandCursor)) vp->setCursor(Qt::PointingHandCursor); else if (!optionPressed && (vp->cursor().shape() != Qt::IBeamCursor)) vp->setCursor(Qt::IBeamCursor); } } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void QtSLiMTextEdit::enterEvent(QEnterEvent *p_event) { // forward to super QPlainTextEdit::enterEvent(p_event); // modifiersChanged() generally keeps our cursor correct, but we do it on enterEvent // as well just as a fallback; for example, if the mouse is inside us on launch and // the modifier is already down, enterEvent() will fix out initial cursor fixMouseCursor(); } #else void QtSLiMTextEdit::enterEvent(QEvent *p_event) { // forward to super QPlainTextEdit::enterEvent(p_event); // modifiersChanged() generally keeps our cursor correct, but we do it on enterEvent // as well just as a fallback; for example, if the mouse is inside us on launch and // the modifier is already down, enterEvent() will fix out initial cursor fixMouseCursor(); } #endif void QtSLiMTextEdit::modifiersChanged(Qt::KeyboardModifiers __attribute__((unused)) newModifiers) { // keyPressEvent() and keyReleaseEvent() are sent to us only when we have the focus, but // we want to change our cursor even when we don't have focus, so we use an event filter fixMouseCursor(); } EidosFunctionSignature_CSP QtSLiMTextEdit::signatureForFunctionName(QString callName, EidosFunctionMap *functionMapPtr) { std::string call_name = callName.toStdString(); // Look for a matching function signature for the call name. for (const auto& function_iter : *functionMapPtr) { const EidosFunctionSignature_CSP &sig = function_iter.second; const std::string &sig_call_name = sig->call_name_; if (sig_call_name.compare(call_name) == 0) return sig; } return nullptr; } EidosMethodSignature_CSP QtSLiMTextEdit::signatureForMethodName(QString callName) { std::string call_name = callName.toStdString(); // Look for a matching method signature for the call name. const std::vector methodSignatures = EidosClass::RegisteredClassMethods(true, true); for (const EidosMethodSignature_CSP &sig : methodSignatures) { const std::string &sig_call_name = sig->call_name_; if (sig_call_name.compare(call_name) == 0) return sig; } return nullptr; } //- (EidosFunctionMap *)functionMapForScriptString:(NSString *)scriptString includingOptionalFunctions:(BOOL)includingOptionalFunctions EidosFunctionMap *QtSLiMTextEdit::functionMapForScriptString(QString scriptString, bool includingOptionalFunctions) { // This returns a function map (owned by the caller) that reflects the best guess we can make, incorporating // any functions known to our delegate, as well as all functions we can scrape from the script string. std::string script_string = scriptString.toStdString(); EidosScript script(script_string); // Tokenize script.Tokenize(true, false); // make bad tokens as needed, don't keep nonsignificant tokens return functionMapForTokenizedScript(script, includingOptionalFunctions); } EidosFunctionMap *QtSLiMTextEdit::functionMapForTokenizedScript(EidosScript &script, bool includingOptionalFunctions) { // This lower-level function takes a tokenized script object and works from there, allowing reuse of work // in the case of attributedSignatureForScriptString:... QtSLiMWindow *windowSLiMController = slimControllerForWindow(); Community *community = (windowSLiMController ? windowSLiMController->community : nullptr); bool invalidSimulation = (windowSLiMController ? windowSLiMController->invalidSimulation() : true); // start with all the functions that are available in the current simulation context EidosFunctionMap *functionMapPtr = nullptr; if (community && !invalidSimulation) functionMapPtr = new EidosFunctionMap(community->FunctionMap()); else functionMapPtr = new EidosFunctionMap(*EidosInterpreter::BuiltInFunctionMap()); // functionMapForEidosTextView: returns the function map for the current interpreter state, and the type-interpreter // stuff we do below gives the delegate no chance to intervene (note that SLiMTypeInterpreter does not get in here, // unlike in the code completion machinery!). But sometimes we want SLiM's zero-gen functions to be added to the map // in all cases; it would be even better to be smart the way code completion is, but that's more work than it's worth. if (includingOptionalFunctions) { // add SLiM functions that are context-dependent Community::AddZeroTickFunctionsToMap(*functionMapPtr); Community::AddSLiMFunctionsToMap(*functionMapPtr); } // OK, now we have a starting point. We now want to use the type-interpreter to add any functions that are declared // in the full script, so that such declarations are known to us even before they have actually been executed. EidosTypeTable typeTable; EidosCallTypeTable callTypeTable; EidosSymbolTable *symbols = gEidosConstantsSymbolTable; symbols = symbolsFromBaseSymbols(symbols); if (symbols) symbols->AddSymbolsToTypeTable(&typeTable); script.ParseInterpreterBlockToAST(true, true); // make bad nodes as needed (i.e. never raise, and produce a correct tree) EidosTypeInterpreter typeInterpreter(script, typeTable, *functionMapPtr, callTypeTable); typeInterpreter.TypeEvaluateInterpreterBlock(); // result not used return functionMapPtr; } EidosSymbolTable *QtSLiMTextEdit::symbolsFromBaseSymbols(EidosSymbolTable *baseSymbols) { // in SLiMgui this is a delegate method, eidosTextView:symbolsFromBaseSymbols: // the point is simply to substitute in a console symbol table when one is available QtSLiMEidosConsole *consoleWindow = slimEidosConsoleForWindow(); if (consoleWindow) return consoleWindow->symbolTable(); return baseSymbols; } void QtSLiMTextEdit::scriptStringAndSelection(QString &scriptString, int &position, int &length, int &offset) { // by default, the entire contents of the textedit are considered "script" scriptString = toPlainText(); QTextCursor selection_cursor(textCursor()); position = selection_cursor.selectionStart(); length = selection_cursor.selectionEnd() - position; offset = 0; } EidosCallSignature_CSP QtSLiMTextEdit::signatureForScriptSelection(QString &callName) { // Note we return a copy of the signature, owned by the caller QString scriptString; int selectionStart, selectionLength, rangeOffset; scriptStringAndSelection(scriptString, selectionStart, selectionLength, rangeOffset); if (scriptString.length()) { std::string script_string = scriptString.toStdString(); EidosScript script(script_string); // Tokenize script.Tokenize(true, false); // make bad tokens as needed, don't keep nonsignificant tokens const std::vector &tokens = script.Tokens(); size_t tokenCount = tokens.size(); // Search forward to find the token position of the start of the selection size_t tokenIndex; for (tokenIndex = 0; tokenIndex < tokenCount; ++tokenIndex) if (tokens[tokenIndex].token_UTF16_start_ >= selectionStart) break; // tokenIndex now has the index of the first token *after* the selection start; it can be equal to tokenCount // Now we want to scan backward from there, balancing parentheses and looking for the pattern "identifier(" int backscanIndex = static_cast(tokenIndex) - 1; int parenCount = 0, lowestParenCountSeen = 0; while (backscanIndex > 0) // last examined position is 1, since we can't look for an identifier at 0 - 1 == -1 { const EidosToken &token = tokens[static_cast(backscanIndex)]; EidosTokenType tokenType = token.token_type_; if (tokenType == EidosTokenType::kTokenLParen) { --parenCount; if (parenCount < lowestParenCountSeen) { const EidosToken &previousToken = tokens[static_cast(backscanIndex) - 1]; EidosTokenType previousTokenType = previousToken.token_type_; if (previousTokenType == EidosTokenType::kTokenIdentifier) { // OK, we found the pattern "identifier("; extract the name of the function/method // We also figure out here whether it is a method call (tokens like ".identifier(") or not callName = QString::fromStdString(previousToken.token_string_); if ((backscanIndex > 1) && (tokens[static_cast(backscanIndex) - 2].token_type_ == EidosTokenType::kTokenDot)) { // This is a method call, so look up its signature that way EidosMethodSignature_CSP callSignature = signatureForMethodName(callName); return std::move(callSignature); // std::move() here avoids a bug in old compilers, according to a compiler warning... } else { // If this is a function declaration like "function(...)identifier(" then show no signature; it's not a function call // Determining this requires a fairly complex backscan, because we also have things like "if (...) identifier(" which // are function calls. This is the price we pay for working at the token level rather than the AST level for this; // so it goes. Note that this backscan is separate from the one done outside this block. BCH 1 March 2018. if ((backscanIndex > 1) && (tokens[static_cast(backscanIndex) - 2].token_type_ == EidosTokenType::kTokenRParen)) { // Start a new backscan starting at the right paren preceding the identifier; we need to scan back to the balancing // left paren, and then see if the next thing before that is "function" or not. int funcCheckIndex = backscanIndex - 2; int funcCheckParens = 0; while (funcCheckIndex >= 0) { const EidosToken &backscanToken = tokens[static_cast(funcCheckIndex)]; EidosTokenType backscanTokenType = backscanToken.token_type_; if (backscanTokenType == EidosTokenType::kTokenRParen) funcCheckParens++; else if (backscanTokenType == EidosTokenType::kTokenLParen) funcCheckParens--; --funcCheckIndex; if (funcCheckParens == 0) break; } if ((funcCheckParens == 0) && (funcCheckIndex >= 0) && (tokens[static_cast(funcCheckIndex)].token_type_ == EidosTokenType::kTokenFunction)) break; } // This is a function call, so look up its signature that way, using our best-guess function map EidosFunctionMap *functionMapPtr = functionMapForTokenizedScript(script, true); EidosFunctionSignature_CSP callSignature = signatureForFunctionName(callName, functionMapPtr); delete functionMapPtr; // note that callSignature survives this deletion because of shared_ptr return std::move(callSignature); // std::move() here avoids a bug in old compilers, according to a compiler warning... } } lowestParenCountSeen = parenCount; } } else if (tokenType == EidosTokenType::kTokenRParen) { ++parenCount; } --backscanIndex; } } return nullptr; } void QtSLiMTextEdit::updateStatusFieldFromSelection(void) { if (scriptType != NoScriptType) { QString callName; EidosCallSignature_CSP signature = signatureForScriptSelection(callName); if (!signature && (scriptType == SLiMScriptType)) { // Handle SLiM callback signatures if (callName == "initialize") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("initialize", nullptr, kEidosValueMaskVOID))); signature = callbackSig; } else if (callName == "first") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("first", nullptr, kEidosValueMaskVOID))); signature = callbackSig; } else if (callName == "early") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("early", nullptr, kEidosValueMaskVOID))); signature = callbackSig; } else if (callName == "late") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("late", nullptr, kEidosValueMaskVOID))); signature = callbackSig; } else if (callName == "mutationEffect") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("mutationEffect", nullptr, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddObject_S("mutationType", gSLiM_MutationType_Class)->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "fitnessEffect") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("fitnessEffect", nullptr, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "interaction") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("interaction", nullptr, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddObject_S("interactionType", gSLiM_InteractionType_Class)->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "mateChoice") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("mateChoice", nullptr, kEidosValueMaskNULL | kEidosValueMaskFloat | kEidosValueMaskObject, gSLiM_Individual_Class))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "modifyChild") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("modifyChild", nullptr, kEidosValueMaskLogical | kEidosValueMaskSingleton))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "recombination") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("recombination", nullptr, kEidosValueMaskLogical | kEidosValueMaskSingleton))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)->AddIntString_OSN("chromosome", gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "survival") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("survival", nullptr, kEidosValueMaskNULL | kEidosValueMaskLogical | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Subpopulation_Class))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "mutation") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("mutation", nullptr, kEidosValueMaskLogical | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Mutation_Class))->AddObject_OSN("mutationType", gSLiM_MutationType_Class, gStaticEidosValueNULLInvisible)->AddObject_OSN("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); signature = callbackSig; } else if (callName == "reproduction") { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("reproduction", nullptr, kEidosValueMaskVOID))->AddObject_OSN("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)->AddString_OSN("sex", gStaticEidosValueNULLInvisible)); signature = callbackSig; } } QString displayString; if (signature) displayString = QString::fromStdString(signature->SignatureString()); else if (!signature && callName.length()) displayString = callName + "() – unrecognized call"; QStatusBar *statusBar = statusBarForWindow(); if (displayString.length()) { // The status bar now supports display of an HTML string, so we use QTextDocument and ColorizeCallSignature() to make one QTextDocument td; td.setPlainText(displayString); if (signature) { QTextCursor tc(&td); tc.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); tc.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); #ifdef __linux__ ColorizeCallSignature(signature.get(), 9, tc); #else ColorizeCallSignature(signature.get(), 11, tc); #endif } // hanging indent for multiline wrapping aesthetics { QTextCursor tc(&td); tc.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); tc.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); QTextBlockFormat blockFormat; blockFormat.setLeftMargin(30); blockFormat.setTextIndent(-30); tc.setBlockFormat(blockFormat); } QString htmlString = td.toHtml(); //qDebug() << "setting HTML:" << htmlString; statusBar->showMessage(htmlString); } else { statusBar->clearMessage(); } // show the script block's declaration to the right of the Jump button QtSLiMWindow *windowSLiMController = dynamic_cast(window()); if (windowSLiMController) windowSLiMController->setScriptBlockLabelTextFromSelection(); } } // Completion support void QtSLiMTextEdit::setCodeCompletionEnabled(bool enabled) { codeCompletionEnabled = enabled; if (codeCompletionEnabled && !completer) { if (completer) QObject::disconnect(completer, nullptr, this, nullptr); completer = new QCompleter(this); // Make a dummy model for construction QStringList words; words << "foo"; words << "bar"; words << "baz"; completer->setModel(new QStringListModel(words, completer)); completer->setModelSorting(QCompleter::UnsortedModel); completer->setCaseSensitivity(Qt::CaseInsensitive); completer->setWrapAround(false); completer->setWidget(this); connect(completer, QOverload::of(&QCompleter::activated), this, &QtSLiMTextEdit::insertCompletion); // note this activated() signal was deprecated in 5.15, use QComboBox::textActivated() which was added in 5.14 } } void QtSLiMTextEdit::insertCompletion(const QString& completionOriginal) { if (completer->widget() != this) return; // If the completion string ends in ") {}" we add newlines to it here; we don't want to show multi-line completions // in the popup, but we want to produce them for the user when the completion is accepted; see slimSpecificCompletion() QString completion = completionOriginal; bool multilineCompletion = false; if (completion.endsWith(") { }")) { completion.replace(") { }", ") {\n\t\n}\n"); multilineCompletion = true; } // The cursor that we used as a completion root gets replaced completely by the completion string NSRange completionRange = rangeForUserCompletion(); if (completionRange.location != NSNotFound) { QTextCursor tc = textCursor(); int endPosition = std::max(tc.selectionEnd(), completionRange.location + completionRange.length); // the completion is off the selection start, but we want to replace any selected text also tc.setPosition(completionRange.location, QTextCursor::MoveAnchor); tc.setPosition(endPosition, QTextCursor::KeepAnchor); // If the character after the completion range is '(', suppress trailing parentheses on the completion QTextCursor afterCompletion(tc); afterCompletion.setPosition(afterCompletion.position(), QTextCursor::MoveAnchor); afterCompletion.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); if (afterCompletion.selectedText() == '(') { if (completion.endsWith("()")) completion.chop(2); } // Replace the completion range with the completion string tc.insertText(completion); // If the completion is multiline, put the insertion point inside the braces of the completion if (multilineCompletion) tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 3); setTextCursor(tc); } else { qApp->beep(); } } void QtSLiMTextEdit::autoindentAfterNewline(void) { // We are called by QtSLiMTextEdit::keyPressEvent() immediately after it calls // super to insert a newline in response to Key_Enter or Key_Return if (scriptType != NoScriptType) { QTextCursor tc = textCursor(); int selStart = tc.selectionStart(), selEnd = tc.selectionEnd(); const QString scriptString = toPlainText(); // verify that we have an insertion point immediately following a newline if ((selStart == selEnd) && (selStart > 0) && (scriptString[selStart - 1] == '\n')) { QTextCursor previousLine = tc; previousLine.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor); previousLine.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); QString lineString = previousLine.selectedText(); QString whitespace; for (int position = 0; position < lineString.length(); ++position) { QChar qch = lineString[position]; if (qch.isSpace()) whitespace.append(qch); else break; } // insert the same whitespace as the previous line had, joining the undo block for the newline insertion if (whitespace.length()) { tc.joinPreviousEditBlock(); tc.insertText(whitespace); tc.endEditBlock(); // BCH 5/24/2025: Fix an autoindent bug that I'm surprised I didn't notice before; if // you're at the end of an indented line, and press return and then press up-arrow, // you move to the wrong position in the previous line, as if the auto-indent had not // occurred. It's weird, because the cursor shows visibly at the correct position, // but then up-arrow reveals that in some way it was actually not in that position. // Anyhow, explicitly setting the text cursor here seems to fix it. Maybe I didn't // notice it before because this is a new bug in Qt 6? If so, this workaround should // be safe on Qt 5. setTextCursor(tc); } } } } void QtSLiMTextEdit::keyPressEvent(QKeyEvent *p_event) { // Without a completer, we just call super if (!completer) { QPlainTextEdit::keyPressEvent(p_event); return; } if (completer->popup()->isVisible()) { // The following keys are forwarded by the completer to the widget switch (p_event->key()) { case Qt::Key_Enter: case Qt::Key_Return: case Qt::Key_Escape: case Qt::Key_Tab: case Qt::Key_Backtab: p_event->ignore(); return; // let the completer do default behavior default: break; } } // if we have a visible completer popup, the key pressed is not one of the special keys above (including escape) // our completion key shortcut is the escape key, so check for that now bool isShortcut = ((p_event->modifiers() == Qt::NoModifier) && p_event->key() == Qt::Key_Escape); // escape if (!isShortcut) { // any key other than escape and the special keys above causes the completion popup to hide completer->popup()->hide(); QPlainTextEdit::keyPressEvent(p_event); // implement autoindent if ((p_event->modifiers() == Qt::NoModifier) && ((p_event->key() == Qt::Key_Enter) || (p_event->key() == Qt::Key_Return))) autoindentAfterNewline(); return; } // we have a completer and the shortcut has been pressed; initiate completion // first, figure out the range of text we are completing (the "root") NSRange completionRange = rangeForUserCompletion(); if (completionRange.location != NSNotFound) { QTextCursor completionRootCursor = textCursor(); completionRootCursor.setPosition(completionRange.location, QTextCursor::MoveAnchor); //completionRootCursor.setPosition(completionRange.location + completionRange.length, QTextCursor::KeepAnchor); // this aligns the popup with the right edge of the selection, but we want the left edge, I think... // get the correct context-sensitive word list for the completer QStringList completions = completionsForPartialWordRange(completionRange, nullptr); completer->setModel(new QStringListModel(completions, completer)); // place the completer appropriately for the cursor; the doc is a bit vague, but this seems to work QRect cr = cursorRect(completionRootCursor); cr.setWidth(completer->popup()->sizeHintForColumn(0) + completer->popup()->verticalScrollBar()->sizeHint().width()); // zero out the completer's completion root; we do not use it, because we implement our own matching algorithm completer->setCompletionPrefix(""); completer->popup()->setCurrentIndex(completer->completionModel()->index(0, 0)); completer->complete(cr); // pop up below the current cursor rect in the textview } else { qApp->beep(); } } // the rest here is completion code adapted from EidosScribe and SLiMgui // we mirror the NSTextView APIs completionsForPartialWordRange:indexOfSelectedItem: and // rangeForUserCompletion to allow our ported code to function identically to SLiMgui // - (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index QStringList QtSLiMTextEdit::completionsForPartialWordRange(NSRange __attribute__((__unused__)) charRange, int * __attribute__((__unused__)) indexOfSelectedItem) { QStringList completions; //NSArray *completions = nil; _completionHandlerWithRangeForCompletion(nullptr, &completions); return completions; } // - (NSRange)rangeForUserCompletion NSRange QtSLiMTextEdit::rangeForUserCompletion(void) { NSRange baseRange = {NSNotFound, 0}; _completionHandlerWithRangeForCompletion(&baseRange, nullptr); return baseRange; } //- (NSMutableArray *)globalCompletionsWithTypes:(EidosTypeTable *)typeTable functions:(EidosFunctionMap *)functionMap keywords:(NSArray *)keywords argumentNames:(NSArray *)argumentNames QStringList QtSLiMTextEdit::globalCompletionsWithTypesFunctionsKeywordsArguments(EidosTypeTable *typeTable, EidosFunctionMap *functionMap, QStringList keywords, QStringList argumentNames) { QStringList globals; // First add entries for symbols in our type table (from Eidos constants, defined symbols, or our delegate) if (typeTable) { std::vector typedSymbols = typeTable->AllSymbols(); for (std::string &symbol_name : typedSymbols) globals << QString::fromStdString(symbol_name); } // Sort the symbols, who knows what order they come from EidosTypeTable in... globals.sort(); // Next, if we have argument names that are completion matches, we want them at the top if (argumentNames.size()) { QStringList oldGlobals = globals; globals = argumentNames; globals.append(oldGlobals); } // Next, a sorted list of functions, with () appended if (functionMap) { for (const auto& function_iter : *functionMap) { const EidosFunctionSignature *sig = function_iter.second.get(); QString functionName = QString::fromStdString(sig->call_name_); // Exclude internal functions such as _Test() if (!functionName.startsWith("_")) { functionName.append("()"); globals.append(functionName); } } } // Finally, provide language keywords as an option if requested if (keywords.size()) globals.append(keywords); return globals; } //- (NSMutableArray *)completionsForKeyPathEndingInTokenIndex:(int)lastDotTokenIndex ofTokenStream:(const std::vector &)tokens withTypes:(EidosTypeTable *)typeTable functions:(EidosFunctionMap *)functionMap callTypes:(EidosCallTypeTable *)callTypeTable keywords:(NSArray *)keywords QStringList QtSLiMTextEdit::completionsForKeyPathEndingInTokenIndexOfTokenStream(int lastDotTokenIndex, const std::vector &tokens, EidosTypeTable *typeTable, EidosFunctionMap *functionMap, EidosCallTypeTable *callTypeTable, QStringList __attribute__((__unused__)) keywords) { const EidosToken *token = &tokens[static_cast(lastDotTokenIndex)]; EidosTokenType token_type = token->token_type_; if (token_type != EidosTokenType::kTokenDot) { qDebug() << "***** completionsForKeyPathEndingInTokenIndex... called for non-kTokenDot token!"; return QStringList(); } // OK, we've got a key path ending in a dot, and we want to return a list of completions that would work for that key path. // We'll trace backward, adding identifiers to a vector to build up the chain of references. If we hit a bracket, we'll // skip back over everything inside it, since subsetting does not change the type; we just need to balance brackets. If we // hit a parenthesis, we do similarly. If we hit other things – a semicolon, a comma, a brace – that terminates the key path chain. std::vector identifiers; std::vector identifiers_are_calls; std::vector identifier_positions; int bracketCount = 0, parenCount = 0; bool lastTokenWasDot = true, justFinishedParenBlock = false; for (int tokenIndex = lastDotTokenIndex - 1; tokenIndex >= 0; --tokenIndex) { token = &tokens[static_cast(tokenIndex)]; token_type = token->token_type_; // skip backward over whitespace and comments; they make no difference to us if ((token_type == EidosTokenType::kTokenWhitespace) || (token_type == EidosTokenType::kTokenComment) || (token_type == EidosTokenType::kTokenCommentLong)) continue; if (bracketCount) { // If we're inside a bracketed stretch, all we do is balance brackets and run backward. We don't even clear lastTokenWasDot, // because a []. sequence puts us in the same situation as having just seen a dot – we're still waiting for an identifier. if (token_type == EidosTokenType::kTokenRBracket) { bracketCount++; continue; } if (token_type == EidosTokenType::kTokenLBracket) { bracketCount--; continue; } // Check for tokens that simply make no sense, and bail if ((token_type == EidosTokenType::kTokenLBrace) || (token_type == EidosTokenType::kTokenRBrace) || (token_type == EidosTokenType::kTokenSemicolon) || (token_type >= EidosTokenType::kFirstIdentifierLikeToken)) return QStringList(); continue; } else if (parenCount) { // If we're inside a paren stretch – which could be a parenthesized expression or a function call – we do similarly // to the brackets case, just balancing parens and running backward. We don't clear lastTokenWasDot, because a // (). sequence puts us in the same situation (almost) as having just seen a dot – waiting for an identifier. if (token_type == EidosTokenType::kTokenRParen) { parenCount++; continue; } if (token_type == EidosTokenType::kTokenLParen) { parenCount--; if (parenCount == 0) justFinishedParenBlock = true; continue; } // Check for tokens that simply make no sense, and bail if ((token_type == EidosTokenType::kTokenLBrace) || (token_type == EidosTokenType::kTokenRBrace) || (token_type == EidosTokenType::kTokenSemicolon) || (token_type >= EidosTokenType::kFirstIdentifierLikeToken)) return QStringList(); continue; } if (!lastTokenWasDot) { // We just saw an identifier, so the only thing that can continue the key path is a dot if (token_type == EidosTokenType::kTokenDot) { lastTokenWasDot = true; justFinishedParenBlock = false; continue; } // the key path has terminated at some non-key-path token, so we're done tracing it break; } // OK, the last token was a dot (or a subset preceding a dot). We're looking for an identifier, but we're willing // to get distracted by a subset sequence, since that does not change the type. Anything else does not make sense. if (token_type == EidosTokenType::kTokenIdentifier) { identifiers.emplace_back(token->token_string_); identifiers_are_calls.push_back(justFinishedParenBlock); identifier_positions.emplace_back(token->token_start_); // set up to continue searching the key path backwards lastTokenWasDot = false; justFinishedParenBlock = false; continue; } else if (token_type == EidosTokenType::kTokenRBracket) { bracketCount++; continue; } else if (token_type == EidosTokenType::kTokenRParen) { parenCount++; continue; } // This makes no sense, so bail return QStringList(); } // If we were in the middle of tracing the key path when the loop ended, then something is wrong, bail. if (lastTokenWasDot || bracketCount || parenCount) return QStringList(); // OK, we've got an identifier chain in identifiers, in reverse order. We want to start at // the beginning of the key path, and figure out what the class of the key path root is int key_path_index = static_cast(identifiers.size()) - 1; std::string &identifier_name = identifiers[static_cast(key_path_index)]; EidosGlobalStringID identifier_ID = EidosStringRegistry::GlobalStringIDForString(identifier_name); bool identifier_is_call = identifiers_are_calls[static_cast(key_path_index)]; const EidosClass *key_path_class = nullptr; if (identifier_is_call) { // The root identifier is a call, so it should be a function call; try to look it up for (const auto& function_iter : *functionMap) { const EidosFunctionSignature *sig = function_iter.second.get(); if (sig->call_name_.compare(identifier_name) == 0) { key_path_class = sig->return_class_; // In some cases, the function signature does not have the information we need, because the class of the return value // of the function depends upon its parameters. This is the case for functions like sample(), rep(), and so forth. // For this case, we have a special mechanism set up, whereby the EidosTypeInterpreter has logged the class of the // return value of function calls that it has evaluated. We can look up the correct class in that log. This is kind // of a gross solution, but short of rewriting all the completion code, it seems to be the easiest fix. (Rewriting // to fix this more properly would involve doing code completion using a type-annotated tree, without any of the // token-stream handling that we have now; that would be a better design, but I'm going to save that rewrite for later.) if (!key_path_class) { auto callTypeIter = callTypeTable->find(identifier_positions[static_cast(key_path_index)]); if (callTypeIter != callTypeTable->end()) key_path_class = callTypeIter->second; } break; } } } else if (typeTable) { // The root identifier is not a call, so it should be a global symbol; try to look it up EidosTypeSpecifier type_specifier = typeTable->GetTypeForSymbol(identifier_ID); if (!!(type_specifier.type_mask & kEidosValueMaskObject)) key_path_class = type_specifier.object_class; } if (!key_path_class) return QStringList(); // unknown symbol at the root // Now we've got a class for the root of the key path; follow forward through the key path to arrive at the final type. while (--key_path_index >= 0) { identifier_name = identifiers[static_cast(key_path_index)]; identifier_is_call = identifiers_are_calls[static_cast(key_path_index)]; EidosGlobalStringID identifier_id = EidosStringRegistry::GlobalStringIDForString(identifier_name); if (identifier_id == gEidosID_none) return QStringList(); // unrecognized identifier in the key path, so there is probably a typo and we can't complete off of it if (identifier_is_call) { // We have a method call; look up its signature and get the class const EidosCallSignature *call_signature = key_path_class->SignatureForMethod(identifier_id); if (!call_signature) return QStringList(); // no signature, so the class does not support the method given key_path_class = call_signature->return_class_; } else { // We have a property; look up its signature and get the class const EidosPropertySignature *property_signature = key_path_class->SignatureForProperty(identifier_id); if (!property_signature) return QStringList(); // no signature, so the class does not support the property given key_path_class = property_signature->value_class_; } if (!key_path_class) return QStringList(); // unknown symbol at the root; the property yields a non-object type } // OK, we've now got a EidosValue object that represents the end of the line; the final dot is off of this object. // So we want to extract all of its properties and methods, and return them all as candidates. QStringList candidates; const EidosClass *terminus = key_path_class; // First, a sorted list of globals for (const auto &symbol_sig : *terminus->Properties()) { if (!symbol_sig->deprecated_) candidates << QString::fromStdString(symbol_sig->property_name_); } candidates.sort(); // Next, a sorted list of methods, with () appended for (const auto &method_sig : *terminus->Methods()) { if (!method_sig->deprecated_) { QString methodName = QString::fromStdString(method_sig->call_name_); methodName.append("()"); candidates << methodName; } } return candidates; } //- (int64_t)eidosScoreAsCompletionOfString:(NSString *)base int64_t QtSLiMTextEdit::scoreForCandidateAsCompletionOfString(QString candidate, QString base) { // Evaluate the quality of the target as a completion for completionBase and return a score. // We look for each character of completionBase in candidate, in order, case-insensitive; all // characters must be present in order for the target to be a completion at all. Beyond that, // a higher score is garnered if the matches in candidate are (1) either uppercase or the 0th character, // and (2) if they are relatively near the beginning, and (3) if they occur contiguously. int64_t score = 0; int baseLength = base.length(); // Do the comparison scan; find a match for each composed character sequence in base. I *think* // QString contains QChars that represent composed character sequences already, so I think in this // port of the Objective-C code maybe I can ignore that issue...? We work use rangeOfString: to do // searches, to avoid issues with diacritical marks, alternative composition sequences, casing, etc. int firstUnusedIndex = 0, firstUnmatchedIndex = 0; do { //NSRange baseRangeToMatch = [base rangeOfComposedCharacterSequenceAtIndex:firstUnmatchedIndex]; //NSString *stringToMatch = [base substringWithRange:baseRangeToMatch]; int baseIndexToMatch = firstUnmatchedIndex; QString stringToMatch = base.mid(baseIndexToMatch, 1); QString uppercaseStringToMatch = stringToMatch.toUpper(); int candidateMatchIndex; if ((stringToMatch == uppercaseStringToMatch) && (firstUnmatchedIndex != 0)) { // If the character in base is uppercase, we only want to match an uppercase character in candidate. // The exception is the first character of base; WTF should match writeTempFile() well. candidateMatchIndex = candidate.indexOf(stringToMatch, firstUnusedIndex); score += 1000; // uppercase match } else { // If the character in base is not uppercase, we will match any case in candidate, but we prefer a // lowercase character if it matches the very next part of candidate, otherwise we prefer uppercase. candidateMatchIndex = candidate.indexOf(stringToMatch, firstUnusedIndex); if (candidateMatchIndex == firstUnusedIndex) { score += 2000; // next-character match is even better than upper-case; continuity trumps camelcase } else { int uppercaseMatchIndex = candidate.indexOf(uppercaseStringToMatch, firstUnusedIndex); if (uppercaseMatchIndex != -1) { candidateMatchIndex = uppercaseMatchIndex; score += 1000; // uppercase match } else if (firstUnusedIndex > 0) { // This match is crap; we're jumping forward to a lowercase letter, so it's unlikely to be what // the user wants. So we bail. This can be commented out to return lower-quality matches. return INT64_MIN; } } } // no match in candidate for the composed character sequence in base; candidate is not a good completion of base if (candidateMatchIndex == -1) return INT64_MIN; // matching the very beginning of candidate is very good; we really want to match the start of a candidate // otherwise, earlier matches are better; a match at position 0 gets the largest score increment if (candidateMatchIndex == 0) score += 100000; else score -= (candidateMatchIndex * 10); // penalize skipping over a capital letter in candidate to get to the match position; iS is not a great match for initializeTreeSequence() compared to initializeSex() for (int skippedIndex = firstUnusedIndex; skippedIndex < candidateMatchIndex; ++skippedIndex) { QString skippedChar = base.mid(skippedIndex, 1); if (skippedChar == skippedChar.toUpper()) score -= 50; } // move firstUnusedIndex to follow the matched range in candidate firstUnusedIndex = candidateMatchIndex + 1; // move to the next composed character sequence in base firstUnmatchedIndex = baseIndexToMatch + 1; if (firstUnmatchedIndex >= baseLength) break; } while (true); // penalize the unused length of the completed string, all else being equal score -= (candidate.length() - firstUnmatchedIndex); // we want argument-name matches to be at the top, always, when they are available, so bump their score if (candidate.endsWith("=")) score += 1000000; //qDebug() << "Score for" << candidate << "given base" << base << "==" << score; return score; } //- (NSArray *)completionsFromArray:(NSArray *)candidates matchingBase:(NSString *)base QStringList QtSLiMTextEdit::completionsFromArrayMatchingBase(QStringList candidates, QString base) { QStringList completions; int candidateCount = candidates.size(); #if 0 // This is simple prefix-based completion; if a candidates begins with base, then it is used for (int candidateIndex = 0; candidateIndex < candidateCount; ++candidateIndex) { QString candidate = candidates[candidateIndex]; if (candidate.startsWith(base)) completions << candidate; } #else // This is part-based completion, where iTr will complete to initializeTreeSeq() and iGTy // will complete to initializeGenomicElementType(). To do this, we use a special comparator // that returns a score for the quality of the match, and then we sort all matches by score. std::vector scores; QStringList unsortedCompletions; for (int candidateIndex = 0; candidateIndex < candidateCount; ++candidateIndex) { QString candidate = candidates[candidateIndex]; int64_t score = scoreForCandidateAsCompletionOfString(candidate, base); if (score != INT64_MIN) { unsortedCompletions << candidate; scores.emplace_back(score); } } if (scores.size()) { std::vector order = EidosSortIndexes(scores.data(), scores.size(), false); for (int64_t index : order) completions << unsortedCompletions[static_cast(index)]; } #endif return completions; } //- (NSArray *)completionsForTokenStream:(const std::vector &)tokens index:(int)lastTokenIndex canExtend:(BOOL)canExtend withTypes:(EidosTypeTable *)typeTable functions:(EidosFunctionMap *)functionMap callTypes:(EidosCallTypeTable *)callTypeTable keywords:(NSArray *)keywords argumentNames:(NSArray *)argumentNames QStringList QtSLiMTextEdit::completionsForTokenStream(const std::vector &tokens, int lastTokenIndex, bool canExtend, EidosTypeTable *typeTable, EidosFunctionMap *functionMap, EidosCallTypeTable *callTypeTable, QStringList keywords, QStringList argumentNames) { // What completions we offer depends on the token stream const EidosToken &token = tokens[static_cast(lastTokenIndex)]; EidosTokenType token_type = token.token_type_; switch (token_type) { case EidosTokenType::kTokenNone: case EidosTokenType::kTokenEOF: case EidosTokenType::kTokenWhitespace: case EidosTokenType::kTokenComment: case EidosTokenType::kTokenCommentLong: case EidosTokenType::kTokenInterpreterBlock: case EidosTokenType::kTokenContextFile: case EidosTokenType::kTokenContextEidosBlock: case EidosTokenType::kFirstIdentifierLikeToken: // These should never be hit return QStringList(); case EidosTokenType::kTokenIdentifier: case EidosTokenType::kTokenIf: case EidosTokenType::kTokenWhile: case EidosTokenType::kTokenFor: case EidosTokenType::kTokenNext: case EidosTokenType::kTokenBreak: case EidosTokenType::kTokenFunction: case EidosTokenType::kTokenReturn: case EidosTokenType::kTokenElse: case EidosTokenType::kTokenDo: case EidosTokenType::kTokenIn: if (canExtend) { QStringList completions; // This is the tricky case, because the identifier we're extending could be the end of a key path like foo.bar[5:8].ba... // We need to move backwards from the current token until we find or fail to find a dot token; if we see a dot we're in // a key path, otherwise we're in the global context and should filter from those candidates for (int previousTokenIndex = lastTokenIndex - 1; previousTokenIndex >= 0; --previousTokenIndex) { const EidosToken &previous_token = tokens[static_cast(previousTokenIndex)]; EidosTokenType previous_token_type = previous_token.token_type_; // if the token we're on is skippable, continue backwards if ((previous_token_type == EidosTokenType::kTokenWhitespace) || (previous_token_type == EidosTokenType::kTokenComment) || (previous_token_type == EidosTokenType::kTokenCommentLong)) continue; // if the token we're on is a dot, we are indeed at the end of a key path, and can fetch the completions for it if (previous_token_type == EidosTokenType::kTokenDot) { completions = completionsForKeyPathEndingInTokenIndexOfTokenStream(previousTokenIndex, tokens, typeTable, functionMap, callTypeTable, keywords); break; } // if we see a semicolon or brace, we are in a completely global context if ((previous_token_type == EidosTokenType::kTokenSemicolon) || (previous_token_type == EidosTokenType::kTokenLBrace) || (previous_token_type == EidosTokenType::kTokenRBrace)) { completions = globalCompletionsWithTypesFunctionsKeywordsArguments(typeTable, functionMap, keywords, QStringList()); break; } // if we see any other token, we are not in a key path; let's assume we're following an operator completions = globalCompletionsWithTypesFunctionsKeywordsArguments(typeTable, functionMap, QStringList(), argumentNames); break; } // If we ran out of tokens, we're at the beginning of the file and so in the global context if (completions.size() == 0) completions = globalCompletionsWithTypesFunctionsKeywordsArguments(typeTable, functionMap, keywords, QStringList()); // Now we have an array of possible completions; we just need to remove those that don't complete the base string, // according to a heuristic algorithm, and sort those that do match by a score of their closeness of match. return completionsFromArrayMatchingBase(completions, QString::fromStdString(token.token_string_)); } else if ((token_type == EidosTokenType::kTokenReturn) || (token_type == EidosTokenType::kTokenElse) || (token_type == EidosTokenType::kTokenDo) || (token_type == EidosTokenType::kTokenIn)) { // If you can't extend and you're following an identifier, you presumably need an operator or a keyword or something; // you can't have two identifiers in a row. The same is true of keywords that do not take an expression after them. // But return, else, do, and in can be followed immediately by an expression, so here we handle that case. Identifiers // and other keywords will drop through to return nil below, expressing that we cannot complete in that case. // We used to put return, else, do, and in down the the operators at the bottom, but when canExtend is YES that // prevents them from completing to other things ("in" to "inSLiMgui", for example); moving them up to this case // allows that completion to work, but necessitates the addition of this block to get the correct functionality when // canExtend is NO. BCH 1/22/2019 return globalCompletionsWithTypesFunctionsKeywordsArguments(typeTable, functionMap, QStringList(), argumentNames); } // If the previous token was an identifier and we can't extend it, the next thing probably needs to be an operator or something return QStringList(); case EidosTokenType::kTokenBad: case EidosTokenType::kTokenNumber: case EidosTokenType::kTokenString: case EidosTokenType::kTokenRParen: case EidosTokenType::kTokenRBracket: case EidosTokenType::kTokenSingleton: // We don't have anything to suggest after such tokens; the next thing will need to be an operator, semicolon, etc. return QStringList(); case EidosTokenType::kTokenDot: // This is the other tricky case, because we're being asked to extend a key path like foo.bar[5:8]. return completionsForKeyPathEndingInTokenIndexOfTokenStream(lastTokenIndex, tokens, typeTable, functionMap, callTypeTable, keywords); case EidosTokenType::kTokenSemicolon: case EidosTokenType::kTokenLBrace: case EidosTokenType::kTokenRBrace: // We are in the global context and anything goes, including a new statement return globalCompletionsWithTypesFunctionsKeywordsArguments(typeTable, functionMap, keywords, QStringList()); case EidosTokenType::kTokenColon: case EidosTokenType::kTokenComma: case EidosTokenType::kTokenLParen: case EidosTokenType::kTokenLBracket: case EidosTokenType::kTokenPlus: case EidosTokenType::kTokenMinus: case EidosTokenType::kTokenMod: case EidosTokenType::kTokenMult: case EidosTokenType::kTokenExp: case EidosTokenType::kTokenAnd: case EidosTokenType::kTokenOr: case EidosTokenType::kTokenDiv: case EidosTokenType::kTokenConditional: case EidosTokenType::kTokenAssign: case EidosTokenType::kTokenAssign_R: case EidosTokenType::kTokenEq: case EidosTokenType::kTokenLt: case EidosTokenType::kTokenLtEq: case EidosTokenType::kTokenGt: case EidosTokenType::kTokenGtEq: case EidosTokenType::kTokenNot: case EidosTokenType::kTokenNotEq: // We are following an operator or similar, so globals are OK but new statements are not return globalCompletionsWithTypesFunctionsKeywordsArguments(typeTable, functionMap, QStringList(), argumentNames); } return QStringList(); } //- (NSArray *)uniquedArgumentNameCompletions:(std::vector *)argumentCompletions QStringList QtSLiMTextEdit::uniquedArgumentNameCompletions(std::vector *argumentCompletions) { // put argument-name completions, if any, at the top of the list; we unique them (preserving order) and add "=" if (argumentCompletions && argumentCompletions->size()) { QStringList completionsWithArgs; for (std::string &arg_completion : *argumentCompletions) completionsWithArgs << QString::fromStdString(arg_completion).append("="); completionsWithArgs.removeDuplicates(); return completionsWithArgs; } return QStringList(); } //- (BOOL)eidosTextView:(EidosTextView *)eidosTextView completionContextWithScriptString:(NSString *)completionScriptString selection:(NSRange)selection typeTable:(EidosTypeTable **)typeTable functionMap:(EidosFunctionMap **)functionMap callTypeTable:(EidosCallTypeTable **)callTypeTable keywords:(NSMutableArray *)keywords argumentNameCompletions:(std::vector *)argNameCompletions void QtSLiMTextEdit::slimSpecificCompletion(QString completionScriptString, NSRange selection, EidosTypeTable **typeTable, EidosFunctionMap **functionMap, EidosCallTypeTable **callTypeTable, QStringList *keywords, std::vector *argNameCompletions) { // Code completion in the console window and other ancillary EidosTextViews should use the standard code completion // machinery in EidosTextView. In the script view, however, we want things to behave somewhat differently. In // other contexts, we want the variables and functions available to depend solely upon the current state of the // simulation; whatever is actually available is what code completion provides. In the script view, however, we // want to be smarter than that. Initialization functions should be available when the user is completing // inside an initialize() callback, and not available otherwise, regardless of the current simulation state. // Similarly, variables associated with particular types of callbacks should always be available within those // callbacks; variables defined in script blocks other than the focal block should not be visible in code // completion; defined constants should be available everywhere; and it should be assumed that variables with // names like pX, mX, gX, and sX have their usual types even if they are not presently defined. This delegate // method accomplishes all of those things, by replacing the standard EidosTextView completion handling. std::string script_string(completionScriptString.toStdString()); SLiMEidosScript script(script_string); // Parse an "interpreter block" bounded by an EOF rather than a "script block" that requires braces script.Tokenize(true, false); // make bad tokens as needed, do not keep nonsignificant tokens script.ParseSLiMFileToAST(true); // make bad nodes as needed (i.e. never raise, and produce a correct tree) // Substitute a type table of class SLiMTypeTable and add any defined symbols to it. We use SLiMTypeTable so that // variables like pX, gX, mX, and sX have a known object type even if they are not presently defined in the simulation. *typeTable = new SLiMTypeTable(); QtSLiMWindow *windowSLiMController = slimControllerForWindow(); QtSLiMEidosConsole *consoleController = (windowSLiMController ? windowSLiMController->ConsoleController() : nullptr); EidosSymbolTable *symbols = (consoleController ? consoleController->symbolTable() : nullptr); if (symbols) symbols->AddSymbolsToTypeTable(*typeTable); // Use the script text view's facility for using type-interpreting to get a "definitive" function map. This way // all functions that are defined, even if below the completion point, end up in the function map. *functionMap = functionMapForScriptString(toPlainText(), false); Community::AddSLiMFunctionsToMap(**functionMap); // Now we scan through the children of the root node, each of which is the root of a SLiM script block. The last // script block is the one we are actually completing inside, but we also want to do a quick scan of any other // blocks we find, solely to add entries for any defineConstant() calls we can decode. const EidosASTNode *script_root = script.AST(); if (script_root && (script_root->children_.size() > 0)) { EidosASTNode *completion_block = script_root->children_.back(); // If the last script block has a range that ends before the start of the selection, then we are completing after the end // of that block, at the outer level of the script. Detect that case and fall through to the handler for it at the end. int32_t completion_block_end = completion_block->token_->token_end_; if (static_cast(selection.location) > completion_block_end) { // Selection is after end of completion_block completion_block = nullptr; } if (completion_block) { for (EidosASTNode *script_block_node : script_root->children_) { // skip species/ticks specifiers, which are identifier token nodes at the top level of the AST with one child if ((script_block_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (script_block_node->children_.size() == 1)) continue; // script_block_node can have various children, such as an sX identifier, start and end ticks, a block type // identifier like late(), and then the root node of the compound statement for the script block. We want to // decode the parts that are important to us, without the complication of making SLiMEidosBlock objects. EidosASTNode *block_statement_root = nullptr; SLiMEidosBlockType block_type = SLiMEidosBlockType::SLiMEidosNoBlockType; for (EidosASTNode *block_child : script_block_node->children_) { EidosToken *child_token = block_child->token_; if (child_token->token_type_ == EidosTokenType::kTokenIdentifier) { const std::string &child_string = child_token->token_string_; if (child_string.compare(gStr_first) == 0) block_type = SLiMEidosBlockType::SLiMEidosEventFirst; else if (child_string.compare(gStr_early) == 0) block_type = SLiMEidosBlockType::SLiMEidosEventEarly; else if (child_string.compare(gStr_late) == 0) block_type = SLiMEidosBlockType::SLiMEidosEventLate; else if (child_string.compare(gStr_initialize) == 0) block_type = SLiMEidosBlockType::SLiMEidosInitializeCallback; else if (child_string.compare(gStr_fitnessEffect) == 0) block_type = SLiMEidosBlockType::SLiMEidosFitnessEffectCallback; else if (child_string.compare(gStr_mutationEffect) == 0) block_type = SLiMEidosBlockType::SLiMEidosMutationEffectCallback; else if (child_string.compare(gStr_interaction) == 0) block_type = SLiMEidosBlockType::SLiMEidosInteractionCallback; else if (child_string.compare(gStr_mateChoice) == 0) block_type = SLiMEidosBlockType::SLiMEidosMateChoiceCallback; else if (child_string.compare(gStr_modifyChild) == 0) block_type = SLiMEidosBlockType::SLiMEidosModifyChildCallback; else if (child_string.compare(gStr_recombination) == 0) block_type = SLiMEidosBlockType::SLiMEidosRecombinationCallback; else if (child_string.compare(gStr_mutation) == 0) block_type = SLiMEidosBlockType::SLiMEidosMutationCallback; else if (child_string.compare(gStr_survival) == 0) block_type = SLiMEidosBlockType::SLiMEidosSurvivalCallback; else if (child_string.compare(gStr_reproduction) == 0) block_type = SLiMEidosBlockType::SLiMEidosReproductionCallback; // Check for an sX designation on a script block and, if found, add a symbol for it else if ((block_child == script_block_node->children_[0]) && (child_string.length() >= 2)) { if (child_string[0] == 's') { bool all_numeric = true; for (size_t idx = 1; idx < child_string.length(); ++idx) if (!isdigit(child_string[idx])) all_numeric = false; if (all_numeric) { EidosGlobalStringID constant_id = EidosStringRegistry::GlobalStringIDForString(child_string); (*typeTable)->SetTypeForSymbol(constant_id, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_SLiMEidosBlock_Class}); } } } } else if (child_token->token_type_ == EidosTokenType::kTokenLBrace) { block_statement_root = block_child; } else if (child_token->token_type_ == EidosTokenType::kTokenFunction) { // We handle function blocks a bit differently; see below block_type = SLiMEidosBlockType::SLiMEidosUserDefinedFunction; if (block_child->children_.size() >= 4) block_statement_root = block_child->children_[3]; } } // Now we know the type of the node, and the root node of its compound statement; extract what we want if (block_statement_root) { // The species/community symbols are defined in all blocks except initialize() blocks; we need to add // and remove them dynamically so that each block has it defined or not defined as necessary. Since // the completion block is last, the symbols will be correctly defined at the end of this process. if (block_type == SLiMEidosBlockType::SLiMEidosInitializeCallback) { std::vector symbol_ids = (*typeTable)->AllSymbolIDs(); for (EidosGlobalStringID symbol_id : symbol_ids) { EidosTypeSpecifier typeSpec = (*typeTable)->GetTypeForSymbol(symbol_id); if ((typeSpec.type_mask == kEidosValueMaskObject) && ((typeSpec.object_class == gSLiM_Community_Class) || (typeSpec.object_class == gSLiM_Species_Class))) (*typeTable)->RemoveTypeForSymbol(symbol_id); } } else { Community *community = (windowSLiMController ? windowSLiMController->community : nullptr); (*typeTable)->SetTypeForSymbol(gID_community, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Community_Class}); if (community) { for (Species *species : community->AllSpecies()) { EidosGlobalStringID species_symbol = species->self_symbol_.first; (*typeTable)->SetTypeForSymbol(species_symbol, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Species_Class}); } } else { // We don't have a community object, so we don't have a vector of species; this is usually because of a failed parse // In this case, we try to keep things functional by just assuming the single-species case and defining "sim" (*typeTable)->SetTypeForSymbol(gID_sim, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Species_Class}); } } // The slimgui symbol is always available within a block, but not at the top level (*typeTable)->SetTypeForSymbol(gID_slimgui, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_SLiMgui_Class}); // Do the same for the zero-tick functions, which should be defined in initialization() blocks and // not in other blocks; we add and remove them dynamically so they are defined as appropriate. We ought // to do this for other block-specific stuff as well (like the stuff below), but it is unlikely to matter. // Note that we consider the zero-gen functions to always be defined inside function blocks, since the // function might be called from the zero gen (we have no way of knowing definitively). if ((block_type == SLiMEidosBlockType::SLiMEidosInitializeCallback) || (block_type == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)) Community::AddZeroTickFunctionsToMap(**functionMap); else Community::RemoveZeroTickFunctionsFromMap(**functionMap); if (script_block_node == completion_block) { // This is the block we're actually completing in the context of; it is also the last block in the script // snippet that we're working with. We want to first define any callback-associated variables for the block. // Note that self is not defined inside functions, even though they are SLiMEidosBlocks; we pretend we are Eidos. if (block_type == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) (*typeTable)->RemoveTypeForSymbol(gID_self); else (*typeTable)->SetTypeForSymbol(gID_self, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_SLiMEidosBlock_Class}); switch (block_type) { case SLiMEidosBlockType::SLiMEidosEventFirst: break; case SLiMEidosBlockType::SLiMEidosEventEarly: break; case SLiMEidosBlockType::SLiMEidosEventLate: break; case SLiMEidosBlockType::SLiMEidosInitializeCallback: (*typeTable)->RemoveSymbolsOfClass(gSLiM_Subpopulation_Class); // subpops defined upstream from us still do not exist for us break; case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosMutationEffectCallback: (*typeTable)->SetTypeForSymbol(gID_mut, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Mutation_Class}); (*typeTable)->SetTypeForSymbol(gID_homozygous, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_effect, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosInteractionCallback: (*typeTable)->SetTypeForSymbol(gID_distance, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_strength, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_receiver, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_exerter, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosMateChoiceCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_sourceSubpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gEidosID_weights, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); break; case SLiMEidosBlockType::SLiMEidosModifyChildCallback: (*typeTable)->SetTypeForSymbol(gID_child, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_parent1, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_isCloning, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_isSelfing, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_parent2, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_sourceSubpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosRecombinationCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_haplosome1, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Haplosome_Class}); (*typeTable)->SetTypeForSymbol(gID_haplosome2, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Haplosome_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_breakpoints, EidosTypeSpecifier{kEidosValueMaskInt, nullptr}); break; case SLiMEidosBlockType::SLiMEidosMutationCallback: (*typeTable)->SetTypeForSymbol(gID_mut, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Mutation_Class}); (*typeTable)->SetTypeForSymbol(gID_parent, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_element, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_GenomicElement_Class}); (*typeTable)->SetTypeForSymbol(gID_haplosome, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Haplosome_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_originalNuc, EidosTypeSpecifier{kEidosValueMaskInt, nullptr}); break; case SLiMEidosBlockType::SLiMEidosSurvivalCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_surviving, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_fitness, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_draw, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); break; case SLiMEidosBlockType::SLiMEidosReproductionCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosUserDefinedFunction: { // Similar to the local variables that are defined for callbacks above, here we need to define the parameters to the // function, by parsing the relevant AST nodes; this is parallel to EidosTypeInterpreter::TypeEvaluate_FunctionDecl() EidosASTNode *function_declaration_node = script_block_node->children_[0]; const EidosASTNode *param_list_node = function_declaration_node->children_[2]; const std::vector ¶m_nodes = param_list_node->children_; std::vector used_param_names; for (EidosASTNode *param_node : param_nodes) { const std::vector ¶m_children = param_node->children_; int param_children_count = static_cast(param_children.size()); if ((param_children_count == 2) || (param_children_count == 3)) { EidosTypeSpecifier ¶m_type = param_children[0]->typespec_; const std::string ¶m_name = param_children[1]->token_->token_string_; // Check param_name; it needs to not be used by another parameter if (std::find(used_param_names.begin(), used_param_names.end(), param_name) != used_param_names.end()) continue; if (param_children_count >= 2) { // param_node has 2 or 3 children (type, identifier, [default]); we don't care about default values (*typeTable)->SetTypeForSymbol(EidosStringRegistry::GlobalStringIDForString(param_name), param_type); } } } break; } case SLiMEidosBlockType::SLiMEidosNoBlockType: break; // never hit } } if (script_block_node == completion_block) { // Make a type interpreter and add symbols to our type table using it // We use SLiMTypeInterpreter because we want to pick up definitions of SLiM constants SLiMTypeInterpreter typeInterpreter(block_statement_root, **typeTable, **functionMap, **callTypeTable); typeInterpreter.TypeEvaluateInterpreterBlock_AddArgumentCompletions(argNameCompletions, script_string.length()); // result not used return; } else { // This is not the block we're completing in. We want to add symbols for any constant-defining calls // in this block; apart from that, this block cannot affect the completion block, due to scoping. // However, constant-defining calls might use the types of variables, like defineConstant("foo", bar) // where the type of foo comes from the type of bar; so we need to keep track of all symbols even // though they will fall out of scope. We therefore use a separate local type table with a reference // upward to our main type table. EidosTypeTable scopedTypeTable(**typeTable); // Make a type interpreter and add symbols to our type table using it // We use SLiMTypeInterpreter because we want to pick up definitions of SLiM constants SLiMTypeInterpreter typeInterpreter(block_statement_root, scopedTypeTable, **functionMap, **callTypeTable); typeInterpreter.SetExternalTypeTable(*typeTable); // defined constants/variables should also go into the global scope typeInterpreter.TypeEvaluateInterpreterBlock(); // result not used } } } } } // We drop through to here if we have a bad or empty script root, or if the final script block (completion_block) didn't // have a compound statement (meaning its starting brace has not yet been typed), or if we're completing outside of any // existing script block. In these sorts of cases, we want to return completions for the outer level of a SLiM script. // This means that standard Eidos language keywords like "while", "next", etc. are not legal, but SLiM script block // keywords like "first", "early", "late", "mutationEffect", "fitnessEffect", "interaction", "mateChoice", "modifyChild", // "recombination", "mutation", "survival", and "reproduction" are. We also add "species" and "ticks" here for // multispecies models. // Note that the strings here are display strings; they are fixed to contain newlines in insertCompletion() keywords->clear(); (*keywords) << "initialize() { }"; (*keywords) << "first() { }"; (*keywords) << "early() { }"; (*keywords) << "late() { }"; (*keywords) << "mutationEffect() { }"; (*keywords) << "fitnessEffect() { }"; (*keywords) << "interaction() { }"; (*keywords) << "mateChoice() { }"; (*keywords) << "modifyChild() { }"; (*keywords) << "recombination() { }"; (*keywords) << "mutation() { }"; (*keywords) << "survival() { }"; (*keywords) << "reproduction() { }"; (*keywords) << "function (void)name(void) { }"; (*keywords) << "species"; (*keywords) << "ticks"; // At the outer level, functions are also not legal (*functionMap)->clear(); // And no variables exist except SLiM objects like pX, gX, mX, sX and species symbols std::vector symbol_ids = (*typeTable)->AllSymbolIDs(); for (EidosGlobalStringID symbol_id : symbol_ids) { EidosTypeSpecifier typeSpec = (*typeTable)->GetTypeForSymbol(symbol_id); if ((typeSpec.type_mask != kEidosValueMaskObject) || (typeSpec.object_class == gSLiM_Community_Class) || (typeSpec.object_class == gSLiM_SLiMgui_Class)) (*typeTable)->RemoveTypeForSymbol(symbol_id); } } //- (void)_completionHandlerWithRangeForCompletion:(NSRange *)baseRange completions:(NSArray **)completions void QtSLiMTextEdit::_completionHandlerWithRangeForCompletion(NSRange *baseRange, QStringList *completions) { QString scriptString; int selectionStart, selectionLength, rangeOffset; scriptStringAndSelection(scriptString, selectionStart, selectionLength, rangeOffset); NSRange selection = {selectionStart, selectionLength}; // ignore charRange and work from the selection int selStart = selection.location; //if (selStart != NSNotFound) // I don't think this can happen in Qt; you always have a text cursor... { // Get the substring up to the start of the selection; that is the range relevant for completion QString scriptSubstring = scriptString.left(selStart); std::string script_string(scriptSubstring.toStdString()); // Do shared completion processing that can be intercepted by our delegate: getting a type table for defined variables, // as well as a function map and any added language keywords, all of which depend upon the point of completion EidosTypeTable typeTable; EidosTypeTable *typeTablePtr = &typeTable; EidosFunctionMap functionMap(*EidosInterpreter::BuiltInFunctionMap()); EidosFunctionMap *functionMapPtr = &functionMap; EidosCallTypeTable callTypeTable; EidosCallTypeTable *callTypeTablePtr = &callTypeTable; QStringList keywords = {"break", "do", "else", "for", "if", "in", "next", "return", "while", "function"}; std::vector argumentCompletions; if (scriptType == ScriptType::SLiMScriptType) slimSpecificCompletion(scriptSubstring, selection, &typeTablePtr, &functionMapPtr, &callTypeTablePtr, &keywords, &argumentCompletions); // set up automatic disposal of a substitute type table or function map provided by delegate std::unique_ptr raii_typeTablePtr((typeTablePtr != &typeTable) ? typeTablePtr : nullptr); std::unique_ptr raii_functionMapPtr((functionMapPtr != &functionMap) ? functionMapPtr : nullptr); std::unique_ptr raii_callTypeTablePtr((callTypeTablePtr != &callTypeTable) ? callTypeTablePtr : nullptr); if (scriptType != ScriptType::SLiMScriptType) { // First, set up a base type table using the symbol table EidosSymbolTable *symbols = gEidosConstantsSymbolTable; symbols = symbolsFromBaseSymbols(symbols); if (symbols) symbols->AddSymbolsToTypeTable(typeTablePtr); // Next, a definitive function map that covers all functions defined in the entire script string (not just the script above // the completion point); this seems best, for mutually recursive functions etc.. Duplicate it back into functionMap and // delete the original, so we don't get confused. EidosFunctionMap *definitive_function_map = functionMapForScriptString(scriptString, false); functionMap = *definitive_function_map; delete definitive_function_map; // Next, add type table entries based on parsing and analysis of the user's code EidosScript script(script_string); script.Tokenize(true, false); // make bad tokens as needed, do not keep nonsignificant tokens script.ParseInterpreterBlockToAST(true, true); // make bad nodes as needed (i.e. never raise, and produce a correct tree) EidosTypeInterpreter typeInterpreter(script, *typeTablePtr, *functionMapPtr, *callTypeTablePtr); typeInterpreter.TypeEvaluateInterpreterBlock_AddArgumentCompletions(&argumentCompletions, script_string.length()); // result not used } // Tokenize; we can't use the tokenization done above, as we want whitespace tokens here... EidosScript script(script_string); script.Tokenize(true, true); // make bad tokens as needed, keep nonsignificant tokens const std::vector &tokens = script.Tokens(); int lastTokenIndex = static_cast(tokens.size()) - 1; bool endedCleanly = false, lastTokenInterrupted = false; // if we ended with an EOF, that means we did not have a raise and there should be no untokenizable range at the end if ((lastTokenIndex >= 0) && (tokens[static_cast(lastTokenIndex)].token_type_ == EidosTokenType::kTokenEOF)) { --lastTokenIndex; endedCleanly = true; } // if we are at the end of a comment, without whitespace following it, then we are actually in the comment, and cannot complete // BCH 5 August 2017: Note that EidosTokenType::kTokenCommentLong is deliberately omitted here; this rule does not apply to it if ((lastTokenIndex >= 0) && (tokens[static_cast(lastTokenIndex)].token_type_ == EidosTokenType::kTokenComment)) { if (baseRange) *baseRange = {NSNotFound, 0}; if (completions) *completions = QStringList(); return; } // if we ended with whitespace or a comment, the previous token cannot be extended while (lastTokenIndex >= 0) { const EidosToken &token = tokens[static_cast(lastTokenIndex)]; if ((token.token_type_ != EidosTokenType::kTokenWhitespace) && (token.token_type_ != EidosTokenType::kTokenComment) && (token.token_type_ != EidosTokenType::kTokenCommentLong)) break; --lastTokenIndex; lastTokenInterrupted = true; } // now diagnose what range we want to use as a basis for completion if (!endedCleanly) { // the selection is at the end of an untokenizable range; we might be in the middle of a string or a comment, // or there might be a tokenization error upstream of us. let's not try to guess what the situation is. if (baseRange) *baseRange = {NSNotFound, 0}; if (completions) *completions = QStringList(); return; } else { if (lastTokenIndex < 0) { // We're at the end of nothing but initial whitespace and comments; or if (!lastTokenInterrupted), // we're at the very beginning of the file. Either way, offer insertion-point completions. if (baseRange) *baseRange = {selection.location + rangeOffset, 0}; if (completions) *completions = globalCompletionsWithTypesFunctionsKeywordsArguments(typeTablePtr, functionMapPtr, keywords, QStringList()); return; } const EidosToken &token = tokens[static_cast(lastTokenIndex)]; EidosTokenType token_type = token.token_type_; // BCH 31 May 2016: If the previous token is a right-paren, that is a tricky case because we could be following // for(), an if(), or while (), in which case we should allow an identifier to follow the right paren, or we could // be following parentheses for grouping, i.e. (a+b), or parentheses for a function call, foo(), in which case we // should not allow an identifier to follow the right paren. This annoyance is basically because the right paren // serves a lot of different functions in the language and so just knowing that we are after one is not sufficient. // So we will walk backwards, balancing our parenthesis count, to try to figure out which case we are in. Note // that even this code is not quite right; it mischaracterizes the do...while() case as allowing an identifier to // follow, because it sees the "while". This is harder to fix, and do...while() is not a common construct, and // the mistake is pretty harmless, so whatever. if (token_type == EidosTokenType::kTokenRParen) { int parenCount = 1; int walkbackIndex = lastTokenIndex; // First walk back until our paren count balances while (--walkbackIndex >= 0) { const EidosToken &walkback_token = tokens[static_cast(walkbackIndex)]; EidosTokenType walkback_token_type = walkback_token.token_type_; if (walkback_token_type == EidosTokenType::kTokenRParen) parenCount++; else if (walkback_token_type == EidosTokenType::kTokenLParen) parenCount--; if (parenCount == 0) break; } // Then walk back over whitespace, and if the first non-white thing we see is right, allow completion while (--walkbackIndex >= 0) { const EidosToken &walkback_token = tokens[static_cast(walkbackIndex)]; EidosTokenType walkback_token_type = walkback_token.token_type_; if ((walkback_token_type != EidosTokenType::kTokenWhitespace) && (walkback_token_type != EidosTokenType::kTokenComment) && (walkback_token_type != EidosTokenType::kTokenCommentLong)) { if ((walkback_token_type == EidosTokenType::kTokenFor) || (walkback_token_type == EidosTokenType::kTokenWhile) || (walkback_token_type == EidosTokenType::kTokenIf)) { // We are at the end of for(), if(), or while(), so we allow global completions as if we were after a semicolon if (baseRange) *baseRange = {selection.location + rangeOffset, 0}; if (completions) *completions = globalCompletionsWithTypesFunctionsKeywordsArguments(typeTablePtr, functionMapPtr, keywords, QStringList()); return; } break; // we didn't hit one of the favored cases, so the code below will reject completion } } } if (lastTokenInterrupted) { // the last token cannot be extended, so if the last token is something an identifier can follow, like an // operator, then we can offer completions at the insertion point based on that, otherwise punt. if ((token_type == EidosTokenType::kTokenNumber) || (token_type == EidosTokenType::kTokenString) || (token_type == EidosTokenType::kTokenRParen) || (token_type == EidosTokenType::kTokenRBracket) || (token_type == EidosTokenType::kTokenIdentifier) || (token_type == EidosTokenType::kTokenIf) || (token_type == EidosTokenType::kTokenWhile) || (token_type == EidosTokenType::kTokenFor) || (token_type == EidosTokenType::kTokenNext) || (token_type == EidosTokenType::kTokenBreak) || (token_type == EidosTokenType::kTokenFunction)) { if (baseRange) *baseRange = {NSNotFound, 0}; if (completions) *completions = QStringList(); return; } if (baseRange) *baseRange = {selection.location + rangeOffset, 0}; if (completions) { QStringList argumentCompletionsArray = uniquedArgumentNameCompletions(&argumentCompletions); *completions = completionsForTokenStream(tokens, lastTokenIndex, false, typeTablePtr, functionMapPtr, callTypeTablePtr, keywords, argumentCompletionsArray); } return; } else { // the last token was not interrupted, so we can offer completions of it if we want to. NSRange tokenRange = {token.token_UTF16_start_, token.token_UTF16_end_ - token.token_UTF16_start_ + 1}; if (token_type >= EidosTokenType::kTokenIdentifier) { if (baseRange) *baseRange = {tokenRange.location + rangeOffset, tokenRange.length}; if (completions) { QStringList argumentCompletionsArray = uniquedArgumentNameCompletions(&argumentCompletions); *completions = completionsForTokenStream(tokens, lastTokenIndex, true, typeTablePtr, functionMapPtr, callTypeTablePtr, keywords, argumentCompletionsArray); } return; } if ((token_type == EidosTokenType::kTokenNumber) || (token_type == EidosTokenType::kTokenString) || (token_type == EidosTokenType::kTokenRParen) || (token_type == EidosTokenType::kTokenRBracket)) { if (baseRange) *baseRange = {NSNotFound, 0}; if (completions) *completions = QStringList(); return; } if (baseRange) *baseRange = {selection.location + rangeOffset, 0}; if (completions) { QStringList argumentCompletionsArray = uniquedArgumentNameCompletions(&argumentCompletions); *completions = completionsForTokenStream(tokens, lastTokenIndex, false, typeTablePtr, functionMapPtr, callTypeTablePtr, keywords, argumentCompletionsArray); } return; } } } } // // LineNumberArea // // This provides line numbers in QtSLiMScriptTextEdit // This code is adapted from https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html class LineNumberArea : public QWidget { public: LineNumberArea(QtSLiMScriptTextEdit *editor); virtual QSize sizeHint() const override { return QSize(codeEditor->lineNumberAreaWidth(), 0); } protected: virtual bool event(QEvent *p_event) override; virtual void paintEvent(QPaintEvent *p_paintEvent) override { codeEditor->lineNumberAreaPaintEvent(p_paintEvent); } virtual void mousePressEvent(QMouseEvent *p_mouseEvent) override { codeEditor->lineNumberAreaMousePressEvent(p_mouseEvent); } virtual void mouseMoveEvent(QMouseEvent *p_mouseEvent) override { codeEditor->lineNumberAreaMouseMoveEvent(p_mouseEvent); } virtual void mouseReleaseEvent(QMouseEvent *p_mouseEvent) override { codeEditor->lineNumberAreaMouseReleaseEvent(p_mouseEvent); } virtual void contextMenuEvent(QContextMenuEvent *p_event) override { codeEditor->lineNumberAreaContextMenuEvent(p_event); } virtual void wheelEvent(QWheelEvent *p_wheelEvent) override { codeEditor->lineNumberAreaWheelEvent(p_wheelEvent); } private: QtSLiMScriptTextEdit *codeEditor; }; LineNumberArea::LineNumberArea(QtSLiMScriptTextEdit *editor) : QWidget(editor), codeEditor(editor) { setMouseTracking(true); // for live tooltip updating (debug point gutter vs. line numbers) } bool LineNumberArea::event(QEvent *p_event) { if (p_event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(p_event); codeEditor->lineNumberAreaToolTipEvent(helpEvent); return true; } return QWidget::event(p_event); } // // QtSLiMScriptTextEdit // QtSLiMScriptTextEdit::QtSLiMScriptTextEdit(const QString &text, QWidget *p_parent) : QtSLiMTextEdit(text, p_parent) { sharedInit(); } QtSLiMScriptTextEdit::QtSLiMScriptTextEdit(QWidget *p_parent) : QtSLiMTextEdit(p_parent) { sharedInit(); } void QtSLiMScriptTextEdit::sharedInit(void) { setCenterOnScroll(true); initializeLineNumbers(); // set up to listen to changes to page guide prefs QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::pageGuidePrefsChanged, this, [this]() { viewport()->update(); }); } void QtSLiMScriptTextEdit::initializeLineNumbers(void) { // This code is adapted from https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html lineNumberArea = new LineNumberArea(this); connect(this->document(), SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int))); connect(this->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(updateLineNumberArea(int))); connect(this, SIGNAL(textChanged()), this, SLOT(updateLineNumberArea())); connect(this, SIGNAL(selectionChanged()), this, SLOT(updateLineNumberArea())); connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(updateLineNumberArea())); connect(this, &QtSLiMScriptTextEdit::selectionChanged, this, &QtSLiMScriptTextEdit::highlightCurrentLine); connect(this, &QtSLiMScriptTextEdit::cursorPositionChanged, this, &QtSLiMScriptTextEdit::highlightCurrentLine); connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, &QtSLiMScriptTextEdit::highlightCurrentLine); // Watch prefs for line numbering and highlighting QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::showLineNumbersPrefChanged, this, [this]() { updateLineNumberArea(); }); connect(&prefsNotifier, &QtSLiMPreferencesNotifier::highlightCurrentLinePrefChanged, this, [this]() { highlightCurrentLine(); }); updateLineNumberAreaWidth(0); highlightCurrentLine(); // We now set up to maintain our debugging icons here too connect(this, SIGNAL(textChanged()), this, SLOT(updateDebugPoints())); // Watch for changes to the controller's change count, so we can disable debug points when the document needs recycling // Also watch for the end of model initialization; species colors may have changed, necessitating an update QtSLiMWindow *controller = slimControllerForWindow(); if (controller) { connect(controller, &QtSLiMWindow::controllerChangeCountChanged, this, &QtSLiMScriptTextEdit::controllerChangeCountChanged); connect(controller, &QtSLiMWindow::controllerTickFinished, this, &QtSLiMScriptTextEdit::controllerTickFinished); } } QtSLiMScriptTextEdit::~QtSLiMScriptTextEdit() { } QStringList QtSLiMScriptTextEdit::linesForRoundedSelection(QTextCursor &p_cursor, bool &movedBack) { // find the start and end of the blocks we're operating on int anchor = p_cursor.anchor(), position = p_cursor.position(); if (anchor > position) std::swap(anchor, position); movedBack = false; QTextCursor startBlockCursor(p_cursor); startBlockCursor.setPosition(anchor, QTextCursor::MoveAnchor); startBlockCursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); QTextCursor endBlockCursor(p_cursor); endBlockCursor.setPosition(position, QTextCursor::MoveAnchor); if (endBlockCursor.atBlockStart() && (position > anchor)) { // the selection includes the newline at the end of the last line; we need to move backward to avoid swallowing the following line endBlockCursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor); movedBack = true; } endBlockCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor); // select the whole lines we're operating on p_cursor.beginEditBlock(); p_cursor.setPosition(startBlockCursor.position(), QTextCursor::MoveAnchor); p_cursor.setPosition(endBlockCursor.position(), QTextCursor::KeepAnchor); // separate the lines, remove a tab at the start of each, and rejoin them QString selectedString = p_cursor.selectedText(); static const QRegularExpression lineEndMatch("\\R", QRegularExpression::UseUnicodePropertiesOption); #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) return selectedString.split(lineEndMatch, QString::KeepEmptyParts); // deprecated in 5.14 #else return selectedString.split(lineEndMatch, Qt::KeepEmptyParts); // added in 5.14 #endif } void QtSLiMScriptTextEdit::copyAsHTML(void) { if (isEnabled()) { QString html = exportAsHtml(); QClipboard *clipboard = QGuiApplication::clipboard(); QMimeData *mimeData = new QMimeData; mimeData->setHtml(html); mimeData->setText(html); clipboard->setMimeData(mimeData); } } void QtSLiMScriptTextEdit::duplicateSelection(void) { if (isEnabled() && !isReadOnly()) { QTextCursor &&edit_cursor = textCursor(); QString selectedString = edit_cursor.selectedText(); int pasteStart = edit_cursor.selectionEnd(); edit_cursor.beginEditBlock(); edit_cursor.setPosition(pasteStart, QTextCursor::MoveAnchor); edit_cursor.insertText(selectedString); int pasteEnd = edit_cursor.position(); edit_cursor.setPosition(pasteStart, QTextCursor::MoveAnchor); edit_cursor.setPosition(pasteEnd, QTextCursor::KeepAnchor); // end the editing block, producing one undo-able operation edit_cursor.endEditBlock(); setTextCursor(edit_cursor); } else { qApp->beep(); } } void QtSLiMScriptTextEdit::shiftSelectionLeft(void) { if (isEnabled() && !isReadOnly()) { QTextCursor &&edit_cursor = textCursor(); bool movedBack; QStringList lines = linesForRoundedSelection(edit_cursor, movedBack); for (QString &line : lines) if (line.length() && (line[0] == '\t')) line.remove(0, 1); QString replacementString = lines.join(QChar::ParagraphSeparator); // end the editing block, producing one undo-able operation edit_cursor.insertText(replacementString); edit_cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, replacementString.length()); edit_cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, replacementString.length()); if (movedBack) edit_cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); edit_cursor.endEditBlock(); setTextCursor(edit_cursor); } else { qApp->beep(); } } void QtSLiMScriptTextEdit::shiftSelectionRight(void) { if (isEnabled() && !isReadOnly()) { QTextCursor &&edit_cursor = textCursor(); bool movedBack; QStringList lines = linesForRoundedSelection(edit_cursor, movedBack); for (QString &line : lines) line.insert(0, '\t'); QString replacementString = lines.join(QChar::ParagraphSeparator); // end the editing block, producing one undo-able operation edit_cursor.insertText(replacementString); edit_cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, replacementString.length()); edit_cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, replacementString.length()); if (movedBack) edit_cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); edit_cursor.endEditBlock(); setTextCursor(edit_cursor); } else { qApp->beep(); } } void QtSLiMScriptTextEdit::commentUncommentSelection(void) { if (isEnabled() && !isReadOnly()) { QTextCursor &&edit_cursor = textCursor(); bool movedBack; QStringList lines = linesForRoundedSelection(edit_cursor, movedBack); // decide whether we are commenting or uncommenting; we are only uncommenting if every line spanned by the selection starts with "//" bool uncommenting = true; for (QString &line : lines) { if (!line.startsWith("//")) { uncommenting = false; break; } } // now do the comment / uncomment if (uncommenting) { for (QString &line : lines) line.remove(0, 2); } else { for (QString &line : lines) line.insert(0, "//"); } QString replacementString = lines.join(QChar::ParagraphSeparator); // end the editing block, producing one undo-able operation edit_cursor.insertText(replacementString); edit_cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, replacementString.length()); edit_cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, replacementString.length()); if (movedBack) edit_cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); edit_cursor.endEditBlock(); setTextCursor(edit_cursor); } else { qApp->beep(); } } void QtSLiMScriptTextEdit::insertFromMimeData(const QMimeData *source) { // if the data pasted is text, we want to convert weird line breaks into newlines // note that the substitution done here strips off any other mime types in source, // but that seems fine; if there's text, we're going to paste text, so whatever. if (source && source->hasText()) { QString text = source->text(); // Unknown characters can be identified with https://www.babelstone.co.uk/Unicode/whatisit.html //qDebug() << "pasted:" << text; // Unicode "U+2028 : LINE SEPARATOR" is the one presently causing me problems, // but U+2029 should be replaced as well; we want vanilla non-unicode line ends. text.replace(QChar::LineSeparator, '\n'); text.replace(QChar::ParagraphSeparator, '\n'); //qDebug() << "substituted:" << text; QMimeData substitute; substitute.setText(text); QPlainTextEdit::insertFromMimeData(&substitute); return; } // call to super to do the work QPlainTextEdit::insertFromMimeData(source); } // From here down is the machinery for providing line numbers with LineNumberArea // This code is adapted from https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html int QtSLiMScriptTextEdit::lineNumberAreaWidth() { QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); // We now show debugging icons in the line number area too, since they are kept by line number // The line number area therefore no longer goes down to width 0 when the pref is disabled if (scriptType == SLiMScriptType) { #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) lineNumberAreaBugWidth = 3 + fontMetrics().width("9") * 2; // deprecated in 5.11 #else lineNumberAreaBugWidth = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * 2; // added in Qt 5.11 #endif } else { lineNumberAreaBugWidth = 0; } if (!prefsNotifier.showLineNumbersPref()) return lineNumberAreaBugWidth; int digits = 1; int max = qMax(1, document()->blockCount()); while (max >= 10) { max /= 10; ++digits; } #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) int space = 13 + fontMetrics().width("9") * digits; // deprecated in 5.11 #else int space = 13 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits; // added in Qt 5.11 #endif return lineNumberAreaBugWidth + space; } void QtSLiMScriptTextEdit::displayFontPrefChanged() { QtSLiMTextEdit::displayFontPrefChanged(); updateLineNumberArea(); } void QtSLiMScriptTextEdit::updateLineNumberAreaWidth(int /* newBlockCount */) { setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); } void QtSLiMScriptTextEdit::updateLineNumberArea(void) { QRect contents_rect = contentsRect(); lineNumberArea->update(0, contents_rect.y(), lineNumberArea->width(), contents_rect.height()); updateLineNumberAreaWidth(0); int dy = verticalScrollBar()->sliderPosition(); if (dy > -1) lineNumberArea->scroll(0, dy); } void QtSLiMScriptTextEdit::resizeEvent(QResizeEvent *e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); } void QtSLiMScriptTextEdit::toggleDebuggingForLine(int lineNumber) { if (scriptType != SLiMScriptType) return; // First figure out whether we have a debugging point at the line in question bool hasExistingCursor = false; //qDebug() << "toggleDebuggingForLine():" << lineNumber; //qDebug() << "block contents:" << document()->findBlockByNumber(lineNumber).text(); for (int cursorIndex = 0; cursorIndex < (int)bugCursors.size(); cursorIndex++) { QTextCursor &existingCursor = bugCursors[cursorIndex]; QTextBlock cursorBlock = existingCursor.block(); int blockNumber = cursorBlock.blockNumber(); if (blockNumber == lineNumber) { hasExistingCursor = true; bugCursors.erase(bugCursors.begin() + cursorIndex); --cursorIndex; } } if (hasExistingCursor) { // This line number had a debugging point; we cleared it above } else { // This line number does not currently have a debugging point; add one QTextDocument *doc = document(); QTextBlock block = doc->findBlockByNumber(lineNumber); QString blockText = block.text(); // Find the first non-whitespace character in the block; the debug point starts at that character int firstNonWhitespace = 0; for (firstNonWhitespace = 0; firstNonWhitespace < blockText.length(); ++firstNonWhitespace) { QChar qch = blockText[firstNonWhitespace]; if ((qch != ' ') && (qch != '\t')) break; } // If the block contains nothing but whitespace, decline to set a debugging point // This kind of makes sense semantically, and in any case if it's an empty line there's no text to put our cursor on if (firstNonWhitespace == blockText.length()) return; // Make a text cursor encompassing the remainder of the block, from the first non-whitespace character QTextCursor tc(block); tc.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, firstNonWhitespace); tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor, 1); // Remember the cursor as a debug point bugCursors.emplace_back(tc); } // Since the cursors changed, we need to recache our line numbers updateDebugPoints(); } void QtSLiMScriptTextEdit::trackLineSelection(int lineNumber, int anchorLineNumber) { QTextDocument *doc = document(); QTextBlock block = doc->findBlockByNumber(lineNumber); QTextCursor block_tc(block); QTextBlock anchor_block = doc->findBlockByNumber(anchorLineNumber); QTextCursor anchor_tc(anchor_block); block_tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor, 1); block_tc.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); int block_start = block_tc.selectionStart(); int block_end = block_tc.selectionEnd(); anchor_tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor, 1); anchor_tc.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); int anchor_start = anchor_tc.selectionStart(); int anchor_end = anchor_tc.selectionEnd(); QTextCursor selection_tc(anchor_tc); if (anchor_start <= block_start) { // the anchor is above the final line, so select from the anchor start to the final end selection_tc.setPosition(block_end, QTextCursor::KeepAnchor); } else { // the anchor is below the final line, so select from the anchor end to the final start selection_tc.setPosition(anchor_end, QTextCursor::MoveAnchor); selection_tc.setPosition(block_start, QTextCursor::KeepAnchor); } setTextCursor(selection_tc); } void QtSLiMScriptTextEdit::updateDebugPoints(void) { // prevent re-entrancy if (coloringDebugPointCursors) return; //qDebug() << "updateDebugPoints():" << bugCursors.size() << "debug cursors exist"; // Generate a new bugLines vector from our text cursors; we vet the cursors at the same time, // and remove any cursors that are zero-length, or that are now duplicates EidosInterpreterDebugPointsSet newBugLines; int bugCursorCount = (int)bugCursors.size(); for (int bugCursorIndex = 0; bugCursorIndex < bugCursorCount; ++bugCursorIndex) { QTextCursor &bugCursor = bugCursors[bugCursorIndex]; bool zeroLength = !bugCursor.hasSelection(); // Fix cursors to encompass their block; they can get out of whack due to typing // This is probably usually unnecessary, but it's a trivial amount of work if (!zeroLength) { bugCursor.setPosition(bugCursor.selectionStart(), QTextCursor::MoveAnchor); bugCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor, 1); } QTextBlock cursorBlock = bugCursor.block(); int blockNumber = cursorBlock.blockNumber(); if (!zeroLength && (newBugLines.set.find(blockNumber) == newBugLines.set.end())) { // this line is not already marked as debug; mark it now newBugLines.set.emplace(blockNumber); continue; } // discard a redundant or zero-length cursor; this generally results from lines being // merged together in the editor (redundant), or being deleted (zero-length) bugCursors.erase(bugCursors.begin() + bugCursorIndex); bugCursorIndex--; bugCursorCount--; } // Set a temporary color on our cursors for debugging #if 0 { coloringDebugPointCursors = true; QTextCharFormat clearFormat; QTextCharFormat highlightFormat; clearFormat.setBackground(QBrush(QColor(255, 255, 255))); highlightFormat.setBackground(QBrush(QColor(220, 255, 220))); QTextCursor tc(document()); tc.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); tc.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); tc.setCharFormat(clearFormat); for (QTextCursor &bugCursor : bugCursors) bugCursor.setCharFormat(highlightFormat); coloringDebugPointCursors = false; } #endif // If bugLines changed, we need to recache/redraw if (newBugLines.set != bugLines.set) { std::swap(newBugLines.set, bugLines.set); if (debugPointsEnabled) enabledBugLines.set = bugLines.set; else enabledBugLines.set.clear(); lineNumberArea->update(); } //qDebug() << " updateDebugPoints():" << bugLines.size() << "debug points set"; } void QtSLiMScriptTextEdit::controllerChangeCountChanged(int changeCount) { if (changeCount == 0) { // recycled and unedited; debug points enabled debugPointsEnabled = true; enabledBugLines.set = bugLines.set; lineNumberArea->update(); } else { // edited without a recycle; debug points disabled debugPointsEnabled = false; enabledBugLines.set.clear(); lineNumberArea->update(); } } void QtSLiMScriptTextEdit::controllerTickFinished(void) { // If we just finished initialize() callbacks, species colors may have changed QtSLiMWindow *controller = slimControllerForWindow(); if (controller && controller->community && (controller->community->Tick() == 1)) lineNumberArea->update(); } // light appearance: standard blue highlight static QColor lineHighlightColor = QtSLiMColorWithHSV(3.6/6.0, 0.1, 1.0, 1.0); // dark appearance: dark blue highlight static QColor lineHighlightColor_DARK = QtSLiMColorWithHSV(4.0/6.0, 0.5, 0.30, 1.0); void QtSLiMScriptTextEdit::highlightCurrentLine() { QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); QList extra_selections; if (!isReadOnly() && prefsNotifier.highlightCurrentLinePref()) { bool inDarkMode = QtSLiMInDarkMode(); QTextEdit::ExtraSelection selection; selection.format.setBackground(inDarkMode ? lineHighlightColor_DARK : lineHighlightColor); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); if (selection.cursor.hasSelection()) { // with a selection, we want to try to highlight the appropriate range // if the selection ends at a newline, we don't want to highlight the // next line; want want to highlight the lines that contain visible text int start = selection.cursor.selectionStart(); selection.cursor.setPosition(start, QTextCursor::MoveAnchor); // still have a movement bug when the selection changes, the highlight doesn't update correctly in some cases // check when/how the message is sent } //qDebug() << "highlightCurrentLine() with position" << selection.cursor.selectionStart(); extra_selections.append(selection); } setExtraSelections(extra_selections); } // light appearance static QColor bugAreaBackground = QtSLiMColorWithWhite(0.95, 1.0); static QColor lineAreaBackground = QtSLiMColorWithWhite(0.92, 1.0); static QColor lineAreaNumber = QtSLiMColorWithWhite(0.75, 1.0); static QColor lineAreaNumberCurrent = QtSLiMColorWithWhite(0.4, 1.0); // dark appearance static QColor bugAreaBackground_DARK = QtSLiMColorWithWhite(0.05, 1.0); static QColor lineAreaBackground_DARK = QtSLiMColorWithWhite(0.08, 1.0); static QColor lineAreaNumber_DARK = QtSLiMColorWithWhite(0.25, 1.0); static QColor lineAreaNumberCurrent_DARK = QtSLiMColorWithWhite(0.6, 1.0); void QtSLiMScriptTextEdit::lineNumberAreaToolTipEvent(QHelpEvent *p_helpEvent) { // provide different tooltips for the debug point gutter versus the line number area QPointF localPos = p_helpEvent->pos(); if ((lineNumberAreaBugWidth == 0) || ((localPos.x() < 0) || (localPos.x() >= lineNumberAreaBugWidth + 2))) // +2 for a little slop QToolTip::showText(p_helpEvent->globalPos(), "

script line numbers

"); else QToolTip::showText(p_helpEvent->globalPos(), "

debug point gutter (click to set/clear a debug point)

"); } void QtSLiMScriptTextEdit::lineNumberAreaPaintEvent(QPaintEvent *p_paintEvent) { QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); bool showBlockColors = true; bool showLineNumbers = prefsNotifier.showLineNumbersPref(); int bugCount = (int)bugLines.set.size(); // Fill the background with the appropriate colors QRect overallRect = contentsRect(); if (overallRect.width() <= 0) return; overallRect.setWidth(lineNumberArea->width()); QRect bugRect = overallRect; QRect lineNumberRect = overallRect; bugRect.setWidth(lineNumberAreaBugWidth); lineNumberRect.adjust(lineNumberAreaBugWidth, 0, 0, 0); bool inDarkMode = QtSLiMInDarkMode(); QPainter painter(lineNumberArea); // We now show slightly different background colors for the debug point gutter vs. the line number area //painter.fillRect(overallRect, inDarkMode ? lineAreaBackground_DARK : lineAreaBackground); painter.fillRect(bugRect, inDarkMode ? bugAreaBackground_DARK : bugAreaBackground); painter.fillRect(lineNumberRect, inDarkMode ? lineAreaBackground_DARK : lineAreaBackground); if ((bugCount == 0) && !showLineNumbers) return; static QIcon *bugIcon_LIGHT = nullptr; static QIcon *bugIcon_DARK = nullptr; if (!inDarkMode && !bugIcon_LIGHT && (bugCount > 0)) bugIcon_LIGHT = new QIcon(":/icons/bug.png"); if (inDarkMode && !bugIcon_DARK && (bugCount > 0)) bugIcon_DARK = new QIcon(":/icons/bug_DARK.png"); QIcon *bugIcon = (inDarkMode ? bugIcon_DARK : bugIcon_LIGHT); // Prepare to show block colors in multispecies mode; we translate into line numbers and colors on demand // We postpone this display until after initialize() so we don't show the default colors, which is weird std::vector blockColorStarts, blockColorEnds; std::vector blockColors; QtSLiMWindow *controller = slimControllerForWindow(); int blockColorCount = 0; if (showBlockColors && controller && controller->community && (controller->community->Tick() >= 1) && blockCursors.size()) { for (int index = 0; index < (int)blockCursors.size(); ++index) { QTextCursor &tc = blockCursors[index]; if (tc.hasSelection()) { QTextCursor start_tc(tc); QTextCursor end_tc(tc); start_tc.setPosition(start_tc.selectionStart(), QTextCursor::MoveAnchor); end_tc.setPosition(end_tc.selectionEnd(), QTextCursor::MoveAnchor); blockColorStarts.emplace_back(start_tc.block().blockNumber()); blockColorEnds.emplace_back(end_tc.block().blockNumber()); blockColors.emplace_back(controller->qcolorForSpecies(blockSpecies[index])); } } blockColorCount = blockColors.size(); } else { showBlockColors = false; } // Draw the numbers and bug symbols (displaying the current line number in col_1) QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int cursorBlockNumber = textCursor().blockNumber(); int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top()); int bottom = top + qRound(blockBoundingRect(block).height()); painter.setPen(inDarkMode ? lineAreaNumber_DARK : lineAreaNumber); painter.save(); painter.setRenderHint(QPainter::SmoothPixmapTransform); while (block.isValid() && (top <= p_paintEvent->rect().bottom())) { if (block.isVisible() && (bottom >= p_paintEvent->rect().top())) { if (showBlockColors) { for (int index = 0; index < blockColorCount; ++index) { int startBlockNumber = blockColorStarts[index]; int endBlockNumber = blockColorEnds[index]; if ((blockNumber >= startBlockNumber) && (blockNumber <= endBlockNumber)) { QRect speciesHighlightBounds(lineNumberRect.left() + lineNumberRect.width() - 4, top, 2, bottom - top); if (blockNumber == startBlockNumber) speciesHighlightBounds.adjust(0, 1, 0, 0); if (blockNumber == endBlockNumber) speciesHighlightBounds.adjust(0, 0, 0, -1); speciesHighlightBounds = speciesHighlightBounds.intersected(overallRect); painter.fillRect(speciesHighlightBounds, blockColors[index]); break; } } } if (showLineNumbers) { if (cursorBlockNumber == blockNumber) painter.setPen(inDarkMode ? lineAreaNumberCurrent_DARK : lineAreaNumberCurrent); QString number = QString::number(blockNumber + 1); painter.drawText(-7, top, lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); if (cursorBlockNumber == blockNumber) painter.setPen(inDarkMode ? lineAreaNumber_DARK : lineAreaNumber); } if (bugCount) { if (bugLines.set.find(blockNumber) != bugLines.set.end()) { QRect bugIconBounds(bugRect.left(), top, bugRect.width(), bottom - top); //painter.fillRect(bugIconBounds, Qt::green); // enforce square bounds for drawing if (bugIconBounds.width() != bugIconBounds.height()) { int iconWidth = bugIconBounds.width(); int iconHeight = bugIconBounds.height(); int adjust = std::abs(iconWidth - iconHeight); int halfAdjust = adjust / 2; int remainder = adjust - halfAdjust; if (iconWidth > iconHeight) bugIconBounds.adjust(halfAdjust, 0, -remainder, 0); else bugIconBounds.adjust(0, halfAdjust, 0, -remainder); } // shrink the bug at large sizes if (bugIconBounds.width() > 24) bugIconBounds.adjust(3, 3, -3, -3); else if (bugIconBounds.width() > 20) bugIconBounds.adjust(2, 2, -2, -2); else if (bugIconBounds.width() > 16) bugIconBounds.adjust(1, 1, -1, -1); // shift left slightly if we can if (bugIconBounds.left() > bugRect.left()) bugIconBounds.adjust(-1, 0, -1, 0); //qDebug() << "bugIconBounds ==" << bugIconBounds; //painter.fillRect(bugIconBounds, Qt::red); if (debugPointsEnabled) bugIcon->paint(&painter, bugIconBounds, Qt::AlignCenter, QIcon::Normal, QIcon::Off); else bugIcon->paint(&painter, bugIconBounds, Qt::AlignCenter, QIcon::Disabled, QIcon::Off); } } } block = block.next(); top = bottom; bottom = top + qRound(blockBoundingRect(block).height()); ++blockNumber; } painter.restore(); } int QtSLiMScriptTextEdit::_blockNumberForLocalY(QPointF localPos) { qreal localY = localPos.y(); //qDebug() << "localY ==" << localY; // Find the position of the click in the document. We loop through the blocks manually. QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top()); int bottom = top + qRound(blockBoundingRect(block).height()); while (block.isValid()) { if (block.isVisible()) { if ((localY >= top) && (localY <= bottom)) { return blockNumber; } else if (localY < top) { return -1; } } block = block.next(); top = bottom; bottom = top + qRound(blockBoundingRect(block).height()); ++blockNumber; } return -1; } void QtSLiMScriptTextEdit::lineNumberAreaMousePressEvent(QMouseEvent *p_mouseEvent) { if (lineNumberAreaBugWidth == 0) return; // For some reason, Qt calls mousePressEvent() first for control-clicks and right-clicks, // and *then* calls contextMenuEvent(), so we need to detect the context menu situation // and return without doing anything. Note that Qt::RightButton is set for control-clicks! if (p_mouseEvent->button() == Qt::RightButton) return; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPointF localPos = p_mouseEvent->localPos(); #else QPointF localPos = p_mouseEvent->position(); #endif if (localPos.x() < 0) return; int blockNumber = _blockNumberForLocalY(localPos); if (blockNumber != -1) { if (localPos.x() >= lineNumberAreaBugWidth) { // This is a click in the line number area, so select the line trackingLineSelection = true; trackingLineAnchor = blockNumber; trackLineSelection(blockNumber, trackingLineAnchor); } else { // This is a click in the debug point column, so toggle debugging toggleDebuggingForLine(blockNumber); } } } void QtSLiMScriptTextEdit::lineNumberAreaMouseMoveEvent(QMouseEvent *p_mouseEvent) { if (!trackingLineSelection) return; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPointF localPos = p_mouseEvent->localPos(); #else QPointF localPos = p_mouseEvent->position(); #endif int blockNumber = _blockNumberForLocalY(localPos); if (blockNumber != -1) { trackLineSelection(blockNumber, trackingLineAnchor); } } void QtSLiMScriptTextEdit::lineNumberAreaMouseReleaseEvent(QMouseEvent *p_mouseEvent) { if (!trackingLineSelection) return; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPointF localPos = p_mouseEvent->localPos(); #else QPointF localPos = p_mouseEvent->position(); #endif int blockNumber = _blockNumberForLocalY(localPos); if (blockNumber != -1) { trackLineSelection(blockNumber, trackingLineAnchor); } trackingLineSelection = false; trackingLineAnchor = -1; } void QtSLiMScriptTextEdit::lineNumberAreaContextMenuEvent(QContextMenuEvent *p_event) { if (lineNumberAreaBugWidth == 0) return; p_event->accept(); QMenu contextMenu("line_area_menu", this); QAction *clearDebugPointsAction = contextMenu.addAction("Clear Debug Points"); // Run the context menu synchronously QAction *action = contextMenu.exec(p_event->globalPos()); // Act upon the chosen action; we just do it right here instead of dealing with slots if (action) { if (action == clearDebugPointsAction) clearDebugPoints(); } } void QtSLiMScriptTextEdit::lineNumberAreaWheelEvent(QWheelEvent *p_wheelEvent) { // We want wheel events in the debug/line number area to scroll the script textview, so it forwards them to us here // I'm not sure of the legality of this, since the event is tailored for a different widget, but it seems to work... wheelEvent(p_wheelEvent); } void QtSLiMScriptTextEdit::clearDebugPoints(void) { bugCursors.clear(); updateDebugPoints(); } void QtSLiMScriptTextEdit::clearScriptBlockColoring(void) { blockCursors.clear(); blockSpecies.clear(); } void QtSLiMScriptTextEdit::addScriptBlockColoring(int startPos, int endPos, Species *species) { QTextCursor tc(document()); tc.setPosition(startPos, QTextCursor::MoveAnchor); tc.setPosition(endPos, QTextCursor::KeepAnchor); blockCursors.emplace_back(tc); blockSpecies.emplace_back(species); } // this method courtesy of SO user Larswad, https://stackoverflow.com/a/15808889/2752221 QString QtSLiMScriptTextEdit::exportAsHtml(void) { // Create a new document from the entire document QTextCursor cursor(document()); cursor.select(QTextCursor::Document); QTextDocument *tempDocument = new QTextDocument; QTextCursor tempCursor(tempDocument); tempCursor.insertFragment(cursor.selection()); tempCursor.select(QTextCursor::Document); // Set the default foreground for the inserted characters QTextCharFormat textfmt = tempCursor.charFormat(); textfmt.setForeground(Qt::black); tempCursor.setCharFormat(textfmt); // Apply the additional formats set by the syntax highlighter QTextBlock start = document()->findBlock(cursor.selectionStart()); QTextBlock end = document()->findBlock(cursor.selectionEnd()); end = end.next(); const int selectionStart = cursor.selectionStart(); const int endOfDocument = tempDocument->characterCount() - 1; for (QTextBlock current = start; current.isValid() and current not_eq end; current = current.next()) { const QTextLayout* layout(current.layout()); foreach (const QTextLayout::FormatRange &range, layout->formats()) { const int formatStart = current.position() + range.start - selectionStart; const int formatEnd = formatStart + range.length; if(formatEnd <= 0 or formatStart >= endOfDocument) continue; tempCursor.setPosition(qMax(formatStart, 0)); tempCursor.setPosition(qMin(formatEnd, endOfDocument), QTextCursor::KeepAnchor); tempCursor.setCharFormat(range.format); } } // Reset the user states since they are not interesting for (QTextBlock block = tempDocument->begin(); block.isValid(); block = block.next()) block.setUserState(-1); // Make sure the text appears pre-formatted, and set the background we want tempCursor.select(QTextCursor::Document); QTextBlockFormat blockFormat = tempCursor.blockFormat(); blockFormat.setNonBreakableLines(true); //blockFormat.setBackground(Qt::black); tempCursor.setBlockFormat(blockFormat); // Select the same range with tempCursor as is selected in the original document QTextCursor selectedRange = textCursor(); tempCursor.setPosition(selectedRange.anchor(), QTextCursor::MoveAnchor); tempCursor.setPosition(selectedRange.position(), QTextCursor::KeepAnchor); // Retrieve the syntax highlighted and formatted html QString html = tempCursor.selection().toHtml(); delete tempDocument; // There is a bug where the first line uses

instead of

, because
    // it is a fragment, I guess.  We can fix that with a regex.
    QRegularExpression pToPreRegex("

"); pToPreRegex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); html.replace(pToPreRegex, "

");
    
    // Change new paragraphs to new lines, so we get one paragraph of text
    // This doesn't work, you always get a new paragraph for each line; 
doesn't work either. //QRegularExpression preRegex("
.?.?
");
    //preRegex.setPatternOptions(QRegularExpression::DotMatchesEverythingOption | QRegularExpression::CaseInsensitiveOption);
    //html.replace(preRegex, "\n");
    
    return html;
}

void QtSLiMScriptTextEdit::paintEvent(QPaintEvent *event)
{
    // If the user wants a "page guide", show a slightly dimmed margin beyond a threshold column
    // Note that Qt has already cleared to the white background of the QTextEdit
    QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance();
    bool showPageGuide = prefs.showPageGuidePref();
    
    if (showPageGuide)
    {
        QFont displayFont = prefs.displayFontPref();
        QFontMetricsF fm(displayFont);
        double marginStart;
        QString marginString(prefs.pageGuideColumnPref(), ' ');
        
#if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0))
        marginStart = fm.width(marginString);                // deprecated in 5.11
#else
        marginStart = fm.horizontalAdvance(marginString);    // added in Qt 5.11
#endif
        
        // adjust by the document margin, which is built into QTextDocument; this took a while to find!
        // this is an inset of the QTextEdit's contents, on all four sides; it defaults to 4, which we do not change
        QTextDocument *doc = document();
        double docMargin = doc->documentMargin();
        
        marginStart += docMargin;
        marginStart = round(marginStart);
        
        QRect bounds = rect();
        
        if (bounds.width() >= marginStart)
        {
            // Because QTextEdit's display lives inside a scrollable area, we use viewport()
            QPainter painter(this->viewport());
            
            QRect margin = bounds.adjusted(marginStart, 0, 0, 0);
            QRect marginEdge = margin;
            
            // draw a one-pixel darker line at the border
            marginEdge.setWidth(1);
            margin.adjust(1, 0, 0, 0);
            
            painter.fillRect(marginEdge, QtSLiMColorWithWhite(0.918, 1.0));
            painter.fillRect(margin, QtSLiMColorWithWhite(0.980, 1.0));
        }
    }
    
    // call super to have it paint; this draws all the text and everything
    QtSLiMTextEdit::paintEvent(event);
}



































================================================
FILE: QtSLiM/QtSLiMScriptTextEdit.h
================================================
//
//  QtSLiMScriptTextEdit.h
//  SLiM
//
//  Created by Ben Haller on 11/24/2019.
//  Copyright (c) 2019-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .

#ifndef QTSLIMSCRIPTTEXTEDIT_H
#define QTSLIMSCRIPTTEXTEDIT_H

#include 
#include 
#include      // for QT_VERSION

#include "eidos_interpreter.h"
#include "eidos_type_interpreter.h"

class QtSLiMOutputHighlighter;
class QtSLiMScriptHighlighter;
class QStatusBar;
class EidosCallSignature;
class QtSLiMWindow;
class QtSLiMEidosConsole;
class QCompleter;
class QHelpEvent;
class QPaintEvent;
class QMouseEvent;

class Species;


// We define NSRange and NSNotFound because we want to be able to represent "no range",
// which QTextCursor cannot do; there is no equivalent of NSNotFound for it.  We should
// never be linked with Objective-C code, so there should be no conflict.  Note these
// definitions are not the same as Apple's; I kept the names just for convenience.

typedef struct _NSRange {
    int location;
    int length;
} NSRange;

static const int NSNotFound = INT_MAX;


// A QPlainTextEdit subclass that provides a signal for an option-click on a script
// symbol.  This also provides some basic smarts for syntax coloring and so forth.
// QtSLiMScriptTextEdit and QtSLiMConsoleTextEdit are both subclasses of this class.
class QtSLiMTextEdit : public QPlainTextEdit
{
    Q_OBJECT
    
public:
    enum ScriptType {
        NoScriptType = 0,
        EidosScriptType,
        SLiMScriptType
    };
    enum ScriptHighlightingType {
        NoHighlighting = 0,
        ScriptHighlighting,
        OutputHighlighting
    };
    
    QtSLiMTextEdit(const QString &text, QWidget *p_parent = nullptr);
    QtSLiMTextEdit(QWidget *p_parent = nullptr);
    virtual ~QtSLiMTextEdit() override;
    
    // configuration
    void setScriptType(ScriptType type);
    void setSyntaxHighlightType(ScriptHighlightingType type);
    void setOptionClickEnabled(bool enabled);
    void setCodeCompletionEnabled(bool enabled);
    
    // highlight errors
    void highlightError(int startPosition, int endPosition);   
    void selectErrorRange(EidosErrorContext &errorContext);
    QPalette qtslimStandardPalette(void);
    QPalette qtslimErrorPalette(void);
    
    // undo/redo availability; named to avoid future collision with QPlainTextEdit for this obvious feature
    bool qtslimIsUndoAvailable(void) { return undoAvailable_; }
    bool qtslimIsRedoAvailable(void) { return redoAvailable_; }
    bool qtslimIsCopyAvailable(void) { return copyAvailable_; }
    
public slots:
    void checkScript(void);
    void prettyprint(void);
    void reformat(void);
    void prettyprintClicked(void);      // decides whether to reformat or prettyprint based on the option key state
    
signals:
    
protected:
    ScriptType scriptType = ScriptType::NoScriptType;
    ScriptHighlightingType syntaxHighlightingType = QtSLiMTextEdit::NoHighlighting;
    QtSLiMOutputHighlighter *outputHighlighter = nullptr;
    QtSLiMScriptHighlighter *scriptHighlighter = nullptr;
    
    void selfInit(void);
    QStatusBar *statusBarForWindow(void);
    QtSLiMWindow *slimControllerForWindow(void);
    QtSLiMEidosConsole *slimEidosConsoleForWindow(void);
    
    // used to track that we are intercepting a mouse event
    bool optionClickEnabled = false;
    bool optionClickIntercepted = false;
    virtual void mousePressEvent(QMouseEvent *p_event) override;
    virtual void mouseMoveEvent(QMouseEvent *p_event) override;
    virtual void mouseReleaseEvent(QMouseEvent *p_event) override;
    void scriptHelpOptionClick(QString searchString);
    
    // used to maintain the correct cursor (pointing hand when option is pressed)
    void fixMouseCursor(void);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    virtual void enterEvent(QEnterEvent *p_event) override;
#else
    virtual void enterEvent(QEvent *p_event) override;
#endif
    
    // keeping track of undo/redo availability
    bool undoAvailable_ = false;
    bool redoAvailable_ = false;
    bool copyAvailable_ = false;
    
    // used for code completion in script textviews
    bool codeCompletionEnabled = false;
    QCompleter *completer = nullptr;
    
    void autoindentAfterNewline(void);
    virtual void keyPressEvent(QKeyEvent *e) override;
    QStringList completionsForPartialWordRange(NSRange charRange, int *indexOfSelectedItem);
    NSRange rangeForUserCompletion(void);
    
    QStringList globalCompletionsWithTypesFunctionsKeywordsArguments(EidosTypeTable *typeTable, EidosFunctionMap *functionMap, QStringList keywords, QStringList argumentNames);
    QStringList completionsForKeyPathEndingInTokenIndexOfTokenStream(int lastDotTokenIndex, const std::vector &tokens, EidosTypeTable *typeTable, EidosFunctionMap *functionMap, EidosCallTypeTable *callTypeTable, QStringList keywords);
    QStringList completionsFromArrayMatchingBase(QStringList candidates, QString base);
    QStringList completionsForTokenStream(const std::vector &tokens, int lastTokenIndex, bool canExtend, EidosTypeTable *typeTable, EidosFunctionMap *functionMap, EidosCallTypeTable *callTypeTable, QStringList keywords, QStringList argumentNames);
    QStringList uniquedArgumentNameCompletions(std::vector *argumentCompletions);
    void _completionHandlerWithRangeForCompletion(NSRange *baseRange, QStringList *completions);
    int64_t scoreForCandidateAsCompletionOfString(QString candidate, QString base);
    void slimSpecificCompletion(QString completionScriptString, NSRange selection, EidosTypeTable **typeTable, EidosFunctionMap **functionMap, EidosCallTypeTable **callTypeTable, QStringList *keywords, std::vector *argNameCompletions);

    EidosFunctionMap *functionMapForScriptString(QString scriptString, bool includingOptionalFunctions);
    EidosFunctionMap *functionMapForTokenizedScript(EidosScript &script, bool includingOptionalFunctions);
    EidosSymbolTable *symbolsFromBaseSymbols(EidosSymbolTable *baseSymbols);
    
    // status bar signatures
    EidosFunctionSignature_CSP signatureForFunctionName(QString callName, EidosFunctionMap *functionMapPtr);
    const std::vector *slimguiAllMethodSignatures(void);
    EidosMethodSignature_CSP signatureForMethodName(QString callName);
    EidosCallSignature_CSP signatureForScriptSelection(QString &callName);
    
    // virtual function for accessing the script portion of the contents; for normal
    // script textedits this is the whole content and the text cursor, but for the
    // console view it is just the snippet of script following the prompt
    virtual void scriptStringAndSelection(QString &scriptString, int &pos, int &len, int &offset);
    
protected slots:
    virtual void displayFontPrefChanged();
    void scriptSyntaxHighlightPrefChanged();
    void outputSyntaxHighlightPrefChanged();
    bool checkScriptSuppressSuccessResponse(bool suppressSuccessResponse);
    void modifiersChanged(Qt::KeyboardModifiers newModifiers);
    
    void updateStatusFieldFromSelection(void);
    
    void insertCompletion(const QString &completion);
    void _prettyprint_reformat(bool reformat);
};

// A QtSLiMTextEdit subclass that provides various smarts for editing Eidos script
class QtSLiMScriptTextEdit : public QtSLiMTextEdit
{
    Q_OBJECT
    
public:
    QtSLiMScriptTextEdit(const QString &text, QWidget *p_parent = nullptr);
    QtSLiMScriptTextEdit(QWidget *p_parent = nullptr);
    virtual ~QtSLiMScriptTextEdit() override;
    
    EidosInterpreterDebugPointsSet &debuggingPoints(void) { return enabledBugLines; }
    
    void clearScriptBlockColoring(void);
    void addScriptBlockColoring(int startPos, int endPos, Species *species);
    
    QString exportAsHtml(void);
    
    virtual void insertFromMimeData(const QMimeData *source) override;
    
public slots:
    void copyAsHTML(void);
    void duplicateSelection(void);
    void shiftSelectionLeft(void);
    void shiftSelectionRight(void);
    void commentUncommentSelection(void);
    void clearDebugPoints(void);
    
protected:
    QStringList linesForRoundedSelection(QTextCursor &cursor, bool &movedBack);
    int _blockNumberForLocalY(QPointF localPos);
    
    // From here down is the machinery for providing line numbers
    // This code is adapted from https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
public:
    void lineNumberAreaToolTipEvent(QHelpEvent *p_helpEvent);
    void lineNumberAreaPaintEvent(QPaintEvent *p_paintEvent);
    void lineNumberAreaMousePressEvent(QMouseEvent *p_mouseEvent);
    void lineNumberAreaMouseMoveEvent(QMouseEvent *p_mouseEvent);
    void lineNumberAreaMouseReleaseEvent(QMouseEvent *p_mouseEvent);
    void lineNumberAreaContextMenuEvent(QContextMenuEvent *p_event);
    void lineNumberAreaWheelEvent(QWheelEvent *p_wheelEvent);
    int lineNumberAreaWidth(void);

protected:
    void sharedInit(void);
    void initializeLineNumbers(void);
    virtual void resizeEvent(QResizeEvent *p_event) override;
    virtual void paintEvent(QPaintEvent *event) override;

protected slots:
    virtual void displayFontPrefChanged() override;
    
private slots:
    void updateLineNumberAreaWidth(int newBlockCount);
    void highlightCurrentLine(void);
    void updateLineNumberArea(QRectF /*rect_f*/) { updateLineNumberArea(); }
    void updateLineNumberArea(int /*slider_pos*/) { updateLineNumberArea(); }
    void updateLineNumberArea(void);
    void updateDebugPoints(void);
    void controllerChangeCountChanged(int changeCount);
    void controllerTickFinished(void);

private:
    QWidget *lineNumberArea;
    
    // Debug points
    int lineNumberAreaBugWidth = 0;
    std::vector bugCursors;                // we use QTextCursor to maintain the positions of debugging points across edits
    EidosInterpreterDebugPointsSet bugLines;            // line numbers for debug points; std::unordered_set or equivalent
    EidosInterpreterDebugPointsSet enabledBugLines;     // for public consumption; empty when debug points are disabled
    bool debugPointsEnabled = true;                     // disabled when edited without a recycle (recycle button is green)
    bool coloringDebugPointCursors = false;             // a flag to prevent re-entrancy
    
    void toggleDebuggingForLine(int lineNumber);
    
    // Line selection by clicking in the line area
    void trackLineSelection(int lineNumber, int anchorLineNumber);
    
    bool trackingLineSelection = false;
    int trackingLineAnchor = 0;
    
    // Species coloring
    std::vector blockCursors;              // we use QTextCursor to maintain the positions of script blocks across edits
    std::vector blockSpecies;                // the corresponding Species object for each block
};


#endif // QTSLIMSCRIPTTEXTEDIT_H
































================================================
FILE: QtSLiM/QtSLiMSyntaxHighlighting.cpp
================================================
//
//  QtSLiMSyntaxHighlighting.cpp
//  SLiM
//
//  Created by Ben Haller on 8/4/2019.
//  Copyright (c) 2019-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .


#include "QtSLiMSyntaxHighlighting.h"
#include "QtSLiMExtras.h"
#include "QtSLiMAppDelegate.h"

#include 
#include 
#include 
#include 

#include 
#include 

#include "eidos_script.h"
#include "slim_globals.h"


//
//  QtSLiMOutputHighlighter
//

QtSLiMOutputHighlighter::QtSLiMOutputHighlighter(QTextDocument *p_parent) :
    QSyntaxHighlighter(p_parent),
    poundRegex(QString("^\\s*#[^\\n]*")),
    commentRegex(QString("//[^\\n]*")),
    globalRegex(QString("\\b[pgm][0-9]+\\b"))
{
    // listen for changes to the application color palette (i.e., dark mode vs. light mode)
    connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, &QtSLiMOutputHighlighter::paletteChanged);
    paletteChanged();
}

void QtSLiMOutputHighlighter::paletteChanged(void)
{
    bool inDarkMode = QtSLiMInDarkMode();
    
    if (!cachedTextFormats || (cachedForDarkMode != inDarkMode))
    {
        poundDirectiveFormat.setForeground(inDarkMode ? QColor(220, 98, 90) : QColor(196, 26, 22));
        commentFormat.setForeground(inDarkMode ? QColor(90, 210, 90) : QColor(0, 116, 0));
        subpopFormat.setForeground(inDarkMode ? QColor(115, 145, 255) : QColor(28, 0, 207));
        genomicElementFormat.setForeground(inDarkMode ? QColor(70, 205, 216) : QColor(63, 110, 116));
        mutationTypeFormat.setForeground(inDarkMode ? QColor(220, 83, 185) : QColor(170, 13, 145));
        
        cachedTextFormats = true;
        
        if (cachedForDarkMode != inDarkMode)
        {
            cachedForDarkMode = inDarkMode;
            rehighlight();
        }
    }
}

void QtSLiMOutputHighlighter::highlightBlock(const QString &text)
{
    if (text.length())
    {
        // highlight globals first; if they occur inside pound or comment regions, their format will be overwritten
        {
            QRegularExpressionMatchIterator matchIterator = globalRegex.globalMatch(text);
            while (matchIterator.hasNext()) {
                QRegularExpressionMatch match = matchIterator.next();
                QString matchString = match.captured();
                
                if (matchString.length() > 0)
                {
                    if (matchString[0] == 'p')
                        setFormat(match.capturedStart(), match.capturedLength(), subpopFormat);
                    else if (matchString[0] == 'g')
                        setFormat(match.capturedStart(), match.capturedLength(), genomicElementFormat);
                    else if (matchString[0] == 'm')
                        setFormat(match.capturedStart(), match.capturedLength(), mutationTypeFormat);
					// we don't presently color sX or iX in the output
                }
            }
        }
        
        // highlight pound lines next, since that overrides the previous coloring rules
        {
            QRegularExpressionMatchIterator matchIterator = poundRegex.globalMatch(text);
            while (matchIterator.hasNext()) {
                QRegularExpressionMatch match = matchIterator.next();
                setFormat(match.capturedStart(), match.capturedLength(), poundDirectiveFormat);
            }
        }
        
        // highlight comments last, since there is no syntax coloring inside them
        {
            QRegularExpressionMatchIterator matchIterator = commentRegex.globalMatch(text);
            while (matchIterator.hasNext()) {
                QRegularExpressionMatch match = matchIterator.next();
                setFormat(match.capturedStart(), match.capturedLength(), commentFormat);
            }
        }
    }
}


//
//  QtSLiMScriptHighlighter
//

QtSLiMScriptHighlighter::QtSLiMScriptHighlighter(QTextDocument *p_parent) : QSyntaxHighlighter(p_parent)
{
    // listen for changes to our document's contents
    // FIXME technically we need to recache and stuff if setDocument() is called, but we never do that in QtSLiM
    connect(p_parent, &QTextDocument::contentsChanged, this, &QtSLiMScriptHighlighter::documentContentsChanged);
    
    // listen for changes to the application color palette (i.e., dark mode vs. light mode)
    connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, &QtSLiMScriptHighlighter::paletteChanged);
    paletteChanged();
}

void QtSLiMScriptHighlighter::paletteChanged(void)
{
    bool inDarkMode = QtSLiMInDarkMode();
    
    if (!cachedTextFormats || (cachedForDarkMode != inDarkMode))
    {
        numberLiteralFormat.setForeground(inDarkMode ? QColor(115, 145, 255) : QColor(28, 0, 207));
        stringLiteralFormat.setForeground(inDarkMode ? QColor(220, 98, 90) : QColor(196, 26, 22));
        commentFormat.setForeground(inDarkMode ? QColor(90, 210, 90) : QColor(0, 116, 0));
        identifierFormat.setForeground(inDarkMode ? QColor(70, 205, 216) : QColor(63, 110, 116));
        keywordFormat.setForeground(inDarkMode ? QColor(220, 83, 185) : QColor(170, 13, 145));
        contextKeywordFormat.setForeground(QColor(80, 13, 145));    // not used at present
        
        cachedTextFormats = true;
        
        if (cachedForDarkMode != inDarkMode)
        {
            cachedForDarkMode = inDarkMode;
            rehighlight();
        }
    }
}

QtSLiMScriptHighlighter::~QtSLiMScriptHighlighter()
{
    // throw out our cached information about the document
    documentContentsChanged();
}

void QtSLiMScriptHighlighter::documentContentsChanged(void)
{
    // Note that this is called by highlightBlock() below, as well as by the QTextDocument::contentsChanged signal
    if (script)
    {
        delete script;
        script = nullptr;
        
        lastProcessedTokenIndex = -1;
    }
}

void QtSLiMScriptHighlighter::highlightBlock(__attribute__((__unused__)) const QString &text)
{
    //qDebug() << "highlightBlock : " << text;
    
    const QTextBlock &block = currentBlock();
    int pos = block.position(), len = block.length();
    
    // Unfortunately, when setPlainText() gets called on the document's textedit, it does not send us
    // a contentsChanged signal until *after* it has asked us to do all of the syntax highlighting
    // for the new script.  So that signal is useless to us, and we have to look for a change ourselves
    // instead, by comparing the script string we have cached to the current script string.  This is
    // not great, since it requires a comparison of the entire script string, which will usually be
    // unchanged.  We optimize by doing this check only when we've been asked to highlight the very
    // first block; when setPlainText() is called, highlighting will proceed from the beginning.
    if (script && (pos == 0))
    {
        const std::string &cached_script_string = script->String();
        const std::string real_script_string = document()->toPlainText().toUtf8().constData();
        
        if (cached_script_string.compare(real_script_string) != 0)
        {
            //qDebug() << "cached script is wrong!";
            documentContentsChanged();
        }
    }
    
    // set up a new cached script if we don't have one
    if (!script)
    {
        script = new EidosScript(document()->toPlainText().toUtf8().constData());
        
        script->Tokenize(true, true);	// make bad tokens as needed, keep nonsignificant tokens
        
        //qDebug() << "   tokenized...";
    }
    
    const std::vector &tokens = script->Tokens();
    int64_t token_count = static_cast(tokens.size());
    int64_t token_index = 0;
    
    if ((lastProcessedTokenIndex != -1) && (lastProcessedTokenIndex < token_count))
    {
        // check whether we can skip tokens processed by earlier calls to highlightBlock,
        // avoiding having to do an O(N) scan for each block, which would be O(N^2) overall
        const EidosToken &lastProcessedToken = tokens[static_cast(lastProcessedTokenIndex)];
        
        if (lastProcessedToken.token_UTF16_end_ < pos)
            token_index = lastProcessedTokenIndex;
    }
    
    //qDebug() << "   token_count == " << token_count << ", initial token_index == " << token_index;
    
    for (; token_index < token_count; ++token_index)
    {
        const EidosToken &token = tokens[static_cast(token_index)];
        
        // a token that starts after the end of the current block means we're done
        int token_start = token.token_UTF16_start_;
        
        if (token_start >= pos + len)
            break;
        
        // a token that ends before the start of the current block means we haven't reached our work yet
        int token_end = token.token_UTF16_end_;
        
        if (token_end < pos)
        {
            lastProcessedTokenIndex = token_index;
            continue;
        }
        
        // remember that we processed this token, unless it extends beyond the end of this block (as whitespace and comments can, among others)
        if (token_end < pos + len)
            lastProcessedTokenIndex = token_index;
        
        // otherwise, the token is in this block and should be colored; from here on, token_start and token_end are within-block positions
        // note that a token might start before this block and extend into it, or extend past the end of this block, so we clip
        token_start -= pos;
        token_end -= pos;
        
        if (token_start < 0)
            token_start = 0;
        if (token_end >= len)
            token_end = len - 1;
        
        if (token.token_type_ >= EidosTokenType::kFirstIdentifierLikeToken)
        {
            setFormat(token_start, token_end - token_start + 1, keywordFormat);
            continue;
        }
        
        switch (token.token_type_)
        {
        case EidosTokenType::kTokenNumber:
            setFormat(token_start, token_end - token_start + 1, numberLiteralFormat);
            break;
        case EidosTokenType::kTokenString:
            setFormat(token_start, token_end - token_start + 1, stringLiteralFormat);
            break;
        case EidosTokenType::kTokenComment:
        case EidosTokenType::kTokenCommentLong:
            setFormat(token_start, token_end - token_start + 1, commentFormat);
            break;
        case EidosTokenType::kTokenIdentifier:
        {
            // most identifiers are left as black; only special ones get colored
            const std::string &token_string = token.token_string_;
            
            if ((token_string.compare("T") == 0) ||
                    (token_string.compare("F") == 0) ||
                    (token_string.compare("E") == 0) ||
                    (token_string.compare("PI") == 0) ||
                    (token_string.compare("INF") == 0) ||
                    (token_string.compare("NAN") == 0) ||
                    (token_string.compare("NULL") == 0))
            {
                setFormat(token_start, token_end - token_start + 1, identifierFormat);
            }
            else
            {
                // Here we handle SLiM-specific syntax coloring, beyond the Eidos coloring done above
                // This is from -[SLiMWindowController eidosConsoleWindowController:tokenStringIsSpecialIdentifier:]
                if (token_string.compare(gStr_community) == 0)
                    setFormat(token_start, token_end - token_start + 1, identifierFormat);
                else if (token_string.compare(gStr_sim) == 0)
                    setFormat(token_start, token_end - token_start + 1, identifierFormat);
                else if (token_string.compare(gStr_slimgui) == 0)
                    setFormat(token_start, token_end - token_start + 1, identifierFormat);
                // -[SLiMWindowController eidosConsoleWindowController:tokenStringIsSpecialIdentifier:] has code
                // here to give a special color (contextKeywordFormat) to the various keywords for callbacks, like
                // "mutationEffect", "initialize", etc.; it is commented out and I don't think we want it
                else
                {
                    int token_length = static_cast(token_string.length());
                    
                    if (token_length >= 2)
                    {
                        char first_ch = token_string[0];
                        
                        if ((first_ch == 'p') || (first_ch == 'g') || (first_ch == 'm') || (first_ch == 's') || (first_ch == 'i'))
                        {
                            bool is_slim_identifier = true;
                            
                            for (int ch_index = 1; ch_index < token_length; ++ch_index)
                            {
                                char idx_ch = token_string[static_cast(ch_index)];
                                
                                if ((idx_ch < '0') || (idx_ch > '9'))
                                {
                                    is_slim_identifier = false;
                                    break;
                                }
                            }
                            
                            if (is_slim_identifier)
                                setFormat(token_start, token_end - token_start + 1, identifierFormat);
                        }
                    }
                    
                }
            }
            break;
        }
        default:
            break;
        }
    }
    
    // Here we deliberately break an optimization in QSyntaxHighlighter.  It uses these block states to
    // determine whether a rehighlight of one block needs to cascade to the next block; for example, a
    // new '/*' inserted in one block might cause the next block to become a comment.  We are not set
    // up to represent such states explicitly for QSyntaxHighlighter's benefit, so we just always poke
    // the block state so that QSyntaxHighlighter always recolors the following blocks, all the way to
    // the end of the script.  This is a bit unfortunate, but in practice it doesn't seem to produce
    // noticeable performance issues, and if it does the user can always turn off syntax coloring.
    setCurrentBlockState(currentBlockState() + 1);
}


































================================================
FILE: QtSLiM/QtSLiMSyntaxHighlighting.h
================================================
//
//  QtSLiMSyntaxHighlighting.h
//  SLiM
//
//  Created by Ben Haller on 8/4/2019.
//  Copyright (c) 2019-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .

#ifndef QTSLIMSYNTAXHIGHLIGHTING_H
#define QTSLIMSYNTAXHIGHLIGHTING_H

#include 
#include 
#include 

class QString;
class QTextDocument;
class EidosScript;


// This one is for the output pane, and is regex-driven
class QtSLiMOutputHighlighter : public QSyntaxHighlighter
{
    Q_OBJECT

public:
    QtSLiMOutputHighlighter(QTextDocument *p_parent = nullptr);

protected:
    virtual void highlightBlock(const QString &text) override;

protected slots:
    void paletteChanged(void);
    
private:
    bool cachedTextFormats = false;
    bool cachedForDarkMode = false;
    
    QRegularExpression poundRegex;
    QTextCharFormat poundDirectiveFormat;
    
    QRegularExpression commentRegex;
    QTextCharFormat commentFormat;
    
    QRegularExpression globalRegex;
    QTextCharFormat subpopFormat;
    QTextCharFormat genomicElementFormat;
    QTextCharFormat mutationTypeFormat;
};

// This one is for the scripting pane, and is AST-driven
class QtSLiMScriptHighlighter : public QSyntaxHighlighter
{
    Q_OBJECT

public:
    QtSLiMScriptHighlighter(QTextDocument *p_parent = nullptr);
    virtual ~QtSLiMScriptHighlighter() override;

protected:
    virtual void highlightBlock(const QString &text) override;
    
protected slots:
    void documentContentsChanged(void);
    void paletteChanged(void);
    
private:
    bool cachedTextFormats = false;
    bool cachedForDarkMode = false;
    QTextCharFormat numberLiteralFormat;
    QTextCharFormat stringLiteralFormat;
    QTextCharFormat commentFormat;
    QTextCharFormat identifierFormat;
    QTextCharFormat keywordFormat;
    QTextCharFormat contextKeywordFormat;
    
    EidosScript *script = nullptr;
    int64_t lastProcessedTokenIndex = -1;
};


#endif // QTSLIMSYNTAXHIGHLIGHTING_H































================================================
FILE: QtSLiM/QtSLiMTablesDrawer.cpp
================================================
//
//  QtSLiMTablesDrawer.cpp
//  SLiM
//
//  Created by Ben Haller on 2/22/2020.
//  Copyright (c) 2020-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .


#include "QtSLiMTablesDrawer.h"
#include "ui_QtSLiMTablesDrawer.h"

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#include "QtSLiMWindow.h"
#include "QtSLiMExtras.h"
#include "QtSLiMAppDelegate.h"

#include "mutation_type.h"
#include "interaction_type.h"
#include "eidos_rng.h"

#include 


// a helper function for making the tooltip images in the mutation and interaction type tables
// this corresponds to SLiMgui's -[SLiMFunctionGraphToolTipView drawRect:]
static QImage imageForMutationOrInteractionType(MutationType *mut_type, InteractionType *interaction_type)
{
    QImage image = QImage(154, 100, QImage::Format_ARGB32);     // double resolution, for high-resolution displays
    QPainter painter(&image);
    
    painter.scale(2.0, 2.0);    // compensate for high resolution
    painter.setRenderHint(QPainter::Antialiasing);
    
    QRect bounds(0, 0, 77, 50);
    
    // Flip coordinate system to match that in SLiMgui, for easy porting
    painter.translate(0, 50);
    painter.scale(1.0, -1.0);
    
    // Frame and fill our tooltip rect
    painter.fillRect(bounds, QtSLiMColorWithWhite(0.95, 1.0));
    //QtSLiMFrameRect(bounds, QtSLiMColorWithWhite(0.75, 1.0), painter);  // not used, since Qt gives our tooltip a frame
    
    // Plan our plotting
	if ((!mut_type && !interaction_type) || (mut_type && interaction_type))
		return image;
	
	size_t sample_size;
	std::vector draws;
	bool draw_positive = false, draw_negative = false;
	bool heights_negative = false;
	double axis_min, axis_max;
	bool draw_axis_midpoint = true, custom_axis_max = false;
	
	if (mut_type)
	{
		// Generate draws for a mutation type; this case is stochastic, based upon a large number of DFE samples.
		// Draw all the values we will plot; we need our own private RNG so we don't screw up the simulation's.
		// Drawing selection coefficients could raise, if they are type "s" and there is an error in the script,
		// so we run the sampling inside a try/catch block; if we get a raise, we just show a "?" in the plot.
		static bool rng_initialized = false;
		static Eidos_RNG_State local_rng;
		
		sample_size = (mut_type->dfe_type_ == DFEType::kScript) ? 100000 : 1000000;	// large enough to make curves pretty smooth, small enough to be reasonably fast
		draws.reserve(sample_size);
		
		if (!rng_initialized)
		{
			_Eidos_InitializeOneRNG(local_rng);
			rng_initialized = true;
		}
		
		_Eidos_SetOneRNGSeed(local_rng, 10);		// arbitrary seed, but the same seed every time
		
		std::swap(local_rng, gEidos_RNG_SINGLE);	// swap in our local RNG for DrawSelectionCoefficient()
		
		//std::clock_t start = std::clock();
		
		try
		{
			for (size_t sample_count = 0; sample_count < sample_size; ++sample_count)
			{
				double draw = mut_type->DrawSelectionCoefficient();
				
				draws.emplace_back(draw);
				
				if (draw < 0.0)			draw_negative = true;
				else if (draw > 0.0)	draw_positive = true;
			}
		}
		catch (...)
		{
			draws.clear();
			draw_negative = true;
			draw_positive = true;
		}
		
		//NSLog(@"Draws took %f seconds", (std::clock() - start) / (double)CLOCKS_PER_SEC);
		
		std::swap(local_rng, gEidos_RNG_SINGLE);	// swap out our local RNG; restore the standard RNG
		
		// figure out axis limits
		if (draw_negative && !draw_positive)
		{
			axis_min = -1.0;
			axis_max = 0.0;
		}
		else if (draw_positive && !draw_negative)
		{
			axis_min = 0.0;
			axis_max = 1.0;
		}
		else
		{
			axis_min = -1.0;
			axis_max = 1.0;
		}
	}
	else // if (interaction_type)
	{
		// Since interaction types are deterministic, we don't need draws; we will just calculate our
		// bin heights directly below.
		sample_size = 0;
		draw_negative = false;
		draw_positive = true;
		axis_min = 0.0;
		if ((interaction_type->max_distance_ < 1.0) || std::isinf(interaction_type->max_distance_))
		{
			axis_max = 1.0;
		}
		else
		{
			axis_max = interaction_type->max_distance_;
			draw_axis_midpoint = false;
			custom_axis_max = true;
		}
		heights_negative = (interaction_type->if_param1_ < 0.0);	// this is a negative-strength interaction, if T
	}
	
	// Draw the graph axes and ticks
    QRect graphRect(bounds.x() + 6, bounds.y() + (heights_negative ? 5 : 14), bounds.width() - 12, bounds.height() - 20);
	int axis_y = (heights_negative ? graphRect.y() + graphRect.height() - 1 : graphRect.y());
	int tickoff3 = (heights_negative ? 1 : -3);
	int tickoff1 = (heights_negative ? 1 : -1);
	QColor axisColor = QtSLiMColorWithWhite(0.2, 1.0);
    
    painter.fillRect(QRect(graphRect.x(), axis_y, graphRect.width(), 1), axisColor);
	
	painter.fillRect(QRect(graphRect.x(), axis_y + tickoff3, 1, 3), axisColor);
	painter.fillRect(QRect(graphRect.x() + qRound((graphRect.width() - 1) * 0.125), axis_y + tickoff1, 1, 1), axisColor);
	painter.fillRect(QRect(graphRect.x() + qRound((graphRect.width() - 1) * 0.25), axis_y + tickoff1, 1, 1), axisColor);
	painter.fillRect(QRect(graphRect.x() + qRound((graphRect.width() - 1) * 0.375), axis_y + tickoff1, 1, 1), axisColor);
	painter.fillRect(QRect(graphRect.x() + qRound((graphRect.width() - 1) * 0.5), axis_y + tickoff3, 1, 3), axisColor);
	painter.fillRect(QRect(graphRect.x() + qRound((graphRect.width() - 1) * 0.625), axis_y + tickoff1, 1, 1), axisColor);
	painter.fillRect(QRect(graphRect.x() + qRound((graphRect.width() - 1) * 0.75), axis_y + tickoff1, 1, 1), axisColor);
	painter.fillRect(QRect(graphRect.x() + qRound((graphRect.width() - 1) * 0.875), axis_y + tickoff1, 1, 1), axisColor);
	painter.fillRect(QRect(graphRect.x() + graphRect.width() - 1, axis_y + tickoff3, 1, 3), axisColor);
    
    // Draw the axis labels
#ifdef __linux__
    painter.setFont(QFont("Times New Roman", 14));  // 7, but double scale
#else
    painter.setFont(QFont("Times New Roman", 18));  // 9, but double scale
#endif
    
    std::ostringstream ss;
	ss << axis_max;
	std::string ss_str = ss.str();
	QString axis_max_pretty_string = QString::fromStdString(ss_str);
	QString axis_min_label = (axis_min == 0.0 ? "0" : "−1");
	QString axis_half_label = (axis_min == 0.0 ? "0.5" : (axis_max == 0.0 ? "−0.5" : "0"));
	QString axis_max_label = (custom_axis_max ? axis_max_pretty_string : (axis_max == 0.0 ? "0" : "1"));
    double min_label_width = painter.boundingRect(QRectF(), 0, axis_min_label).width() / 2.0;       // /2.0 to compensate for scaled font size
    double half_label_width = painter.boundingRect(QRectF(), 0, axis_half_label).width() / 2.0;
    double max_label_width = painter.boundingRect(QRectF(), 0, axis_max_label).width() / 2.0;
    double min_label_halfwidth = min_label_width / 2.0;
    double half_label_halfwidth = half_label_width / 2.0;
    double max_label_halfwidth = max_label_width / 2.0;
    double label_y = (heights_negative ? bounds.y() + bounds.height() - 11 : bounds.y() + 2);
    QPointF min_label_point = painter.transform().map(QPointF(bounds.x() + 7 - min_label_halfwidth, label_y));
    QPointF half_label_point = painter.transform().map(QPointF(bounds.x() + 39 - half_label_halfwidth, label_y));
    QPointF max_label_point = painter.transform().map(QPointF(custom_axis_max ? bounds.x() + 72 - max_label_width : bounds.x() + 71 - max_label_halfwidth, label_y));
    
    painter.setWorldMatrixEnabled(false);
    painter.drawText(min_label_point, axis_min_label);
    if (draw_axis_midpoint)
        painter.drawText(half_label_point, axis_half_label);
    painter.drawText(max_label_point, axis_max_label);
    painter.setWorldMatrixEnabled(true);
    
    // If we had an exception while drawing values, just show a question mark and return
	if (mut_type && !draws.size())
	{
#ifdef __linux__
        painter.setFont(QFont("Times New Roman", 28));  // 14, but double scale
#else
        painter.setFont(QFont("Times New Roman", 36));  // 18, but double scale
#endif
        
        QString labelText("?");
        int labelWidth = painter.boundingRect(QRect(), 0, labelText).width();
        double labelX = bounds.x() + qRound((bounds.width() - labelWidth / 2.0) / 2.0); // inner /2.0 compensates for the double-scaled font size, which QPainter does not do, oddly
        double labelY = bounds.y() + 22;
        QPointF labelPoint = painter.transform().map(QPointF(labelX, labelY));
        
        painter.setWorldMatrixEnabled(false);
        painter.drawText(labelPoint, labelText);
        painter.setWorldMatrixEnabled(true);
	}
    
    QRect interiorRect(graphRect.x(), graphRect.y() + (heights_negative ? 0 : 2), graphRect.width(), graphRect.height() - 2);
	
	// Tabulate the distribution from the samples we took; the math here is a bit subtle, because when we are doing a -1 to +1 axis
	// we want those values to fall at bin centers, but when we're doing 0 to +1 or -1 to 0 we want 0 to fall at the bin edge.
	int half_bin_count = interiorRect.width();
	int bin_count = half_bin_count * 2;								// 2x bins to look nice on Retina displays
	double *bins = static_cast(calloc(static_cast(bin_count), sizeof(double)));
	
	if (sample_size)
	{
		// sample-based tabulation into a histogram; mutation types only, right now
		for (size_t sample_count = 0; sample_count < sample_size; ++sample_count)
		{
			double sel_coeff = draws[sample_count];
			int bin_index;
			
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wfloat-equal"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wfloat-equal"
			if ((axis_min == -1.0) && (axis_max == 1.0))
				bin_index = static_cast(floor(((sel_coeff + 1.0) / 2.0) * (bin_count - 1) + 0.5));
			else if ((axis_min == -1.0) && (axis_max == 0.0))
				bin_index = static_cast(ceil((sel_coeff + 1.0) * (bin_count - 1 - 0.5) + 0.5));		// 0.0 maps to bin_count - 1, -1.0 maps to the center of bin 0
			else // if ((axis_min == 0.0) && (axis_max == 1.0))
				bin_index = static_cast(floor(sel_coeff * (bin_count - 1 + 0.5)));					// 0.0 maps to 0, 1.0 maps to the center of bin_count - 1
#pragma clang diagnostic pop
#pragma GCC diagnostic pop
			
			if ((bin_index >= 0) && (bin_index < bin_count))
				bins[bin_index]++;
		}
	}
	else
	{
		// non-sample-based construction of a function by evaluation; interaction types only, right now
		double max_x = interaction_type->max_distance_;
		
		for (int bin_index = 0; bin_index < bin_count; ++bin_index)
		{
			double bin_left = (bin_index / static_cast(bin_count)) * axis_max;
			double bin_right = ((bin_index + 1) / static_cast(bin_count)) * axis_max;
			double total_value = 0.0;
			
			for (int evaluate_index = 0; evaluate_index <= 999; ++evaluate_index)
			{
				double evaluate_x = bin_left + (bin_right - bin_left) / 999;
				
				if (evaluate_x < max_x)
					total_value += interaction_type->CalculateStrengthNoCallbacks(evaluate_x);
			}
			
			bins[bin_index] = total_value / 1000.0;
		}
	}
	
    // If we only have samples equal to zero, replicate the center column for symmetry
	if (!draw_positive && !draw_negative)
	{
		double zero_count = std::max(bins[half_bin_count - 1], bins[half_bin_count]);	// whichever way it rounds...
		
		bins[half_bin_count - 1] = zero_count;
		bins[half_bin_count] = zero_count;
	}
	
	// Find the maximum-magnitude bin count
	double max_bin = 0;
	
	if (heights_negative)
	{
		for (int bin_index = 0; bin_index < bin_count; ++bin_index)
			max_bin = std::min(max_bin, bins[bin_index]);
	}
	else
	{
		for (int bin_index = 0; bin_index < bin_count; ++bin_index)
			max_bin = std::max(max_bin, bins[bin_index]);
	}
	
    // Plot the bins
    QColor plotColor = Qt::black;
	
	if (heights_negative)
	{
		for (int bin_index = 0; bin_index < bin_count; ++bin_index)
		{
			if (bins[bin_index] < 0)
			{
				double height = interiorRect.height() * (bins[bin_index] / max_bin);
				
                painter.fillRect(QRectF(interiorRect.x() + bin_index * 0.5, interiorRect.y() + interiorRect.height() - height, 0.5, height), plotColor);
			}
		}
	}
	else
	{
		for (int bin_index = 0; bin_index < bin_count; ++bin_index)
		{
			if (bins[bin_index] > 0)
                painter.fillRect(QRectF(interiorRect.x() + bin_index * 0.5, interiorRect.y(), 0.5, interiorRect.height() * (bins[bin_index] / max_bin)), plotColor);
		}
	}
	
	free(bins);
    return image;
}
    

//
//  QtSLiMTablesDrawer
//

QtSLiMTablesDrawer::QtSLiMTablesDrawer(QtSLiMWindow *p_parent) :
    QWidget(p_parent, Qt::Window),
    parentSLiMWindow(p_parent),
    ui(new Ui::QtSLiMTablesDrawer)
{
    ui->setupUi(this);
    initializeUI();
}

QtSLiMTablesDrawer::~QtSLiMTablesDrawer()
{
    delete ui;
}

QHeaderView *QtSLiMTablesDrawer::configureTableView(QTableView *tableView)
{
    QHeaderView *tableHHeader = tableView->horizontalHeader();
    QHeaderView *tableVHeader = tableView->verticalHeader();
    
    tableHHeader->setMinimumSectionSize(1);
    tableVHeader->setMinimumSectionSize(1);
    
    tableHHeader->setSectionsClickable(false);
    tableHHeader->setSectionsMovable(false);
    
    QFont headerFont = tableHHeader->font();
    QFont cellFont = tableView->font();
#ifdef __linux__
    headerFont.setPointSize(8);
    cellFont.setPointSize(8);
#else
    headerFont.setPointSize(11);
    cellFont.setPointSize(11);
#endif
    tableHHeader->setFont(headerFont);
    tableView->setFont(cellFont);
    
    tableVHeader->setSectionResizeMode(QHeaderView::Fixed);
    tableVHeader->setDefaultSectionSize(18);
    
    return tableHHeader;
}

void QtSLiMTablesDrawer::initializeUI(void)
{
    // no window icon
#ifdef __APPLE__
    // set the window icon only on macOS; on Linux it changes the app icon as a side effect
    setWindowIcon(QIcon());
#endif
    
    // prevent this window from keeping the app running when all main windows are closed
    setAttribute(Qt::WA_QuitOnClose, false);
    
    // Make the models for the tables; this is a sort of datasource concept, except
    // that because C++ is not sufficiently dynamic it has to be a separate object
    mutTypeTableModel_ = new QtSLiMMutTypeTableModel(parentSLiMWindow);
    ui->mutationTypeTable->setModel(mutTypeTableModel_);
    
    geTypeTableModel_ = new QtSLiMGETypeTypeTableModel(parentSLiMWindow);
    ui->genomicElementTypeTable->setModel(geTypeTableModel_);

    interactionTypeTableModel_ = new QtSLiMInteractionTypeTableModel(parentSLiMWindow);
    ui->interactionTypeTable->setModel(interactionTypeTableModel_);

    eidosBlockTableModel_ = new QtSLiMEidosBlockTableModel(parentSLiMWindow);
    ui->eidosBlockTable->setModel(eidosBlockTableModel_);
    
    // Configure the table views, then set column widths and sizing behavior
    {
        QHeaderView *mutTypeTableHHeader = configureTableView(ui->mutationTypeTable);
        
        mutTypeTableHHeader->resizeSection(0, 53);
        mutTypeTableHHeader->resizeSection(1, 43);
        mutTypeTableHHeader->resizeSection(2, 53);
        //mutTypeTableHHeader->resizeSection(3, ?);
        mutTypeTableHHeader->setSectionResizeMode(0, QHeaderView::Fixed);
        mutTypeTableHHeader->setSectionResizeMode(1, QHeaderView::Fixed);
        mutTypeTableHHeader->setSectionResizeMode(2, QHeaderView::Fixed);
        mutTypeTableHHeader->setSectionResizeMode(3, QHeaderView::Stretch);
        
        // pre-configure for our image tooltips with an off-white background
        ui->mutationTypeTable->setStyleSheet("QToolTip{border: 0px; padding: 0px; margin-top: 1px; background-color: '#F2F2F2'; opacity: 255;}");
    }
    {
        QHeaderView *geTypeTableHHeader = configureTableView(ui->genomicElementTypeTable);
        
        geTypeTableHHeader->resizeSection(0, 53);
        geTypeTableHHeader->resizeSection(1, 43);
        //geTypeTableHHeader->resizeSection(2, ?);
        geTypeTableHHeader->setSectionResizeMode(0, QHeaderView::Fixed);
        geTypeTableHHeader->setSectionResizeMode(1, QHeaderView::Fixed);
        geTypeTableHHeader->setSectionResizeMode(2, QHeaderView::Stretch);
        
        QAbstractItemDelegate *tableDelegate = new QtSLiMGETypeTypeTableDelegate(ui->genomicElementTypeTable);
        ui->genomicElementTypeTable->setItemDelegate(tableDelegate);
    }
    {
        QHeaderView *interactionTypeTableHHeader = configureTableView(ui->interactionTypeTable);
        
        interactionTypeTableHHeader->resizeSection(0, 53);
        interactionTypeTableHHeader->resizeSection(1, 43);
        interactionTypeTableHHeader->resizeSection(2, 53);
        //interactionTypeTableHHeader->resizeSection(3, ?);
        interactionTypeTableHHeader->setSectionResizeMode(0, QHeaderView::Fixed);
        interactionTypeTableHHeader->setSectionResizeMode(1, QHeaderView::Fixed);
        interactionTypeTableHHeader->setSectionResizeMode(2, QHeaderView::Fixed);
        interactionTypeTableHHeader->setSectionResizeMode(3, QHeaderView::Stretch);
        
        // pre-configure for our image tooltips with an off-white background
        ui->interactionTypeTable->setStyleSheet("QToolTip{border: 0px; padding: 0px; margin-top: 1px; background-color: '#F2F2F2'; opacity: 255;}");
    }
    {
        QHeaderView *eidosBlockTableHHeader = configureTableView(ui->eidosBlockTable);
        
        eidosBlockTableHHeader->resizeSection(0, 53);
        eidosBlockTableHHeader->resizeSection(1, 63);
        eidosBlockTableHHeader->resizeSection(2, 63);
        //eidosBlockTableHHeader->resizeSection(3, ?);
        eidosBlockTableHHeader->setSectionResizeMode(0, QHeaderView::Fixed);
        eidosBlockTableHHeader->setSectionResizeMode(1, QHeaderView::Fixed);
        eidosBlockTableHHeader->setSectionResizeMode(2, QHeaderView::Fixed);
        eidosBlockTableHHeader->setSectionResizeMode(3, QHeaderView::Stretch);
    }
    
    // make window actions for all global menu items
    qtSLiMAppDelegate->addActionsForGlobalMenuItems(this);
}

void QtSLiMTablesDrawer::closeEvent(QCloseEvent *p_event)
{
    // send our close signal
    emit willClose();
    
    // use super's default behavior
    QWidget::closeEvent(p_event);
}


//
//  Define models for the four table views
//

QtSLiMMutTypeTableModel::QtSLiMMutTypeTableModel(QObject *p_parent) : QAbstractTableModel(p_parent)
{
    // p_parent must be a pointer to QtSLiMWindow, which holds our model information
    if (dynamic_cast(p_parent) == nullptr)
        throw p_parent;
}

QtSLiMMutTypeTableModel::~QtSLiMMutTypeTableModel()
{
}

int QtSLiMMutTypeTableModel::rowCount(const QModelIndex & /* p_parent */) const
{
    QtSLiMWindow *controller = static_cast(parent());
    Community *community = controller->community;
    
    if (community)
        return static_cast(community->AllMutationTypes().size());
    
    return 0;
}

int QtSLiMMutTypeTableModel::columnCount(const QModelIndex & /* p_parent */) const
{
    return 4;
}

QVariant QtSLiMMutTypeTableModel::data(const QModelIndex &p_index, int role) const
{
    if (!p_index.isValid())
        return QVariant();
    
    QtSLiMWindow *controller = static_cast(parent());
    Community *community = controller->community;
    
    if (!community)
        return QVariant();
    
    if (role == Qt::DisplayRole)
    {
        const std::map &mutationTypes = community->AllMutationTypes();
        int mutationTypeCount = static_cast(mutationTypes.size());
        
        if (p_index.row() < mutationTypeCount)
        {
            auto mutTypeIter = mutationTypes.begin();
            
            std::advance(mutTypeIter, p_index.row());
            slim_objectid_t mutTypeID = mutTypeIter->first;
            MutationType *mutationType = mutTypeIter->second;
            
            if (p_index.column() == 0)
            {
                QString idString = QString("m%1").arg(mutTypeID);
                
                if (community->all_species_.size() > 1)
                    idString.append(" ").append(QString::fromStdString(mutationType->species_.avatar_));
                
                return QVariant(idString);
            }
            else if (p_index.column() == 1)
            {
                return QVariant(QString("%1").arg(static_cast(mutationType->dominance_coeff_), 0, 'f', 3));
            }
            else if (p_index.column() == 2)
            {
                switch (mutationType->dfe_type_)
                {
                    case DFEType::kFixed:			return QVariant(QString("fixed"));
                    case DFEType::kGamma:			return QVariant(QString("gamma"));
                    case DFEType::kExponential:		return QVariant(QString("exp"));
                    case DFEType::kNormal:			return QVariant(QString("normal"));
                    case DFEType::kWeibull:			return QVariant(QString("Weibull"));
                    case DFEType::kLaplace:			return QVariant(QString("Laplace"));
                    case DFEType::kScript:			return QVariant(QString("script"));
                }
            }
            else if (p_index.column() == 3)
            {
                QString paramString;
                
                if (mutationType->dfe_type_ == DFEType::kScript)
                {
                    // DFE type 's' has parameters of type string
                    for (unsigned int paramIndex = 0; paramIndex < mutationType->dfe_strings_.size(); ++paramIndex)
                    {
                        QString dfe_string = QString::fromStdString(mutationType->dfe_strings_[paramIndex]);
                        
                        paramString += ("\"" + dfe_string + "\"");
                        
                        if (paramIndex < mutationType->dfe_strings_.size() - 1)
                            paramString += ", ";
                    }
                }
                else
                {
                    // All other DFEs have parameters of type double
                    for (unsigned int paramIndex = 0; paramIndex < mutationType->dfe_parameters_.size(); ++paramIndex)
                    {
                        QString paramSymbol;
                        
                        switch (mutationType->dfe_type_)
                        {
                            case DFEType::kFixed:			paramSymbol = "s"; break;
                            case DFEType::kGamma:			paramSymbol = (paramIndex == 0 ? "s̄" : "α"); break;
                            case DFEType::kExponential:		paramSymbol = "s̄"; break;
                            case DFEType::kNormal:			paramSymbol = (paramIndex == 0 ? "s̄" : "σ"); break;
                            case DFEType::kWeibull:			paramSymbol = (paramIndex == 0 ? "λ" : "k"); break;
                            case DFEType::kLaplace:			paramSymbol = (paramIndex == 0 ? "s̄" : "b"); break;
                            case DFEType::kScript:			break;
                        }
                        
                        paramString += QString("%1=%2").arg(paramSymbol).arg(mutationType->dfe_parameters_[paramIndex], 0, 'f', 3);
                        
                        if (paramIndex < mutationType->dfe_parameters_.size() - 1)
                            paramString += ", ";
                    }
                }
                
                return QVariant(paramString);
            }
        }
    }
    else if (role == Qt::ToolTipRole)
    {
        const std::map &mutationTypes = community->AllMutationTypes();
        int mutationTypeCount = static_cast(mutationTypes.size());
        
        if (p_index.row() < mutationTypeCount)
        {
            auto mutTypeIter = mutationTypes.begin();
            std::advance(mutTypeIter, p_index.row());
            
            // Display an image in the tooltip; thanks to https://stackoverflow.com/a/34300771/2752221
            QImage image = imageForMutationOrInteractionType(mutTypeIter->second, nullptr);
            
            // the image is high-dpi; smoothly downscale the image to standard DPI if we're not on a high-DPI screen
            QWidget *tablesWindow = controller->TablesDrawerController();
            QWindow *tablesWindowHandle = tablesWindow ? tablesWindow->windowHandle() : nullptr;
            double dpi = tablesWindowHandle ? tablesWindowHandle->devicePixelRatio() : 1.0;
            
            if (dpi < 1.5)
                image = image.scaled(QSize(77, 50), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
            
            QByteArray image_data;
            QBuffer buffer(&image_data);
            image.save(&buffer, "PNG", 100);
            return QString("").arg(QString(image_data.toBase64()));
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (p_index.column())
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 3: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    
    return QVariant();
}

QVariant QtSLiMMutTypeTableModel::headerData(int section,
                                             Qt::Orientation /* orientation */,
                                             int role) const
{
    if (role == Qt::DisplayRole)
    {
        switch (section)
        {
        case 0: return QVariant("ID");
        case 1: return QVariant("h");
        case 2: return QVariant("DFE");
        case 3: return QVariant("Params");
        default: return QVariant("");
        }
    }
    else if (role == Qt::ToolTipRole)
    {
        switch (section)
        {
        case 0: return QVariant("the ID for the mutation type");
        case 1: return QVariant("the dominance coefficient");
        case 2: return QVariant("the distribution of fitness effects");
        case 3: return QVariant("the DFE parameters");
        default: return QVariant("");
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (section)
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 3: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    
    return QVariant();
}
    
void QtSLiMMutTypeTableModel::reloadTable(void)
{
    beginResetModel();
    endResetModel();
}


QtSLiMGETypeTypeTableModel::QtSLiMGETypeTypeTableModel(QObject *p_parent) : QAbstractTableModel(p_parent)
{
    // p_parent must be a pointer to QtSLiMWindow, which holds our model information
    if (dynamic_cast(p_parent) == nullptr)
        throw p_parent;
}

QtSLiMGETypeTypeTableModel::~QtSLiMGETypeTypeTableModel()
{
}

int QtSLiMGETypeTypeTableModel::rowCount(const QModelIndex & /* p_parent */) const
{
    QtSLiMWindow *controller = static_cast(parent());
    Community *community = controller->community;
    
    if (community)
        return static_cast(community->AllGenomicElementTypes().size());
    
    return 0;
}

int QtSLiMGETypeTypeTableModel::columnCount(const QModelIndex & /* p_parent */) const
{
    return 3;
}

QVariant QtSLiMGETypeTypeTableModel::data(const QModelIndex &p_index, int role) const
{
    if (!p_index.isValid())
        return QVariant();
    
    QtSLiMWindow *controller = static_cast(parent());
    Community *community = controller->community;
    
    if (!community)
        return QVariant();
    
    if (role == Qt::DisplayRole)
    {
        const std::map &genomicElementTypes = community->AllGenomicElementTypes();
        int genomicElementTypeCount = static_cast(genomicElementTypes.size());
        
        if (p_index.row() < genomicElementTypeCount)
        {
            auto genomicElementTypeIter = genomicElementTypes.begin();
            
            std::advance(genomicElementTypeIter, p_index.row());
            slim_objectid_t genomicElementTypeID = genomicElementTypeIter->first;
            GenomicElementType *genomicElementType = genomicElementTypeIter->second;
            
            if (p_index.column() == 0)
            {
                QString idString = QString("g%1").arg(genomicElementTypeID);
                
                if (community->all_species_.size() > 1)
                    idString.append(" ").append(QString::fromStdString(genomicElementType->species_.avatar_));
                
                return QVariant(idString);
            }
            else if (p_index.column() == 1)
            {
                float red, green, blue, alpha;
                
                controller->colorForGenomicElementType(genomicElementType, genomicElementTypeID, &red, &green, &blue, &alpha);
                
                QColor geTypeColor = QColor::fromRgbF(static_cast(red), static_cast(green), static_cast(blue), static_cast(alpha));
                QRgb geTypeRGB = geTypeColor.rgb();
                
                return QVariant(geTypeRGB); // return the color as an unsigned int
            }
            else if (p_index.column() == 2)
            {
                QString paramString;
                
                for (unsigned int mutTypeIndex = 0; mutTypeIndex < genomicElementType->mutation_fractions_.size(); ++mutTypeIndex)
                {
                    MutationType *mutType = genomicElementType->mutation_type_ptrs_[mutTypeIndex];
                    double mutTypeFraction = genomicElementType->mutation_fractions_[mutTypeIndex];
                    
                    paramString += QString("m%1=%2").arg(mutType->mutation_type_id_).arg(mutTypeFraction, 0, 'f', 3);
                    
                    if (mutTypeIndex < genomicElementType->mutation_fractions_.size() - 1)
                        paramString += ", ";
                }
                
                return QVariant(paramString);
            }
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (p_index.column())
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    
    return QVariant();
}

QVariant QtSLiMGETypeTypeTableModel::headerData(int section,
                                                Qt::Orientation /* orientation */,
                                                int role) const
{
    if (role == Qt::DisplayRole)
    {
        switch (section)
        {
        case 0: return QVariant("ID");
        case 1: return QVariant("Color");
        case 2: return QVariant("Mutation types");
        default: return QVariant("");
        }
    }
    else if (role == Qt::ToolTipRole)
    {
        switch (section)
        {
        case 0: return QVariant("the ID for the genomic element type");
        case 1: return QVariant("the color used in SLiMgui");
        case 2: return QVariant("the mutation types drawn from");
        default: return QVariant("");
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (section)
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    return QVariant();
}
    
void QtSLiMGETypeTypeTableModel::reloadTable(void)
{
    beginResetModel();
    endResetModel();
}


QtSLiMInteractionTypeTableModel::QtSLiMInteractionTypeTableModel(QObject *p_parent) : QAbstractTableModel(p_parent)
{
    // p_parent must be a pointer to QtSLiMWindow, which holds our model information
    if (dynamic_cast(p_parent) == nullptr)
        throw p_parent;
}

QtSLiMInteractionTypeTableModel::~QtSLiMInteractionTypeTableModel()
{
}

int QtSLiMInteractionTypeTableModel::rowCount(const QModelIndex & /* p_parent */) const
{
    QtSLiMWindow *controller = static_cast(parent());
    Community *community = controller->community;
    
    if (community)
        return static_cast(community->AllInteractionTypes().size());
    
    return 0;
}

int QtSLiMInteractionTypeTableModel::columnCount(const QModelIndex & /* p_parent */) const
{
    return 4;
}

QVariant QtSLiMInteractionTypeTableModel::data(const QModelIndex &p_index, int role) const
{
    if (!p_index.isValid())
        return QVariant();
    
    QtSLiMWindow *controller = static_cast(parent());
    Community *community = controller->community;
    
    if (!community)
        return QVariant();
    
    if (role == Qt::DisplayRole)
    {
        const std::map &interactionTypes = community->AllInteractionTypes();
        int interactionTypeCount = static_cast(interactionTypes.size());
        
        if (p_index.row() < interactionTypeCount)
        {
            auto interactionTypeIter = interactionTypes.begin();
            
            std::advance(interactionTypeIter, p_index.row());
            slim_objectid_t interactionTypeID = interactionTypeIter->first;
            InteractionType *interactionType = interactionTypeIter->second;
            
            if (p_index.column() == 0)
            {
                QString idString = QString("i%1").arg(interactionTypeID);
                
                return QVariant(idString);
            }
            else if (p_index.column() == 1)
            {
                return QVariant(QString("%1").arg(interactionType->max_distance_, 0, 'f', 3));
            }
            else if (p_index.column() == 2)
            {
                switch (interactionType->if_type_)
                {
                    case SpatialKernelType::kFixed:				return QVariant(QString("fixed"));
                    case SpatialKernelType::kLinear:			return QVariant(QString("linear"));
                    case SpatialKernelType::kExponential:		return QVariant(QString("exp"));
                    case SpatialKernelType::kNormal:			return QVariant(QString("normal"));
                    case SpatialKernelType::kCauchy:			return QVariant(QString("Cauchy"));
                    case SpatialKernelType::kStudentsT:			return QVariant(QString("Student's t"));
                }
            }
            else if (p_index.column() == 3)
            {
                QString paramString;
                
                // the first parameter is always the maximum interaction strength
                paramString += QString("f=%1").arg(interactionType->if_param1_, 0, 'f', 3);
                
                // append second parameters where applicable
                switch (interactionType->if_type_)
                {
                    case SpatialKernelType::kFixed:
                    case SpatialKernelType::kLinear:
                        break;
                    case SpatialKernelType::kExponential:
                        paramString += QString(", β=%1").arg(interactionType->if_param2_, 0, 'f', 3);
                        break;
                    case SpatialKernelType::kNormal:
                        paramString += QString(", σ=%1").arg(interactionType->if_param2_, 0, 'f', 3);
                        break;
                    case SpatialKernelType::kCauchy:
                        paramString += QString(", γ=%1").arg(interactionType->if_param2_, 0, 'f', 3);
                        break;
                    case SpatialKernelType::kStudentsT:
                        paramString += QString(", ν=%1, σ=%2").arg(interactionType->if_param2_, 0, 'f', 3).arg(interactionType->if_param3_, 0, 'f', 3);
                        break;
                }
                
                return QVariant(paramString);
            }
        }
    }
    else if (role == Qt::ToolTipRole)
    {
        const std::map &interactionTypes = community->AllInteractionTypes();
        int interactionTypeCount = static_cast(interactionTypes.size());
        
        if (p_index.row() < interactionTypeCount)
        {
            auto interactionTypeIter = interactionTypes.begin();
            std::advance(interactionTypeIter, p_index.row());
            
            // Display an image in the tooltip; thanks to https://stackoverflow.com/a/34300771/2752221
            QImage image = imageForMutationOrInteractionType(nullptr, interactionTypeIter->second);
            
            // the image is high-dpi; smoothly downscale the image to standard DPI if we're not on a high-DPI screen
            QWidget *tablesWindow = controller->TablesDrawerController();
            QWindow *tablesWindowHandle = tablesWindow ? tablesWindow->windowHandle() : nullptr;
            double dpi = tablesWindowHandle ? tablesWindowHandle->devicePixelRatio() : 1.0;
            
            if (dpi < 1.5)
                image = image.scaled(QSize(77, 50), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
            
            QByteArray image_data;
            QBuffer buffer(&image_data);
            image.save(&buffer, "PNG", 100);
            return QString("").arg(QString(image_data.toBase64()));
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (p_index.column())
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 3: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    
    return QVariant();
}

QVariant QtSLiMInteractionTypeTableModel::headerData(int section,
                                                     Qt::Orientation /* orientation */,
                                                     int role) const
{
    if (role == Qt::DisplayRole)
    {
        switch (section)
        {
        case 0: return QVariant("ID");
        case 1: return QVariant("max");
        case 2: return QVariant("IF");
        case 3: return QVariant("Params");
        default: return QVariant("");
        }
    }
    else if (role == Qt::ToolTipRole)
    {
        switch (section)
        {
        case 0: return QVariant("the ID for the interaction type");
        case 1: return QVariant("the maximum interaction distance");
        case 2: return QVariant("the interaction function");
        case 3: return QVariant("the interaction function parameters");
        default: return QVariant("");
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (section)
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 3: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    return QVariant();
}
    
void QtSLiMInteractionTypeTableModel::reloadTable(void)
{
    beginResetModel();
    endResetModel();
}


QtSLiMEidosBlockTableModel::QtSLiMEidosBlockTableModel(QObject *p_parent) : QAbstractTableModel(p_parent)
{
    // p_parent must be a pointer to QtSLiMWindow, which holds our model information
    if (dynamic_cast(p_parent) == nullptr)
        throw p_parent;
}

QtSLiMEidosBlockTableModel::~QtSLiMEidosBlockTableModel()
{
}

int QtSLiMEidosBlockTableModel::rowCount(const QModelIndex & /* p_parent */) const
{
    QtSLiMWindow *controller = static_cast(parent());
    
    if (controller && !controller->invalidSimulation())
        return static_cast(controller->community->AllScriptBlocks().size());
    
    return 0;
}

int QtSLiMEidosBlockTableModel::columnCount(const QModelIndex & /* p_parent */) const
{
    return 4;
}

QVariant QtSLiMEidosBlockTableModel::data(const QModelIndex &p_index, int role) const
{
    if (!p_index.isValid())
        return QVariant();
    
    QtSLiMWindow *controller = static_cast(parent());
    
    if (!controller || controller->invalidSimulation())
        return QVariant();
    
    if (role == Qt::DisplayRole)
    {
        Community *community = controller->community;
        std::vector &scriptBlocks = community->AllScriptBlocks();
        int scriptBlockCount = static_cast(scriptBlocks.size());
        
        if (p_index.row() < scriptBlockCount)
        {
            SLiMEidosBlock *scriptBlock = scriptBlocks[static_cast(p_index.row())];
            
            if (p_index.column() == 0)
            {
                slim_objectid_t block_id = scriptBlock->block_id_;
                QString idString;
                
                if (scriptBlock->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)
                    idString = "—";
                else if (block_id == -1)
                    idString = "—";
                else
                    idString = QString("s%1").arg(block_id);
                
                if ((community->all_species_.size() > 1) && scriptBlock->species_spec_)
                    idString.append(" ").append(QString::fromStdString(scriptBlock->species_spec_->avatar_));
                else if ((community->all_species_.size() > 1) && scriptBlock->ticks_spec_)
                    idString.append(" ").append(QString::fromStdString(scriptBlock->ticks_spec_->avatar_));
                
                return QVariant(idString);
            }
            else if (p_index.column() == 1)
            {
                if (scriptBlock->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)
                    return QVariant("—");
                else if (!scriptBlock->tick_range_evaluated_)
                    return QVariant("?");
                else if (scriptBlock->tick_range_is_sequence_ == false)
                    return QVariant("...");
                else if (scriptBlock->tick_start_ == -1)
                    return QVariant("MIN");
                else
                    return QVariant(QString("%1").arg(scriptBlock->tick_start_));
            }
            else if (p_index.column() == 2)
            {
                if (scriptBlock->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)
                    return QVariant("—");
                else if (!scriptBlock->tick_range_evaluated_)
                    return QVariant("?");
                else if (scriptBlock->tick_range_is_sequence_ == false)
                    return QVariant("...");
                else if (scriptBlock->tick_end_ == SLIM_MAX_TICK + 1)
                    return QVariant("MAX");
                else
                    return QVariant(QString("%1").arg(scriptBlock->tick_end_));
            }
            else if (p_index.column() == 3)
            {
                switch (scriptBlock->type_)
                {
                    case SLiMEidosBlockType::SLiMEidosEventFirst:				return QVariant("first()");
                    case SLiMEidosBlockType::SLiMEidosEventEarly:				return QVariant("early()");
                    case SLiMEidosBlockType::SLiMEidosEventLate:				return QVariant("late()");
                    case SLiMEidosBlockType::SLiMEidosInitializeCallback:		return QVariant("initialize()");
                    case SLiMEidosBlockType::SLiMEidosMutationEffectCallback:	return QVariant("mutationEffect()");
                    case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback:	return QVariant("fitnessEffect()");
                    case SLiMEidosBlockType::SLiMEidosInteractionCallback:		return QVariant("interaction()");
                    case SLiMEidosBlockType::SLiMEidosMateChoiceCallback:		return QVariant("mateChoice()");
                    case SLiMEidosBlockType::SLiMEidosModifyChildCallback:		return QVariant("modifyChild()");
                    case SLiMEidosBlockType::SLiMEidosRecombinationCallback:	return QVariant("recombination()");
                    case SLiMEidosBlockType::SLiMEidosMutationCallback:			return QVariant("mutation()");
                    case SLiMEidosBlockType::SLiMEidosSurvivalCallback:			return QVariant("survival()");
                    case SLiMEidosBlockType::SLiMEidosReproductionCallback:		return QVariant("reproduction()");
                    case SLiMEidosBlockType::SLiMEidosUserDefinedFunction:
                    {
                        EidosASTNode *function_decl_node = scriptBlock->root_node_->children_[0];
                        EidosASTNode *function_name_node = function_decl_node->children_[1];
                        QString function_name = QString::fromStdString(function_name_node->token_->token_string_);
                        
                        return function_name + "()";
                    }
                    case SLiMEidosBlockType::SLiMEidosNoBlockType:				return QVariant("");	// never hit
                }
            }
        }
    }
    else if (role == Qt::ToolTipRole)
    {
        Community *community = controller->community;
        std::vector &scriptBlocks = community->AllScriptBlocks();
        int scriptBlockCount = static_cast(scriptBlocks.size());
        
        if (p_index.row() < scriptBlockCount)
        {
            SLiMEidosBlock *scriptBlock = scriptBlocks[static_cast(p_index.row())];
            const char *script_string = scriptBlock->compound_statement_node_->token_->token_string_.c_str();
            QString q_script_string = QString::fromStdString(script_string);
            
            q_script_string.replace('\t', "   ");
            
            return q_script_string;
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (p_index.column())
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 3: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    
    return QVariant();
}

QVariant QtSLiMEidosBlockTableModel::headerData(int section,
                                                Qt::Orientation /* orientation */,
                                                int role) const
{
    if (role == Qt::DisplayRole)
    {
        switch (section)
        {
        case 0: return QVariant("ID");
        case 1: return QVariant("Start");
        case 2: return QVariant("End");
        case 3: return QVariant("Type");
        default: return QVariant("");
        }
    }
    else if (role == Qt::ToolTipRole)
    {
        switch (section)
        {
        case 0: return QVariant("the ID for the script block");
        case 1: return QVariant("the start tick");
        case 2: return QVariant("the end tick");
        case 3: return QVariant("the script block type");
        default: return QVariant("");
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        switch (section)
        {
        case 0: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 1: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 2: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        case 3: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        }
    }
    return QVariant();
}
    
void QtSLiMEidosBlockTableModel::reloadTable(void)
{
    beginResetModel();
    endResetModel();
}


//
//  Drawing delegates for custom drawing in the table views
//

QtSLiMGETypeTypeTableDelegate::~QtSLiMGETypeTypeTableDelegate(void)
{
}

void QtSLiMGETypeTypeTableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    if (index.column() == 1)
    {
        // Get the color for the genomic element type, which has been encoded as an unsigned int in a QVariant
        QVariant data = index.data();
        QRgb rgbData = static_cast(data.toUInt());
        QColor boxColor(rgbData);
        
        // Calculate a rect for the color swatch in the center of the item's field
        QRect itemRect = option.rect;
        int centerX = static_cast(itemRect.center().x());
        int halfSide = static_cast((itemRect.height() - 8) / 2);
        QRect boxRect = QRect(centerX - halfSide, itemRect.top() + 5, halfSide * 2, halfSide * 2);
        
        // Fill and frame
        painter->fillRect(boxRect, boxColor);
        QtSLiMFrameRect(boxRect, Qt::black, *painter);
    }
    else
    {
        // Let super draw
        QStyledItemDelegate::paint(painter, option, index);
    }
}



































================================================
FILE: QtSLiM/QtSLiMTablesDrawer.h
================================================
//
//  QtSLiMTablesDrawer.h
//  SLiM
//
//  Created by Ben Haller on 2/22/2020.
//  Copyright (c) 2020-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .

#ifndef QTSLIMTABLESDRAWER_H
#define QTSLIMTABLESDRAWER_H

#include 
#include 
#include 

class QCloseEvent;
class QtSLiMWindow;
class QTableView;
class QHeaderView;

class QtSLiMMutTypeTableModel;
class QtSLiMGETypeTypeTableModel;
class QtSLiMInteractionTypeTableModel;
class QtSLiMEidosBlockTableModel;


namespace Ui {
class QtSLiMTablesDrawer;
}

class QtSLiMTablesDrawer : public QWidget
{
    Q_OBJECT
    
public:
    QtSLiMWindow *parentSLiMWindow = nullptr;     // a copy of parent with the correct class, for convenience
    
    explicit QtSLiMTablesDrawer(QtSLiMWindow *p_parent = nullptr);
    virtual ~QtSLiMTablesDrawer() override;
    
signals:
    void willClose(void);
    
private slots:
    virtual void closeEvent(QCloseEvent *p_event) override;
    
private:
    Ui::QtSLiMTablesDrawer *ui;
    
    QtSLiMMutTypeTableModel *mutTypeTableModel_ = nullptr;
    QtSLiMGETypeTypeTableModel *geTypeTableModel_ = nullptr;
    QtSLiMInteractionTypeTableModel *interactionTypeTableModel_ = nullptr;
    QtSLiMEidosBlockTableModel *eidosBlockTableModel_ = nullptr;
    
    QHeaderView *configureTableView(QTableView *tableView);
    void initializeUI(void);
    
    friend QtSLiMWindow;
};


//
//  Declare models for the four table views; has to be in header file for MOC
//

class QtSLiMMutTypeTableModel : public QAbstractTableModel
{
    Q_OBJECT    
    
public:
    QtSLiMMutTypeTableModel(QObject *p_parent = nullptr);
    virtual ~QtSLiMMutTypeTableModel() override;

    virtual int rowCount(const QModelIndex &p_parent = QModelIndex()) const override;
    virtual int columnCount(const QModelIndex &p_parent = QModelIndex()) const override;

    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
    
    void reloadTable(void);
};

class QtSLiMGETypeTypeTableModel : public QAbstractTableModel
{
    Q_OBJECT    
    
public:
    QtSLiMGETypeTypeTableModel(QObject *p_parent = nullptr);
    virtual ~QtSLiMGETypeTypeTableModel() override;

    virtual int rowCount(const QModelIndex &p_parent = QModelIndex()) const override;
    virtual int columnCount(const QModelIndex &p_parent = QModelIndex()) const override;

    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
    
    void reloadTable(void);
};

class QtSLiMInteractionTypeTableModel : public QAbstractTableModel
{
    Q_OBJECT    
    
public:
    QtSLiMInteractionTypeTableModel(QObject *p_parent = nullptr);
    virtual ~QtSLiMInteractionTypeTableModel() override;

    virtual int rowCount(const QModelIndex &p_parent = QModelIndex()) const override;
    virtual int columnCount(const QModelIndex &p_parent = QModelIndex()) const override;

    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
    
    void reloadTable(void);
};

class QtSLiMEidosBlockTableModel : public QAbstractTableModel
{
    Q_OBJECT    
    
public:
    QtSLiMEidosBlockTableModel(QObject *p_parent = nullptr);
    virtual ~QtSLiMEidosBlockTableModel() override;

    virtual int rowCount(const QModelIndex &p_parent = QModelIndex()) const override;
    virtual int columnCount(const QModelIndex &p_parent = QModelIndex()) const override;

    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
    
    void reloadTable(void);
};


//
//  Drawing delegates for custom drawing in the table views
//

class QtSLiMGETypeTypeTableDelegate : public QStyledItemDelegate
{
    Q_OBJECT
    
public:
    QtSLiMGETypeTypeTableDelegate(QObject *p_parent = nullptr) : QStyledItemDelegate(p_parent) {}
    virtual ~QtSLiMGETypeTypeTableDelegate(void) override;
    
    virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};


#endif // QTSLIMTABLESDRAWER_H
































================================================
FILE: QtSLiM/QtSLiMTablesDrawer.ui
================================================


 QtSLiMTablesDrawer
 
  
   
    0
    0
    400
    810
   
  
  
   
    280
    500
   
  
  
   
    510
    16777215
   
  
  
   Object Tables
  
  
   
    10
   
   
    10
   
   
    10
   
   
    10
   
   
    
     
      Mutation Types:
     
    
   
   
    
     
      Qt::NoFocus
     
     
      Qt::ScrollBarAlwaysOn
     
     
      QAbstractItemView::NoEditTriggers
     
     
      false
     
     
      false
     
     
      QAbstractItemView::NoSelection
     
     
      false
     
     
      false
     
     
      false
     
     
      false
     
    
   
   
    
     
      Qt::Vertical
     
     
      QSizePolicy::Fixed
     
     
      
       20
       4
      
     
    
   
   
    
     
      Genomic Element Types:
     
    
   
   
    
     
      Qt::NoFocus
     
     
      Qt::ScrollBarAlwaysOn
     
     
      QAbstractItemView::NoEditTriggers
     
     
      false
     
     
      false
     
     
      QAbstractItemView::NoSelection
     
     
      false
     
     
      false
     
     
      false
     
     
      false
     
    
   
   
    
     
      Qt::Vertical
     
     
      QSizePolicy::Fixed
     
     
      
       20
       4
      
     
    
   
   
    
     
      Interaction Types:
     
    
   
   
    
     
      Qt::NoFocus
     
     
      Qt::ScrollBarAlwaysOn
     
     
      QAbstractItemView::NoEditTriggers
     
     
      false
     
     
      false
     
     
      QAbstractItemView::NoSelection
     
     
      false
     
     
      false
     
     
      false
     
     
      false
     
    
   
   
    
     
      Qt::Vertical
     
     
      QSizePolicy::Fixed
     
     
      
       20
       4
      
     
    
   
   
    
     
      Eidos Blocks:
     
    
   
   
    
     
      Qt::NoFocus
     
     
      Qt::ScrollBarAlwaysOn
     
     
      QAbstractItemView::NoEditTriggers
     
     
      false
     
     
      false
     
     
      QAbstractItemView::NoSelection
     
     
      false
     
     
      false
     
     
      false
     
     
      false
     
    
   
  
 
 
 



================================================
FILE: QtSLiM/QtSLiMVariableBrowser.cpp
================================================
//
//  QtSLiMVariableBrowser.cpp
//  SLiM
//
//  Created by Ben Haller on 4/17/2019.
//  Copyright (c) 2020-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .


#include "QtSLiMVariableBrowser.h"
#include "ui_QtSLiMVariableBrowser.h"

#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

#include "QtSLiMEidosConsole.h"
#include "QtSLiMAppDelegate.h"
#include "QtSLiMExtras.h"

#include "eidos_symbol_table.h"


static int QtSLiMVarBrowserRowHeight = 0;  // a global, for simplicity; 0 is uninitialized, -1 is failed to init


//
// This subclass of QStyledItemDelegate provides custom drawing for the outline view.
//

QtSLiMVariableBrowserDelegate::~QtSLiMVariableBrowserDelegate(void)
{
}

void QtSLiMVariableBrowserDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    // On Ubuntu, items get shown as having "focus" even when they're not selectable, which I dislike; this disables that appearance
    // See https://stackoverflow.com/a/2061871/2752221
    QStyleOptionViewItem modifiedOption(option);
    if (modifiedOption.state & QStyle::State_HasFocus)
        modifiedOption.state = modifiedOption.state ^ QStyle::State_HasFocus;
    
    // then let super draw
    QStyledItemDelegate::paint(painter, modifiedOption, index);
}


//
//  QtSLiMBrowserItem
//

QtSLiMBrowserItem::QtSLiMBrowserItem(QString name, EidosValue_SP value, int elementIndex, bool isEllipsis) :
    QTreeWidgetItem(), symbol_name(name), eidos_value(value), element_index(elementIndex), is_ellipsis(isEllipsis)
{
    // We want to display Eidos constants in gray text, to de-emphasize them.  For now, we just hard-code them
    // as a hack, because we *don't* want SLiM constants (sim, g1, p1, etc.) to display dimmed
    is_eidos_constant = false;
    if ((name == "T") || (name == "F") || (name == "E") || (name == "PI") || (name == "INF") || (name == "NAN") || (name == "NULL"))
        is_eidos_constant = true;
    
    // If we contain children, they won't be added under us until we get expanded, so force the indicator on
    // Note this applies both to the row for the vector (count > 0) and rows for elements (count > 0)
    if (eidos_value && (eidos_value->Type() == EidosValueType::kValueObject) && (eidos_value->Count() > 0))
    {
        setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
        has_children = true;
    }
    else
    {
        has_children = false;
    }
    
    // Precompute the hash value for this item.  This is done up front and cached, so that it is available
    // even if our eidos_value object element gets deallocated.  We use it to confirm that two browser items
    // truly refer to the same Eidos object.  The hash encapsulates the symbol name, the element index, and
    // the object-element type.  It does not encapsulate the number of elements; we want matches to carry
    // over across changes in vector length.  Note that of course hash collisions can occur.  That will
    // result in the expansion of the wrong item during a browser reload; it is not fatal, so as long as it
    // is very rare, it's OK.  Since the same symbol might provide a different EidosValue object every time
    // it is accessed (as properties often would), and we can't look inside the elements, this is the best
    // we can do.  It should be quite reliable.
    item_hash = qHash(symbol_name) ^ static_cast(element_index);
    
    if (eidos_value && (eidos_value->Type() == EidosValueType::kValueObject))
        item_hash ^= (std::hash{}(eidos_value->ElementType()) << 16);
}

QtSLiMBrowserItem::~QtSLiMBrowserItem(void)
{
    //qDebug() << "QtSLiMBrowserItem::~QtSLiMBrowserItem";
    eidos_value.reset();
}

QVariant QtSLiMBrowserItem::data(int column, int role) const
{
    if (role == Qt::DisplayRole)
    {
        if (column == 0)            return symbol_name;
        if (is_ellipsis)            return QVariant();
        if (!eidos_value)           return (column == 3) ? "" : QVariant();
        if (element_index != -1)    return QVariant();
        
        // the remainder of this assumes the above conditions: the column is not 0, the item is not an ellipsis item,
        // there is an associated eidos_value, and the element_index is -1 (we're representing a whole vector)
        if (column == 1)
        {
            EidosValueType value_type = eidos_value->Type();
            std::string type_string = StringForEidosValueType(value_type);
            QString typeString = QString::fromStdString(type_string);
            
            if (value_type == EidosValueType::kValueObject)
            {
                EidosValue_Object *object_value = static_cast(eidos_value.get());
                const std::string &element_string = object_value->ElementType();
                QString elementString = QString::fromStdString(element_string);
                
                typeString.append('<');
                typeString.append(elementString);
                typeString.append('>');
            }
            
            return typeString;
        }
        else if (column == 2)
        {
            return eidos_value->Count();
        }
        else if (column == 3)
        {
            int value_count = eidos_value->Count();
            std::ostringstream outstream;
            
            // print values as a comma-separated list with strings quoted; halfway between print() and cat()
            for (int value_index = 0; value_index < value_count; ++value_index)
            {
                EidosValue_SP element_value = eidos_value->GetValueAtIndex(value_index, nullptr);
                
                if (value_index > 0)
                {
                    outstream << ", ";
                    
                    // terminate the list at some reasonable point, otherwise we generate massively long strings for large vectors...
                    if (value_index > 50)
                    {
                        outstream << ", ...";
                        break;
                    }
                }
                outstream << *element_value;
            }
            
            return QString::fromStdString(outstream.str()).simplified();
        }
    }
    else if (role == Qt::TextAlignmentRole)
    {
        if (column == 2)
            return static_cast(Qt::AlignHCenter | Qt::AlignVCenter);
        else
            return static_cast(Qt::AlignLeft | Qt::AlignVCenter);
    }
    else if (role == Qt::ForegroundRole)
    {
        bool inDarkMode = QtSLiMInDarkMode();
        
        if (inDarkMode)
            return QBrush(is_eidos_constant ? Qt::gray : Qt::white);
        else
            return QBrush(is_eidos_constant ? Qt::darkGray : Qt::black);
    }
    else if (role == Qt::FontRole)
    {
        static QFont *defaultFont = nullptr;
        static QFont *italicFont = nullptr;
        
        if (!defaultFont)
        {
            defaultFont = new QFont(QTreeWidgetItem::data(column, Qt::FontRole).value());
            italicFont = new QFont(*defaultFont);
            italicFont->setItalic(true);
        }
        
        return (element_index == -1) ? *defaultFont : *italicFont;
    }
    else if (role == Qt::SizeHintRole)
    {
        if (QtSLiMVarBrowserRowHeight > 0)
            return QSize(0, QtSLiMVarBrowserRowHeight);
        else
            return QTreeWidgetItem::data(column, Qt::SizeHintRole);
    }
    
    return QVariant();
}


//
//  QtSLiMVariableBrowser
//

QtSLiMVariableBrowser::QtSLiMVariableBrowser(QtSLiMEidosConsole *p_parent) :
    QWidget(p_parent, Qt::Window),    // the console window has us as a parent, but is still a standalone window
    parentEidosConsole(p_parent),
    ui(new Ui::QtSLiMVariableBrowser)
{
    ui->setupUi(this);
    
    // no window icon
#ifdef __APPLE__
    // set the window icon only on macOS; on Linux it changes the app icon as a side effect
    setWindowIcon(QIcon());
#endif
    
    // prevent this window from keeping the app running when all main windows are closed
    setAttribute(Qt::WA_QuitOnClose, false);
    
    // Restore the saved window position; see https://doc.qt.io/qt-5/qsettings.html#details
    QSettings settings;
    
    settings.beginGroup("QtSLiMVariableBrowser");
    resize(settings.value("size", QSize(400, 300)).toSize());
    move(settings.value("pos", QPoint(25, 445)).toPoint());
    settings.endGroup();
    
    // tree widget settings
    QTreeWidget *browserTree = ui->browserTreeWidget;
    
    QAbstractItemDelegate *outlineDelegate = new QtSLiMVariableBrowserDelegate(browserTree);
    browserTree->setItemDelegate(outlineDelegate);
    
#if defined(__linux__)
    {
        // use a smaller font for the outline on Linux
        QFont browserFont(browserTree->font());
        browserFont.setPointSizeF(browserFont.pointSizeF() - 1);
        browserTree->setFont(browserFont);
    }
#endif
    
    browserTree->setHeaderLabels(QStringList{"Symbol", "Type", "Size", "Values"});
#if defined(__APPLE__)
    browserTree->headerItem()->setTextAlignment(0, Qt::AlignVCenter);
    browserTree->headerItem()->setTextAlignment(1, Qt::AlignVCenter);
    browserTree->headerItem()->setTextAlignment(2, Qt::AlignCenter);
    browserTree->headerItem()->setTextAlignment(3, Qt::AlignVCenter);
#else
    browserTree->headerItem()->setTextAlignment(0, Qt::AlignTop);
    browserTree->headerItem()->setTextAlignment(1, Qt::AlignTop);
    browserTree->headerItem()->setTextAlignment(2, Qt::AlignHCenter | Qt::AlignTop);
    browserTree->headerItem()->setTextAlignment(3, Qt::AlignTop);
#endif
    browserTree->setColumnWidth(0, 180);
    browserTree->setColumnWidth(1, 180);
    browserTree->setColumnWidth(2, 75);
    browserTree->header()->setMinimumHeight(21);
    browserTree->header()->setSectionResizeMode(QHeaderView::Fixed);
    browserTree->header()->setSectionsMovable(false);
    browserTree->setMinimumWidth(500);
    browserTree->setUniformRowHeights(true);
    
    // handle expand/collapse events
    connect(browserTree, &QTreeWidget::itemExpanded, this, &QtSLiMVariableBrowser::itemExpanded);
    connect(browserTree, &QTreeWidget::itemCollapsed, this, &QtSLiMVariableBrowser::itemCollapsed);
    connect(browserTree, &QTreeWidget::itemClicked, this, &QtSLiMVariableBrowser::itemClicked);
    
    // watch the tree widget's vertical scroller, to restore the scroll position
    connect(ui->browserTreeWidget->verticalScrollBar(), &QScrollBar::valueChanged, this, &QtSLiMVariableBrowser::scrollerChanged);
    
    // initial state
    reloadBrowser(true);
    
    // make window actions for all global menu items
    qtSLiMAppDelegate->addActionsForGlobalMenuItems(this);
}

QtSLiMVariableBrowser::~QtSLiMVariableBrowser()
{
    clearSavedExpansionState();
    
    delete ui;
}

void QtSLiMVariableBrowser::closeEvent(QCloseEvent *p_event)
{
    // Save the window position; see https://doc.qt.io/qt-5/qsettings.html#details
    QSettings settings;
    
    settings.beginGroup("QtSLiMVariableBrowser");
    settings.setValue("size", size());
    settings.setValue("pos", pos());
    settings.endGroup();
    
    // send our close signal
    emit willClose();
    
    // use super's default behavior
    QWidget::closeEvent(p_event);
}

void QtSLiMVariableBrowser::reloadBrowser(bool nowValidState)
{
    QTreeWidget *browserTree = ui->browserTreeWidget;
    
    // Take over the old browser tree so we can consult it for things to expand
    QTreeWidgetItem *root = browserTree->invisibleRootItem();
    
    //qDebug() << "   RELOAD: root ==" << root << "and has" << root->childCount() << "children";
    
    if (root->childCount() > 0)
    {
        if (old_children.count() == 0)
        {
            // The root currently has items under it; if we have no valid saved state already,
            // the current state of the tree represents a state we want to save
            old_children = root->takeChildren();
            old_scroll_position = browserTree->verticalScrollBar()->value();
            //qDebug() << "   SAVED OLD TREE ITEMS FOR MATCHING";
            
            // We don't use the EidosValues in the saved tree at all, since they will potentially be stale;
            // to free up the memory involved, we go through the saved tree and wipe the values to nullptr
            for (QTreeWidgetItem *old_root_child : static_cast &>(old_children))
                wipeEidosValuesFromSubtree(old_root_child);
        }
        else
        {
            // We didn't want to save the current state, since we already have a saved state that
            // has not been invalidated by a more recent user action; so just clear the root
            browserTree->clear();
        }
    }
    
    // Make the new root items; we do not attempt to reuse the old items, not worth the complication
    if (parentEidosConsole)
    {
        EidosSymbolTable *symbols = parentEidosConsole->symbolTable();
        
        if (symbols)
        {
            // Add constants first, then non-constants
            for (int isConstant = 1; isConstant >= 0; --isConstant)
            {
                std::vector symbolNamesVec = isConstant ? symbols->ReadOnlySymbols() : symbols->ReadWriteSymbols();
                size_t symbolNamesCount = symbolNamesVec.size();
                
                for (size_t index = 0; index < symbolNamesCount;++ index)
                {
                    const std::string &symbolName = symbolNamesVec[index];
                    EidosValue_SP symbolValue = symbols->GetValueOrRaiseForSymbol(EidosStringRegistry::GlobalStringIDForString(symbolName));
                    QtSLiMBrowserItem *item = new QtSLiMBrowserItem(QString::fromStdString(symbolName), std::move(symbolValue));
                    browserTree->addTopLevelItem(item);
                    
                    // figure out the correct row height, which we have to do after making a row item
                    if (QtSLiMVarBrowserRowHeight == 0)
                    {
                        QRect item_rect = browserTree->visualItemRect(item);
                        int defaultHeight = item_rect.height();
                        QtSLiMVarBrowserRowHeight = (defaultHeight > 0) ? defaultHeight + 2 : -1;
                    }
                }
            }
        }
    }
    
    // If we're now in a valid state, we'll try to match our old expanded state; otherwise, we have
    // just emptied out the tree, but are in an invalid state where we cannot repopulate it
    if (nowValidState)
    {
        // Analyze the old children and try to expand items to match the previous state
        doingMatching = true;
        
        for (QTreeWidgetItem *old_root_child : static_cast &>(old_children))
            matchExpansionOfOldItem(old_root_child, root);
        
        // Try to restore the scroll position to where it was when we saved the tree
        // We use visualItemRect() for its side effect of forcing relayout, giving the
        // vertical scroller the right value range; I don't see an API for that
        if (browserTree->invisibleRootItem()->childCount() > 0)
        {
            browserTree->visualItemRect(browserTree->invisibleRootItem()->child(0));
            browserTree->verticalScrollBar()->setValue(old_scroll_position);
        }
        
        doingMatching = false;
        
        // Note that we hang on to the old expanded state; we will continue to use it until it is invalidated
    }
}

void QtSLiMVariableBrowser::matchExpansionOfOldItem(QTreeWidgetItem *itemToMatch, QTreeWidgetItem *parentToSearch)
{
    // The old tree item itemToMatch was expanded; we have been asked to find a matching item in parentToSearch and expand it
    QtSLiMBrowserItem *browserItemToMatch = dynamic_cast(itemToMatch);
    
    if (browserItemToMatch && browserItemToMatch->childCount() > 0)
    {
        //qDebug() << "Old symbol" << browserItemToMatch->symbol_name << "was expanded, looking for match...";
        //qDebug() << "   parentToSearch ==" << parentToSearch << "and has" << parentToSearch->childCount() << "children";
        
        // Old state we're trying to match; note that the EidosValue here may contain freed objects!
        // We thus can't look inside the EidosValue to establish a match, nor do we want to use pointer
        // equality (the underlying object might have been dealloced and a new object alloced at the
        // same address; and in any case the same symbol might have changed address, as properties
        // would unless their value is cached).  Instead, to establish a match we use a precomputed
        // hash value that includes symbol name, element index, and object-element type.
        uint item_hash_to_match = browserItemToMatch->item_hash;
        
        // We consider a "match" to have an identical hash; it must also have children to expand
        for (int parentToSearchIndex = 0; parentToSearchIndex < parentToSearch->childCount(); parentToSearchIndex++)
        {
            QTreeWidgetItem *childItemToCheck = parentToSearch->child(parentToSearchIndex);
            QtSLiMBrowserItem *childBrowserItemToCheck = dynamic_cast(childItemToCheck);
            
            if (childBrowserItemToCheck && childBrowserItemToCheck->has_children)
            {
                //qDebug() << "   checking child item with symbol" << childBrowserItemToCheck->symbol_name << "...";
                
                if (childBrowserItemToCheck->item_hash == item_hash_to_match)
                {
                    // We have a match, so we expand it
                    //qDebug() << "      MATCH: expanding...";
                    ui->browserTreeWidget->expandItem(childBrowserItemToCheck);
                    
                    // If the items involved are vectors with >1 child, expand to the right number of children
                    int targetChildCount = browserItemToMatch->childCount();
                    int currentChildCount = childBrowserItemToCheck->childCount();
                    
                    while (currentChildCount < targetChildCount)
                    {
                        QTreeWidgetItem *lastExpanded = childBrowserItemToCheck->child(currentChildCount - 1);
                        QtSLiMBrowserItem *lastBrowserItem = dynamic_cast(lastExpanded);
                        
                        if (!lastBrowserItem || !lastBrowserItem->is_ellipsis)
                            break;
                        
                        expandEllipsisItem(lastBrowserItem);
                        currentChildCount = childBrowserItemToCheck->childCount();
                    }
                    
                    // Since this item was expanded, we might have further matches in need of expansion under it
                    for (int old_child_index = 0; old_child_index < browserItemToMatch->childCount(); old_child_index++)
                    {
                        QTreeWidgetItem *old_item_child = browserItemToMatch->child(old_child_index);
                        matchExpansionOfOldItem(old_item_child, childBrowserItemToCheck);
                    }
                }
                else
                {
                    //qDebug() << "      no match :  hashes are" << item_hash_to_match << "and" << childBrowserItemToCheck->item_hash;
                }
            }
        }
    }
}

void QtSLiMVariableBrowser::wipeEidosValuesFromSubtree(QTreeWidgetItem *item)
{
    QtSLiMBrowserItem *browserItem = dynamic_cast(item);
    
    if (browserItem)
    {
        browserItem->eidos_value.reset();
        
        for (int child_index = 0; child_index < browserItem->childCount(); child_index++)
            wipeEidosValuesFromSubtree(browserItem->child(child_index));
    }
}

void QtSLiMVariableBrowser::appendIndexedItemsToItem(QtSLiMBrowserItem *browserItem, int startIndex)
{
    EidosValue *eidos_value = browserItem->eidos_value.get();
    
    if (eidos_value && (eidos_value->Type() == EidosValueType::kValueObject))
    {
        int elementCount = eidos_value->Count();
        int appendCount = std::max(10, startIndex);           // reveal twice as many items with each expansion
        int lastIndex = ((startIndex + appendCount - 1) > (elementCount - 1)) ? (elementCount - 1) : (startIndex + appendCount - 1);
        int index;
        
        for (index = startIndex; index <= lastIndex; ++index)
        {
            QString childName = QString("%1[%2]").arg(browserItem->symbol_name).arg(index);
            QtSLiMBrowserItem *childItem = new QtSLiMBrowserItem(childName, browserItem->eidos_value, index);   // elements refer to the main vector
            
            browserItem->addChild(childItem);
        }
        
        if (index < elementCount - 1)
        {
            QtSLiMBrowserItem *ellipsisItem = new QtSLiMBrowserItem("...", EidosValue_SP(nullptr), index, true);
            
            browserItem->addChild(ellipsisItem);
        }
    }
}

void QtSLiMVariableBrowser::itemExpanded(QTreeWidgetItem *item)
{
    clearSavedExpansionState();     // invalidate our saved expansion state
    
    QtSLiMBrowserItem *browserItem = dynamic_cast(item);
    EidosValue *eidos_value = browserItem->eidos_value.get();
    int element_index = browserItem->element_index;
    
    if (eidos_value && (eidos_value->Type() == EidosValueType::kValueObject))
    {
        int elementCount = eidos_value->Count();
		
		// values which are of object type and contain more than one element get displayed as a list of elements
		if ((elementCount > 1) && (element_index == -1))
		{
            appendIndexedItemsToItem(browserItem, 0);
		}
        else if ((elementCount == 1) || (element_index != -1))
        {
            // display property values for either (a) a object vector of length 1, or (b) an element of an object vector
            // we used to display zero-length property values for zero-length object vectors, but don't any more
            int display_index = (element_index != -1) ? element_index : 0;
            EidosValue_Object *eidos_object_vector = static_cast(eidos_value);
            EidosObject *eidos_object = eidos_object_vector->ObjectElementAtIndex_NOCAST(display_index, nullptr);
			const EidosClass *object_class = eidos_object->Class();
			const std::vector *properties = object_class->Properties();
			size_t propertyCount = properties->size();
			bool oldSuppressWarnings = gEidosSuppressWarnings, inaccessibleCaught = false;
			
			gEidosSuppressWarnings = true;		// prevent warnings from questionable property accesses from producing warnings in the user's output pane
			
			for (size_t index = 0; index < propertyCount; ++index)
			{
				const EidosPropertySignature_CSP &propertySig = (*properties)[index];
				const std::string &symbolName = propertySig->property_name_;
				EidosGlobalStringID symbolID = propertySig->property_id_;
                QString symbolString = QString::fromStdString(symbolName);
				EidosValue_SP symbolValue;
				
				// protect against raises in property accesses due to inaccessible properties
				try {
					symbolValue = eidos_object->GetProperty(symbolID);
				} catch (...) {
					//std::cout << "caught inaccessible property " << symbolName << std::endl;
					inaccessibleCaught = true;
				}
				
                QtSLiMBrowserItem *childItem = new QtSLiMBrowserItem(symbolString, std::move(symbolValue));
				
                item->addChild(childItem);
			}
			
			gEidosSuppressWarnings = oldSuppressWarnings;
			
			if (inaccessibleCaught)
			{
				// throw away the raise message(s) so they don't confuse us
				gEidosTermination.clear();
				gEidosTermination.str(gEidosStr_empty_string);
			}
        }
    }
}

void QtSLiMVariableBrowser::itemCollapsed(QTreeWidgetItem *item)
{
    clearSavedExpansionState();     // invalidate our saved expansion state
    
    // Take the children from item and free them
    QList item_children = item->takeChildren();
    
    while (!item_children.isEmpty())
        delete item_children.takeFirst();
}

void QtSLiMVariableBrowser::expandEllipsisItem(QtSLiMBrowserItem *browserItem)
{
    clearSavedExpansionState();     // invalidate our saved expansion state
    
    QTreeWidgetItem *parentItem = browserItem->parent();
    QtSLiMBrowserItem *parentBrowserItem = dynamic_cast(parentItem);
    
    if (parentBrowserItem)
    {
        appendIndexedItemsToItem(parentBrowserItem, browserItem->element_index);
        parentBrowserItem->removeChild(browserItem);
        delete browserItem;
    }
}

void QtSLiMVariableBrowser::itemClicked(QTreeWidgetItem *item, int /* column */)
{
    // If the item is an ellipsis item, expand it to show more items
    QtSLiMBrowserItem *browserItem = dynamic_cast(item);
    
    if (browserItem)
    {
        if (browserItem->is_ellipsis)
        {
            expandEllipsisItem(browserItem);
        }
        else if (browserItem->isExpanded())
        {
            ui->browserTreeWidget->collapseItem(browserItem);
        }
        else    // collapsed
        {
            ui->browserTreeWidget->expandItem(browserItem);
        }
    }
}

void QtSLiMVariableBrowser::clearSavedExpansionState(void)
{
    // We want to save the expansion state only when the user changes it.  That way,
    // we will re-expand correctly even after a recycle and several steps; we'll
    // remember what used to be open until the user changes it.
    
    // Matching involves doing various expansions; that should not trigger a discard
    // of the saved state, because it doesn't come from a user action
    if (doingMatching)
        return;
    
    while (!old_children.isEmpty())
        delete old_children.takeFirst();
}

void QtSLiMVariableBrowser::scrollerChanged(void)
{
    if (doingMatching)
        return;
    
    // If the user drags the scroller to a new position, we want to remember and restore
    // that new position even if we're restoring a tree that was saved earlier
    old_scroll_position = ui->browserTreeWidget->verticalScrollBar()->value();
}


































================================================
FILE: QtSLiM/QtSLiMVariableBrowser.h
================================================
//
//  QtSLiMVariableBrowser.h
//  SLiM
//
//  Created by Ben Haller on 4/17/2019.
//  Copyright (c) 2020-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .

#ifndef QTSLIMVARIABLEBROWSER_H
#define QTSLIMVARIABLEBROWSER_H

#include 
#include 
#include 

#include "eidos_value.h"

class QCloseEvent;
class QtSLiMEidosConsole;


// A QTreeWidgetItem subclass that keeps associated information

class QtSLiMBrowserItem : public QTreeWidgetItem
{
    // no Q_OBJECT; QTreeWidgetItem is not a QObject subclass!
    
public:
    QtSLiMBrowserItem(QString name, EidosValue_SP value) : QtSLiMBrowserItem(name, value, -1) {}
    QtSLiMBrowserItem(QString name, EidosValue_SP value, int index) : QtSLiMBrowserItem(name, value, index, false) {}
    QtSLiMBrowserItem(QString name, EidosValue_SP value, int index, bool isEllipsis);
    virtual ~QtSLiMBrowserItem(void) override;
    
    virtual QVariant data(int column, int role) const override;
    
    QString symbol_name;            // the name as displayed in the browser
    EidosValue_SP eidos_value;      // the EidosValue referred to by this item (perhaps just one element of it)
    int element_index;              // -1 if this item refers to the whole value; otherwise, an element index
    uint item_hash;                 // a precomputed hash value that can be used to confirm that items match
    bool is_eidos_constant;         // true if this is one of the built-in Eidos constants; cached for speed
    bool is_ellipsis;               // true if this item is a "..." representing more undisplayed elements
    bool has_children;              // true if this item has children (which might not be created yet)
};


// This subclass of QStyledItemDelegate provides custom drawing for the outline view.

class QtSLiMVariableBrowserDelegate : public QStyledItemDelegate
{
    Q_OBJECT
    
public:
    QtSLiMVariableBrowserDelegate(QObject *p_parent = nullptr) : QStyledItemDelegate(p_parent) {}
    virtual ~QtSLiMVariableBrowserDelegate(void) override;
    
    virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};


namespace Ui {
class QtSLiMVariableBrowser;
}

class QtSLiMVariableBrowser : public QWidget
{
    Q_OBJECT
    
public:
    QtSLiMEidosConsole *parentEidosConsole = nullptr;     // a copy of parent with the correct class, for convenience
    
    explicit QtSLiMVariableBrowser(QtSLiMEidosConsole *p_parent = nullptr);
    virtual ~QtSLiMVariableBrowser() override;
    
    void reloadBrowser(bool nowValidState);
    
public slots:
    void itemExpanded(QTreeWidgetItem *item);
    void itemCollapsed(QTreeWidgetItem *item);
    void itemClicked(QTreeWidgetItem *item, int column);
    
signals:
    void willClose(void);
    
private slots:
    virtual void closeEvent(QCloseEvent *p_event) override;
    
private:
    Ui::QtSLiMVariableBrowser *ui;
    
    void appendIndexedItemsToItem(QtSLiMBrowserItem *browserItem, int startIndex);
    void matchExpansionOfOldItem(QTreeWidgetItem *itemToMatch, QTreeWidgetItem *parentToSearch);
    void expandEllipsisItem(QtSLiMBrowserItem *browserItem);
    void clearSavedExpansionState(void);
    void wipeEidosValuesFromSubtree(QTreeWidgetItem *item);
    void scrollerChanged(void);
    
    QList old_children;  // a saved tree that we will try to match next reload
    int old_scroll_position = 0;            // a saved scroll position, parallel to old_children
    bool doingMatching = false;
};


#endif // QTSLIMVARIABLEBROWSER_H




































================================================
FILE: QtSLiM/QtSLiMVariableBrowser.ui
================================================


 QtSLiMVariableBrowser
 
  
   
    0
    0
    400
    300
   
  
  
   Variable Browser
  
  
   
    0
   
   
    0
   
   
    0
   
   
    0
   
   
    0
   
   
    
     
      
       300
       200
      
     
     
      
       11
      
     
     
      Qt::StrongFocus
     
     
      QFrame::NoFrame
     
     
      QFrame::Plain
     
     
      0
     
     
      Qt::ScrollBarAlwaysOff
     
     
      QAbstractItemView::NoEditTriggers
     
     
      false
     
     
      QAbstractItemView::NoSelection
     
     
      13
     
     
      false
     
     
      
       1
      
     
    
   
  
 
 
 



================================================
FILE: QtSLiM/QtSLiMWindow.cpp
================================================
//
//  QtSLiMWindow.cpp
//  SLiM
//
//  Created by Ben Haller on 7/11/2019.
//  Copyright (c) 2019-2025 Benjamin C. Haller.  All rights reserved.
//	A product of the Messer Lab, http://messerlab.org/slim/
//

//	This file is part of SLiM.
//
//	SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//	SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License along with SLiM.  If not, see .


#include "QtSLiMWindow.h"
#include "ui_QtSLiMWindow.h"
#include "QtSLiMAppDelegate.h"
#include "QtSLiMPreferences.h"
#include "QtSLiMFindPanel.h"
#include "QtSLiMHelpWindow.h"
#include "QtSLiMEidosConsole.h"
#include "QtSLiMVariableBrowser.h"
#include "QtSLiMDebugOutputWindow.h"
#include "QtSLiMTablesDrawer.h"
#include "QtSLiMScriptTextEdit.h"
#include "QtSLiM_SLiMgui.h"

#include "QtSLiMGraphView.h"
#include "QtSLiMGraphView_1DPopulationSFS.h"
#include "QtSLiMGraphView_1DSampleSFS.h"
#include "QtSLiMGraphView_2DPopulationSFS.h"
#include "QtSLiMGraphView_2DSampleSFS.h"
#include "QtSLiMGraphView_LossTimeHistogram.h"
#include "QtSLiMGraphView_FixationTimeHistogram.h"
#include "QtSLiMGraphView_AgeDistribution.h"
#include "QtSLiMGraphView_LifetimeReproduction.h"
#include "QtSLiMGraphView_PopulationVisualization.h"
#include "QtSLiMGraphView_FitnessOverTime.h"
#include "QtSLiMGraphView_PopSizeOverTime.h"
#include "QtSLiMGraphView_FrequencyTrajectory.h"
#include "QtSLiMGraphView_PopFitnessDist.h"
#include "QtSLiMGraphView_SubpopFitnessDists.h"
#include "QtSLiMGraphView_MultispeciesPopSizeOverTime.h"
#include "QtSLiMHaplotypeManager.h"
#include "QtSLiMGraphView_CustomPlot.h"

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
#include 
#endif
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 

#include 
#include 
#include 
#include 
#include 

#include "individual.h"
#include "eidos_test.h"
#include "slim_test.h"
#include "log_file.h"

#ifdef _OPENMP
#error Building SLiMgui to run in parallel is not currently supported.
#endif


// This allows us to use Qt::QueuedConnection with EidosErrorContext
Q_DECLARE_METATYPE(EidosErrorContext)
static int EidosErrorContext_metatype_id = qRegisterMetaType();


static std::string defaultWFScriptString(void)
{
    return std::string(
                "// set up a simple neutral simulation\n"
                "initialize() {\n"
                "	initializeMutationRate(1e-7);\n"
                "	\n"
                "	// m1 mutation type: neutral\n"
                "	initializeMutationType(\"m1\", 0.5, \"f\", 0.0);\n"
                "	\n"
                "	// g1 genomic element type: uses m1 for all mutations\n"
                "	initializeGenomicElementType(\"g1\", m1, 1.0);\n"
                "	\n"
                "	// uniform chromosome of length 100 kb with uniform recombination\n"
                "	initializeGenomicElement(g1, 0, 99999);\n"
                "	initializeRecombinationRate(1e-8);\n"
                "}\n"
                "\n"
                "// create a population of 500 individuals\n"
                "1 early() {\n"
                "	sim.addSubpop(\"p1\", 500);\n"
                "}\n"
                "\n"
                "// output samples of 10 haplosomes periodically, all fixed mutations at end\n"
                "1000 late() { p1.outputSample(10); }\n"
                "2000 late() { p1.outputSample(10); }\n"
                "2000 late() { sim.outputFixedMutations(); }\n");
}

static std::string defaultWFScriptString_NC(void)
{
    return std::string(
        "initialize() {\n"
        "	initializeMutationRate(1e-7);\n"
        "	initializeMutationType(\"m1\", 0.5, \"f\", 0.0);\n"
        "	initializeGenomicElementType(\"g1\", m1, 1.0);\n"
        "	initializeGenomicElement(g1, 0, 99999);\n"
        "	initializeRecombinationRate(1e-8);\n"
        "}\n"
        "\n"
        "1 early() {\n"
        "	sim.addSubpop(\"p1\", 500);\n"
        "}\n"
        "\n"
        "2000 late() { sim.outputFixedMutations(); }\n");
}

static std::string defaultNonWFScriptString(void)
{
    return std::string(
                "// set up a simple neutral nonWF simulation\n"
                "initialize() {\n"
                "	initializeSLiMModelType(\"nonWF\");\n"
                "	defineConstant(\"K\", 500);	// carrying capacity\n"
                "	\n"
                "	// neutral mutations, which are allowed to fix\n"
                "	initializeMutationType(\"m1\", 0.5, \"f\", 0.0);\n"
                "	m1.convertToSubstitution = T;\n"
                "	\n"
                "	initializeGenomicElementType(\"g1\", m1, 1.0);\n"
                "	initializeGenomicElement(g1, 0, 99999);\n"
                "	initializeMutationRate(1e-7);\n"
                "	initializeRecombinationRate(1e-8);\n"
                "}\n"
                "\n"
                "// each individual reproduces itself once\n"
                "reproduction() {\n"
                "	subpop.addCrossed(individual, subpop.sampleIndividuals(1));\n"
                "}\n"
                "\n"
                "// create an initial population of 10 individuals\n"
                "1 early() {\n"
                "	sim.addSubpop(\"p1\", 10);\n"
                "}\n"
                "\n"
                "// provide density-dependent selection\n"
                "early() {\n"
                "	p1.fitnessScaling = K / p1.individualCount;\n"
                "}\n"
                "\n"
                "// output all fixed mutations at end\n"
                "2000 late() { sim.outputFixedMutations(); }\n");
}

static std::string defaultNonWFScriptString_NC(void)
{
    return std::string(
        "initialize() {\n"
        "	initializeSLiMModelType(\"nonWF\");\n"
        "	defineConstant(\"K\", 500);\n"
        "	\n"
        "	initializeMutationType(\"m1\", 0.5, \"f\", 0.0);\n"
        "	m1.convertToSubstitution = T;\n"
        "	\n"
        "	initializeGenomicElementType(\"g1\", m1, 1.0);\n"
        "	initializeGenomicElement(g1, 0, 99999);\n"
        "	initializeMutationRate(1e-7);\n"
        "	initializeRecombinationRate(1e-8);\n"
        "}\n"
        "\n"
        "reproduction() {\n"
        "	subpop.addCrossed(individual, subpop.sampleIndividuals(1));\n"
        "}\n"
        "\n"
        "1 early() {\n"
        "	sim.addSubpop(\"p1\", 10);\n"
        "}\n"
        "\n"
        "early() {\n"
        "	p1.fitnessScaling = K / p1.individualCount;\n"
        "}\n"
        "\n"
        "2000 late() { sim.outputFixedMutations(); }\n");
}


QtSLiMWindow::QtSLiMWindow(QtSLiMWindow::ModelType modelType, bool includeComments) : QMainWindow(nullptr), ui(new Ui::QtSLiMWindow)
{
    init();
    setCurrentFile(QString());
    
    // set up the initial script
    std::string untitledScriptString;
    if (includeComments)
        untitledScriptString = (modelType == QtSLiMWindow::ModelType::WF) ? defaultWFScriptString() : defaultNonWFScriptString();
    else
        untitledScriptString = (modelType == QtSLiMWindow::ModelType::WF) ? defaultWFScriptString_NC() : defaultNonWFScriptString_NC();
    
    lastSavedString = QString::fromStdString(untitledScriptString);
    lastSavedDate = QDateTime::currentDateTime();
    scriptChangeObserved = false;
    
    ui->scriptTextEdit->setPlainText(lastSavedString);
    
    if (consoleController)
        consoleController->invalidateSymbolTableAndFunctionMap();
    
    setScriptStringAndInitializeSimulation(untitledScriptString);
    
    if (consoleController)
        consoleController->validateSymbolTableAndFunctionMap();
    
    // Update all our UI to reflect the current state of the simulation
    updateAfterTickFull(true);
    resetSLiMChangeCount();     // no recycle change count; the current model is correct
    setWindowModified(false);    // untitled windows consider themselves unmodified
}

QtSLiMWindow::QtSLiMWindow(const QString &fileName) : QMainWindow(nullptr), ui(new Ui::QtSLiMWindow)
{
    init();
    loadFile(fileName);
}

QtSLiMWindow::QtSLiMWindow(const QString &recipeName, const QString &recipeScript) : QMainWindow(nullptr), ui(new Ui::QtSLiMWindow)
{
    init();
    setCurrentFile(QString());
    setWindowFilePath(recipeName);
    isRecipe = true;
    isTransient = false;
    
    // set up the initial script
    lastSavedString = recipeScript;
    lastSavedDate = QDateTime::currentDateTime();
    scriptChangeObserved = false;
    
    ui->scriptTextEdit->setPlainText(recipeScript);
    setScriptStringAndInitializeSimulation(recipeScript.toUtf8().constData());
    
    // Update all our UI to reflect the current state of the simulation
    updateAfterTickFull(true);
    resetSLiMChangeCount();     // no recycle change count; the current model is correct
    setWindowModified(false);    // untitled windows consider themselves unmodified
}

void QtSLiMWindow::init(void)
{
    // On macOS, we turn off the automatic quit on last window close, for Qt 5.15.2.
    // However, Qt's treatment of the menu bar seems to be a bit buggy unless a main window exists.
    // That main window can be hidden; it just needs to exist.  So here we just allow our main
    // window(s) to leak,so that Qt is happy.  This sucks, obviously, but really it seems unlikely
    // to matter.  The window will notice its zombified state when it is closed, and will free
    // resources and mark itself as a zombie so it doesn't get included in the Window menu, etc.
    // Builds against older Qt versions will just quit on the last window close, because
    // QTBUG-86874 and QTBUG-86875 prevent this from working.
#ifdef __APPLE__
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 2))
    // no set of the attribute on Qt 5.15.2; we will *not* delete on close
#else
    setAttribute(Qt::WA_DeleteOnClose);
#endif
#else
    setAttribute(Qt::WA_DeleteOnClose);
#endif
    isUntitled = true;
    isRecipe = false;
    
    // create the window UI
    ui->setupUi(this);
    
    // hide the species bar initially so it doesn't interfere with the sizing done by interpolateSplitters()
    ui->speciesBarWidget->setHidden(true);
    
    ui->speciesBar->setAcceptDrops(false);
    ui->speciesBar->setDocumentMode(false);
    ui->speciesBar->setDrawBase(false);
    ui->speciesBar->setExpanding(false);
    ui->speciesBar->setMovable(false);
    ui->speciesBar->setShape(QTabBar::RoundedNorth);
    ui->speciesBar->setTabsClosable(false);
    ui->speciesBar->setUsesScrollButtons(false);
    
    connect(ui->speciesBar, &QTabBar::currentChanged, this, &QtSLiMWindow::selectedSpeciesChanged);
    
    // add splitters with the species bar hidden; this sets correct heights on things
    interpolateSplitters();
    initializeUI();
    
    // with everything built, mark ourselves as transient (recipes and files will mark this false after us)
    isTransient = true;
    
    // wire up our continuous play and tick play timers
    connect(&continuousPlayInvocationTimer_, &QTimer::timeout, this, &QtSLiMWindow::_continuousPlay);
    connect(&continuousProfileInvocationTimer_, &QTimer::timeout, this, &QtSLiMWindow::_continuousProfile);
    connect(&playOneStepInvocationTimer_, &QTimer::timeout, this, &QtSLiMWindow::_playOneStep);
    
    // wire up deferred display of script errors and termination messages
    connect(this, &QtSLiMWindow::terminationWithMessage, this, &QtSLiMWindow::showTerminationMessage, Qt::QueuedConnection);
    
    // forward option-clicks in our views to the help window
    ui->scriptTextEdit->setOptionClickEnabled(true);
    ui->outputTextEdit->setOptionClickEnabled(false);
    
    // the script textview completes, the output textview does not
    ui->scriptTextEdit->setCodeCompletionEnabled(true);
    ui->outputTextEdit->setCodeCompletionEnabled(false);
    
    // We set the working directory for new windows to ~/Desktop/, since it makes no sense for them to use the location of the app.
    // Each running simulation will track its own working directory, and the user can set it with a button in the SLiMgui window.
    // BCH 4/2/2020: Per request from PLR, we will now use the Desktop as the default directory only if we were launched by Finder
    // or equivalent; if we were launched by a shell, we will use the working directory given us by that shell.  See issue #76
    if (qtSLiMAppDelegate->launchedFromShell())
        sim_working_dir = qtSLiMAppDelegate->QtSLiMCurrentWorkingDirectory();
    else
#ifdef _WIN32
        sim_working_dir = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString();
#else
        sim_working_dir = Eidos_ResolvedPath("~/Desktop");
#endif
    
    // Check that our chosen working directory actually exists; if not, use ~
    struct stat buffer;
    
    if (stat(sim_working_dir.c_str(), &buffer) != 0)
#ifdef _WIN32
        sim_working_dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation).toStdString();
#else
        sim_working_dir = Eidos_ResolvedPath("~");
#endif
    
    sim_requested_working_dir = sim_working_dir;	// return to the working dir on recycle unless the user overrides it
    
    // Wire up things that set the window to be modified.
    connect(ui->scriptTextEdit, &QPlainTextEdit::textChanged, this, &QtSLiMWindow::documentWasModified);
    connect(ui->scriptTextEdit, &QPlainTextEdit::textChanged, this, &QtSLiMWindow::scriptTexteditChanged);
    
    // Watch for app activation to check for external modification of our file
    connect(qApp, &QApplication::applicationStateChanged, this, &QtSLiMWindow::appStateChanged);
    
    // Watch for changes to the selection in the population tableview
    connect(ui->subpopTableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QtSLiMWindow::subpopSelectionDidChange);
    
    // Watch for changes to our change count, for the recycle button color
    connect(this, &QtSLiMWindow::controllerChangeCountChanged, this, [this]() { updateRecycleButtonIcon(false); });
    
    // Ensure that the tick lineedit does not have the initial keyboard focus and has no selection; hard to do!
    // BCH 5 August 2022: this code is no longer working, the tick lineedit still has initial focus, sigh; I added
    // the call to ui->scriptTextEdit->setFocus() and that seems to do it, not sure why I didn't do that before;
    // but since this seems to be fragile, I'm going to leave *both* approaches in the code here, maybe which
    // approach works depends on the Qt version or the platform or something.  Forward in all directions!
    ui->tickLineEdit->setFocusPolicy(Qt::FocusPolicy::NoFocus);
    QTimer::singleShot(0, this, [this]() { ui->tickLineEdit->setFocusPolicy(Qt::FocusPolicy::StrongFocus); });
    ui->scriptTextEdit->setFocus();
    
    // watch for a change to light mode / dark mode, to customize display of the play speed slider for example
    connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::applicationPaletteChanged, this, &QtSLiMWindow::applicationPaletteChanged);
    applicationPaletteChanged();
    
    // Instantiate the help panel up front so that it responds instantly; slows down our launch, but it seems better to me...
    QtSLiMHelpWindow::instance();
    
	// Allocate a RNG temporarily, so it exists when we create the console controller and run self-tests
    // This is just for the duration of this init() method; we will tear it down again below
    if (!sim_RNG_initialized)
    {
        _Eidos_InitializeOneRNG(sim_RNG);
        sim_RNG_initialized = true;
    }
    
    // Create our console window; we want one all the time, so that it keeps live symbols for code completion for us
    if (!consoleController)
    {
        consoleController = new QtSLiMEidosConsole(this);
        if (consoleController)
        {
            // wire ourselves up to monitor the console for closing, to fix our button state
            connect(consoleController, &QtSLiMEidosConsole::willClose, this, [this]() {
                ui->consoleButton->setChecked(false);
                showConsoleReleased();
            });
        }
        else
        {
            qDebug() << "Could not create console controller";
        }
    }
    
    // Create our debug output window; we want one all the time, so we can log to it
    debugOutputWindow_ = new QtSLiMDebugOutputWindow(this);
    
    connect(&debugButtonFlashTimer_, &QTimer::timeout, this, &QtSLiMWindow::handleDebugButtonFlash);
    
    // We need to update our button/menu enable state whenever the focus or the active window changes
    connect(qApp, &QApplication::focusChanged, this, &QtSLiMWindow::updateUIEnabling);
    connect(qtSLiMAppDelegate, &QtSLiMAppDelegate::activeWindowListChanged, this, &QtSLiMWindow::updateUIEnabling);
    
    // We also do it specifically when the Edit menu is about to show, to correctly validate undo/redo in all cases
    // Note that it is not simple to do this revalidation when a keyboard shortcut is pressed, but happily (?), Qt
    // ignores the action validation state in that case anyway; undo/redo is delivered even if the action is disabled
    connect(ui->menuEdit, &QMenu::aboutToShow, this, &QtSLiMWindow::updateUIEnabling);
    
    // And also when about to show the Graph menu, because the Create Haplotype Plot items might need tweaking
    connect(ui->menuGraph, &QMenu::aboutToShow, this, &QtSLiMWindow::updateUIEnabling);
    
    // And also when about to show the Script menu, because the Show/Hide menu items might not be accurately named
    connect(ui->menuScript, &QMenu::aboutToShow, this, &QtSLiMWindow::updateUIEnabling);
    
    // The app delegate wants to know our play state so it can change the app icon
    connect(this, &QtSLiMWindow::playStateChanged, qtSLiMAppDelegate, &QtSLiMAppDelegate::playStateChanged);
    
    // Set the window icon, overriding the app icon
#ifdef __APPLE__
    // set the window icon only on macOS; on Linux it changes the app icon as a side effect
    setWindowIcon(qtSLiMAppDelegate->slimDocumentIcon());
#endif
    
    // Run self-tests if modifiers are down, if we are the first window opened
    // Note that this alters the state of the app: mutation ids have been used, the RNG has been used,
    // lots of objects have been leaked due to raises, etc.  So this should be hidden/optional/undocumented.
    static bool beenHere = false;
    
    if (!beenHere)
    {
        bool optionPressed = QGuiApplication::queryKeyboardModifiers().testFlag(Qt::AltModifier);
        bool shiftPressed = QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier);
        
        if (optionPressed && shiftPressed)
        {
            willExecuteScript();
            
            std::cerr << "Running Eidos self-test..." << std::endl;
            RunEidosTests();
            std::cerr << std::endl << std::endl;
            std::cerr << "Running SLiM self-test..." << std::endl;
            RunSLiMTests();
            
            didExecuteScript();
        }
        
        beenHere = true;
    }
    
    // Tear down the temporary RNG that we created above
    if (sim_RNG_initialized)
	{
		_Eidos_FreeOneRNG(sim_RNG);
		sim_RNG_initialized = false;
	}
}

void QtSLiMWindow::interpolateVerticalSplitter(void)
{
    const int splitterMargin = 8;
    QLayout *parentLayout = ui->centralWidget->layout();
    QVBoxLayout *firstSubLayout = ui->overallTopLayout;
    QHBoxLayout *secondSubLayout = ui->overallBottomLayout;
    
    // force geometry calculation, which is lazy
    setAttribute(Qt::WA_DontShowOnScreen, true);
    show();
    hide();
    setAttribute(Qt::WA_DontShowOnScreen, false);
    
    // get the geometry we need
    QSize firstSubSize = firstSubLayout->sizeHint();
    QMargins marginsP = QMargins(8, 8, 8, 8); //parentLayout->contentsMargins();
    QMargins marginsS1 = firstSubLayout->contentsMargins();
    QMargins marginsS2 = secondSubLayout->contentsMargins();
    
    // change fixed-size views to be flexible, so they cooperate with the splitters
    firstSubLayout->setStretch(0, 1);
    ui->subpopTableView->setMaximumHeight(QWIDGETSIZE_MAX);
    ui->individualsWidget->setMaximumHeight(QWIDGETSIZE_MAX);
    ui->topRightLayout->setStretch(4, 1);
#if !defined(__APPLE__)
    ui->topRightLayout->setSpacing(3);  // a platform-dependent value that prevents a couple of pixels of "play" above the play speed slider, for reasons I don't understand
#else
    ui->topRightLayout->setSpacing(4);
#endif
    ui->playSpeedSlider->setFixedHeight(ui->playSpeedSlider->sizeHint().height());
    
    // empty out parentLayout
    QLayoutItem *child;
    while ((child = parentLayout->takeAt(0)) != nullptr);
    
    ui->topBottomDividerLine->setParent(nullptr);
    ui->topBottomDividerLine = nullptr;
    
    // make the new top-level widgets and transfer in their contents
    overallTopWidget = new QWidget(nullptr);
    overallTopWidget->setLayout(firstSubLayout);
    overallTopWidget->setMinimumHeight(firstSubSize.height() + (splitterMargin - 5));   // there is already 5 pixels of margin at the bottom of overallTopWidget due to layout details
    firstSubLayout->setContentsMargins(QMargins(marginsS1.left() + marginsP.left(), marginsS1.top() + marginsP.top(), marginsS1.right() + marginsP.right(), marginsS1.bottom() + (splitterMargin - 5)));
    
    overallBottomWidget = new QWidget(nullptr);
    overallBottomWidget->setLayout(secondSubLayout);
    secondSubLayout->setContentsMargins(QMargins(marginsS2.left() + marginsP.left(), marginsS2.top() + splitterMargin, marginsS2.right() + marginsP.right(), marginsS2.bottom() + marginsP.bottom()));
    
    // make the QSplitter between the top and bottom and add the top-level widgets to it
    overallSplitter = new QtSLiMSplitter(Qt::Vertical, this);
    
    overallSplitter->setChildrenCollapsible(true);
    overallSplitter->addWidget(overallTopWidget);
    overallSplitter->addWidget(overallBottomWidget);
    overallSplitter->setHandleWidth(std::max(9, overallSplitter->handleWidth() + 3));   // ends up 9 on Ubuntu, 10 on macOS
    overallSplitter->setStretchFactor(0, 1);
    overallSplitter->setStretchFactor(1, 100);    // initially, give all height to the bottom widget
    
    // and finally, add the splitter to the parent layout
    parentLayout->addWidget(overallSplitter);
    parentLayout->setContentsMargins(0, 0, 0, 0);
}

void QtSLiMWindow::interpolateHorizontalSplitter(void)
{
    const int splitterMargin = 8;
    QLayout *parentLayout = overallBottomWidget->layout();
    QVBoxLayout *firstSubLayout = ui->scriptLayout;
    QVBoxLayout *secondSubLayout = ui->outputLayout;
    
    // force geometry calculation, which is lazy
    setAttribute(Qt::WA_DontShowOnScreen, true);
    show();
    hide();
    setAttribute(Qt::WA_DontShowOnScreen, false);
    
    // get the geometry we need
    QMargins marginsP = parentLayout->contentsMargins();
    QMargins marginsS1 = firstSubLayout->contentsMargins();
    QMargins marginsS2 = secondSubLayout->contentsMargins();
    
    // empty out parentLayout
    QLayoutItem *child;
    while ((child = parentLayout->takeAt(0)) != nullptr);
    
    // make the new top-level widgets and transfer in their contents
    scriptWidget = new QWidget(nullptr);
    scriptWidget->setLayout(firstSubLayout);
    firstSubLayout->setContentsMargins(QMargins(marginsS1.left() + marginsP.left(), marginsS1.top() + marginsP.top(), marginsS1.right() + splitterMargin, marginsS1.bottom() + marginsP.bottom()));
    
    outputWidget = new QWidget(nullptr);
    outputWidget->setLayout(secondSubLayout);
    secondSubLayout->setContentsMargins(QMargins(marginsS2.left() + splitterMargin, marginsS2.top() + marginsP.top(), marginsS2.right() + marginsP.right(), marginsS2.bottom() + marginsP.bottom()));
    
    // make the QSplitter between the left and right and add the subsidiary widgets to it
    bottomSplitter = new QtSLiMSplitter(Qt::Horizontal, this);
    
    bottomSplitter->setChildrenCollapsible(true);
    bottomSplitter->addWidget(scriptWidget);
    bottomSplitter->addWidget(outputWidget);
    bottomSplitter->setHandleWidth(std::max(9, bottomSplitter->handleWidth() + 3));   // ends up 9 on Ubuntu, 10 on macOS
    bottomSplitter->setStretchFactor(0, 2);
    bottomSplitter->setStretchFactor(1, 1);    // initially, give 2/3 of the width to the script widget
    
    // and finally, add the splitter to the parent layout
    parentLayout->addWidget(bottomSplitter);
    parentLayout->setContentsMargins(0, 0, 0, 0);
}

void QtSLiMWindow::interpolateSplitters(void)
{
#if 1
    // This case is hit if splitters are enabled; it adds a top-level vertical splitter and a subsidiary horizontal splitter
    // We do this at runtime, rather than in QtSLiMWindow.ui, to preserve the non-splitter option, and because the required
    // alterations are complex and depend upon the (platform-dependent) initial calculated sizes of the various elements
    interpolateVerticalSplitter();
    interpolateHorizontalSplitter();
#else
    // This case is hit if splitters are disabled; it does a little cleanup of elements that exist to support the splitters
    ui->topRightLayout->removeItem(ui->playControlsSpacerExpanding);
    delete ui->playControlsSpacerExpanding;
    ui->playControlsSpacerExpanding = nullptr;
#endif
}

void QtSLiMWindow::addChromosomeWidgets(QVBoxLayout *chromosomeLayout, QtSLiMChromosomeWidget *overviewWidget, QtSLiMChromosomeWidget *zoomedWidget)
{
    if (!chromosomeConfig)
        chromosomeConfig = new QtSLiMChromosomeWidgetController(this, nullptr, nullptr, "");
    
    overviewWidget->setController(chromosomeConfig);
	overviewWidget->setDependentChromosomeView(zoomedWidget);
	
    zoomedWidget->setController(chromosomeConfig);
	zoomedWidget->setDependentChromosomeView(nullptr);
    
    // Add these widgets to our vectors of chromosome widgets
    chromosomeWidgetLayouts.push_back(chromosomeLayout);
    chromosomeOverviewWidgets.push_back(overviewWidget);
    chromosomeZoomedWidgets.push_back(zoomedWidget);
}

void QtSLiMWindow::initializeUI(void)
{
    glueUI();
    
    // fix the layout of the window
    ui->scriptHeaderLayout->setSpacing(4);
    ui->scriptHeaderLayout->setContentsMargins(0, 0, 0, 0);
    ui->scriptHeaderLabel->setContentsMargins(8, 0, 15, 0);

    ui->outputHeaderLayout->setSpacing(4);
    ui->outputHeaderLayout->setContentsMargins(0, 0, 0, 0);
    ui->outputHeaderLabel->setContentsMargins(8, 0, 15, 0);

    ui->playControlsLayout->setSpacing(8);
    ui->playControlsLayout->setContentsMargins(0, 0, 0, 0);
    
    // substitute a custom layout subclass for playControlsLayout to lay out the profile button specially
    {
        int indexOfPlayControlsLayout = -1;
        
        // QLayout::indexOf(QLayoutItem *layoutItem) wasn't added until 5.12, oddly
        for (int i = 0; i < ui->topRightLayout->count(); ++i)
            if (ui->topRightLayout->itemAt(i) == ui->playControlsLayout)
                indexOfPlayControlsLayout = i;
        
        if (indexOfPlayControlsLayout >= 0)
        {
            QtSLiMPlayControlsLayout *newPlayControlsLayout = new QtSLiMPlayControlsLayout();
            ui->topRightLayout->insertItem(indexOfPlayControlsLayout, newPlayControlsLayout);
            newPlayControlsLayout->setParent(ui->topRightLayout);   // surprising that insertItem() doesn't do this...; but this sets our parentWidget also, correctly
            
            // Transfer over the contents of the old layout
            while (ui->playControlsLayout->count())
            {
                QLayoutItem *layoutItem = ui->playControlsLayout->takeAt(0);
                newPlayControlsLayout->addItem(layoutItem);
            }
            
            // Transfer properties of the old layout
            newPlayControlsLayout->setSpacing(ui->playControlsLayout->spacing());
            newPlayControlsLayout->setContentsMargins(ui->playControlsLayout->contentsMargins());
            
            // Get rid of the old layout
            ui->topRightLayout->removeItem(ui->playControlsLayout);
            ui->playControlsLayout = nullptr;
            
            // Remember the new layout
            ui->playControlsLayout = newPlayControlsLayout;
        }
        else
        {
            qDebug() << "Couldn't find playControlsLayout!";
        }
    }
    
    // set the script types and syntax highlighting appropriately
    ui->scriptTextEdit->setScriptType(QtSLiMTextEdit::SLiMScriptType);
    ui->scriptTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::ScriptHighlighting);
    
    ui->outputTextEdit->setScriptType(QtSLiMTextEdit::NoScriptType);
    ui->outputTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::OutputHighlighting);
    
    // set up the script block label, to the right of the Jump menu
    QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance();
    
    connect(&prefsNotifier, &QtSLiMPreferencesNotifier::displayFontPrefChanged, this, &QtSLiMWindow::displayFontPrefChanged);
    displayFontPrefChanged();
    
    // set button states
    ui->toggleDrawerButton->setChecked(false);
    
    // Set up the population table view
    populationTableModel_ = new QtSLiMPopulationTableModel(this);
    ui->subpopTableView->setModel(populationTableModel_);
    ui->subpopTableView->setHorizontalHeader(new QtSLiMPopulationTableHeaderView(Qt::Orientation::Horizontal, this));
    
    QHeaderView *popTableHHeader = ui->subpopTableView->horizontalHeader();
    QHeaderView *popTableVHeader = ui->subpopTableView->verticalHeader();
    
    popTableHHeader->setMinimumSectionSize(1);
    popTableVHeader->setMinimumSectionSize(1);
    
    popTableHHeader->resizeSection(0, 65);
    //popTableHHeader->resizeSection(1, 60);
    popTableHHeader->resizeSection(2, 40);
    popTableHHeader->resizeSection(3, 40);
    popTableHHeader->resizeSection(4, 40);
    popTableHHeader->resizeSection(5, 40);
    popTableHHeader->setSectionsClickable(false);
    popTableHHeader->setSectionsMovable(false);
    popTableHHeader->setSectionResizeMode(0, QHeaderView::Fixed);
    popTableHHeader->setSectionResizeMode(1, QHeaderView::Stretch);
    popTableHHeader->setSectionResizeMode(2, QHeaderView::Fixed);
    popTableHHeader->setSectionResizeMode(3, QHeaderView::Fixed);
    popTableHHeader->setSectionResizeMode(4, QHeaderView::Fixed);
    popTableHHeader->setSectionResizeMode(5, QHeaderView::Fixed);
    
    QFont headerFont = popTableHHeader->font();
    QFont cellFont = ui->subpopTableView->font();
#ifdef __linux__
    headerFont.setPointSize(8);
    cellFont.setPointSize(8);
#else
    headerFont.setPointSize(11);
    cellFont.setPointSize(11);
#endif
    popTableHHeader->setFont(headerFont);
    ui->subpopTableView->setFont(cellFont);
    
    popTableVHeader->setSectionResizeMode(QHeaderView::Fixed);
    popTableVHeader->setDefaultSectionSize(18);
    
    // Set up our built-in chromosome widgets; this should be the only place these ui outlets are used!
    addChromosomeWidgets(ui->chromosomeWidgetLayout, ui->chromosomeOverview, ui->chromosomeZoomed);
    
    // Restore the saved window position; see https://doc.qt.io/qt-5/qsettings.html#details
    QSettings settings;
    
    settings.beginGroup("QtSLiMMainWindow");
    resize(settings.value("size", QSize(950, 700)).toSize());
    move(settings.value("pos", QPoint(100, 100)).toPoint());
    settings.endGroup();
    
    // Ask the app delegate to handle the recipes menu for us
    qtSLiMAppDelegate->setUpRecipesMenu(ui->menuOpenRecipe, ui->actionFindRecipe);
    
    // Likewise for the recent documents menu
    QMenu *recentMenu = new QMenu("Open Recent", this);
    ui->actionOpenRecent->setMenu(recentMenu);
    
    qtSLiMAppDelegate->setUpRecentsMenu(recentMenu);
    
    // Set up the Window menu, which updates on demand
    connect(ui->menuWindow, &QMenu::aboutToShow, this, &QtSLiMWindow::updateWindowMenu);
}

void QtSLiMWindow::displayFontPrefChanged(void)
{
    // Xcode doesn't use its monospace for this, and it does look a bit out of place in the UI
    // So let's try it allowing the font to remain the default system font...?
//    QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance();
//    QFont displayFont = prefs.displayFontPref(nullptr);
    
//    displayFont.setPointSize(13);
//    ui->scriptBlockLabel->setFont(displayFont);
}

void QtSLiMWindow::applicationPaletteChanged(void)
{
    bool inDarkMode = QtSLiMInDarkMode();
    
    // Custom colors for the play slider; note that this completely overrides the style sheet in the .ui file!
    if (inDarkMode)
    {
        ui->playSpeedSlider->setStyleSheet(
                    R"V0G0N(
                    QSlider::groove:horizontal {
                        border: 1px solid #606060;
                        border-radius: 1px;
                        height: 2px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */
                        background: #808080;
                        margin: 2px 0;
                    }
                    QSlider::groove:horizontal:disabled {
                        border: 1px solid #505050;
                        border-radius: 1px;
                        height: 2px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */
                        background: #606060;
                        margin: 2px 0;
                    }
                    
                    QSlider::handle:horizontal {
                        background: #f0f0f0;
                        border: 1px solid #b0b0b0;
                        width: 8px;
                        margin: -4px 0;
                        border-radius: 4px;
                    }
                    QSlider::handle:horizontal:disabled {
                        background: #606060;
                        border: 1px solid #505050;
                        width: 8px;
                        margin: -4px 0;
                        border-radius: 4px;
                    })V0G0N");
    }
    else
    {
        ui->playSpeedSlider->setStyleSheet(
                    R"V0G0N(
                    QSlider::groove:horizontal {
                        border: 1px solid #888888;
                        border-radius: 1px;
                        height: 2px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */
                        background: #a0a0a0;
                        margin: 2px 0;
                    }
                    QSlider::groove:horizontal:disabled {
                        border: 1px solid #cccccc;
                        border-radius: 1px;
                        height: 2px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */
                        background: #e0e0e0;
                        margin: 2px 0;
                    }
                    
                    QSlider::handle:horizontal {
                        background: #ffffff;
                        border: 1px solid #909090;
                        width: 8px;
                        margin: -4px 0;
                        border-radius: 4px;
                    }
                    QSlider::handle:horizontal:disabled {
                        background: #ffffff;
                        border: 1px solid #d0d0d0;
                        width: 8px;
                        margin: -4px 0;
                        border-radius: 4px;
                    })V0G0N");
    }
}

void QtSLiMWindow::displayStartupMessage(void)
{
    // Set the initial status bar message; called by QtSLiMAppDelegate::appDidFinishLaunching()
    bool inDarkMode = QtSLiMInDarkMode();
    QString message(inDarkMode ? "SLiM %1, %2 build."
                               : "SLiM %1, %2 build.");
    
    ui->statusBar->showMessage(message.arg(QString(SLIM_VERSION_STRING)).arg(
#if DEBUG
                                   "debug"
#else
                                   "release"
#endif
                                   ));    
}

QtSLiMScriptTextEdit *QtSLiMWindow::scriptTextEdit(void)
{
    return ui->scriptTextEdit;
}

QtSLiMTextEdit *QtSLiMWindow::outputTextEdit(void)
{
    return ui->outputTextEdit;
}

QtSLiMWindow::~QtSLiMWindow()
{
    // Do this first, in case it uses any ivars that will be freed
    setInvalidSimulation(true);
    
    // Disconnect our connections having to do with focus changes, since they can fire
    // during our destruction while we are in an invalid state
    disconnect(qApp, QMetaMethod(), this, QMetaMethod());
    disconnect(qtSLiMAppDelegate, QMetaMethod(), this, QMetaMethod());
    
    // Then tear down the UI
    delete ui;

    // Disconnect delegate relationships
    if (consoleController)
        consoleController->parentSLiMWindow = nullptr;
    
    // Free resources
    if (community)
    {
        delete community;
        community = nullptr;
        focalSpecies = nullptr;
    }
    if (slimgui)
	{
		delete slimgui;
		slimgui = nullptr;
	}
	
	if (sim_RNG_initialized)
	{
    	_Eidos_FreeOneRNG(sim_RNG);
    	sim_RNG_initialized = false;
	}
	
    // The console is owned by us, and it owns the variable browser.  Since the parent
    // relationships are set up, they should be released by Qt automatically.
    if (consoleController)
    {
        //if (consoleController->browserController)
        //  consoleController->browserController->hide();
        consoleController->hide();
    }
}

void QtSLiMWindow::invalidateUI(void)
{
    // This is called only on macOS, when a window closes.  We can't be deleted, because
    // that screws up the global menu bar.  Instead, we need to go into a zombie state,
    // by freeing up our graph windows, console, etc., but remain allocated (but hidden).
    // The main goal is erasing all traces of us in the user interface; freeing the
    // maximal amount of memory is less of a concern, since we're not talking about
    // that much memory anyway.
    
    // First set a flag indicating that we're going into zombie mode
    isZombieWindow_ = true;
    
    // Set some other state to prevent ourselves from being reused in any way
    isUntitled = false;
    isTransient = false;
    currentFile = "ZOMBIE ZOMBIE ZOMBIE ZOMBIE ZOMBIE";
    
    // Stop all timers, so we don't try to play in the background
    continuousPlayElapsedTimer_.invalidate();
    continuousPlayInvocationTimer_.stop();
    continuousProfileInvocationTimer_.stop();
    playOneStepInvocationTimer_.stop();
    
    continuousPlayOn_ = false;
    profilePlayOn_ = false;
    nonProfilePlayOn_ = false;
    tickPlayOn_ = false;
    
    // Recycle to throw away any bulky simulation state; set the default script first to avoid errors
    // Note that this creates a species named "sim" even if the window being closed was multispecies!
    ui->scriptTextEdit->setPlainText(QString::fromStdString(defaultWFScriptString()));
    recycleClicked();
    
    // Close the variable browser and Eidos console
    if (consoleController)
    {
        QtSLiMVariableBrowser *browser = consoleController->variableBrowser();
        
        if (browser)
            browser->close();
        
        consoleController->close();
    }
    
    // Close the tables drawer
    if (tablesDrawerController)
        tablesDrawerController->close();
    
    // Close all other subsidiary windows
    const QObjectList &child_objects = children();
    
    for (QObject *child_object : child_objects)
    {
        QWidget *child_widget = qobject_cast(child_object);
        
        if (child_widget && child_widget->isVisible() && (child_widget->windowFlags() & Qt::Window))
            child_widget->close();
    }
}

QtSLiMGraphView *QtSLiMWindow::graphViewWithTitle(QString title)
{
    // This searches through our child views for a graph window with the requested title
    const QObjectList &child_objects = children();
    
    for (QObject *child_object : child_objects)
    {
        QWidget *child_widget = qobject_cast(child_object);
        
        if (child_widget && child_widget->isVisible() && (child_widget->windowFlags() & Qt::Window))
        {
            QtSLiMGraphView *graphView = graphViewForGraphWindow(child_widget);
            
            if (graphView && (graphView->graphTitle() == title))
                return graphView;
        }
    }
    
    return nullptr;
}

int QtSLiMWindow::graphViewCount(void)
{
    // This searches through our child views for graph windows and returns a count
    int count = 0;
    
    const QObjectList &child_objects = children();
    
    for (QObject *child_object : child_objects)
    {
        QWidget *child_widget = qobject_cast(child_object);
        
        if (child_widget && child_widget->isVisible() && (child_widget->windowFlags() & Qt::Window))
        {
            QtSLiMGraphView *graphView = graphViewForGraphWindow(child_widget);
            
            if (graphView)
                count++;
        }
    }
    
    return count;
}

const QColor &QtSLiMWindow::blackContrastingColorForIndex(int index)
{
    static std::vector colorArray;
	
	if (colorArray.size() == 0)
	{
        colorArray.emplace_back(QtSLiMColorWithHSV(0.65, 0.65, 1.00, 1.0));
        colorArray.emplace_back(QtSLiMColorWithHSV(0.55, 1.00, 1.00, 1.0));
        colorArray.emplace_back(QtSLiMColorWithHSV(0.40, 1.00, 0.90, 1.0));
        colorArray.emplace_back(QtSLiMColorWithHSV(0.16, 1.00, 1.00, 1.0));
        colorArray.emplace_back(QtSLiMColorWithHSV(0.08, 0.65, 1.00, 1.0));
        colorArray.emplace_back(QtSLiMColorWithHSV(0.00, 0.65, 1.00, 1.0));
        colorArray.emplace_back(QtSLiMColorWithHSV(0.80, 0.65, 1.00, 1.0));
        colorArray.emplace_back(QtSLiMColorWithHSV(0.00, 0.00, 0.80, 1.0));
	}
	
    return ((index >= 0) && (index <= 6)) ? colorArray[static_cast(index)] : colorArray[7];
}

const QColor &QtSLiMWindow::whiteContrastingColorForIndex(int index)
{
    static std::vector colorArray;
    
    if (colorArray.size() == 0)
    {
        colorArray.emplace_back(QtSLiMColorWithHSV(0.65, 0.75, 1.00, 1.0));
		colorArray.emplace_back(QtSLiMColorWithHSV(0.55, 1.00, 1.00, 1.0));
		colorArray.emplace_back(QtSLiMColorWithHSV(0.40, 1.00, 0.80, 1.0));
		colorArray.emplace_back(QtSLiMColorWithHSV(0.08, 0.75, 1.00, 1.0));
		colorArray.emplace_back(QtSLiMColorWithHSV(0.00, 0.85, 1.00, 1.0));
		colorArray.emplace_back(QtSLiMColorWithHSV(0.80, 0.85, 1.00, 1.0));
		colorArray.emplace_back(QtSLiMColorWithHSV(0.00, 0.00, 0.50, 1.0));
    }
    
    return ((index >= 0) && (index <= 5)) ? colorArray[static_cast(index)] : colorArray[6];
}

void QtSLiMWindow::colorForGenomicElementType(GenomicElementType *elementType, slim_objectid_t elementTypeID, float *p_red, float *p_green, float *p_blue, float *p_alpha)
{
	if (elementType && !elementType->color_.empty())
	{
        *p_red = elementType->color_red_;
        *p_green = elementType->color_green_;
        *p_blue = elementType->color_blue_;
        *p_alpha = 1.0f;
	}
	else
	{
        auto elementColorIter = genomicElementColorRegistry.find(elementTypeID);
		const QColor *elementColor = nullptr;
        
		if (elementColorIter == genomicElementColorRegistry.end())
		{
			elementColor = &QtSLiMWindow::blackContrastingColorForIndex(static_cast(genomicElementColorRegistry.size()));
            
            genomicElementColorRegistry.emplace(elementTypeID, *elementColor);
		}
        else
        {
            elementColor = &elementColorIter->second;
        }
		
        *p_red = static_cast(elementColor->redF());
        *p_green = static_cast(elementColor->greenF());
        *p_blue = static_cast(elementColor->blueF());
        *p_alpha = static_cast(elementColor->alphaF());
	}
}

QColor QtSLiMWindow::qcolorForSpecies(Species *species)
{
    if (species->color_.length() > 0)
        return QtSLiMColorWithRGB(species->color_red_, species->color_green_, species->color_blue_, 1.0);
    
    return whiteContrastingColorForIndex(species->species_id_);
}

void QtSLiMWindow::colorForSpecies(Species *species, float *p_red, float *p_green, float *p_blue, float *p_alpha)
{
    if (species->color_.length() > 0)
    {
        *p_red = species->color_red_;
        *p_green = species->color_green_;
        *p_blue = species->color_blue_;
        *p_alpha = 1.0;
        return;
    }
    
    const QColor &speciesColor = whiteContrastingColorForIndex(species->species_id_);
    
    *p_red = static_cast(speciesColor.redF());
    *p_green = static_cast(speciesColor.greenF());
    *p_blue = static_cast(speciesColor.blueF());
    *p_alpha = static_cast(speciesColor.alphaF());
}


//
//  Document support
//

void QtSLiMWindow::closeEvent(QCloseEvent *p_event)
{
    if (maybeSave())
    {
        // We used to save the window size/position here, but now that is done in moveEvent() / resizeEvent()
        p_event->accept();
        
        // In case we are playing when we get closed, emit a signal to un-highlight the app icon
        if (continuousPlayOn_)
        {
            continuousPlayOn_ = false;
            //updateUIEnabling();           // not needed, the window is going away anyway
            emit playStateChanged();
        }
        
        // On macOS, we turn off the automatic quit on last window close, for Qt 5.15.2.
        // In that case, we no longer get freed when we close, because we need to stick around
        // to make the global menubar work; see QtSLiMWindow::init().  So when we're closing,
        // we now free up the resources we hold and mark ourselves as a zombie window.
        // Builds against older Qt versions will just quit on the last window close, because
        // QTBUG-86874 and QTBUG-86875 prevent this from working.
#ifdef __APPLE__
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 2))
        invalidateUI();
#endif
#endif
    }
    else
    {
        p_event->ignore();
        qtSLiMAppDelegate->closeRejected();
    }
}

void QtSLiMWindow::moveEvent(QMoveEvent *p_event)
{
    if (donePositioning_)
    {
        // Save the window position; see https://doc.qt.io/qt-5/qsettings.html#details
        QSettings settings;
        
        settings.beginGroup("QtSLiMMainWindow");
        settings.setValue("size", size());
        settings.setValue("pos", pos());
        settings.endGroup();
        
        //qDebug() << "moveEvent() after done positioning";
    }
    
    QWidget::moveEvent(p_event);
}

void QtSLiMWindow::resizeEvent(QResizeEvent *p_event)
{
    if (donePositioning_)
    {
        // Save the window position; see https://doc.qt.io/qt-5/qsettings.html#details
        QSettings settings;
        
        settings.beginGroup("QtSLiMMainWindow");
        settings.setValue("size", size());
        settings.setValue("pos", pos());
        settings.endGroup();
        
        //qDebug() << "resizeEvent() after done positioning";
    }
    
    QWidget::resizeEvent(p_event);
}

void QtSLiMWindow::showEvent(QShowEvent *p_event)
{
    QWidget::showEvent(p_event);
    
    if (!testAttribute(Qt::WA_DontShowOnScreen))
    {
        //qDebug() << "showEvent() : done positioning";
        donePositioning_ = true;
    }
}

bool QtSLiMWindow::isScriptModified(void)
{
    // We used to use Qt's isWindowModified() change-tracking system.  Unfortunately, apparently that is broken on Debian;
    // see https://github.com/MesserLab/SLiM/issues/370.  It looks like Qt internally calls textChanged() and modifies the
    // document when it shouldn't, resulting in untitled documents being marked dirty.  So now we check whether the
    // script string has been changed from when it was last saved to disk, or from its initial state if it is not
    // based on a disk file.  Once a change has been observed, the document stays dirty; it doesn't revert to clean if
    // the script string goes back to its original state (although smart, that would be non-standard).  BCH 10/24/2023
    if (scriptChangeObserved)
        return true;
    
    QString curScriptString = ui->scriptTextEdit->toPlainText();
    
    if (lastSavedString != curScriptString)
    {
        scriptChangeObserved = true;    // sticky until saved
        return true;
    }
    
    return false;
}

bool QtSLiMWindow::windowIsReuseable(void)
{
    return (isUntitled && !isRecipe && isTransient && (slimChangeCount == 0) && !isScriptModified());
}

bool QtSLiMWindow::save()
{
    return isUntitled ? saveAs() : saveFile(currentFile);
}

bool QtSLiMWindow::saveAs()
{
    QString fileName;
    
    if (isUntitled)
    {
        QSettings settings;
        QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
        QString directory = settings.value("QtSLiMDefaultSaveDirectory", QVariant(desktopPath)).toString();
        QFileInfo fileInfo(QDir(directory), "Untitled.slim");
        QString path = fileInfo.absoluteFilePath();
        
        fileName = QFileDialog::getSaveFileName(this, "Save As", path);
        
        if (!fileName.isEmpty())
            settings.setValue("QtSLiMDefaultSaveDirectory", QVariant(QFileInfo(fileName).path()));
    }
    else
    {
        // propose saving to the existing filename in the existing directory
        fileName = QFileDialog::getSaveFileName(this, "Save As", currentFile);
    }
    
    if (fileName.isEmpty())
        return false;

    return saveFile(fileName);
}

void QtSLiMWindow::revert()
{
    if (isUntitled)
    {
        qApp->beep();
    }
    else
    {
        const QMessageBox::StandardButton ret = QMessageBox::warning(this, "SLiMgui", "Are you sure you want to revert?  All changes will be lost.", QMessageBox::Yes | QMessageBox::Cancel);
        
        switch (ret) {
        case QMessageBox::Yes:
            loadFile(currentFile);
            break;
        case QMessageBox::Cancel:
            break;
        default:
            break;
        }
    }
}

bool QtSLiMWindow::maybeSave()
{
    // the recycle button change state is irrelevant; the document change state is what matters
    if (!isScriptModified())
        return true;
    
    const QMessageBox::StandardButton ret = QMessageBox::warning(this, "SLiMgui", "The document has been modified.\nDo you want to save your changes?", QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
    
    switch (ret) {
    case QMessageBox::Save:
        return save();
    case QMessageBox::Cancel:
        return false;
    default:
        break;
    }
    return true;
}

void QtSLiMWindow::loadFile(const QString &fileName)
{
    QFile file(fileName);
    
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        QMessageBox::warning(this, "SLiMgui", QString("Cannot read file %1:\n%2.").arg(QDir::toNativeSeparators(fileName), file.errorString()));
        return;
    }
    
    QTextStream in(&file);
    QString contents = in.readAll();
    
    lastSavedString = contents;
    lastSavedDate = QDateTime::currentDateTime();
    scriptChangeObserved = false;
    
    ui->scriptTextEdit->setPlainText(contents);
    
    if (consoleController)
        consoleController->invalidateSymbolTableAndFunctionMap();
    
    clearOutputClicked();
    setScriptStringAndInitializeSimulation(contents.toUtf8().constData());
    
    if (consoleController)
        consoleController->validateSymbolTableAndFunctionMap();
    
    setCurrentFile(fileName);
    
    // Update all our UI to reflect the current state of the simulation
    updateAfterTickFull(true);
    resetSLiMChangeCount();     // no recycle change count; the current model is correct
    setWindowModified(false);   // loaded windows start unmodified
}

void QtSLiMWindow::loadRecipe(const QString &recipeName, const QString &recipeScript)
{
    if (consoleController)
        consoleController->invalidateSymbolTableAndFunctionMap();
    
    clearOutputClicked();
    
    lastSavedString = recipeScript;
    lastSavedDate = QDateTime::currentDateTime();
    scriptChangeObserved = false;
    
    ui->scriptTextEdit->setPlainText(recipeScript);
    setScriptStringAndInitializeSimulation(recipeScript.toUtf8().constData());
    
    if (consoleController)
        consoleController->validateSymbolTableAndFunctionMap();
    
    setWindowFilePath(recipeName);
    isRecipe = true;
    isTransient = false;
    
    // Update all our UI to reflect the current state of the simulation
    updateAfterTickFull(true);
    resetSLiMChangeCount();     // no recycle change count; the current model is correct
    setWindowModified(false);   // loaded windows start unmodified
}

bool QtSLiMWindow::saveFile(const QString &fileName)
{
    QFile file(fileName);
    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        QMessageBox::warning(this, "SLiMgui", QString("Cannot write file %1:\n%2.").arg(QDir::toNativeSeparators(fileName), file.errorString()));
        return false;
    }
    
    lastSavedString = ui->scriptTextEdit->toPlainText();
    lastSavedDate = QDateTime::currentDateTime();
    scriptChangeObserved = false;

    QTextStream out(&file);
    out << lastSavedString;

    setCurrentFile(fileName);
    return true;
}

void QtSLiMWindow::setCurrentFile(const QString &fileName)
{
    static int sequenceNumber = 1;

    isUntitled = fileName.isEmpty();
    
    if (isUntitled) {
        if (sequenceNumber == 1)
            currentFile = QString("Untitled");
        else
            currentFile = QString("Untitled %1").arg(sequenceNumber);
        sequenceNumber++;
    } else {
        currentFile = QFileInfo(fileName).canonicalFilePath();
    }

    ui->scriptTextEdit->document()->setModified(false);
    setWindowModified(false);
    if (!isUntitled)
        isTransient = false;
    
    if (!isUntitled)
        qtSLiMAppDelegate->prependToRecentFiles(currentFile);
    
    setWindowFilePath(currentFile);
}

void QtSLiMWindow::documentWasModified()
{
    // This method should be called whenever anything happens that makes us want to mark a window as "dirty" – confirm before closing.
    // This is not quite the same as scriptTexteditChanged(), which is called whenever anything happens that makes the recycle
    // button go green; recycling resets the recycle button to gray, whereas saving resets the document state to unmodified.
    // We could be called for things that are saveable but do not trigger a need for recycling.
    
    // Things are a little more complicated now, because of a Qt bug on Debian that calls us even though the document has not,
    // in fact, been modified.  So we now determine the window modified state by comparing the script string to the last
    // saved / original script string.  See isScriptModified().  BCH 10/24/2023
    
    //setWindowModified(true);              // the old way that produces buggy behavior
    setWindowModified(isScriptModified());  // the new way that checks whether the script has actually changed
}

void QtSLiMWindow::tile(const QMainWindow *previous)
{
    if (!previous)
        return;
    int topFrameWidth = previous->geometry().top() - previous->pos().y();
    if (!topFrameWidth)
        topFrameWidth = 40;
    const QPoint position = previous->pos() + 2 * QPoint(topFrameWidth, topFrameWidth);
    
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
    // In some versions of Qt5, such as 5.9.5, QScreen did not yet exist
    if (QApplication::desktop()->availableGeometry(this).contains(rect().bottomRight() + position))
#else
    if (this->screen()->availableGeometry().contains(rect().bottomRight() + position))
#endif
        move(position);
}

void QtSLiMWindow::appStateChanged(Qt::ApplicationState state)
{
    if (state == Qt::ApplicationState::ApplicationActive)
    {
        // Apparently we have a re-entrancy problem on Linux (see https://github.com/MesserLab/SLiM/issues/476),
        // so now we use this flag to prevent that from happening.  It should be set true around all QMessageBox
        // displays below.
        if (currentlyWarningAboutDiskFile)
            return;
        
        // the motivation for listening to these state changes is to check for externally-edited
        // documents; that can only happen for files that have been saved to disk
        if (!isUntitled && !isRecipe && !isTransient && !isZombieWindow_ && currentFile.length() && lastSavedDate.isValid())
        {
            if (QFile::exists(currentFile))
            {
                // apparently the file now exists; reset that warning flag
                warnedAboutNotExistingOnDisk = false;
                
                QFileInfo fileInfo(currentFile);
                QString filename = fileInfo.fileName();         // last path component, for showing to the user
                QDateTime modDate = fileInfo.lastModified();
                
                if (modDate > lastSavedDate)
                {
                    // check for readability different file contents
                    QFile file(currentFile);
                    
                    if (!file.open(QFile::ReadOnly | QFile::Text)) {
                        // avoid showing the same warning twice, unless we see that the problem is fixed
                        if (warnedAboutUnreadabilityOnDisk)
                            return;
                        warnedAboutUnreadabilityOnDisk = true;
                        
                        // if the file is not readable at all, we want to warn if it comes back with unexpected contents
                        warnedAboutExternalEditing = false;
                        lastExternalChangeString.clear();
                        
                        currentlyWarningAboutDiskFile = true;
                        QMessageBox::warning(this, "SLiMgui", QString("File %1 appears to have been modified externally (on disk), but cannot be read; you may wish to check permissions.").arg(filename));
                        currentlyWarningAboutDiskFile = false;
                        return;
                    }
                    
                    // apparently the file is now readable; reset that warning flag
                    warnedAboutUnreadabilityOnDisk = false;
                    
                    QTextStream in(&file);
                    QString contents = in.readAll();
                    
                    if (contents == lastSavedString)
                    {
                        // the mod date was tweaked, but the file contents are the same; silently update our mod date
                        //qDebug() << "no mod: date changed but identical contents";
                        lastSavedDate = modDate;
                        
                        // we might have warned before, and now returned to our previous state;
                        // in that case, we should forget that we warned about it before
                        warnedAboutExternalEditing = false;
                        lastExternalChangeString.clear();
                        
                        return;
                    }
                    
                    // If we already warned the user about these file contents, return without
                    // warning again; we don't want to warn again unless the contents change again
                    if (warnedAboutExternalEditing && (contents == lastExternalChangeString))
                        return;
                    
                    //qDebug() << "EXTERNAL MOD!";
                    
                    // We are going to warn about the external change below; set our state accordingly
                    warnedAboutExternalEditing = true;
                    lastExternalChangeString = contents;
                    
                    // The file has changed externally; we need to ask the user what to do
                    QMessageBox::StandardButton ret = QMessageBox::No;
                    
                    if (isScriptModified())
                    {
                        // If the script in SLiMgui has been changed (i.e., there are unsaved changes), reloading
                        // is quite dangerous so we require user confirmation with a default of No.
                        QString prompt = QString("File %1 has been modified externally (on disk); do you wish to reload it?\n\nThere are unsaved changes in SLiMgui; if you reload, those changes will be lost!").arg(filename);
                        
                        currentlyWarningAboutDiskFile = true;
                        ret = QMessageBox::critical(this, "SLiMgui", prompt, QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
                        currentlyWarningAboutDiskFile = false;
                    }
                    else
                    {
                        // If that is not the case, we can suggest the reload as a safer operation with a default
                        // of Yes.  In this case, we also allow the user to auto-confirm in Preferences.
                        QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance();
                        
                        if (prefsNotifier.reloadOnSafeExternalEditsPref())
                        {
                            ret = QMessageBox::Yes;
                        }
                        else
                        {
                            QString prompt = QString("File %1 has been modified externally (on disk); do you wish to reload it?\n\n(There are no unsaved changes in SLiMgui that would be lost.  In the Preferences panel you can choose to automatically reload, in this case.)").arg(filename);
                            
                            currentlyWarningAboutDiskFile = true;
                            ret = QMessageBox::question(this, "SLiMgui", prompt, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
                            currentlyWarningAboutDiskFile = false;
                        }
                    }
                    
                    if (ret == QMessageBox::Yes)
                    {
                        loadFile(currentFile);
                        
                        // since we reloaded our file, we want to warn again if it happens again
                        warnedAboutExternalEditing = false;
                        lastExternalChangeString.clear();
                    }
                }
                else
                {
                    //qDebug() << "no mod: mod date equal";
                }
            }
            else
            {
                // avoid showing the same warning twice, unless we see that the problem is fixed
                if (warnedAboutNotExistingOnDisk)
                    return;
                warnedAboutNotExistingOnDisk = true;
                
                // if the file doesn't exist on disk at all, we want to warn if it comes back with unexpected contents
                warnedAboutExternalEditing = false;
                lastExternalChangeString.clear();
                
                currentlyWarningAboutDiskFile = true;
                QMessageBox::warning(this, "SLiMgui", QString("File %1 no longer exists on disk; you may wish to re-save or close.").arg(QDir::toNativeSeparators(currentFile)));
                currentlyWarningAboutDiskFile = false;
                return;
            }
        }
        else
        {
            //qDebug() << "no mod: unsaved file";
        }
    }
}


//
//  Simulation state
//

std::vector QtSLiMWindow::listedSubpopulations(void)
{
    // This funnel method provides the vector of subpopulations that we are displaying in the population table
    // It handles the multispecies case and the "all" species tab for us
    std::vector listedSubpops;
    Species *displaySpecies = focalDisplaySpecies();
    
    if (displaySpecies)
    {
        // If we have a displaySpecies, we just show all of the subpopulations in the species
        for (auto &iter : displaySpecies->population_.subpops_)
            listedSubpops.push_back(iter.second);
    }
    else if (!invalidSimulation() && community && community->simulation_valid_)
    {
        // If we don't, then we show all subpopulations of all species; this is the "all" tab
        for (Species *species : community->AllSpecies())
            for (auto &iter : species->population_.subpops_)
                listedSubpops.push_back(iter.second);
        
        // Sort by id, not by species
        std::sort(listedSubpops.begin(), listedSubpops.end(), [](Subpopulation *l, Subpopulation *r) { return l->subpopulation_id_ < r->subpopulation_id_; });
    }
    
    return listedSubpops;     // note these are sorted by id, not by species, unlike selectedSubpopulations()
}

std::vector QtSLiMWindow::selectedSubpopulations(void)
{
    Species *displaySpecies = focalDisplaySpecies();
    std::vector selectedSubpops;
	
    if (community && community->simulation_valid_)
    {
        for (Species *species : community->all_species_)
        {
            if (!displaySpecies || (displaySpecies == species))
            {
                Population &population = species->population_;
                
                for (auto popIter : population.subpops_)
                {
                    Subpopulation *subpop = popIter.second;
                    
                    if (subpop->gui_selected_)
                        selectedSubpops.emplace_back(subpop);
                }
            }
        }
    }
    
	return selectedSubpops;     // note these are sorted by species, not by id, unlike listedSubpopulations()
}

void QtSLiMWindow::setInvalidSimulation(bool p_invalid)
{
    if (invalidSimulation_ != p_invalid)
    {
        invalidSimulation_ = p_invalid;
        updateUIEnabling();
    }
}

void QtSLiMWindow::setReachedSimulationEnd(bool p_reachedEnd)
{
    if (reachedSimulationEnd_ != p_reachedEnd)
    {
        reachedSimulationEnd_ = p_reachedEnd;
        updateUIEnabling();
    }
}

void QtSLiMWindow::setContinuousPlayOn(bool p_flag)
{
    if (continuousPlayOn_ != p_flag)
    {
        continuousPlayOn_ = p_flag;
        updateUIEnabling();
        emit playStateChanged();
    }
}

void QtSLiMWindow::setTickPlayOn(bool p_flag)
{
    if (tickPlayOn_ != p_flag)
    {
        tickPlayOn_ = p_flag;
        updateUIEnabling();
    }
}

void QtSLiMWindow::setProfilePlayOn(bool p_flag)
{
    if (profilePlayOn_ != p_flag)
    {
        profilePlayOn_ = p_flag;
        updateUIEnabling();
    }
}

void QtSLiMWindow::setNonProfilePlayOn(bool p_flag)
{
    if (nonProfilePlayOn_ != p_flag)
    {
        nonProfilePlayOn_ = p_flag;
        updateUIEnabling();
    }
}

bool QtSLiMWindow::offerAndExecuteAutofix(QTextCursor target, QString replacement, QString explanation, QString terminationMessage)
{
    QString informativeText = "SLiMgui has found an issue with your script that it knows how to fix:\n\n";
    informativeText.append(explanation);
    informativeText.append("\n\nWould you like SLiMgui to automatically fix it, and then recycle?\n");
    
    QMessageBox messageBox(this);
    messageBox.setText("Autofixable Error");
    messageBox.setInformativeText(informativeText);
    messageBox.setDetailedText(terminationMessage.trimmed());
    messageBox.setIcon(QMessageBox::Warning);
    
    // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion
    // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::ApplicationModal
    // seems necessary so floating windows can't be on top of the message box
    messageBox.setWindowModality(Qt::ApplicationModal);
    messageBox.setFixedWidth(700);      // seems to be ignored
    messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
    
    int button = messageBox.exec();
    
    if (button == QMessageBox::Yes)
    {
        target.insertText(replacement);
        recycleClicked();
        return true;
    }
    
    return false;
}

bool QtSLiMWindow::checkTerminationForAutofix(QString terminationMessage)
{
    QTextCursor selection = ui->scriptTextEdit->textCursor();
    QString selectionString = selection.selectedText();
    
    // Note that is important to test the selection string to make sure it makes sense, because the error position might not be correct!
    
    // get the four characters prior to the selected error range, to recognize if the error is preceded by "sim."; note this is a heuristic, not precise
    QTextCursor beforeSelection4 = selection;
    beforeSelection4.setPosition(beforeSelection4.selectionStart(), QTextCursor::MoveAnchor);
    beforeSelection4.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 4);
    beforeSelection4.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 4);
    QString beforeSelection4String = beforeSelection4.selectedText();
    
    //
    //  Changes for SLiM 4.0: multispecies SLiM, mostly, plus fitness() -> mutationEffect() and fitness(NULL) -> fitnessEffect()
    //
    
    // early() events are no longer default
    if (terminationMessage.contains("unexpected token {") &&
            terminationMessage.contains("expected an event declaration") &&
            terminationMessage.contains("early() is no longer a default script block type") &&
            (selectionString == "{"))
        return offerAndExecuteAutofix(selection, "early() {", "Script blocks no longer default to `early()`; `early()` must be explicitly specified.", terminationMessage);
    
    // sim to community changes
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "createLogFile") &&
            terminationMessage.contains("method createLogFile() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `createLogFile()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "deregisterScriptBlock") &&
            terminationMessage.contains("method deregisterScriptBlock() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `deregisterScriptBlock()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "registerFirstEvent") &&
            terminationMessage.contains("method registerFirstEvent() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `registerFirstEvent()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "registerEarlyEvent") &&
            terminationMessage.contains("method registerEarlyEvent() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `registerEarlyEvent()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "registerLateEvent") &&
            terminationMessage.contains("method registerLateEvent() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `registerLateEvent()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "rescheduleScriptBlock") &&
            terminationMessage.contains("method rescheduleScriptBlock() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `rescheduleScriptBlock()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "simulationFinished") &&
            terminationMessage.contains("method simulationFinished() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `simulationFinished()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "outputUsage") &&
            terminationMessage.contains("method outputUsage() is not defined on object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `outputUsage()` method has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "logFiles") &&
            terminationMessage.contains("property logFiles is not defined for object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `logFiles` property has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "generationStage") &&
            terminationMessage.contains("property generationStage is not defined for object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `generationStage` property has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "modelType") &&
            terminationMessage.contains("property modelType is not defined for object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `modelType` property has been moved to the Community class.", terminationMessage);
    
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "verbosity") &&
            terminationMessage.contains("property verbosity is not defined for object element type Species"))
        return offerAndExecuteAutofix(beforeSelection4, "community.", "The `verbosity` property has been moved to the Community class.", terminationMessage);
    
    // generation to tick changes
    if (terminationMessage.contains("property originGeneration is not defined for object element type Mutation") &&
            (selectionString == "originGeneration"))
        return offerAndExecuteAutofix(selection, "originTick", "The `originGeneration` property has been removed from Mutation; in its place is `originTick` (which measures in ticks, not generations).", terminationMessage);

    if (terminationMessage.contains("property originGeneration is not defined for object element type Substitution") &&
            (selectionString == "originGeneration"))
        return offerAndExecuteAutofix(selection, "originTick", "The `originGeneration` property has been removed from Substitution; in its place is `originTick` (which measures in ticks, not generations).", terminationMessage);

    if (terminationMessage.contains("property fixationGeneration is not defined for object element type Substitution") &&
            (selectionString == "fixationGeneration"))
        return offerAndExecuteAutofix(selection, "fixationTick", "The `fixationGeneration` property has been removed from Substitution; in its place is `fixationTick` (which measures in ticks, not generations).", terminationMessage);
    
    // generation to cycle changes
    if (terminationMessage.contains("property generation is not defined for object element type Species") &&
            (selectionString == "generation"))
        return offerAndExecuteAutofix(selection, "cycle", "The `generation` property of Species has been renamed to `cycle`.", terminationMessage);
    
    if (terminationMessage.contains("property generationStage is not defined for object element type Community") &&
            (selectionString == "generationStage"))
        return offerAndExecuteAutofix(selection, "cycleStage", "The `generationStage` property of Community has been renamed to `cycleStage`.", terminationMessage);
    
    if (terminationMessage.contains("method addGeneration() is not defined on object element type LogFile") &&
            (selectionString == "addGeneration"))
        return offerAndExecuteAutofix(selection, "addCycle", "The `addGeneration()` method of Community has been renamed to `addCycle()`.", terminationMessage);
    
    if (terminationMessage.contains("method addGenerationStage() is not defined on object element type LogFile") &&
            (selectionString == "addGenerationStage"))
        return offerAndExecuteAutofix(selection, "addCycleStage", "The `addGenerationStage()` method of Community has been renamed to `addCycleStage()`.", terminationMessage);
    
    // removal of various callback pseudo-parameters
    // genome1 and genome2 are now handled below, since there are two possible fixes now
    if (terminationMessage.contains("undefined identifier childGenome1") &&
            (selectionString == "childGenome1"))
        return offerAndExecuteAutofix(selection, "child.genome1", "The `childGenome1` pseudo-parameter has been removed; it is now accessed as `child.genome1`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier childGenome2") &&
            (selectionString == "childGenome2"))
        return offerAndExecuteAutofix(selection, "child.genome2", "The `childGenome2` pseudo-parameter has been removed; it is now accessed as `child.genome2`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier parent1Genome1") &&
            (selectionString == "parent1Genome1"))
        return offerAndExecuteAutofix(selection, "parent1.genome1", "The `parent1Genome1` pseudo-parameter has been removed; it is now accessed as `parent1.genome1`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier parent1Genome2") &&
            (selectionString == "parent1Genome2"))
        return offerAndExecuteAutofix(selection, "parent1.genome2", "The `parent1Genome2` pseudo-parameter has been removed; it is now accessed as `parent1.genome2`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier parent2Genome1") &&
            (selectionString == "parent2Genome1"))
        return offerAndExecuteAutofix(selection, "parent2.genome1", "The `parent2Genome1` pseudo-parameter has been removed; it is now accessed as `parent2.genome1`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier parent2Genome2") &&
            (selectionString == "parent2Genome2"))
        return offerAndExecuteAutofix(selection, "parent2.genome2", "The `parent2Genome2` pseudo-parameter has been removed; it is now accessed as `parent2.genome2`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier childIsFemale") &&
            (selectionString == "childIsFemale"))
        return offerAndExecuteAutofix(selection, "(child.sex == \"F\")", "The `childIsFemale` pseudo-parameter has been removed; it is now accessed as `child.sex == \"F\"`.", terminationMessage);
    
    // changes to InteractionType -evaluate()
    if (terminationMessage.contains("missing required argument subpops") && (selectionString == "evaluate"))
    {
        QTextCursor entireCall = selection;
        entireCall.setPosition(entireCall.selectionStart(), QTextCursor::MoveAnchor);
        entireCall.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 11);
        QString entireCallString = entireCall.selectedText();
        
        if (entireCallString == "evaluate();")
            return offerAndExecuteAutofix(entireCall, "evaluate(sim.subpopulations);", "The evaluate() method now requires a vector of subpopulations to evaluate.", terminationMessage);
    }
    
    if (terminationMessage.contains("named argument immediate skipped over required argument subpops") && (selectionString == "evaluate"))
    {
        QTextCursor entireCall = selection;
        entireCall.setPosition(entireCall.selectionStart(), QTextCursor::MoveAnchor);
        entireCall.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 22);
        QString entireCallString = entireCall.selectedText();
        
        if ((entireCallString == "evaluate(immediate=T);") || (entireCallString == "evaluate(immediate=F);"))
            return offerAndExecuteAutofix(entireCall, "evaluate(sim.subpopulations);", "The evaluate() method no longer supports immediate evaluation, and the `immediate` parameter has been removed.", terminationMessage);
    }
    
    if (terminationMessage.contains("unrecognized named argument immediate") && (selectionString == "evaluate"))
    {
        {
            QTextCursor callEnd = selection;
            callEnd.setPosition(callEnd.selectionStart(), QTextCursor::MoveAnchor);
            callEnd.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1);
            callEnd.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 15);
            QString callEndString = callEnd.selectedText();
            
            if ((callEndString == ", immediate=T);") || (callEndString == ", immediate=F);"))
                return offerAndExecuteAutofix(callEnd, ");", "The evaluate() method no longer supports immediate evaluation, and the `immediate` parameter has been removed.", terminationMessage);
        }
        
        {
            QTextCursor callEnd = selection;
            callEnd.setPosition(callEnd.selectionStart(), QTextCursor::MoveAnchor);
            callEnd.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1);
            callEnd.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 14);
            QString callEndString = callEnd.selectedText();
            
            if ((callEndString == ",immediate=T);") || (callEndString == ",immediate=F);"))
                return offerAndExecuteAutofix(callEnd, ");", "The evaluate() method no longer supports immediate evaluation, and the `immediate` parameter has been removed.", terminationMessage);
        }
        
        {
            QTextCursor callEnd = selection;
            callEnd.setPosition(callEnd.selectionStart(), QTextCursor::MoveAnchor);
            callEnd.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1);
            callEnd.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 17);
            QString callEndString = callEnd.selectedText();
            
            if ((callEndString == ", immediate = T);") || (callEndString == ", immediate = F);"))
                return offerAndExecuteAutofix(callEnd, ");", "The evaluate() method no longer supports immediate evaluation, and the `immediate` parameter has been removed.", terminationMessage);
        }
        
        {
            QTextCursor callEnd = selection;
            callEnd.setPosition(callEnd.selectionStart(), QTextCursor::MoveAnchor);
            callEnd.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1);
            callEnd.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 16);
            QString callEndString = callEnd.selectedText();
            
            if ((callEndString == ",immediate = T);") || (callEndString == ",immediate = F);"))
                return offerAndExecuteAutofix(callEnd, ");", "The evaluate() method no longer supports immediate evaluation, and the `immediate` parameter has been removed.", terminationMessage);
        }
    }
    
    // API changes in anticipation of multi-phenotype
    if (terminationMessage.contains("unexpected identifier @fitness; expected an event declaration"))
    {
        {
            QTextCursor callbackDecl = selection;
            callbackDecl.setPosition(callbackDecl.selectionStart(), QTextCursor::MoveAnchor);
            callbackDecl.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 14);
            QString callbackDeclString = callbackDecl.selectedText();
            
            if (callbackDeclString == "fitness(NULL, ")
                return offerAndExecuteAutofix(callbackDecl, "fitnessEffect(", "The fitness(NULL) callback type is now called a fitnessEffect() callback.", terminationMessage);
        }
        
        {
            QTextCursor callbackDecl = selection;
            callbackDecl.setPosition(callbackDecl.selectionStart(), QTextCursor::MoveAnchor);
            callbackDecl.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 13);
            QString callbackDeclString = callbackDecl.selectedText();
            
            if (callbackDeclString == "fitness(NULL,")
                return offerAndExecuteAutofix(callbackDecl, "fitnessEffect(", "The fitness(NULL) callback type is now called a fitnessEffect() callback.", terminationMessage);
            if (callbackDeclString == "fitness(NULL)")
                return offerAndExecuteAutofix(callbackDecl, "fitnessEffect()", "The fitness(NULL) callback type is now called a fitnessEffect() callback.", terminationMessage);
        }
        
        {
            QTextCursor callbackDecl = selection;
            callbackDecl.setPosition(callbackDecl.selectionStart(), QTextCursor::MoveAnchor);
            callbackDecl.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 9);
            QString callbackDeclString = callbackDecl.selectedText();
            
            if (callbackDeclString == "fitness(m")
                return offerAndExecuteAutofix(callbackDecl, "mutationEffect(m", "The fitness() callback type is now called a mutationEffect() callback.", terminationMessage);
        }
    }
    
    if (terminationMessage.contains("undefined identifier relFitness"))
        return offerAndExecuteAutofix(selection, "effect", "The `relFitness` pseudo-parameter has been renamed to `effect`.", terminationMessage);
    
    // other deprecated APIs, unrelated to multispecies and multi-phenotype
    if ((beforeSelection4String == "sim.") &&
            (selectionString == "inSLiMgui") &&
            terminationMessage.contains("property inSLiMgui is not defined for object element type Species"))
    {
        QTextCursor simAndSelection = beforeSelection4;
        simAndSelection.setPosition(selection.selectionEnd(), QTextCursor::KeepAnchor);
        
        return offerAndExecuteAutofix(simAndSelection, "exists(\"slimgui\")", "The `inSLiMgui` property has been removed; now use `exists(\"slimgui\")`.", terminationMessage);
    }
    
    //
    //  Shift from genome to haplosome for SLiM 5.0
    //
    
    if (terminationMessage.contains("could not find an Eidos class named 'Genome'") &&
            (selectionString == "Genome"))
        return offerAndExecuteAutofix(selection, "Haplosome", "The `Genome` class has been renamed to `Haplosome`.", terminationMessage);
    
    if (terminationMessage.contains("property genomeType is not defined for object element type Haplosome") &&
            (selectionString == "genomeType"))
        return offerAndExecuteAutofix(selection, "chromosome.type", "The `genomeType` property of Haplosome has been removed; it is now accessed as `chromosome.type`.", terminationMessage);
    
    if (terminationMessage.contains("property isNullGenome is not defined for object element type Haplosome") &&
            (selectionString == "isNullGenome"))
        return offerAndExecuteAutofix(selection, "isNullHaplosome", "The `isNullGenome` property of Haplosome has been renamed to `isNullHaplosome`.", terminationMessage);
    
    if (terminationMessage.contains("property genomePedigreeID is not defined for object element type Haplosome") &&
            (selectionString == "genomePedigreeID"))
        return offerAndExecuteAutofix(selection, "haplosomePedigreeID", "The `genomePedigreeID` property of Haplosome has been renamed to `haplosomePedigreeID`.", terminationMessage);
    
    if (terminationMessage.contains("method mutationCountsInGenomes() is not defined on object element type Haplosome") &&
            (selectionString == "mutationCountsInGenomes"))
        return offerAndExecuteAutofix(selection, "mutationCountsInHaplosomes", "The `mutationCountsInGenomes()` method of Haplosome has been renamed to `mutationCountsInHaplosomes()`.", terminationMessage);
    
    if (terminationMessage.contains("method mutationFrequenciesInGenomes() is not defined on object element type Haplosome") &&
            (selectionString == "mutationFrequenciesInGenomes"))
        return offerAndExecuteAutofix(selection, "mutationFrequenciesInHaplosomes", "The `mutationFrequenciesInGenomes` property of Haplosome has been renamed to `mutationFrequenciesInHaplosomes`.", terminationMessage);
    
    if (terminationMessage.contains("property genomes is not defined for object element type Individual") &&
            (selectionString == "genomes"))
        return offerAndExecuteAutofix(selection, "haplosomes", "The `genomes` property of Individual has been renamed to `haplosomes`.", terminationMessage);
    
    if (terminationMessage.contains("property genomesNonNull is not defined for object element type Individual") &&
            (selectionString == "genomesNonNull"))
        return offerAndExecuteAutofix(selection, "haplosomesNonNull", "The `genomesNonNull` property of Individual has been renamed to `haplosomesNonNull`.", terminationMessage);
    
    if (terminationMessage.contains("property genome1 is not defined for object element type Individual") &&
            (selectionString == "genome1"))
        return offerAndExecuteAutofix(selection, "haploidGenome1", "The `genome1` property of Individual has been renamed to `haploidGenome1`.", terminationMessage);

    if (terminationMessage.contains("property genome2 is not defined for object element type Individual") &&
            (selectionString == "genome2"))
        return offerAndExecuteAutofix(selection, "haploidGenome2", "The `genome2` property of Individual has been renamed to `haploidGenome2`.", terminationMessage);
    
    if (terminationMessage.contains("property genomes is not defined for object element type Subpopulation") &&
            (selectionString == "genomes"))
        return offerAndExecuteAutofix(selection, "haplosomes", "The `genomes` property of Subpopulation has been renamed to `haplosomes`.", terminationMessage);

    if (terminationMessage.contains("property genomesNonNull is not defined for object element type Subpopulation") &&
            (selectionString == "genomesNonNull"))
        return offerAndExecuteAutofix(selection, "haplosomesNonNull", "The `genomesNonNull` property of Subpopulation has been renamed to `haplosomesNonNull`.", terminationMessage);

    if (terminationMessage.contains("property chromosome is not defined for object element type Species") &&
            (selectionString == "chromosome"))
        return offerAndExecuteAutofix(selection, "chromosomes", "The `chromosome` property of Species has been renamed to `chromosomes`.", terminationMessage);     // actually, this property was left in, for now

    if (terminationMessage.contains("property chromosomeType is not defined for object element type Species") &&
            (selectionString == "chromosomeType"))
        return offerAndExecuteAutofix(selection, "chromosome.type", "The `chromosomeType` property of Species has been removed; it is now accessed as `chromosome.type`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier genome") &&
            (selectionString == "genome"))
        return offerAndExecuteAutofix(selection, "haplosome", "The `genome` pseudo-parameter has been renamed to `haplosome`.", terminationMessage);

    // genome1 and genome2 for some callback types were removed in favor of individual.genome1 and individual.genome2 for SLiM 4.0,
    // and used to be autofixed above; however, in recombination() callbacks genome1 has become haplosome1 and genome2 has
    // become haplosome2, which conflicted with the previous autofix.  This sequence of fixes works for all cases.
    if (terminationMessage.contains("undefined identifier genome1") &&
            (selectionString == "genome1"))
        return offerAndExecuteAutofix(selection, "haplosome1", "The `genome1` pseudo-parameter has been renamed to `haplosome1`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier haplosome1") &&
            (selectionString == "haplosome1"))
        return offerAndExecuteAutofix(selection, "individual.haploidGenome1", "The `haplosome1` pseudo-parameter has been removed; it is now accessed as `individual.haploidGenome1`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier genome2") &&
            (selectionString == "genome2"))
        return offerAndExecuteAutofix(selection, "haplosome2", "The `genome2` pseudo-parameter has been renamed to `haplosome2`.", terminationMessage);

    if (terminationMessage.contains("undefined identifier haplosome2") &&
            (selectionString == "haplosome2"))
        return offerAndExecuteAutofix(selection, "individual.haploidGenome2", "The `haplosome2` pseudo-parameter has been removed; it is now accessed as `individual.haploidGenome2`.", terminationMessage);
    
    // Other SLiM 5.0 autofixes
    if (terminationMessage.contains("method readFromVCF() is not defined on object element type Haplosome") &&
            (selectionString == "readFromVCF"))
        return offerAndExecuteAutofix(selection, "readHaplosomesFromVCF", "The `readFromVCF()` method of Haplosome has been renamed to `readHaplosomesFromVCF()`.", terminationMessage);
    
    if (terminationMessage.contains("method readFromMS() is not defined on object element type Haplosome") &&
            (selectionString == "readFromMS"))
        return offerAndExecuteAutofix(selection, "readHaplosomesFromMS", "The `readFromMS()` method of Haplosome has been renamed to `readHaplosomesFromMS()`.", terminationMessage);
    
    if (terminationMessage.contains("property haploidDominanceCoeff is not defined for object element type MutationType") &&
            (selectionString == "haploidDominanceCoeff"))
        return offerAndExecuteAutofix(selection, "hemizygousDominanceCoeff", "The `haploidDominanceCoeff` property of MutationType has been renamed to `hemizygousDominanceCoeff`.", terminationMessage);
    
    if (terminationMessage.contains("method output() is not defined on object element type Haplosome") &&
            (selectionString == "output"))
        return offerAndExecuteAutofix(selection, "outputHaplosomes", "The `output()` method of Haplosome has been renamed to `outputHaplosomes()`.", terminationMessage);
    
    if (terminationMessage.contains("method outputMS() is not defined on object element type Haplosome") &&
            (selectionString == "outputMS"))
        return offerAndExecuteAutofix(selection, "outputHaplosomesToMS", "The `outputMS()` method of Haplosome has been renamed to `outputHaplosomesToMS()`.", terminationMessage);
    
    if (terminationMessage.contains("method outputVCF() is not defined on object element type Haplosome") &&
            (selectionString == "outputVCF"))
        return offerAndExecuteAutofix(selection, "outputHaplosomesToVCF", "The `outputVCF()` method of Haplosome has been renamed to `outputHaplosomesToVCF()`.", terminationMessage);
    
    return false;
}

void QtSLiMWindow::showTerminationMessage(QString terminationMessage, EidosErrorContext errorContext)
{
    //qDebug() << terminationMessage;
    
    // Depending on the circumstances of the error, we might be able to select a range in our input file to show what caused the error
	if (!changedSinceRecycle())
    {
		ui->scriptTextEdit->selectErrorRange(errorContext);
        
        // check to see if this is an error we can assist the user in fixing; if they choose to autofix, we are done
        if (checkTerminationForAutofix(terminationMessage))
            return;
    }
    
    // Show an error sheet/panel
    QString fullMessage(terminationMessage);
    
    fullMessage.append("\nThis error has invalidated the simulation; it cannot be run further.  Once the script is fixed, you can recycle the simulation and try again.");
    
    QMessageBox messageBox(this);
    messageBox.setText("Simulation Runtime Error");
    messageBox.setInformativeText(fullMessage);
    messageBox.setIcon(QMessageBox::Warning);
    
    // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion
    // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::ApplicationModal
    // seems necessary so floating windows can't be on top of the message box
    messageBox.setWindowModality(Qt::ApplicationModal);
    messageBox.setFixedWidth(700);      // seems to be ignored
    messageBox.exec();
    
    // Show the error in the status bar also
    statusBar()->showMessage("" + terminationMessage.trimmed().toHtmlEscaped() + "");
}

void QtSLiMWindow::checkForSimulationTermination(void)
{
    std::string &&terminationMessage = gEidosTermination.str();

    if (!terminationMessage.empty())
    {
        // Get the termination message and clear the global
        QString message = QString::fromStdString(terminationMessage);

        gEidosTermination.clear();
        gEidosTermination.str("");

        // Get the error position and clear the global
        EidosErrorContext errorContext = gEidosErrorContext;
        
        ClearErrorContext();
        
        // Send the signal, which connects up to QtSLiMWindow::showTerminationMessage() through a Qt::QueuedConnection
        emit terminationWithMessage(message, errorContext);
        
        // Now we need to clean up so we are in a displayable state.  Note that we don't even attempt to dispose
        // of the old simulation object; who knows what state it is in, touching it might crash.
        community = nullptr;
        focalSpecies = nullptr;
        slimgui = nullptr;

		if (sim_RNG_initialized)
		{
        	_Eidos_FreeOneRNG(sim_RNG);
        	sim_RNG_initialized = false;
		}
		
        setReachedSimulationEnd(true);
        setInvalidSimulation(true);
    }
}

void QtSLiMWindow::startNewSimulationFromScript(void)
{
    if (community)
    {
        delete community;
        community = nullptr;
        focalSpecies = nullptr;
    }
    if (slimgui)
    {
        delete slimgui;
        slimgui = nullptr;
    }
    
    // forget any script block coloring
    ui->scriptTextEdit->clearScriptBlockColoring();

	// Free the old simulation RNG and make a new one, to have clean state
	if (sim_RNG_initialized)
	{
		_Eidos_FreeOneRNG(sim_RNG);
		sim_RNG_initialized = false;
	}
	
	_Eidos_InitializeOneRNG(sim_RNG);
	sim_RNG_initialized = true;
	
	// The Eidos RNG may be set up already; if so, get rid of it.  When we are not running, we keep the
	// Eidos RNG in an initialized state, to catch errors with the swapping of RNG state.  Nobody should
	// use it when we have not swapped in our own RNG.
	if (gEidos_RNG_Initialized)
	{
		_Eidos_FreeOneRNG(gEidos_RNG_SINGLE);
		gEidos_RNG_Initialized = false;
	}
	
	// Swap in our RNG
	std::swap(sim_RNG, gEidos_RNG_SINGLE);
	std::swap(sim_RNG_initialized, gEidos_RNG_Initialized);

    std::istringstream infile(scriptString);

    try
    {
        community = new Community();
        community->InitializeFromFile(infile);
        community->InitializeRNGFromSeed(nullptr);
        community->FinishInitialization();
        
        community->SetDebugPoints(&ui->scriptTextEdit->debuggingPoints());

		// Swap out our RNG
		std::swap(sim_RNG, gEidos_RNG_SINGLE);
		std::swap(sim_RNG_initialized, gEidos_RNG_Initialized);

        // We also reset various Eidos/SLiM instance state; each SLiMgui window is independent
        sim_next_pedigree_id = 0;
        sim_next_mutation_id = 0;
        sim_suppress_warnings = false;

        // The current working directory was set up in -init to be ~/Desktop, and should not be reset here; if the
        // user has changed it, that change ought to stick across recycles.  So this bounces us back to the last dir chosen.
        sim_working_dir = sim_requested_working_dir;

        setReachedSimulationEnd(false);
        setInvalidSimulation(false);
        hasImported_ = false;
    }
    catch (...)
    {
        // BCH 12/25/2022: adding this to swap out our RNG after a raise, seems better...
		std::swap(sim_RNG, gEidos_RNG_SINGLE);
		std::swap(sim_RNG_initialized, gEidos_RNG_Initialized);
        
        if (community)
            community->simulation_valid_ = false;
        setReachedSimulationEnd(true);
        checkForSimulationTermination();
    }

    if (community)
    {
        // make a new SLiMgui instance to represent SLiMgui in Eidos
        slimgui = new SLiMgui(*community, this);

        // set up the "slimgui" symbol for it immediately
        // BCH 11/7/2025: note this symbol is now protected in SLiM_ConfigureContext()
        community->simulation_constants_->InitializeConstantSymbolEntry(slimgui->SymbolTableEntry());
    }
    
    if (community && community->simulation_valid_ && (community->all_species_.size() > 1))
    {
        // set up script block coloring
        std::vector &blocks = community->AllScriptBlocks();
        
        for (SLiMEidosBlock *block : blocks)
        {
            Species *species = (block->species_spec_ ? block->species_spec_ : (block->ticks_spec_ ? block->ticks_spec_ : nullptr));
            
            if (species && !block->script_ && block->root_node_ && block->root_node_->token_)
            {
                EidosToken *block_root_token = block->root_node_->token_;
                int startPos = block_root_token->token_UTF16_start_;
                int endPos = block_root_token->token_UTF16_end_;
                
                ui->scriptTextEdit->addScriptBlockColoring(startPos, endPos, species);
            }
        }
    }
}

void QtSLiMWindow::setScriptStringAndInitializeSimulation(std::string string)
{
    scriptString = string;
    startNewSimulationFromScript();
}

Species *QtSLiMWindow::focalDisplaySpecies(void)
{
    // SLiMgui focuses on one species at a time in its main window display; this method should be called to obtain that species.
	// This funnel method checks for various invalid states and returns nil; callers should check for a nil return as needed.
	if (!invalidSimulation_ && community && community->simulation_valid_)
	{
        // If we have a focal species set already, it must be valid (the community still exists), so return it
        if (focalSpecies)
            return focalSpecies;
        
        // If "all" is chosen, we return nullptr, which represents that state
        if (focalSpeciesName.compare("all") == 0)
            return nullptr;
        
        // If not, we'll choose a species from the species list if there are any
		const std::vector &all_species = community->AllSpecies();
		
		if (all_species.size() >= 1)
        {
            // If we have a species name remembered, try to choose that species again
            if (focalSpeciesName.length())
            {
                for (Species *species : all_species)
                {
                    if (species->name_.compare(focalSpeciesName) == 0)
                    {
                        focalSpecies = species;
                        return focalSpecies;
                    }
                }
            }
            
            // Failing that, choose the first declared species and remember its name
            focalSpecies = all_species[0];
            focalSpeciesName = focalSpecies->name_;
        }
	}
	
	return nullptr;
}

Chromosome *QtSLiMWindow::focalChromosome(void)
{
    // There needs to be a focal display species to answer this question; if
    // we are on the "all" tab in a multispecies model, there are multiple
    // focal chromosomes, so we return nullptr.
    Species *species = focalDisplaySpecies();
    
    if (!species)
        return nullptr;
    
    // If there is one focal display species, then the first overview widget
    // if the one being displayed (see updateChromosomeViewSetup()).
    QtSLiMChromosomeWidget *overviewWidget = chromosomeOverviewWidgets[0];
    
    Chromosome *chromosome = overviewWidget->focalChromosome();
    
    return chromosome;
}

void QtSLiMWindow::selectedSpeciesChanged(void)
{
    // We don't want to react to automatic tab changes as we are adding or removing tabs from the species bar
    if (reloadingSpeciesBar)
        return;
    
    int speciesIndex = ui->speciesBar->currentIndex();
    const std::vector &allSpecies = community->AllSpecies();
    
    if (speciesIndex == (int)allSpecies.size())
    {
        // this is the "all" tab
        focalSpecies = nullptr;
        focalSpeciesName = "all";
    }
    else
    {
        if ((speciesIndex < 0) || (speciesIndex >= (int)allSpecies.size()))
        {
            qDebug() << "selectedSpeciesChanged() index" << speciesIndex << "out of range";
            return;
        }
        
        focalSpecies = allSpecies[speciesIndex];
        focalSpeciesName = focalSpecies->name_;
    }
    
    //qDebug() << "selectedSpeciesChanged(): changed to species name" << QString::fromStdString(focalSpeciesName);
    
    // do a full update to show the state for the new species
    updateAfterTickFull(true);
    updateUIEnabling();
}

QtSLiMGraphView *QtSLiMWindow::graphViewForGraphWindow(QWidget *p_window)
{
    if (p_window)
    {
        QLayout *window_layout = p_window->layout();
        
        if (window_layout && (window_layout->count() > 0))
        {
            QLayoutItem *item = window_layout->itemAt(0);
            
            if (item)
                return qobject_cast(item->widget());
        }
    }
    return nullptr;
}

void QtSLiMWindow::updateOutputViews(void)
{
    QtSLiMDebugOutputWindow *debugWindow = debugOutputWindow();
    std::string &&newOutput = gSLiMOut.str();
	
	if (!newOutput.empty())
	{
        QString str = QString::fromStdString(newOutput);
		
		// So, ideally we would stay pinned at the bottom if the user had scrolled to the bottom, but would stay
		// at the user's chosen scroll position above the bottom if they chose such a position.  Unfortunately,
		// this doesn't seem to work.  I'm not quite sure why.  Particularly when large amounts of output get
		// added quickly, the scroller doesn't seem to catch up, and then it reads here as not being at the
		// bottom, and so we become unpinned even though we used to be pinned.  I'm going to just give up, for
		// now, and always scroll to the bottom when new output comes out.  That's what many other such apps
		// do anyway; it's a little annoying if you're trying to read old output, but so it goes.
		
		//NSScrollView *enclosingScrollView = [outputTextView enclosingScrollView];
		//BOOL scrolledToBottom = YES; //(![enclosingScrollView hasVerticalScroller] || [[enclosingScrollView verticalScroller] doubleValue] == 1.0);
		
        // ui->outputTextEdit->append(str) would seem the obvious thing to do, but that adds an extra newline (!),
        // so it can't be used.  WTF.  The solution here does not preserve the user's scroll position; see discussion at
        // https://stackoverflow.com/questions/13559990/how-to-append-text-to-qplaintextedit-without-adding-newline-and-keep-scroll-at
        // which has a complex solution involving subclassing QPlainTextEdit... sigh...
        ui->outputTextEdit->moveCursor(QTextCursor::End);
        ui->outputTextEdit->insertPlainText(str);
        ui->outputTextEdit->moveCursor(QTextCursor::End);
        
		//if ([[NSUserDefaults standardUserDefaults] boolForKey:defaultsSyntaxHighlightOutputKey])
		//	[outputTextView recolorAfterChanges];
		
		// if the user was scrolled to the bottom, we keep them there; otherwise, we let them stay where they were
		//if (scrolledToBottom)
		//	[outputTextView scrollRangeToVisible:NSMakeRange([[outputTextView string] length], 0)];
		
        // We add run output to the appropriate subview of the output viewer, too; it shows up in both places
        if (debugWindow)
            debugWindow->takeRunOutput(str);
        
		// clear any error flags set on the stream and empty out its string so it is ready to receive new output
		gSLiMOut.clear();
		gSLiMOut.str("");
	}
    
    // BCH 2/9/2021: We now handle the error output here too, since we want to be in charge of how the
    // debug window shows itself, etc.  We follow the same strategy as above; comments have been removed.
    std::string &&newErrors = gSLiMError.str();
    
    if (!newErrors.empty())
    {
        QString str = QString::fromStdString(newErrors);
        
        // BCH 7/17/2024: Decided to send debug output to the main window also, not just the debug output tab
        // of the debug window; otherwise important messages get lost.  So the main window shows both.
        ui->outputTextEdit->moveCursor(QTextCursor::End);
        ui->outputTextEdit->insertPlainText(str);
        ui->outputTextEdit->moveCursor(QTextCursor::End);
        
        if (debugWindow)
        {
            debugWindow->takeDebugOutput(str);
            
            // Flash the debugging output button to alert the user to new output
            flashDebugButton();
        }
        
        gSLiMError.clear();
        gSLiMError.str("");
    }
    
    // BCH 5/15/2022: And now scheduling stream output happens here too, following the pattern above.
    std::string &&newSchedulingOutput = gSLiMScheduling.str();
    
    if (!newSchedulingOutput.empty())
    {
        QString str = QString::fromStdString(newSchedulingOutput);
        
        if (debugWindow)
            debugWindow->takeSchedulingOutput(str);
        
        gSLiMScheduling.clear();
        gSLiMScheduling.str("");
    }
    
    // Scan through LogFile instances kept by the sim and flush them to the debug window
    if (debugWindow && !invalidSimulation_ && community)
    {
        for (LogFile *logfile : community->log_file_registry_)
        {
            for (auto &lineElements : logfile->emitted_lines_)
            {
                // This call takes a vector of string elements comprising one logfile output line
                debugWindow->takeLogFileOutput(lineElements, logfile->user_file_path_);
            }
            
            logfile->emitted_lines_.clear();
        }
    }
    
    // Scan through file output kept by the sim and flush it to the debug window
    if (debugWindow && !invalidSimulation_ && community)
    {
        for (size_t index = 0; index < community->file_write_paths_.size(); ++index)
        {
            // This call takes a vector of lines comprising all the output for one file
            debugWindow->takeFileOutput(community->file_write_buffers_[index], community->file_write_appends_[index], community->file_write_paths_[index]);
        }
        
        community->file_write_paths_.clear();
        community->file_write_buffers_.clear();
        community->file_write_appends_.clear();
    }
}

void QtSLiMWindow::flashDebugButton(void)
{
    // every 40 is one cycle up and down, to red and back; so 200 gives five cycles,
    // which seems good for catching the user's attention effectively; maybe excessive,
    // but that's better than being missed...
    if (debugButtonFlashCount_ == 0)
        debugButtonFlashCount_ = 200;
    else if (debugButtonFlashCount_ < 200)
        debugButtonFlashCount_ += 40;       // new output adds one cycle, up to the max of five
    
    debugButtonFlashTimer_.start(0);
}

void QtSLiMWindow::stopDebugButtonFlash(void)
{
    // called when the button gets clicked, pressed, etc.
    debugButtonFlashCount_ = 0;
    ui->debugOutputButton->setTemporaryIconOpacity(0.0);
    debugButtonFlashTimer_.stop();
}

void QtSLiMWindow::handleDebugButtonFlash(void)
{
    // decrement with each tick
    --debugButtonFlashCount_;
    if (debugButtonFlashCount_ < 0)
        debugButtonFlashCount_ = 0;
    
    // set opacity of the red overlay based on the counter, and reschedule ourselves as needed
    if (debugButtonFlashCount_ == 0)
    {
        stopDebugButtonFlash();
    }
    else
    {
        int opacity_int = debugButtonFlashCount_ % 40;
        const double PI = 3.141592653589793;    // not in the C++ standard until C++20!
        //double opacity_float = std::max(0.0, std::sin(PI * opacity_int / 40.0));                              // dwell on red; not as nice, I decided
        double opacity_float = std::max(0.0, 1.0 - (std::cos(2 * PI * opacity_int / 40.0) * 0.5 + 0.5));        // equal time red and non-red
        
        //qDebug() << "debugButtonFlashCount_" << debugButtonFlashCount_ << ", opacity_int" << opacity_int << ", opacity_float" << opacity_float;
        
        ui->debugOutputButton->setTemporaryIconOpacity(opacity_float);
        
        if (debugButtonFlashTimer_.interval() != 17)   // about 60 Hz
            debugButtonFlashTimer_.start(17);
    }
}

void QtSLiMWindow::updateTickCounter(void)
{
    Species *displaySpecies = focalDisplaySpecies();
    
    if (!displaySpecies)
        ui->cycleLineEdit->setText("");
    else if (community->Tick() == 0)
        ui->cycleLineEdit->setText("initialize()");
    else
         ui->cycleLineEdit->setText(QString::number(displaySpecies->Cycle()));
    
    if (!community)
    {
        ui->tickLineEdit->setText("");
        ui->tickLineEdit->setProgress(0.0);
    }
    else if (community->Tick() == 0)
    {
        ui->tickLineEdit->setText("initialize()");
        ui->tickLineEdit->setProgress(0.0);
    }
    else
    {
        slim_tick_t tick = community->Tick();
        slim_tick_t lastTick = community->EstimatedLastTick();
        
        double progress = (lastTick > 0) ? (tick / (double)lastTick) : 0.0;
        
        ui->tickLineEdit->setText(QString::number(tick));
        ui->tickLineEdit->setProgress(progress);
    }
}

void QtSLiMWindow::updateSpeciesBar(void)
{
    // Update the species bar as needed; we do this only after initialization, to avoid a hide/show on recycle of multispecies models
    if (!invalidSimulation_ && community && community->simulation_valid_ && (community->Tick() >= 1))
    {
        bool speciesBarVisibleNow = !ui->speciesBarWidget->isHidden();
        bool speciesBarShouldBeVisible = (community->all_species_.size() > 1);
        
        if (speciesBarVisibleNow && !speciesBarShouldBeVisible)
        {
            ui->speciesBar->setEnabled(false);
            ui->speciesBarWidget->setHidden(true);
            
            reloadingSpeciesBar = true;
            
            while (ui->speciesBar->count())
                ui->speciesBar->removeTab(0);
            
            reloadingSpeciesBar = false;
        }
        else if (!speciesBarVisibleNow && speciesBarShouldBeVisible)
        {
            ui->speciesBar->setEnabled(true);
            ui->speciesBarWidget->setHidden(false);
            
            if ((ui->speciesBar->count() == 0) && (community->all_species_.size() > 0))
            {
                // add tabs for species when shown
                int selectedSpeciesIndex = 0;
                bool avatarsOnly = (community->all_species_.size() > 2);
                
                reloadingSpeciesBar = true;
                
                for (Species *species : community->all_species_)
                {
                    QString tabLabel = QString::fromStdString(species->avatar_);
                    
                    if (!avatarsOnly)
                    {
                        tabLabel.append(" ");
                        tabLabel.append(QString::fromStdString(species->name_));
                    }
                    
                    int newTabIndex = ui->speciesBar->addTab(tabLabel);
                    
                    ui->speciesBar->setTabToolTip(newTabIndex, QString::fromStdString(species->name_).prepend("Species "));
                    
                    if (focalSpeciesName.length() && (species->name_.compare(focalSpeciesName) == 0))
                        selectedSpeciesIndex = newTabIndex;
                }
                
                {
                    // add the "all" tab
                    QString allLabel = QString::fromUtf8("\xF0\x9F\x94\x85");   // "low brightness symbol", https://www.compart.com/en/unicode/U+1F505
                
                    if (!avatarsOnly)
                        allLabel.append(" all");
                    
                    int newTabIndex = ui->speciesBar->addTab(allLabel);
                    
                    ui->speciesBar->setTabToolTip(newTabIndex, "Show all species together");
                    
                    if (focalSpeciesName.length() && (focalSpeciesName.compare("all") == 0))
                        selectedSpeciesIndex = newTabIndex;
                }
                
                reloadingSpeciesBar = false;
                
                //qDebug() << "selecting index" << selectedSpeciesIndex << "for name" << QString::fromStdString(focalSpeciesName);
                ui->speciesBar->setCurrentIndex(selectedSpeciesIndex);
            }
        }
    }
    else
    {
        // Whenever we're invalid or uninitialized, we hide the species bar and disable and remove all the tabs
        ui->speciesBar->setEnabled(false);
        ui->speciesBarWidget->setHidden(true);
        
        reloadingSpeciesBar = true;
        
        while (ui->speciesBar->count())
            ui->speciesBar->removeTab(0);
        
        reloadingSpeciesBar = false;
    }
}

void QtSLiMWindow::removeExtraChromosomeViews(void)
{
    while (chromosomeOverviewWidgets.size() > 1)
    {
        QVBoxLayout *widgetLayout = chromosomeWidgetLayouts.back();
        
        ui->chromosomeLayout->removeItem(widgetLayout);
        
        QtSLiMClearLayout(widgetLayout, /* deleteWidgets */ true);
        delete widgetLayout;
        
        ui->chromosomeLayout->update();
        
        chromosomeWidgetLayouts.pop_back();
        chromosomeOverviewWidgets.pop_back();
        chromosomeZoomedWidgets.pop_back();
    }
    
    // Sometimes the call above to QtSLiMClearLayout hangs for up to a second.  This appears to be due to
    // disposing of the OpenGL context used for the widget, and might be an AMD Radeon issue.  Here's a backtrace
    // I managed to get from sample.  The only thing I can think of to do about this would be to keep the view
    // around and reuse it, to avoid having to dispose of its context.  But this may be specific to my hardware;
    // probably not worth jumping through hoops to address.  BCH 5/9/2022
    //
    // 845 QtSLiMChromosomeWidget::~QtSLiMChromosomeWidget()  (in SLiMgui) + 188  [0x1033354ac]  QtSLiMChromosomeWidget.cpp:140
    //   845 QOpenGLWidget::~QOpenGLWidget()  (in QtWidgets) + 39  [0x104811887]  qopenglwidget.cpp:1020
    //     844 QOpenGLWidgetPrivate::reset()  (in QtWidgets) + 226  [0x104810582]  qopenglwidget.cpp:719
    //       844 QOpenGLContext::~QOpenGLContext()  (in QtGui) + 24  [0x104e58f68]  qopenglcontext.cpp:690
    //         844 QOpenGLContext::destroy()  (in QtGui) + 200  [0x104e58818]  qopenglcontext.cpp:653
    //           844 QCocoaGLContext::~QCocoaGLContext()  (in libqcocoa.dylib) + 14  [0x105c648ce]  qcocoaglcontext.mm:354
    //             844 QCocoaGLContext::~QCocoaGLContext()  (in libqcocoa.dylib) + 51  [0x105c64753]  qcocoaglcontext.mm:355
    //               844 -[NSOpenGLContext dealloc]  (in AppKit) + 62  [0x7fff34349987]
    //                 844 CGLReleaseContext  (in OpenGL) + 178  [0x7fff4166942a]
    //                   843 gliDestroyContext  (in GLEngine) + 127  [0x7fff4168f9d1]
    //                     843 gldDestroyContext  (in libGPUSupportMercury.dylib) + 114  [0x7fff57fe6745]
    //                       842 glrTerminateContext  (in AMDRadeonX6000GLDriver) + 42  [0x11f390257]
}

void QtSLiMWindow::updateChromosomeViewSetup(void)
{
    Species *displaySpecies = focalDisplaySpecies();
    
    QtSLiMChromosomeWidget *overviewWidget = chromosomeOverviewWidgets[0];
    QtSLiMChromosomeWidget *zoomedWidget = chromosomeZoomedWidgets[0];
    
    if (invalidSimulation_ || !community || !community->simulation_valid_ || (community->Tick() == 0))
    {
        // We are in an invalid state of some kind, so we want one chromosome view that is displaying the empty state
        overviewWidget->setFocalDisplaySpecies(nullptr);
        zoomedWidget->setFocalDisplaySpecies(nullptr);
        
        removeExtraChromosomeViews();
    }
    else if (displaySpecies)
    {
        // We have a focal display species, so we want just one chromosome view, displaying that species
        overviewWidget->setFocalDisplaySpecies(displaySpecies);
        zoomedWidget->setFocalDisplaySpecies(displaySpecies);
        
        removeExtraChromosomeViews();
    }
    else if (chromosomeOverviewWidgets.size() != community->all_species_.size())
    {
        // We are on the "all" species tab in a multispecies model; create a chromosome view for each species
        // We should always arrive at this state through the "invalid state" case above as an intermediate
        removeExtraChromosomeViews();
        
        for (int index = 0; index < (int)community->all_species_.size(); ++index)
        {
            displaySpecies = community->all_species_[index];
            
            if (index == 0)
            {
                // overviewWidget and zoomedWidget were set above and are used for index == 0
            }
            else
            {
                // Beyond the built-in chromosome view, we create the rest dynamically
                // This code is based directly on the MOC code for the built-in views
                QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Expanding);
                sizePolicy1.setHorizontalStretch(0);
                sizePolicy1.setVerticalStretch(0);
                
                QVBoxLayout *chromosomeWidgetLayout = new QVBoxLayout();
                chromosomeWidgetLayout->setSpacing(4);
                
                overviewWidget = new QtSLiMChromosomeWidget(ui->centralWidget);
                sizePolicy1.setHeightForWidth(overviewWidget->sizePolicy().hasHeightForWidth());
                overviewWidget->setSizePolicy(sizePolicy1);
                overviewWidget->setMinimumSize(QSize(0, 20));
                overviewWidget->setMaximumSize(QSize(16777215, 20));
                chromosomeWidgetLayout->addWidget(overviewWidget);
                
                zoomedWidget = new QtSLiMChromosomeWidget(ui->centralWidget);
                sizePolicy1.setHeightForWidth(zoomedWidget->sizePolicy().hasHeightForWidth());
                zoomedWidget->setSizePolicy(sizePolicy1);
                zoomedWidget->setMinimumSize(QSize(0, 65));
                zoomedWidget->setMaximumSize(QSize(16777215, 65));
                chromosomeWidgetLayout->addWidget(zoomedWidget);
                
                ui->chromosomeLayout->insertLayout(1, chromosomeWidgetLayout);
                
                addChromosomeWidgets(chromosomeWidgetLayout, overviewWidget, zoomedWidget);
            }
            
            overviewWidget->setFocalDisplaySpecies(displaySpecies);
            zoomedWidget->setFocalDisplaySpecies(displaySpecies);
        }
    }
}

void QtSLiMWindow::updateAfterTickFull(bool fullUpdate)
{
    // fullUpdate is used to suppress some expensive updating to every third update
	if (!fullUpdate)
	{
		if (++partialUpdateCount_ >= 3)
		{
			partialUpdateCount_ = 0;
			fullUpdate = true;
		}
	}
    
    // Update the species bar and then fetch the focal species after that update, which might change it
    updateSpeciesBar();
	
    // Create or destroy chromosome views for each species, and set the species for each chromosome view
    updateChromosomeViewSetup();
    
    // Flush any buffered output to files every full update, so that the user sees changes to the files without too much delay
    // NOTE THAT THE WORKING DIRECTORY HAS BEEN CHANGED BACK AT THIS POINT!
	if (fullUpdate)
    {
		bool flush_success = Eidos_FlushFiles();
        
        if (!flush_success)
        {
            // Showing a message right here is a bit disruptive to the flow of the code; for example, if the step button is pressed,
            // it will stick down and bad things will happen.  So we need to actually halt the simulation with the error.  We might
            // as well do that with the standard error termination mechanism.  Hopefully this will never be hit anyway.  BCH 3/18/2024
            gEidosTermination.clear();
            gEidosTermination.str("");
            gEidosTermination << "ERROR (Eidos_FlushFiles): A compressed file buffer failed to write out to disk.  Please check file paths, filesystem writeability and permissions, available disk space, and other possible causes of file I/O problems.\n";
            ClearErrorContext();
        }
    }
	
	// Check whether the simulation has terminated due to an error; if so, show an error message with a delayed perform
	checkForSimulationTermination();
	
	// The rest of the code here needs to be careful about the invalid state; we do want to update our controls when invalid, but sim is nil.
    bool inInvalidState = (!community || !community->simulation_valid_ || invalidSimulation());
    
    if (fullUpdate)
        updateOutputViews();
    
    // Minimal population table updating.  When the list of subpops changes, we always need to redisplay, otherwise the display
    // list is outdated and might contain deallocated Subpopulations.  Other than that, though, we only want to redisplay
    // on full updates, because reloading and redisplaying the tableview can be quite expensive.  The QtSLiMPopulationTableModel
    // keeps a cache of its display list, and we can use that to see whether a redisplay is needed or not.
    std::vector newDisplaySubpops = listedSubpopulations();
    
    if (fullUpdate || populationTableModel_->needsUpdateForDisplaySubpops(newDisplaySubpops))
	{
        //qDebug() << "UPDATING TABLE";
        
		// Reloading the subpop tableview is tricky, because we need to preserve the selection across the reload, while also noting that the selection is forced
		// to change when a subpop goes extinct.  The current selection is noted in the gui_selected_ ivar of each subpop.  So what we do here is reload the tableview
		// while suppressing our usual update of our selection state, and then we try to re-impose our selection state on the new tableview content.  If a subpop
		// went extinct, we will fail to notice the selection change; but that is OK, since we force an update of populationView and chromosomeZoomed below anyway.
		reloadingSubpopTableview = true;                        // suppresses QtSLiMWindow::subpopSelectionDidChange()
        populationTableModel_->reloadTable(newDisplaySubpops);  // invalidates newDisplaySubpops with std::swap()
		
        int subpopCount = populationTableModel_->rowCount();
        
		if (subpopCount > 0)
		{
            ui->subpopTableView->selectionModel()->reset();
            
			for (int i = 0; i < subpopCount; ++i)
			{
                Subpopulation *subpop = populationTableModel_->subpopAtIndex(i);
                
				if (subpop->gui_selected_)
                {
                    QModelIndex modelIndex = ui->subpopTableView->model()->index(i, 0);
                    
                    ui->subpopTableView->selectionModel()->select(modelIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows);
                }
			}
		}
        else
        {
            ui->subpopTableView->selectionModel()->clear();
        }
		
		reloadingSubpopTableview = false;
        
        // We don't want to allow an empty selection, maybe; if we are now in that state, and there are subpops to select, select them all
        // See also subpopSelectionDidChange() which also needs to do this
        if ((ui->subpopTableView->selectionModel()->selectedRows().size() == 0) && subpopCount)
            ui->subpopTableView->selectAll();
	}
    else
    {
        //qDebug() << "skipping unnecessary table update";
    }
	
	// Now update our other UI, some of which depends upon the state of subpopTableView
    ui->individualsWidget->update();
    
    if (fullUpdate)
        updateTickCounter();
    
    if (fullUpdate)
    {
        double elapsedTimeInSLiM = elapsedCPUClock_ / static_cast(CLOCKS_PER_SEC);
        
        if (elapsedTimeInSLiM == 0.0)
            ui->statusBar->clearMessage();
        else
        {
            bool inDarkMode = QtSLiMInDarkMode();
            QString message(inDarkMode ? "%1 CPU seconds elapsed inside SLiM; %2 MB memory usage in SLiM; %3 mutations segregating, %4 substitutions."
                                       : "%1 CPU seconds elapsed inside SLiM; %2 MB memory usage in SLiM; %3 mutations segregating, %4 substitutions.");
            
            if (!inInvalidState)
            {
                int totalRegistrySize = 0;
                
                for (Species *species : community->AllSpecies())
                {
                    int registry_size;
                    
                    species->population_.MutationRegistry(®istry_size);
                    totalRegistrySize += registry_size;
                }
                
                // Tally up usage across the simulation
                SLiMMemoryUsage_Community usage_community;
                SLiMMemoryUsage_Species usage_all_species;
                
                EIDOS_BZERO(&usage_all_species, sizeof(SLiMMemoryUsage_Species));
                
                community->TabulateSLiMMemoryUsage_Community(&usage_community, nullptr);
                
                for (Species *species : community->AllSpecies())
                {
                    SLiMMemoryUsage_Species usage_one_species;
                    
                    species->TabulateSLiMMemoryUsage_Species(&usage_one_species);
                    AccumulateMemoryUsageIntoTotal_Species(usage_one_species, usage_all_species);
                }
                
                double current_memory_MB = (usage_community.totalMemoryUsage + usage_all_species.totalMemoryUsage) / (1024.0 * 1024.0);
                
                // Tally up substitutions across the simulation
                int totalSubstitutions = 0;
                
                for (Species *species : community->AllSpecies())
                    totalSubstitutions += species->population_.substitutions_.size();
                
                ui->statusBar->showMessage(message.arg(elapsedTimeInSLiM, 0, 'f', 6)
                                           .arg(current_memory_MB, 0, 'f', 1)
                                           .arg(totalRegistrySize)
                                           .arg(totalSubstitutions));
            }
            else
                ui->statusBar->showMessage(message.arg(elapsedTimeInSLiM, 0, 'f', 6));
        }
	}
    
	// Update stuff that only needs updating when the script is re-parsed, not after every tick
	if (inInvalidState || community->mutation_types_changed_)
	{
        if (tablesDrawerController && tablesDrawerController->mutTypeTableModel_)
            tablesDrawerController->mutTypeTableModel_->reloadTable();
		
		if (community)
			community->mutation_types_changed_ = false;
	}
	
	if (inInvalidState || community->genomic_element_types_changed_)
	{
        if (tablesDrawerController && tablesDrawerController->geTypeTableModel_)
            tablesDrawerController->geTypeTableModel_->reloadTable();
		
		if (community)
			community->genomic_element_types_changed_ = false;
	}
	
	if (inInvalidState || community->interaction_types_changed_)
	{
        if (tablesDrawerController && tablesDrawerController->interactionTypeTableModel_)
            tablesDrawerController->interactionTypeTableModel_->reloadTable();
		
		if (community)
			community->interaction_types_changed_ = false;
	}
	
	if (inInvalidState || community->scripts_changed_)
	{
        if (tablesDrawerController && tablesDrawerController->eidosBlockTableModel_)
            tablesDrawerController->eidosBlockTableModel_->reloadTable();
		
		if (community)
			community->scripts_changed_ = false;
	}
	
	if (inInvalidState || community->chromosome_changed_)
	{
        for (QtSLiMChromosomeWidget *overviewWidget : chromosomeOverviewWidgets)
        {
            overviewWidget->restoreLastSelection();
            overviewWidget->update();
        }
        
		if (community)
			community->chromosome_changed_ = false;
	}
	
	// Update graph windows as well; this will usually trigger an update() but may do other updating work as well
    // BCH 9/26/2024: This mechanism is now used to update chromosome views as well.  We now have two signals,
    // one for partial updates and one for full updates; a given receiver should connect to just one of these signals.
    // The partial update signal is always emitted; the full update signal is emitted only for full updates,
    // which are less frequent.
    if (fullUpdate)
        emit controllerFullUpdateAfterTick();
    
    emit controllerPartialUpdateAfterTick();
}

void QtSLiMWindow::updatePlayButtonIcon(bool pressed)
{
    bool highlighted = ui->playButton->isChecked() ^ pressed;
    
    ui->playButton->qtslimSetHighlight(highlighted);
}

void QtSLiMWindow::updateProfileButtonIcon(bool pressed)
{
    bool highlighted = ui->profileButton->isChecked() ^ pressed;
    
    if (profilePlayOn_)
        ui->profileButton->qtslimSetIcon("profile_R", !highlighted);    // flipped intentionally
    else
        ui->profileButton->qtslimSetIcon("profile", highlighted);
}

void QtSLiMWindow::updateRecycleButtonIcon(bool pressed)
{
    if (slimChangeCount)
        ui->recycleButton->qtslimSetIcon("recycle_G", pressed);
    else
        ui->recycleButton->qtslimSetIcon("recycle", pressed);
}

void QtSLiMWindow::updateUIEnabling(void)
{
    // First we update all the UI that belongs exclusively to ourselves: buttons, labels, etc.
    ui->playOneStepButton->setEnabled(!reachedSimulationEnd_ && !continuousPlayOn_);
    ui->playButton->setEnabled(!reachedSimulationEnd_ && !profilePlayOn_);
    ui->profileButton->setEnabled(!reachedSimulationEnd_ && !nonProfilePlayOn_ && !tickPlayOn_);
    ui->recycleButton->setEnabled(!continuousPlayOn_);
    
    ui->playSpeedSlider->setEnabled(!invalidSimulation_);
    
    if (invalidSimulation_)
    {
        // when an error occurs, we want these textfields to have a dimmed/disabled appearance
        ui->tickLineEdit->setAppearance(/* enabled */ false, /* dimmed */ true);
        ui->cycleLineEdit->setAppearance(/* enabled */ false, /* dimmed */ true);
    }
    else
    {
        // otherwise, we want an enabled _appearance_ at all times, but we have to disable them
        // to prevent editing during play; so we set the text color to prevent it from dimming
        // note that the cycle lineedit is always disabled, but follows the appearance of the tick lineedit;
        // the "editable but dimmed" visual appearance is actually a little different so hopefully this is clear
        bool editingAllowed = (!reachedSimulationEnd_ && !continuousPlayOn_);
        
        ui->tickLineEdit->setAppearance(/* enabled */ editingAllowed, /* dimmed */ false);
        ui->cycleLineEdit->setAppearance(/* enabled */ false, /* dimmed */ false);
    }
    
    ui->toggleDrawerButton->setEnabled(true);
    
    ui->clearDebugButton->setEnabled(true);
    ui->checkScriptButton->setEnabled(!continuousPlayOn_);
    ui->prettyprintButton->setEnabled(!continuousPlayOn_);
    ui->scriptHelpButton->setEnabled(true);
    ui->consoleButton->setEnabled(true);
    ui->browserButton->setEnabled(true);
    ui->jumpToPopupButton->setEnabled(true);
    
    ui->chromosomeActionButton->setEnabled(!invalidSimulation_);
    ui->chromosomeDisplayButton->setEnabled(!invalidSimulation_);
    ui->clearOutputButton->setEnabled(!invalidSimulation_);
    ui->dumpPopulationButton->setEnabled(!invalidSimulation_);
    ui->debugOutputButton->setEnabled(true);
    ui->graphPopupButton->setEnabled(!invalidSimulation_);
    ui->changeDirectoryButton->setEnabled(!continuousPlayOn_);
    
    ui->scriptTextEdit->setReadOnly(continuousPlayOn_);
    ui->outputTextEdit->setReadOnly(true);
    
    ui->tickLabel->setEnabled(!invalidSimulation_);
    ui->cycleLabel->setEnabled(!invalidSimulation_);
    ui->outputHeaderLabel->setEnabled(!invalidSimulation_);
    
    // Tell the console controller to enable/disable its buttons
    if (consoleController)
        consoleController->setInterfaceEnabled(!continuousPlayOn_);
    
    // Then, if we are the focused or active window, we update the menus to reflect our state
    // If there's a focused/active window but it isn't us, we reflect that situation with a different method
    // Keep in mind that in Qt each QMainWindow has its own menu bar, its own actions, etc.; this is not global state!
    // This means we spend a little time updating menu enable states that are not visible anyway, but it's fast
    QWidget *currentFocusWidget = qApp->focusWidget();
    QWidget *focusWindow = (currentFocusWidget ? currentFocusWidget->window() : qtSLiMAppDelegate->activeWindow());
    
    if (focusWindow == this) {
        //qDebug() << "updateMenuEnablingACTIVE()";
        updateMenuEnablingACTIVE(currentFocusWidget);
    } else {
        //qDebug() << "updateMenuEnablingINACTIVE()";
        updateMenuEnablingINACTIVE(currentFocusWidget, focusWindow);
    }
}

void QtSLiMWindow::updateMenuEnablingACTIVE(QWidget *p_focusWidget)
{
    // Enable/disable actions (i.e., menu items) when our window is active.  Note that this
    // does not enable/disable buttons; that is done in QtSLiMWindow::updateUIEnabling().
    
    ui->actionClose->setEnabled(true);
    ui->actionSave->setEnabled(true);
    ui->actionSaveAs->setEnabled(true);
    ui->actionRevertToSaved->setEnabled(!isUntitled);
    
    //ui->menuSimulation->setEnabled(true);     // commented out these menu-level enable/disables; they flash weirdly and are distracting
    ui->actionStep->setEnabled(!reachedSimulationEnd_ && !continuousPlayOn_);
    ui->actionPlay->setEnabled(!reachedSimulationEnd_ && !profilePlayOn_);
    ui->actionPlay->setText(nonProfilePlayOn_ ? "Stop" : "Play");
    ui->actionProfile->setEnabled(!reachedSimulationEnd_ && !nonProfilePlayOn_ && !tickPlayOn_);
    ui->actionProfile->setText(profilePlayOn_ ? "Stop" : "Profile");
    ui->actionRecycle->setEnabled(!continuousPlayOn_);
    
    //ui->menuScript->setEnabled(true);
    ui->actionClearDebug->setEnabled(true);
    ui->actionCheckScript->setEnabled(!continuousPlayOn_);
    ui->actionPrettyprintScript->setEnabled(!continuousPlayOn_);
    ui->actionReformatScript->setEnabled(!continuousPlayOn_);
    ui->actionShowScriptHelp->setEnabled(true);
    ui->actionBiggerFont->setEnabled(true);
    ui->actionSmallerFont->setEnabled(true);
    ui->actionShowEidosConsole->setEnabled(true);
    ui->actionShowVariableBrowser->setEnabled(true);
    ui->actionShowDebuggingOutput->setEnabled(true);
    
    ui->actionClearOutput->setEnabled(!invalidSimulation_);
    ui->actionExecuteAll->setEnabled(false);
    ui->actionExecuteSelection->setEnabled(false);
    ui->actionDumpPopulationState->setEnabled(!invalidSimulation_);
    ui->actionChangeWorkingDirectory->setEnabled(!continuousPlayOn_);
    
    // see QtSLiMWindow::graphPopupButtonRunMenu() for parallel code involving the graph popup button
    Species *displaySpecies = focalDisplaySpecies();
    bool graphItemsEnabled = displaySpecies && !invalidSimulation_;
    
    //ui->menuGraph->setEnabled(graphItemsEnabled);
    ui->actionGraph_1D_Population_SFS->setEnabled(graphItemsEnabled);
	ui->actionGraph_1D_Sample_SFS->setEnabled(graphItemsEnabled);
	ui->actionGraph_2D_Population_SFS->setEnabled(graphItemsEnabled);
	ui->actionGraph_2D_Sample_SFS->setEnabled(graphItemsEnabled);
	ui->actionGraph_Mutation_Frequency_Trajectories->setEnabled(graphItemsEnabled);
	ui->actionGraph_Mutation_Loss_Time_Histogram->setEnabled(graphItemsEnabled);
	ui->actionGraph_Mutation_Fixation_Time_Histogram->setEnabled(graphItemsEnabled);
	ui->actionGraph_Population_Fitness_Distribution->setEnabled(graphItemsEnabled);
	ui->actionGraph_Subpopulation_Fitness_Distributions->setEnabled(graphItemsEnabled);
	ui->actionGraph_Fitness_Time->setEnabled(graphItemsEnabled);
	ui->actionGraph_Age_Distribution->setEnabled(graphItemsEnabled);
	ui->actionGraph_Lifetime_Reproduce_Output->setEnabled(graphItemsEnabled);
	ui->actionGraph_Population_Size_Time->setEnabled(graphItemsEnabled);
	ui->actionGraph_Population_Visualization->setEnabled(graphItemsEnabled);
    ui->actionGraph_Multispecies_Population_Size_Time->setEnabled(!invalidSimulation_);     // displaySpecies not required
    
    // the haplotype plot menu items are a bit complicated
    bool haplotypePlotEnabled = displaySpecies && !continuousPlayOn_ && displaySpecies->population_.subpops_.size();
    ui->actionCreate_Haplotype_Plot_All->setEnabled(haplotypePlotEnabled);
    ui->actionCreate_Haplotype_Plot_Selected->setEnabled(haplotypePlotEnabled && (focalChromosome() != nullptr));
    
    if (haplotypePlotEnabled && (displaySpecies->Chromosomes().size() > 1))
    {
        // enabled with 2+ chromosomes, we show both menu items
        ui->actionCreate_Haplotype_Plot_All->setText("Create Haplotype Plot (all chromosomes)");
        ui->actionCreate_Haplotype_Plot_Selected->setVisible(true);
    }
    else
    {
        // disabled or with 0 or 1 chromosomes, we show only actionCreate_Haplotype_Plot_All
        ui->actionCreate_Haplotype_Plot_All->setText("Create Haplotype Plot");
        ui->actionCreate_Haplotype_Plot_Selected->setVisible(false);
    }
    
    updateMenuEnablingSHARED(p_focusWidget);
}

void QtSLiMWindow::updateMenuEnablingINACTIVE(QWidget *p_focusWidget, QWidget *focusWindow)
{
    // Enable/disable actions (i.e., menu items) when our window is inactive.  Note that this
    // does not enable/disable buttons; that is done in QtSLiMWindow::updateUIEnabling().
    
    QWidget *currentActiveWindow = QApplication::activeWindow();
    ui->actionClose->setEnabled(currentActiveWindow ? true : false);
    
    ui->actionSave->setEnabled(false);
    ui->actionSaveAs->setEnabled(false);
    ui->actionRevertToSaved->setEnabled(false);
    
    //ui->menuSimulation->setEnabled(false);
    ui->actionStep->setEnabled(false);
    ui->actionPlay->setEnabled(false);
    ui->actionPlay->setText("Play");
    ui->actionProfile->setEnabled(false);
    ui->actionProfile->setText("Profile");
    ui->actionRecycle->setEnabled(false);
    
    // The script menu state, if we are inactive, is mostly either (a) governed by the front console
    // controller, or (b) is disabled, if a console controller is not active
    QtSLiMEidosConsole *eidosConsole = dynamic_cast(focusWindow);
    bool consoleFocused = (eidosConsole != nullptr);
    bool consoleFocusedAndEditable = ((eidosConsole != nullptr) && !continuousPlayOn_);
    
    //ui->menuScript->setEnabled(consoleFocused);
    ui->actionCheckScript->setEnabled(consoleFocusedAndEditable);
    ui->actionPrettyprintScript->setEnabled(consoleFocusedAndEditable);
    ui->actionReformatScript->setEnabled(consoleFocusedAndEditable);
    ui->actionClearOutput->setEnabled(consoleFocused);
    ui->actionExecuteAll->setEnabled(consoleFocusedAndEditable);
    ui->actionExecuteSelection->setEnabled(consoleFocusedAndEditable);
    
    // but these menu items apply only to QtSLiMWindow, not to the Eidos console
    ui->actionClearDebug->setEnabled(false);
    ui->actionDumpPopulationState->setEnabled(false);
    ui->actionChangeWorkingDirectory->setEnabled(false);
    
    //ui->menuGraph->setEnabled(false);
    ui->actionGraph_1D_Population_SFS->setEnabled(false);
	ui->actionGraph_1D_Sample_SFS->setEnabled(false);
	ui->actionGraph_2D_Population_SFS->setEnabled(false);
	ui->actionGraph_2D_Sample_SFS->setEnabled(false);
	ui->actionGraph_Mutation_Frequency_Trajectories->setEnabled(false);
	ui->actionGraph_Mutation_Loss_Time_Histogram->setEnabled(false);
	ui->actionGraph_Mutation_Fixation_Time_Histogram->setEnabled(false);
	ui->actionGraph_Population_Fitness_Distribution->setEnabled(false);
	ui->actionGraph_Subpopulation_Fitness_Distributions->setEnabled(false);
	ui->actionGraph_Fitness_Time->setEnabled(false);
	ui->actionGraph_Age_Distribution->setEnabled(false);
	ui->actionGraph_Lifetime_Reproduce_Output->setEnabled(false);
	ui->actionGraph_Population_Size_Time->setEnabled(false);
	ui->actionGraph_Population_Visualization->setEnabled(false);
    
    // the haplotype plot menu items take on a generic appearance when disabled
    ui->actionCreate_Haplotype_Plot_All->setEnabled(false);
    ui->actionCreate_Haplotype_Plot_All->setText("Create Haplotype Plot");
    ui->actionCreate_Haplotype_Plot_Selected->setEnabled(false);
    ui->actionCreate_Haplotype_Plot_Selected->setVisible(false);
    
    // we can show our various windows as long as we can reach the controller window
    QtSLiMWindow *slimWindow = qtSLiMAppDelegate->dispatchQtSLiMWindowFromSecondaries();
    bool canReachSLiMWindow = !!slimWindow;
    
    ui->actionShowScriptHelp->setEnabled(canReachSLiMWindow);
    ui->actionShowEidosConsole->setEnabled(canReachSLiMWindow);
    ui->actionShowVariableBrowser->setEnabled(canReachSLiMWindow);
    ui->actionShowDebuggingOutput->setEnabled(canReachSLiMWindow);
    
    updateMenuEnablingSHARED(p_focusWidget);
}

void QtSLiMWindow::updateMenuEnablingSHARED(QWidget *p_focusWidget)
{
    // Here we update the enable state for menu items, such as cut/copy/paste, that go to
    // the p_focusWidget whatever window it might be in; "first responder" in Cocoa parlance
    QLineEdit *lE = dynamic_cast(p_focusWidget);
    QTextEdit *tE = dynamic_cast(p_focusWidget);
    QPlainTextEdit *ptE = dynamic_cast(p_focusWidget);
    QtSLiMTextEdit *stE = dynamic_cast(tE);
    bool hasEnabledDestination = (lE && lE->isEnabled()) || (tE && tE->isEnabled()) || (ptE && ptE->isEnabled());
    bool hasEnabledModifiableDestination = (lE && lE->isEnabled() && !lE->isReadOnly()) ||
            (tE && tE->isEnabled() && !tE->isReadOnly()) || (ptE && ptE->isEnabled() && !ptE->isReadOnly());
    bool hasUndoableDestination = (lE && lE->isEnabled() && !lE->isReadOnly() && lE->isUndoAvailable()) ||
            (tE && tE->isEnabled() && !tE->isReadOnly() && tE->isUndoRedoEnabled()) ||
            (ptE && ptE->isEnabled() && !ptE->isReadOnly() && ptE->isUndoRedoEnabled());
    bool hasRedoableDestination = (lE && lE->isEnabled() && !lE->isReadOnly() && lE->isRedoAvailable()) ||
            (tE && tE->isEnabled() && !tE->isReadOnly() && tE->isUndoRedoEnabled()) ||
            (ptE && ptE->isEnabled() && !ptE->isReadOnly() && ptE->isUndoRedoEnabled());
    bool hasCopyableDestination = (lE && lE->isEnabled() && lE->selectedText().length()) ||
            (tE && tE->isEnabled()) || (ptE && ptE->isEnabled());
    
    if (stE)
    {
        // refine our assessment of undo/redo/copy capability if possible
        hasUndoableDestination = hasUndoableDestination && stE->qtslimIsUndoAvailable();
        hasRedoableDestination = hasRedoableDestination && stE->qtslimIsRedoAvailable();
        hasCopyableDestination = hasCopyableDestination && stE->qtslimIsCopyAvailable();
    }
    
    ui->actionUndo->setEnabled(hasUndoableDestination);
    ui->actionRedo->setEnabled(hasRedoableDestination);
    ui->actionCut->setEnabled(hasEnabledModifiableDestination);
    ui->actionCopy->setEnabled(hasCopyableDestination);
    ui->actionPaste->setEnabled(hasEnabledModifiableDestination);
    ui->actionDelete->setEnabled(hasEnabledModifiableDestination);
    ui->actionSelectAll->setEnabled(hasEnabledDestination);
    
    ui->actionBiggerFont->setEnabled(true);
    ui->actionSmallerFont->setEnabled(true);
    
    // actions handled by QtSLiMScriptTextEdit only
    QtSLiMScriptTextEdit *scriptEdit = dynamic_cast(p_focusWidget);
    bool isScriptTextEdit = (!!scriptEdit);
    bool isModifiableScriptTextEdit = (isScriptTextEdit && !scriptEdit->isReadOnly());
    
    ui->actionDuplicate->setEnabled(isModifiableScriptTextEdit);
    ui->actionCopyAsHTML->setEnabled(isScriptTextEdit);
    ui->actionShiftLeft->setEnabled(isModifiableScriptTextEdit);
    ui->actionShiftRight->setEnabled(isModifiableScriptTextEdit);
    ui->actionCommentUncomment->setEnabled(isModifiableScriptTextEdit);
    
    // actions handled by the Find panel only
    QtSLiMFindPanel &findPanelInstance = QtSLiMFindPanel::instance();
    bool hasFindTarget = (findPanelInstance.targetTextEditRequireModifiable(false) != nullptr);
    bool hasModifiableFindTarget = (findPanelInstance.targetTextEditRequireModifiable(true) != nullptr);
    
    ui->actionFindShow->setEnabled(true);
    ui->actionFindNext->setEnabled(hasFindTarget);
    ui->actionFindPrevious->setEnabled(hasFindTarget);
    ui->actionReplaceAndFind->setEnabled(hasModifiableFindTarget);
    ui->actionUseSelectionForFind->setEnabled(hasFindTarget);
    ui->actionUseSelectionForReplace->setEnabled(hasFindTarget);
    ui->actionJumpToSelection->setEnabled(hasFindTarget);
    ui->actionJumpToLine->setEnabled(hasFindTarget);
    
    findPanelInstance.fixEnableState();   // give it a chance to update its buttons whenever we update
}

void QtSLiMWindow::updateWindowMenu(void)
{
    // Clear out old actions, up to the separator
    do
    {
        const QList actions = ui->menuWindow->actions();
        QAction *lastAction = actions.last();
        
        if (!lastAction)
            break;
        if ((lastAction->objectName().length() == 0) || (lastAction->objectName() == "action"))
            break;
        
        // I have seen this loop fail to terminate, because apparently asking for an action
        // to be removed sometimes fails.  So now we watch the number of actions and make
        // sure it goes down, otherwise we bail.  BCH 1/29/2024
        int actionCount = actions.count();
        
        ui->menuWindow->removeAction(lastAction);
        
        if (ui->menuWindow->actions().count() >= actionCount)
        {
            qDebug() << "QtSLiMWindow::updateWindowMenu() menu clearing terminating due to malfunction";
            break;
        }
    }
    while (true);
    
    // Get the main windows, in sorted order
    const QList allWidgets = QApplication::allWidgets();
    std::vector> windows;
    
    for (QWidget *widget : allWidgets)
    {
        QtSLiMWindow *mainWin = qobject_cast(widget);
        
        if (mainWin && !mainWin->isZombieWindow_)
        {
            QString title = mainWin->windowTitle();
            
            if (title.endsWith("[*]"))
                title.chop(3);
            
            windows.emplace_back(title.toStdString(), mainWin);
        }
    }
    
    std::sort(windows.begin(), windows.end(), [](const std::pair &l, const std::pair &r) { return l.first < r.first; });
    
    // Make new actions
    QWidget *activeWindow = qtSLiMAppDelegate->activeWindow();
    
    for (const auto &pair : windows)
    {
        QString title = QString::fromStdString(pair.first);
        QtSLiMWindow *mainWin = pair.second;
        
        if (mainWin)
        {
            QAction *action = ui->menuWindow->addAction(title, mainWin, [mainWin]() { QtSLiMMakeWindowVisibleAndExposed(mainWin); });
            action->setCheckable(mainWin == activeWindow);  // only set checkable if checked, to avoid the empty checkbox on Ubuntu
            action->setChecked(mainWin == activeWindow);
            action->setObjectName("__QtSLiM_window__");
            
            // Get the subwindows, in sorted order
            std::vector> subwindows;
            
            for (QWidget *widget : allWidgets)
            {
                QWidget *finalParent = widget->parentWidget();
                
                while (finalParent && (finalParent != mainWin))
                    finalParent = finalParent->parentWidget();
                
                if ((qobject_cast(widget) == nullptr) &&
                        (finalParent == mainWin) &&
                        (widget->isVisible()) &&
                        (((widget->windowFlags() & Qt::Window) == Qt::Window) || ((widget->windowFlags() & Qt::Tool) == Qt::Tool)))
                {
                    QString subwindowTitle = widget->windowTitle();
                    
                    if (subwindowTitle.length())
                    {
                        if (graphViewForGraphWindow(widget))
                            subwindowTitle.prepend("Graph: ");
                        subwindows.emplace_back(subwindowTitle.toStdString(), widget);
                    }
                }
            }
            
            std::sort(subwindows.begin(), subwindows.end(), [](const std::pair &l, const std::pair &r) { return l.first < r.first; });
            
            // Add indented subitems for windows owned by this main window
            for (const auto &subpair : subwindows)
            {
                QString subwindowTitle = QString::fromStdString(subpair.first);
                QWidget *subwindow = subpair.second;
                
                QAction *subwindowAction = ui->menuWindow->addAction(subwindowTitle.prepend("    "), subwindow, [subwindow]() { QtSLiMMakeWindowVisibleAndExposed(subwindow); });
                subwindowAction->setCheckable(subwindow == activeWindow);  // only set checkable if checked, to avoid the empty checkbox on Ubuntu
                subwindowAction->setChecked(subwindow == activeWindow);
                subwindowAction->setObjectName("__QtSLiM_subwindow__");
            }
        }
    }
}


//
//  profiling
//

#if (SLIMPROFILING == 1)

void QtSLiMWindow::colorScriptWithProfileCountsFromNode(const EidosASTNode *node, double elapsedTime, int32_t baseIndex, QTextDocument *doc, QTextCharFormat &baseFormat)
{
    // First color the range for this node
	eidos_profile_t count = node->profile_total_;
	
	if (count > 0)
	{
		int32_t start = 0, end = 0;
		
		node->FullUTF16Range(&start, &end);
		
		start -= baseIndex;
		end -= baseIndex;
		
		QTextCursor colorCursor(doc);
        colorCursor.setPosition(start);
        colorCursor.setPosition(end + 1, QTextCursor::KeepAnchor);
        
        QColor backgroundColor = slimColorForFraction(Eidos_ElapsedProfileTime(count) / elapsedTime);
		QTextCharFormat colorFormat = baseFormat;
        
        colorFormat.setBackground(backgroundColor);
        colorCursor.setCharFormat(colorFormat);
	}
	
	// Then let child nodes color
	for (const EidosASTNode *child : node->children_)
        colorScriptWithProfileCountsFromNode(child, elapsedTime, baseIndex, doc, baseFormat);
}

void QtSLiMWindow::displayProfileResults(void)
{
    // Make a new window to show the profile results
    QWidget *profile_window = new QWidget(this, Qt::Window);    // the profile window has us as a parent, but is still a standalone window
    QString title = profile_window->windowTitle();
    
    if (title.length() == 0)
        title = "Untitled";
    
    profile_window->setWindowTitle("Profile Report for " + title);
    profile_window->setMinimumSize(500, 200);
    profile_window->resize(500, 600);
    profile_window->move(50, 50);
#ifdef __APPLE__
    // set the window icon only on macOS; on Linux it changes the app icon as a side effect
    profile_window->setWindowIcon(QIcon());
#endif
    
    // make window actions for all global menu items
    qtSLiMAppDelegate->addActionsForGlobalMenuItems(profile_window);
    
    // Make a QPlainTextEdit to hold the results
    QHBoxLayout *window_layout = new QHBoxLayout;
    QPlainTextEdit *textEdit = new QPlainTextEdit();
    
    profile_window->setLayout(window_layout);
    
    window_layout->setContentsMargins(0, 0, 0, 0);
    window_layout->setSpacing(0);
    window_layout->addWidget(textEdit);
    
    textEdit->setFrameStyle(QFrame::NoFrame);
    textEdit->setReadOnly(true);
    
    // Change the background color for the palette to white (rather than letting it be black when in dark mode)
    QPalette p = textEdit->palette();
    p.setColor(QPalette::Active, QPalette::Base, Qt::white);
    textEdit->setPalette(p);
    textEdit->setBackgroundVisible(false);
    
    // Make the text document that will hold the profile results
    QTextDocument *doc = textEdit->document();
    QTextCursor tc = textEdit->textCursor();
    
    doc->setDocumentMargin(10);
    
    // Choose our fonts; the variable names here are historical, they are not necessarily menlo/optima...
    QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance();
    QFont menlo11(prefs.displayFontPref());
    qreal displayFontSize = menlo11.pointSizeF();
    qreal scaleFactor = displayFontSize / 11.0;     // The unscaled sizes are geared toward Optima on the Mac
    
#if defined(__linux__)
    // On Linux font sizes seem to run large, who knows why, so reduce the scale factor somewhat to compensate
    scaleFactor *= 0.75;
#endif

    QFont optimaFont;
    {
        // We want a body font of Optima on the Mac; on non-Mac platforms we'll just use the default system font for now
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
        QFontDatabase fontdb;
        QStringList families = fontdb.families();
#else
        QStringList families = QFontDatabase::families();
#endif
        
        // Use filter() to look for matches, since the foundry can be appended after the name (why isn't this easier??)
        if (families.filter("Optima").size() > 0)              // good on Mac
            optimaFont = QFont("Optima", 13);
    }
    
    QFont optima18b(optimaFont);
    QFont optima14b(optimaFont);
    QFont optima13(optimaFont);
    QFont optima13i(optimaFont);
    QFont optima8(optimaFont);
    QFont optima3(optimaFont);
    
    optima18b.setPointSizeF(scaleFactor * 18);
    optima18b.setWeight(QFont::Bold);
    optima14b.setPointSizeF(scaleFactor * 14);
    optima14b.setWeight(QFont::Bold);
    optima13.setPointSizeF(scaleFactor * 13);
    optima13i.setPointSizeF(scaleFactor * 13);
    optima13i.setItalic(true);
    optima8.setPointSizeF(scaleFactor * 8);
    optima3.setPointSizeF(scaleFactor * 3);
    
    // Make the QTextCharFormat objects we will use.  Note that we override the usual foreground/background colors
    // that come from light/dark mode; because we change the background color of text, we want to use a balck-on-white
    // base palette whether we are in light or dark mode, otherwise things get complicated, especially since the user
    // might switch between light/dark after the profile is displayed
    QTextCharFormat optima18b_d, optima14b_d, optima13_d, optima13i_d, optima8_d, optima3_d, menlo11_d;
    
    optima18b_d.setFont(optima18b);
    optima14b_d.setFont(optima14b);
    optima13_d.setFont(optima13);
    optima13i_d.setFont(optima13i);
    optima8_d.setFont(optima8);
    optima3_d.setFont(optima3);
    menlo11_d.setFont(menlo11);
    
    optima18b_d.setBackground(Qt::white);
    optima14b_d.setBackground(Qt::white);
    optima13_d.setBackground(Qt::white);
    optima13i_d.setBackground(Qt::white);
    optima8_d.setBackground(Qt::white);
    optima3_d.setBackground(Qt::white);
    menlo11_d.setBackground(Qt::white);
    
    optima18b_d.setForeground(Qt::black);
    optima14b_d.setForeground(Qt::black);
    optima13_d.setForeground(Qt::black);
    optima13i_d.setForeground(Qt::black);
    optima8_d.setForeground(Qt::black);
    optima3_d.setForeground(Qt::black);
    menlo11_d.setForeground(Qt::black);
    
    // Adjust the tab width to the monospace font we have chosen
    double tabWidth = 0;
    QFontMetricsF fm(menlo11);
    
#if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0))
    tabWidth = fm.width("   ");                // deprecated in 5.11
#else
    tabWidth = fm.horizontalAdvance("   ");    // added in Qt 5.11
#endif
    
#if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0))
    textEdit->setTabStopWidth((int)floor(tabWidth));      // deprecated in 5.10
#else
    textEdit->setTabStopDistance(tabWidth);               // added in 5.10
#endif
    
    // Build the report attributed string
    QDateTime profileStartDate = QDateTime::fromSecsSinceEpoch(community->profile_start_date);
	QDateTime profileEndDate = QDateTime::fromSecsSinceEpoch(community->profile_end_date);
	
    QString startDateString = profileStartDate.toString("M/d/yy, h:mm:ss AP");
    QString endDateString = profileEndDate.toString("M/d/yy, h:mm:ss AP");
	double elapsedWallClockTime = (std::chrono::duration_cast(community->profile_end_clock - community->profile_start_clock).count()) / (double)1000000L;
    double elapsedCPUTimeInSLiM = community->profile_elapsed_CPU_clock / static_cast(CLOCKS_PER_SEC);
	double elapsedWallClockTimeInSLiM = Eidos_ElapsedProfileTime(community->profile_elapsed_wall_clock);
	slim_tick_t elapsedSLiMTicks = community->profile_end_tick - community->profile_start_tick;
    
    tc.insertText("Profile Report\n", optima18b_d);
    tc.insertText(" \n", optima3_d);
    
    tc.insertText("Model: " + title + "\n", optima13_d);
    tc.insertText(" \n", optima8_d);
    
    tc.insertText("Run start: " + startDateString + "\n", optima13_d);
    tc.insertText("Run end: " + endDateString + "\n", optima13_d);
    tc.insertText(" \n", optima8_d);
    
#ifdef _OPENMP
    tc.insertText(QString("Maximum parallel threads: %1\n").arg(gEidosMaxThreads), optima13_d);
    tc.insertText(" \n", optima8_d);
#endif
    
    tc.insertText(QString("Elapsed wall clock time: %1 s\n").arg(elapsedWallClockTime, 0, 'f', 2), optima13_d);
    tc.insertText(QString("Elapsed wall clock time inside SLiM core (corrected): %1 s\n").arg(elapsedWallClockTimeInSLiM, 0, 'f', 2), optima13_d);
    tc.insertText(QString("Elapsed CPU time inside SLiM core (uncorrected): %1 s\n").arg(elapsedCPUTimeInSLiM, 0, 'f', 2), optima13_d);
    tc.insertText(QString("Elapsed ticks: %1%2\n").arg(elapsedSLiMTicks).arg((community->profile_start_tick == 0) ? " (including initialize)" : ""), optima13_d);
    tc.insertText(" \n", optima8_d);
    
    tc.insertText(QString("Profile block external overhead: %1 ticks (%2 s)\n").arg(gEidos_ProfileOverheadTicks, 0, 'f', 2).arg(gEidos_ProfileOverheadSeconds, 0, 'g', 4), optima13_d);
    tc.insertText(QString("Profile block internal lag: %1 ticks (%2 s)\n").arg(gEidos_ProfileLagTicks, 0, 'f', 2).arg(gEidos_ProfileLagSeconds, 0, 'g', 4), optima13_d);
    tc.insertText(" \n", optima8_d);
    
    size_t total_usage = community->profile_total_memory_usage_Community.totalMemoryUsage + community->profile_total_memory_usage_AllSpecies.totalMemoryUsage;
	size_t average_usage = total_usage / community->total_memory_tallies_;
	size_t last_usage = community->profile_last_memory_usage_Community.totalMemoryUsage + community->profile_last_memory_usage_AllSpecies.totalMemoryUsage;
	
    tc.insertText(QString("Average tick SLiM memory use: %1\n").arg(stringForByteCount(average_usage)), optima13_d);
    tc.insertText(QString("Final tick SLiM memory use: %1\n").arg(stringForByteCount(last_usage)), optima13_d);
    
	//
	//	Cycle stage breakdown
	//
	if (elapsedWallClockTimeInSLiM > 0.0)
	{
		bool isWF = (community->ModelType() == SLiMModelType::kModelTypeWF);
		double elapsedStage0Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[0]);
		double elapsedStage1Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[1]);
		double elapsedStage2Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[2]);
		double elapsedStage3Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[3]);
		double elapsedStage4Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[4]);
		double elapsedStage5Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[5]);
		double elapsedStage6Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[6]);
        double elapsedStage7Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[7]);
        double elapsedStage8Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[8]);
		double percentStage0 = (elapsedStage0Time / elapsedWallClockTimeInSLiM) * 100.0;
		double percentStage1 = (elapsedStage1Time / elapsedWallClockTimeInSLiM) * 100.0;
		double percentStage2 = (elapsedStage2Time / elapsedWallClockTimeInSLiM) * 100.0;
		double percentStage3 = (elapsedStage3Time / elapsedWallClockTimeInSLiM) * 100.0;
		double percentStage4 = (elapsedStage4Time / elapsedWallClockTimeInSLiM) * 100.0;
		double percentStage5 = (elapsedStage5Time / elapsedWallClockTimeInSLiM) * 100.0;
		double percentStage6 = (elapsedStage6Time / elapsedWallClockTimeInSLiM) * 100.0;
        double percentStage7 = (elapsedStage7Time / elapsedWallClockTimeInSLiM) * 100.0;
        double percentStage8 = (elapsedStage8Time / elapsedWallClockTimeInSLiM) * 100.0;
		int fw = 4;
		
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage0Time));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage1Time));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage2Time));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage3Time));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage4Time));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage5Time));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage6Time));
        fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage7Time));
        fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage8Time));
		
		tc.insertText(" \n", optima13_d);
		tc.insertText("Cycle stage breakdown\n", optima14b_d);
		tc.insertText(" \n", optima3_d);
		
		tc.insertText(QString("%1 s (%2%)").arg(elapsedStage0Time, fw, 'f', 2).arg(percentStage0, 5, 'f', 2), menlo11_d);
		tc.insertText(" : initialize() callback execution\n", optima13_d);
		
        tc.insertText(QString("%1 s (%2%)").arg(elapsedStage1Time, fw, 'f', 2).arg(percentStage1, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 0 – first() event execution\n" : " : stage 0 – first() event execution\n"), optima13_d);
		
		tc.insertText(QString("%1 s (%2%)").arg(elapsedStage2Time, fw, 'f', 2).arg(percentStage2, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 1 – early() event execution\n" : " : stage 1 – offspring generation\n"), optima13_d);
		
		tc.insertText(QString("%1 s (%2%)").arg(elapsedStage3Time, fw, 'f', 2).arg(percentStage3, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 2 – offspring generation\n" : " : stage 2 – early() event execution\n"), optima13_d);
		
		tc.insertText(QString("%1 s (%2%)").arg(elapsedStage4Time, fw, 'f', 2).arg(percentStage4, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 3 – generation swap\n" : " : stage 3 – fitness calculation\n"), optima13_d);
		
		tc.insertText(QString("%1 s (%2%)").arg(elapsedStage5Time, fw, 'f', 2).arg(percentStage5, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 4 – bookkeeping (fixed mutation removal, etc.)\n" : " : stage 4 – viability/survival selection\n"), optima13_d);
		
		tc.insertText(QString("%1 s (%2%)").arg(elapsedStage6Time, fw, 'f', 2).arg(percentStage6, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 5 – late() event execution\n" : " : stage 5 – bookkeeping (fixed mutation removal, etc.)\n"), optima13_d);
		
		tc.insertText(QString("%1 s (%2%)").arg(elapsedStage7Time, fw, 'f', 2).arg(percentStage7, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 6 – fitness calculation\n" : " : stage 6 – late() event execution\n"), optima13_d);
        
        tc.insertText(QString("%1 s (%2%)").arg(elapsedStage8Time, fw, 'f', 2).arg(percentStage8, 5, 'f', 2), menlo11_d);
		tc.insertText((isWF ? " : stage 7 – tree sequence auto-simplification\n" : " : stage 7 – tree sequence auto-simplification\n"), optima13_d);
	}
	
	//
	//	Callback type breakdown
	//
	if (elapsedWallClockTimeInSLiM > 0.0)
	{
        double elapsedTime_first = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosEventFirst]);
		double elapsedTime_early = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosEventEarly]);
		double elapsedTime_late = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosEventLate]);
		double elapsedTime_initialize = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosInitializeCallback]);
		double elapsedTime_mutationEffect = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosMutationEffectCallback]);
		double elapsedTime_fitnessEffect = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosFitnessEffectCallback]);
		double elapsedTime_interaction = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosInteractionCallback]);
		double elapsedTime_matechoice = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosMateChoiceCallback]);
		double elapsedTime_modifychild = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosModifyChildCallback]);
		double elapsedTime_recombination = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosRecombinationCallback]);
		double elapsedTime_mutation = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosMutationCallback]);
		double elapsedTime_reproduction = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosReproductionCallback]);
        double elapsedTime_survival = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosSurvivalCallback]);
		double percent_first = (elapsedTime_first / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_early = (elapsedTime_early / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_late = (elapsedTime_late / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_initialize = (elapsedTime_initialize / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_fitness = (elapsedTime_mutationEffect / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_fitnessglobal = (elapsedTime_fitnessEffect / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_interaction = (elapsedTime_interaction / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_matechoice = (elapsedTime_matechoice / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_modifychild = (elapsedTime_modifychild / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_recombination = (elapsedTime_recombination / elapsedWallClockTimeInSLiM) * 100.0;
		double percent_mutation = (elapsedTime_mutation / elapsedWallClockTimeInSLiM) * 100.0;
        double percent_reproduction = (elapsedTime_reproduction / elapsedWallClockTimeInSLiM) * 100.0;
        double percent_survival = (elapsedTime_survival / elapsedWallClockTimeInSLiM) * 100.0;
		int fw = 4, fw2 = 4;
		
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_first));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_early));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_late));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_initialize));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_mutationEffect));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_fitnessEffect));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_interaction));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_matechoice));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_modifychild));
		fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_recombination));
        fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_mutation));
        fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_reproduction));
        fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_survival));
		
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_first));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_early));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_late));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_initialize));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_fitness));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_fitnessglobal));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_interaction));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_matechoice));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_modifychild));
		fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_recombination));
        fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_mutation));
        fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_reproduction));
        fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_survival));
		
		tc.insertText(" \n", optima13_d);
		tc.insertText("Callback type breakdown\n", optima14b_d);
		tc.insertText(" \n", optima3_d);
		
		// Note these are out of numeric order, but in cycle stage order
		if (community->ModelType() == SLiMModelType::kModelTypeWF)
		{
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_initialize, fw, 'f', 2).arg(percent_initialize, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : initialize() callbacks\n", optima13_d);
			
            tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_first, fw, 'f', 2).arg(percent_first, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : first() events\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_early, fw, 'f', 2).arg(percent_early, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : early() events\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_matechoice, fw, 'f', 2).arg(percent_matechoice, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : mateChoice() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_recombination, fw, 'f', 2).arg(percent_recombination, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : recombination() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_mutation, fw, 'f', 2).arg(percent_mutation, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : mutation() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_modifychild, fw, 'f', 2).arg(percent_modifychild, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : modifyChild() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_late, fw, 'f', 2).arg(percent_late, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : late() events\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_mutationEffect, fw, 'f', 2).arg(percent_fitness, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : mutationEffect() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_fitnessEffect, fw, 'f', 2).arg(percent_fitnessglobal, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : fitnessEffect() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_interaction, fw, 'f', 2).arg(percent_interaction, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : interaction() callbacks\n", optima13_d);
		}
		else
		{
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_initialize, fw, 'f', 2).arg(percent_initialize, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : initialize() callbacks\n", optima13_d);
			
            tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_first, fw, 'f', 2).arg(percent_first, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : first() events\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_reproduction, fw, 'f', 2).arg(percent_reproduction, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : reproduction() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_recombination, fw, 'f', 2).arg(percent_recombination, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : recombination() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_mutation, fw, 'f', 2).arg(percent_mutation, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : mutation() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_modifychild, fw, 'f', 2).arg(percent_modifychild, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : modifyChild() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_early, fw, 'f', 2).arg(percent_early, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : early() events\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_mutationEffect, fw, 'f', 2).arg(percent_fitness, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : mutationEffect() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_fitnessEffect, fw, 'f', 2).arg(percent_fitnessglobal, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : fitnessEffect() callbacks\n", optima13_d);
			
            tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_survival, fw, 'f', 2).arg(percent_survival, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : survival() callbacks\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_late, fw, 'f', 2).arg(percent_late, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : late() events\n", optima13_d);
			
			tc.insertText(QString("%1 s (%2%)").arg(elapsedTime_interaction, fw, 'f', 2).arg(percent_interaction, fw2, 'f', 2), menlo11_d);
			tc.insertText(" : interaction() callbacks\n", optima13_d);
		}
	}
	
	//
	//	Script block profiles
	//
	if (elapsedWallClockTimeInSLiM > 0.0)
	{
		{
			std::vector &script_blocks = community->AllScriptBlocks();
			
			// Convert the profile counts in all script blocks into self counts (excluding the counts of nodes below them)
			for (SLiMEidosBlock *script_block : script_blocks)
				if (script_block->type_ != SLiMEidosBlockType::SLiMEidosUserDefinedFunction)		// exclude function blocks; not user-visible
					script_block->root_node_->ConvertProfileTotalsToSelfCounts();
		}
		{
			tc.insertText(" \n", optima13_d);
			tc.insertText("Script block profiles (as a fraction of corrected wall clock time)\n", optima14b_d);
			tc.insertText(" \n", optima3_d);
			
			std::vector &script_blocks = community->AllScriptBlocks();
			bool firstBlock = true, hiddenInconsequentialBlocks = false;
			
			for (SLiMEidosBlock *script_block : script_blocks)
			{
				if (script_block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)
					continue;
				
				const EidosASTNode *profile_root = script_block->root_node_;
				double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts());	// relies on ConvertProfileTotalsToSelfCounts() being called above!
				double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0;
				
				if ((total_block_time >= 0.01) || (percent_block_time >= 0.01))
				{
					if (!firstBlock)
						tc.insertText(" \n \n", menlo11_d);
					firstBlock = false;
					
					const std::string &script_std_string = profile_root->token_->token_string_;
					QString script_string = QString::fromStdString(script_std_string);
					
					tc.insertText(QString("%1 s (%2%):\n").arg(total_block_time, 0, 'f', 2).arg(percent_block_time, 0, 'f', 2), menlo11_d);
					tc.insertText(" \n", optima3_d);
					
                    int colorBase = tc.position();
                    tc.insertText(script_string, menlo11_d);
                    colorScriptWithProfileCountsFromNode(profile_root, elapsedWallClockTimeInSLiM, profile_root->token_->token_UTF16_start_ - colorBase, doc, menlo11_d);
				}
				else
					hiddenInconsequentialBlocks = true;
			}
			
			if (hiddenInconsequentialBlocks)
			{
				tc.insertText(" \n", menlo11_d);
				tc.insertText(" \n", optima3_d);
				tc.insertText("(blocks using < 0.01 s and < 0.01% of total wall clock time are not shown)", optima13i_d);
			}
		}
		{
			tc.insertText(" \n", menlo11_d);
			tc.insertText(" \n", optima13_d);
			tc.insertText("Script block profiles (as a fraction of within-block wall clock time)\n", optima14b_d);
			tc.insertText(" \n", optima3_d);
			
			std::vector &script_blocks = community->AllScriptBlocks();
			bool firstBlock = true, hiddenInconsequentialBlocks = false;
			
			for (SLiMEidosBlock *script_block : script_blocks)
			{
				if (script_block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)
					continue;
				
				const EidosASTNode *profile_root = script_block->root_node_;
				double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts());	// relies on ConvertProfileTotalsToSelfCounts() being called above!
				double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0;
				
				if ((total_block_time >= 0.01) || (percent_block_time >= 0.01))
				{
					if (!firstBlock)
						tc.insertText(" \n \n", menlo11_d);
					firstBlock = false;
					
					const std::string &script_std_string = profile_root->token_->token_string_;
                    QString script_string = QString::fromStdString(script_std_string);
					
					tc.insertText(QString("%1 s (%2%):\n").arg(total_block_time, 0, 'f', 2).arg(percent_block_time, 0, 'f', 2), menlo11_d);
					tc.insertText(" \n", optima3_d);
					
                    int colorBase = tc.position();
                    tc.insertText(script_string, menlo11_d);
                    if (total_block_time > 0.0)
                        colorScriptWithProfileCountsFromNode(profile_root, total_block_time, profile_root->token_->token_UTF16_start_ - colorBase, doc, menlo11_d);
				}
				else
					hiddenInconsequentialBlocks = true;
			}
			
			if (hiddenInconsequentialBlocks)
			{
				tc.insertText(" \n", menlo11_d);
				tc.insertText(" \n", optima3_d);
				tc.insertText("(blocks using < 0.01 s and < 0.01% of total wall clock time are not shown)", optima13i_d);
			}
		}
	}
	
	//
	//	User-defined functions (if any)
	//
	if (elapsedWallClockTimeInSLiM > 0.0)
	{
		EidosFunctionMap &function_map = community->FunctionMap();
		std::vector userDefinedFunctions;
		
		for (const auto &functionPairIter : function_map)
		{
			const EidosFunctionSignature *signature = functionPairIter.second.get();
			
			if (signature->body_script_ && signature->user_defined_)
			{
				signature->body_script_->AST()->ConvertProfileTotalsToSelfCounts();
				userDefinedFunctions.emplace_back(signature);
			}
		}
		
		if (userDefinedFunctions.size())
		{
			tc.insertText(" \n", menlo11_d);
			tc.insertText(" \n", optima13_d);
			tc.insertText("User-defined functions (as a fraction of corrected wall clock time)\n", optima14b_d);
			tc.insertText(" \n", optima3_d);
			
			bool firstBlock = true, hiddenInconsequentialBlocks = false;
			
			for (const EidosFunctionSignature *signature : userDefinedFunctions)
			{
				const EidosASTNode *profile_root = signature->body_script_->AST();
				double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts());	// relies on ConvertProfileTotalsToSelfCounts() being called above!
				double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0;
				
				if ((total_block_time >= 0.01) || (percent_block_time >= 0.01))
				{
					if (!firstBlock)
						tc.insertText(" \n \n", menlo11_d);
					firstBlock = false;
					
					const std::string &script_std_string = profile_root->token_->token_string_;
					QString script_string = QString::fromStdString(script_std_string);
					const std::string &&signature_string = signature->SignatureString();
					QString signatureString = QString::fromStdString(signature_string);
					
					tc.insertText(QString("%1 s (%2%):\n").arg(total_block_time, 0, 'f', 2).arg(percent_block_time, 0, 'f', 2), menlo11_d);
					tc.insertText(" \n", optima3_d);
					tc.insertText(signatureString + "\n", menlo11_d);
					
                    int colorBase = tc.position();
                    tc.insertText(script_string, menlo11_d);
                    colorScriptWithProfileCountsFromNode(profile_root, elapsedWallClockTimeInSLiM, profile_root->token_->token_UTF16_start_ - colorBase, doc, menlo11_d);
				}
				else
					hiddenInconsequentialBlocks = true;
			}
			
			if (hiddenInconsequentialBlocks)
			{
				tc.insertText(" \n", menlo11_d);
				tc.insertText(" \n", optima3_d);
				tc.insertText("(functions using < 0.01 s and < 0.01% of total wall clock time are not shown)", optima13i_d);
			}
		}
		if (userDefinedFunctions.size())
		{
			tc.insertText(" \n", menlo11_d);
			tc.insertText(" \n", optima13_d);
			tc.insertText("User-defined functions (as a fraction of within-block wall clock time)\n", optima14b_d);
			tc.insertText(" \n", optima3_d);
			
			bool firstBlock = true, hiddenInconsequentialBlocks = false;
			
			for (const EidosFunctionSignature *signature : userDefinedFunctions)
			{
				const EidosASTNode *profile_root = signature->body_script_->AST();
				double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts());	// relies on ConvertProfileTotalsToSelfCounts() being called above!
				double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0;
				
				if ((total_block_time >= 0.01) || (percent_block_time >= 0.01))
				{
					if (!firstBlock)
						tc.insertText(" \n \n", menlo11_d);
					firstBlock = false;
					
					const std::string &script_std_string = profile_root->token_->token_string_;
					QString script_string = QString::fromStdString(script_std_string);
					const std::string &&signature_string = signature->SignatureString();
					QString signatureString = QString::fromStdString(signature_string);
					
					tc.insertText(QString("%1 s (%2%):\n").arg(total_block_time, 0, 'f', 2).arg(percent_block_time, 0, 'f', 2), menlo11_d);
					tc.insertText(" \n", optima3_d);
					tc.insertText(signatureString + "\n", menlo11_d);
					
                    int colorBase = tc.position();
                    tc.insertText(script_string, menlo11_d);
                    if (total_block_time > 0.0)
                        colorScriptWithProfileCountsFromNode(profile_root, total_block_time, profile_root->token_->token_UTF16_start_ - colorBase, doc, menlo11_d);
				}
				else
					hiddenInconsequentialBlocks = true;
			}
			
			if (hiddenInconsequentialBlocks)
			{
				tc.insertText(" \n", menlo11_d);
				tc.insertText(" \n", optima3_d);
				tc.insertText("(functions using < 0.01 s and < 0.01% of total wall clock time are not shown)", optima13i_d);
			}
		}
	}
	
#if SLIM_USE_NONNEUTRAL_CACHES
	//
	//	MutationRun metrics, presented per Species
	//
    for (Species *focal_species : community->all_species_)
	{
        tc.insertText(" \n", menlo11_d);
		tc.insertText(" \n", optima13_d);
		tc.insertText("MutationRun usage", optima14b_d);
        if (community->all_species_.size() > 1)
        {
            tc.insertText(" (", optima14b_d);
            tc.insertText(QString::fromStdString(focal_species->avatar_), optima14b_d);
            tc.insertText(" ", optima14b_d);
            tc.insertText(QString::fromStdString(focal_species->name_), optima14b_d);
            tc.insertText(")", optima14b_d);
        }
        tc.insertText("\n", optima14b_d);
		tc.insertText(" \n", optima3_d);
		
        if (!focal_species->HasGenetics())
        {
            tc.insertText("(omitted for no-genetics species)", optima13i_d);
            continue;
        }
        
        {
            int64_t regime_tallies[3];
            int64_t regime_tallies_total = static_cast(focal_species->profile_nonneutral_regime_history_.size());
            
            for (int regime = 0; regime < 3; ++regime)
                regime_tallies[regime] = 0;
            
            for (int32_t regime : focal_species->profile_nonneutral_regime_history_)
                if ((regime >= 1) && (regime <= 3))
                    regime_tallies[regime - 1]++;
                else
                    regime_tallies_total--;
            
            for (int regime = 0; regime < 3; ++regime)
            {
                tc.insertText(QString("%1%").arg((regime_tallies[regime] / static_cast(regime_tallies_total)) * 100.0, 6, 'f', 2), menlo11_d);
                tc.insertText(QString(" of ticks : regime %1 (%2)\n").arg(regime + 1).arg(regime == 0 ? "no mutationEffect() callbacks" : (regime == 1 ? "constant neutral mutationEffect() callbacks only" : "unpredictable mutationEffect() callbacks present")), optima13_d);
            }
            
            tc.insertText(" \n", optima8_d);
        }
		
        tc.insertText(QString("%1").arg(focal_species->profile_max_mutation_index_), menlo11_d);
		tc.insertText(" maximum simultaneous mutations\n", optima13_d);
        
        
        const std::vector &chromosomes = focal_species->Chromosomes();
		
		for (Chromosome *focal_chromosome : chromosomes)
		{
            tc.insertText(" \n", optima13_d);
            tc.insertText("Chromosome ", optima13i_d);
            tc.insertText(QString::fromStdString(focal_chromosome->Symbol()), optima13i_d);
            tc.insertText(":\n", optima13i_d);
            tc.insertText(" \n", optima3_d);
            
            {
                int64_t power_tallies[20];	// we only go up to 1024 mutruns right now, but this gives us some headroom
                int64_t power_tallies_total = static_cast(focal_chromosome->profile_mutcount_history_.size());
                
                for (int power = 0; power < 20; ++power)
                    power_tallies[power] = 0;
                
                for (int32_t count : focal_chromosome->profile_mutcount_history_)
                {
                    int power = static_cast(round(log2(count)));
                    
                    power_tallies[power]++;
                }
                
                for (int power = 0; power < 20; ++power)
                {
                    if (power_tallies[power] > 0)
                    {
                        tc.insertText(QString("%1%").arg((power_tallies[power] / static_cast(power_tallies_total)) * 100.0, 6, 'f', 2), menlo11_d);
                        tc.insertText(QString(" of ticks : %1 mutation runs per haplosome\n").arg(static_cast(round(pow(2.0, power)))), optima13_d);
                    }
                }
            }
            
            tc.insertText(" \n", optima8_d);
            
            tc.insertText(QString("%1").arg(focal_chromosome->profile_mutation_total_usage_), menlo11_d);
            tc.insertText(" mutations referenced, summed across all ticks\n", optima13_d);
            
            tc.insertText(QString("%1").arg(focal_chromosome->profile_nonneutral_mutation_total_), menlo11_d);
            tc.insertText(" mutations considered potentially nonneutral\n", optima13_d);
            
            tc.insertText(QString("%1%").arg(((focal_chromosome->profile_mutation_total_usage_ - focal_chromosome->profile_nonneutral_mutation_total_) / static_cast(focal_chromosome->profile_mutation_total_usage_)) * 100.0, 0, 'f', 2), menlo11_d);
            tc.insertText(" of mutations excluded from fitness calculations\n", optima13_d);
            
            
            tc.insertText(" \n", optima8_d);
            
            tc.insertText(QString("%1").arg(focal_chromosome->profile_mutrun_total_usage_), menlo11_d);
            tc.insertText(" mutation runs referenced, summed across all ticks\n", optima13_d);
            
            tc.insertText(QString("%1").arg(focal_chromosome->profile_unique_mutrun_total_), menlo11_d);
            tc.insertText(" unique mutation runs maintained among those\n", optima13_d);
            
            tc.insertText(QString("%1%").arg((focal_chromosome->profile_mutrun_nonneutral_recache_total_ / static_cast(focal_chromosome->profile_unique_mutrun_total_)) * 100.0, 6, 'f', 2), menlo11_d);
            tc.insertText(" of mutation run nonneutral caches rebuilt per tick\n", optima13_d);
            
            tc.insertText(QString("%1%").arg(((focal_chromosome->profile_mutrun_total_usage_ - focal_chromosome->profile_unique_mutrun_total_) / static_cast(focal_chromosome->profile_mutrun_total_usage_)) * 100.0, 6, 'f', 2), menlo11_d);
            tc.insertText(" of mutation runs shared among haplosomes\n", optima13_d);
        }
	}
#endif
	
	{
		//
		//	Memory usage metrics
		//
        SLiMMemoryUsage_Community &mem_tot_C = community->profile_total_memory_usage_Community;
		SLiMMemoryUsage_Species &mem_tot_S = community->profile_total_memory_usage_AllSpecies;
		SLiMMemoryUsage_Community &mem_last_C = community->profile_last_memory_usage_Community;
		SLiMMemoryUsage_Species &mem_last_S = community->profile_last_memory_usage_AllSpecies;
		uint64_t div = static_cast(community->total_memory_tallies_);
		double ddiv = community->total_memory_tallies_;
        double average_total = (mem_tot_C.totalMemoryUsage + mem_tot_S.totalMemoryUsage) / ddiv;
		double final_total = mem_last_C.totalMemoryUsage + mem_last_S.totalMemoryUsage;
		
		tc.insertText(" \n", optima13_d);
		tc.insertText("SLiM memory usage (average / final tick)\n", optima14b_d);
		tc.insertText(" \n", optima3_d);
		
        QTextCharFormat colored_menlo = menlo11_d;
        
		// Chromosome
		tc.insertText(attributedStringForByteCount(mem_tot_S.chromosomeObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.chromosomeObjects, final_total, colored_menlo), colored_menlo);
        tc.insertText(QString(" : Chromosome objects (%1 / %2)\n").arg(mem_tot_S.chromosomeObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.chromosomeObjects_count), optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.chromosomeMutationRateMaps / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.chromosomeMutationRateMaps, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : mutation rate maps\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.chromosomeRecombinationRateMaps / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.chromosomeRecombinationRateMaps, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : recombination rate maps\n", optima13_d);

		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.chromosomeAncestralSequence / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.chromosomeAncestralSequence, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : ancestral nucleotides\n", optima13_d);
		
        // Community
        tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_C.communityObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_C.communityObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : Community object\n", optima13_d);
		
		// Haplosome
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.haplosomeObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.haplosomeObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : Haplosome objects (%1 / %2)\n").arg(mem_tot_S.haplosomeObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.haplosomeObjects_count), optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.haplosomeExternalBuffers / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.haplosomeExternalBuffers, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : external MutationRun* buffers\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.haplosomeUnusedPoolSpace / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.haplosomeUnusedPoolSpace, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : unused pool space\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.haplosomeUnusedPoolBuffers / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.haplosomeUnusedPoolBuffers, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : unused pool buffers\n", optima13_d);
		
		// GenomicElement
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.genomicElementObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.genomicElementObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : GenomicElement objects (%1 / %2)\n").arg(mem_tot_S.genomicElementObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.genomicElementObjects_count), optima13_d);
		
		// GenomicElementType
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.genomicElementTypeObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.genomicElementTypeObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : GenomicElementType objects (%1 / %2)\n").arg(mem_tot_S.genomicElementTypeObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.genomicElementTypeObjects_count), optima13_d);
		
		// Individual
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.individualObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.individualObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : Individual objects (%1 / %2)\n").arg(mem_tot_S.individualObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.individualObjects_count), optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.individualHaplosomeVectors / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.individualHaplosomeVectors, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : external Haplosome* buffers\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.individualJunkyardAndHaplosomes / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.individualJunkyardAndHaplosomes, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : individuals awaiting reuse\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.individualUnusedPoolSpace / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.individualUnusedPoolSpace, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : unused pool space\n", optima13_d);
		
		// InteractionType
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_C.interactionTypeObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_C.interactionTypeObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : InteractionType objects (%1 / %2)\n").arg(mem_tot_C.interactionTypeObjects_count / ddiv, 0, 'f', 2).arg(mem_last_C.interactionTypeObjects_count), optima13_d);
		
		if (mem_tot_C.interactionTypeObjects_count || mem_last_C.interactionTypeObjects_count)
		{
			tc.insertText("   ", menlo11_d);
			tc.insertText(attributedStringForByteCount(mem_tot_C.interactionTypeKDTrees / div, average_total, colored_menlo), colored_menlo);
			tc.insertText(" / ", optima13_d);
			tc.insertText(attributedStringForByteCount(mem_last_C.interactionTypeKDTrees, final_total, colored_menlo), colored_menlo);
			tc.insertText(" : k-d trees\n", optima13_d);
			
			tc.insertText("   ", menlo11_d);
			tc.insertText(attributedStringForByteCount(mem_tot_C.interactionTypePositionCaches / div, average_total, colored_menlo), colored_menlo);
			tc.insertText(" / ", optima13_d);
			tc.insertText(attributedStringForByteCount(mem_last_C.interactionTypePositionCaches, final_total, colored_menlo), colored_menlo);
			tc.insertText(" : position caches\n", optima13_d);
			
			tc.insertText("   ", menlo11_d);
			tc.insertText(attributedStringForByteCount(mem_tot_C.interactionTypeSparseVectorPool / div, average_total, colored_menlo), colored_menlo);
			tc.insertText(" / ", optima13_d);
			tc.insertText(attributedStringForByteCount(mem_last_C.interactionTypeSparseVectorPool, final_total, colored_menlo), colored_menlo);
			tc.insertText(" : sparse arrays\n", optima13_d);
		}
		
		// Mutation
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.mutationObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.mutationObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : Mutation objects (%1 / %2)\n").arg(mem_tot_S.mutationObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.mutationObjects_count), optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_C.mutationRefcountBuffer / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_C.mutationRefcountBuffer, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : refcount buffer\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_C.mutationUnusedPoolSpace / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_C.mutationUnusedPoolSpace, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : unused pool space\n", optima13_d);
		
		// MutationRun
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.mutationRunObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.mutationRunObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : MutationRun objects (%1 / %2)\n").arg(mem_tot_S.mutationRunObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.mutationRunObjects_count), optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.mutationRunExternalBuffers / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.mutationRunExternalBuffers, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : external MutationIndex buffers\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.mutationRunNonneutralCaches / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.mutationRunNonneutralCaches, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : nonneutral mutation caches\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.mutationRunUnusedPoolSpace / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.mutationRunUnusedPoolSpace, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : unused pool space\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.mutationRunUnusedPoolBuffers / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.mutationRunUnusedPoolBuffers, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : unused pool buffers\n", optima13_d);
		
		// MutationType
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.mutationTypeObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.mutationTypeObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : MutationType objects (%1 / %2)\n").arg(mem_tot_S.mutationTypeObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.mutationTypeObjects_count), optima13_d);
		
		// Species
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.speciesObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.speciesObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : Species objects\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.speciesTreeSeqTables / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.speciesTreeSeqTables, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : tree-sequence tables\n", optima13_d);
		
		// Subpopulation
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.subpopulationObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.subpopulationObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : Subpopulation objects (%1 / %2)\n").arg(mem_tot_S.subpopulationObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.subpopulationObjects_count), optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.subpopulationFitnessCaches / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.subpopulationFitnessCaches, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : fitness caches\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.subpopulationParentTables / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.subpopulationParentTables, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : parent tables\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.subpopulationSpatialMaps / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.subpopulationSpatialMaps, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : spatial maps\n", optima13_d);
		
		if (mem_tot_S.subpopulationSpatialMapsDisplay || mem_last_S.subpopulationSpatialMapsDisplay)
		{
			tc.insertText("   ", menlo11_d);
			tc.insertText(attributedStringForByteCount(mem_tot_S.subpopulationSpatialMapsDisplay / div, average_total, colored_menlo), colored_menlo);
			tc.insertText(" / ", optima13_d);
			tc.insertText(attributedStringForByteCount(mem_last_S.subpopulationSpatialMapsDisplay, final_total, colored_menlo), colored_menlo);
			tc.insertText(" : spatial map display (SLiMgui only)\n", optima13_d);
		}
		
		// Substitution
		tc.insertText(" \n", optima8_d);
		tc.insertText(attributedStringForByteCount(mem_tot_S.substitutionObjects / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_S.substitutionObjects, final_total, colored_menlo), colored_menlo);
		tc.insertText(QString(" : Substitution objects (%1 / %2)\n").arg(mem_tot_S.substitutionObjects_count / ddiv, 0, 'f', 2).arg(mem_last_S.substitutionObjects_count), optima13_d);
		
		// Eidos
		tc.insertText(" \n", optima8_d);
		tc.insertText("Eidos:\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_C.eidosASTNodePool / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_C.eidosASTNodePool, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : EidosASTNode pool\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_C.eidosSymbolTablePool / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_C.eidosSymbolTablePool, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : EidosSymbolTable pool\n", optima13_d);
		
		tc.insertText("   ", menlo11_d);
		tc.insertText(attributedStringForByteCount(mem_tot_C.eidosValuePool / div, average_total, colored_menlo), colored_menlo);
		tc.insertText(" / ", optima13_d);
		tc.insertText(attributedStringForByteCount(mem_last_C.eidosValuePool, final_total, colored_menlo), colored_menlo);
		tc.insertText(" : EidosValue pool\n", optima13_d);
        
        tc.insertText("   ", menlo11_d);
        tc.insertText(attributedStringForByteCount(mem_tot_C.fileBuffers / div, average_total, colored_menlo), colored_menlo);
        tc.insertText(" / ", optima13_d);
        tc.insertText(attributedStringForByteCount(mem_last_C.fileBuffers, final_total, colored_menlo), colored_menlo);
        tc.insertText(" : File buffers", optima13_d);
    }
    
    // Done, show the window
    tc.setPosition(0);
    textEdit->setTextCursor(tc);
    profile_window->show();    
}

#endif	// (SLIMPROFILING == 1)


//
//  simulation play mechanics
//

void QtSLiMWindow::willExecuteScript(void)
{
    // Whenever we are about to execute script, we swap in our random number generator; at other times, gEidos_rng is NULL.
    // The goal here is to keep each SLiM window independent in its random number sequence.
    if (gEidos_RNG_Initialized)
        qDebug() << "eidosConsoleWindowControllerWillExecuteScript: gEidos_rng already set up!";

    if (!sim_RNG_initialized)
        qDebug() << "sim_RNG is not yet set up!";
    
	std::swap(sim_RNG, gEidos_RNG_SINGLE);
	std::swap(sim_RNG_initialized, gEidos_RNG_Initialized);

    // We also swap in the pedigree id and mutation id counters; each SLiMgui window is independent
    gSLiM_next_pedigree_id = sim_next_pedigree_id;
    gSLiM_next_mutation_id = sim_next_mutation_id;
    gEidosSuppressWarnings = sim_suppress_warnings;

    // Set the current directory to its value for this window
    errno = 0;
    int retval = chdir(sim_working_dir.c_str());

    if (retval == -1)
        qDebug() << "willExecuteScript: Unable to set the working directory to " << sim_working_dir.c_str() << " (error " << errno << ")";
}

void QtSLiMWindow::didExecuteScript(void)
{
    // Swap our random number generator back out again; see -eidosConsoleWindowControllerWillExecuteScript
	std::swap(sim_RNG, gEidos_RNG_SINGLE);
	std::swap(sim_RNG_initialized, gEidos_RNG_Initialized);

    // Swap out our pedigree id and mutation id counters; see -eidosConsoleWindowControllerWillExecuteScript
    // Setting to -100000 here is not necessary, but will maybe help find bugs...
    sim_next_pedigree_id = gSLiM_next_pedigree_id;
    gSLiM_next_pedigree_id = -100000;

    sim_next_mutation_id = gSLiM_next_mutation_id;
    gSLiM_next_mutation_id = -100000;

    sim_suppress_warnings = gEidosSuppressWarnings;
    gEidosSuppressWarnings = false;

    // Get the current working directory; each SLiM window has its own cwd, which may have been changed in script since ...WillExecuteScript:
    sim_working_dir = Eidos_CurrentDirectory();

    // Return to the app's working directory when not running SLiM/Eidos code
    std::string &app_cwd = qtSLiMAppDelegate->QtSLiMCurrentWorkingDirectory();
    errno = 0;
    int retval = chdir(app_cwd.c_str());

    if (retval == -1)
        qDebug() << "didExecuteScript: Unable to set the working directory to " << app_cwd.c_str() << " (error " << errno << ")";
}

bool QtSLiMWindow::runSimOneTick(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    // This method should always be used when calling out to run the simulation, because it swaps the correct random number
    // generator stuff in and out bracketing the call to RunOneTick().  This bracketing would need to be done around
    // any other call out to the simulation that caused it to use random numbers, too, such as subsample output.
    bool stillRunning = true;

    willExecuteScript();

    // We always take a start clock measurement, to tally elapsed time spent running the model
    clock_t startCPUClock = clock();
    
#if (SLIMPROFILING == 1)
	if (profilePlayOn_)
	{
		// We put the wall clock measurements on the inside since we want those to be maximally accurate,
		// as profile report percentages are fractions of the total elapsed wall clock time.
		SLIM_PROFILE_BLOCK_START();

		stillRunning = community->RunOneTick();

		SLIM_PROFILE_BLOCK_END(community->profile_elapsed_wall_clock);
	}
    else
#endif
    {
        stillRunning = community->RunOneTick();
    }
    
    // Take an end clock time to tally elapsed time spent running the model
    clock_t endCPUClock = clock();
    
    elapsedCPUClock_ += (endCPUClock - startCPUClock);
    
#if (SLIMPROFILING == 1)
	if (profilePlayOn_)
        community->profile_elapsed_CPU_clock += (endCPUClock - startCPUClock);
#endif
    
    didExecuteScript();

    // We also want to let graphViews know when each tick has finished, in case they need to pull data from the sim.  Note this
    // happens after every tick, not just when we are updating the UI, so drawing and setNeedsDisplay: should not happen here.
    emit controllerTickFinished();

    return stillRunning;
}

void QtSLiMWindow::_continuousPlay(void)
{
    // NOTE this code is parallel to the code in _continuousProfile()
	if (!invalidSimulation_)
	{
        QElapsedTimer playStartTimer;
        playStartTimer.start();
        
		double speedSliderValue = ui->playSpeedSlider->value() / 100.0;     // scale is 0 to 100, since only integer values are allowed by QSlider
		double intervalSinceStarting = continuousPlayElapsedTimer_.nsecsElapsed() / 1000000000.0;
		
		// Calculate frames per second; this equation must match the equation in playSpeedChanged:
		double maxTicksPerSecond = 1000000000.0;	// bounded, to allow -eidos_pauseExecution to interrupt us
		
		if (speedSliderValue < 0.99999)
			maxTicksPerSecond = (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * 839;
		
		//qDebug() << "speedSliderValue == " << speedSliderValue << ", maxTicksPerSecond == " << maxTicksPerSecond;
		
		// We keep a local version of reachedSimulationEnd, because calling setReachedSimulationEnd: every tick
		// can actually be a large drag for simulations that run extremely quickly – it can actually exceed the time
		// spent running the simulation itself!  Moral of the story, KVO is wicked slow.
		bool reachedEnd = reachedSimulationEnd_;
		
		do
		{
			if (continuousPlayTicksCompleted_ / intervalSinceStarting >= maxTicksPerSecond)
				break;
			
            if (tickPlayOn_ && (community->Tick() >= targetTick_))
                break;
            
            reachedEnd = !runSimOneTick();
			
			continuousPlayTicksCompleted_++;
		}
		while (!reachedEnd && (playStartTimer.nsecsElapsed() / 1000000000.0) < 0.02);
		
		setReachedSimulationEnd(reachedEnd);
		
		if (!reachedSimulationEnd_ && (!tickPlayOn_ || !(community->Tick() >= targetTick_)))
		{
            updateAfterTickFull((playStartTimer.nsecsElapsed() / 1000000000.0) > 0.04);
			continuousPlayInvocationTimer_.start(0);
		}
		else
		{
			// stop playing
			updateAfterTickFull(true);
            
            if (nonProfilePlayOn_)
                playOrProfile(PlayType::kNormalPlay);       // click the Play button
            else if (tickPlayOn_)
                playOrProfile(PlayType::kTickPlay);   // click the Play button
			
			// bounce our icon; if we are not the active app, to signal that the run is done
			//[NSApp requestUserAttention:NSInformationalRequest];
		}
	}
}

void QtSLiMWindow::_continuousProfile(void)
{
	// NOTE this code is parallel to the code in _continuousPlay()
	if (!invalidSimulation_)
	{
        QElapsedTimer playStartTimer;
        playStartTimer.start();
		
		// We keep a local version of reachedSimulationEnd, because calling setReachedSimulationEnd: every tick
		// can actually be a large drag for simulations that run extremely quickly – it can actually exceed the time
		// spent running the simulation itself!  Moral of the story, KVO is wicked slow.
		bool reachedEnd = reachedSimulationEnd_;
		
		if (!reachedEnd)
		{
			do
			{
                reachedEnd = !runSimOneTick();
				
				continuousPlayTicksCompleted_++;
			}
            while (!reachedEnd && (playStartTimer.nsecsElapsed() / 1000000000.0) < 0.02);
			
            setReachedSimulationEnd(reachedEnd);
		}
		
		if (!reachedSimulationEnd_)
		{
            updateAfterTickFull((playStartTimer.nsecsElapsed() / 1000000000.0) > 0.04);
            continuousProfileInvocationTimer_.start(0);
		}
		else
		{
			// stop profiling
            updateAfterTickFull(true);
			playOrProfile(PlayType::kProfilePlay);   // click the Profile button
			
			// bounce our icon; if we are not the active app, to signal that the run is done
			//[NSApp requestUserAttention:NSInformationalRequest];
		}
	}
}

void QtSLiMWindow::playOrProfile(PlayType playType)
{
#if DEBUG
	if (playType == PlayType::kProfilePlay)
	{
        ui->profileButton->setChecked(false);
        updateProfileButtonIcon(false);
		
        QMessageBox messageBox(this);
        messageBox.setText("Release build required");
        messageBox.setInformativeText("In order to obtain accurate timing information that is relevant to the actual runtime of a model, profiling requires that you are running a Release build of SLiMgui.");
        messageBox.setIcon(QMessageBox::Warning);
        
        // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion
        // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::ApplicationModal
        // seems necessary so floating windows can't be on top of the message box
        messageBox.setWindowModality(Qt::ApplicationModal);
        messageBox.exec();
		
		return;
	}
#endif
    
#if (SLIMPROFILING != 1)
	if (playType == PlayType::kProfilePlay)
	{
        ui->profileButton->setChecked(false);
        updateProfileButtonIcon(false);
		
        QMessageBox messageBox(this);
        messageBox.setText("Profiling disabled");
        messageBox.setInformativeText("Profiling has been disabled in this build of SLiMgui.  Please change the definition of SLIMPROFILING to 1 in the project's .pro files.");
        messageBox.setIcon(QMessageBox::Warning);
        
        // see https://forum.qt.io/topic/160751/error-panel-goes-underneath-floating-window-causing-confusion
        // regarding the choice between Qt::WindowModal and Qt::ApplicationModal; here Qt::ApplicationModal
        // seems necessary so floating windows can't be on top of the message box
        messageBox.setWindowModality(Qt::ApplicationModal);
        messageBox.exec();
		
		return;
	}
#endif
    
    if (!continuousPlayOn_)
	{
        // log information needed to track our play speed
        continuousPlayElapsedTimer_.restart();
		continuousPlayTicksCompleted_ = 0;
        
		setContinuousPlayOn(true);
		if (playType == PlayType::kProfilePlay)
            setProfilePlayOn(true);
        else if (playType == PlayType::kNormalPlay)
            setNonProfilePlayOn(true);
        else if (playType == PlayType::kTickPlay)
            setTickPlayOn(true);
		
		// keep the button on; this works for the button itself automatically, but when the menu item is chosen this is needed
		if (playType == PlayType::kProfilePlay)
		{
            ui->profileButton->setChecked(true);
            updateProfileButtonIcon(false);
		}
		else    // kNormalPlay and kTickPlay
		{
            ui->playButton->setChecked(true);
            updatePlayButtonIcon(false);
			//[self placeSubview:playButton aboveSubview:profileButton];
		}
		
		// invalidate the console symbols, and don't validate them until we are done
		if (consoleController)
            consoleController->invalidateSymbolTableAndFunctionMap();
		
#if (SLIMPROFILING == 1)
		// prepare profiling information if necessary
		if (playType == PlayType::kProfilePlay)
			community->StartProfiling();
#endif
		
		// start playing/profiling
		if (playType == PlayType::kProfilePlay)
            continuousProfileInvocationTimer_.start(0);
        else    // kNormalPlay and kTickPlay
            continuousPlayInvocationTimer_.start(0);
	}
	else
	{
#if (SLIMPROFILING == 1)
		// close out profiling information if necessary
		if ((playType == PlayType::kProfilePlay) && community && !invalidSimulation_)
			community->StopProfiling();
#endif
		
        // stop our recurring perform request
		if (playType == PlayType::kProfilePlay)
            continuousProfileInvocationTimer_.stop();
        else
            continuousPlayInvocationTimer_.stop();
		
        setContinuousPlayOn(false);
        if (playType == PlayType::kProfilePlay)
            setProfilePlayOn(false);
        else if (playType == PlayType::kNormalPlay)
            setNonProfilePlayOn(false);
        else if (playType == PlayType::kTickPlay)
            setTickPlayOn(false);
		
		// keep the button off; this works for the button itself automatically, but when the menu item is chosen this is needed
		if (playType == PlayType::kProfilePlay)
		{
            ui->profileButton->setChecked(false);
            updateProfileButtonIcon(false);
		}
		else    // kNormalPlay and kTickPlay
		{
            ui->playButton->setChecked(false);
            updatePlayButtonIcon(false);
			//[self placeSubview:profileButton aboveSubview:playButton];
		}
		
        // clean up and update UI
		if (consoleController)
            consoleController->validateSymbolTableAndFunctionMap();
        
		updateAfterTickFull(true);
		
#if (SLIMPROFILING == 1)
		// If we just finished profiling, display a report
		if ((playType == PlayType::kProfilePlay) && community && !invalidSimulation_)
			displayProfileResults();
#endif
	}
}

//
//	Eidos SLiMgui method forwards
//

void QtSLiMWindow::finish_eidos_pauseExecution(void)
{
	// this gets called by performSelectorOnMainThread: after _continuousPlay: has broken out of its loop
	// if the simulation has already ended, or is invalid, or is not in continuous play, it does nothing
	if (!invalidSimulation_ && !reachedSimulationEnd_ && continuousPlayOn_ && nonProfilePlayOn_ && !profilePlayOn_ && !tickPlayOn_)
	{
		playOrProfile(PlayType::kNormalPlay);	// this will simulate a press of the play button to stop continuous play
		
		// bounce our icon; if we are not the active app, to signal that the run is done
		//[NSApp requestUserAttention:NSInformationalRequest];
	}
}

EidosValue_SP QtSLiMWindow::eidos_logFileData(LogFile *logFile, EidosValue *column_value)
{
    // start by flushing any pending output to the debug output window
    updateOutputViews();
    
    // then fetch the data from the debug output window
    QtSLiMDebugOutputWindow *debugOutput = debugOutputWindow();
    
    if (column_value->Type() == EidosValueType::kValueInt)
        return debugOutput->dataForColumn(logFile, column_value->IntAtIndex_NOCAST(0, nullptr));
    else
        return debugOutput->dataForColumn(logFile, column_value->StringAtIndex_NOCAST(0, nullptr));
    
    return gStaticEidosValueNULL;
}

void QtSLiMWindow::eidos_openDocument(QString path)
{
    if (path.endsWith(".pdf", Qt::CaseInsensitive))
    {
        // Block opening PDFs; SLiMgui supported PDF but QtSLiM doesn't, so we should explicitly intercept and error out, otherwise we'll try to open the PDF as a SLiM model
		// FIXME: This shouldn't be using EIDOS_TERMINATION!
        EIDOS_TERMINATION << "ERROR (QtSLiMWindow::eidos_openDocument): opening PDF files is not supported in SLiMgui; using PNG instead is suggested." << EidosTerminate(nullptr);
    }
    
    qtSLiMAppDelegate->openFile(path, this);
}

void QtSLiMWindow::eidos_pauseExecution(void)
{
    if (!invalidSimulation_ && !reachedSimulationEnd_ && continuousPlayOn_ && nonProfilePlayOn_ && !profilePlayOn_ && !tickPlayOn_)
	{
		continuousPlayTicksCompleted_ = UINT64_MAX - 1;			// this will break us out of the loop in _continuousPlay: at the end of this tick
        
        QMetaObject::invokeMethod(this, "finish_eidos_pauseExecution", Qt::QueuedConnection);   // this will actually stop continuous play
	}
}

QtSLiMGraphView_CustomPlot *QtSLiMWindow::eidos_createPlot(QString title, double *x_range, double *y_range, QString x_label, QString y_label, double width, double height, bool horizontalGrid, bool verticalGrid, bool fullBox, double axisLabelSize, double tickLabelSize)
{
    QtSLiMGraphView *graphView = graphViewWithTitle(title);
    QtSLiMGraphView_CustomPlot *customPlot = nullptr;
    QWidget *graphWindow = nullptr;
    bool createdWindow = false;
    
    if (graphView)
    {
        customPlot = dynamic_cast(graphView);
        
        if (!customPlot)
            EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): a plot window exists with the given title, but it is not a custom plot window." << EidosTerminate(nullptr);
        
        graphWindow = graphView->window();
    }
    else
    {
        customPlot = new QtSLiMGraphView_CustomPlot(this, this);
        
        // width/height are 0 if they were NULL in the Eidos call; supply the default size here
        if (width == 0)
            width = 300;
        
        if (height == 0)
            height = 300;
        
        if ((width < 250) || (height < 250))
            EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires the window width and height to be at least 250 pixels." << EidosTerminate(nullptr);
        
        graphWindow = graphWindowWithView(customPlot, width, height);
        createdWindow = true;
    }
    
    if (customPlot && graphWindow)
    {
        // calling createPlot() resets the plot's state, creating it afresh even if it already exists
        customPlot->controllerRecycled();
        customPlot->setBorderless(false, 0, 0, 0, 0);
        customPlot->setTitle(title);
        customPlot->setXLabel(x_label);
        customPlot->setYLabel(y_label);
        customPlot->setDataRanges(x_range, y_range);
        customPlot->setShowHorizontalGrid(horizontalGrid);
        customPlot->setShowVerticalGrid(verticalGrid);
        customPlot->setShowFullBox(fullBox);
        
        if (axisLabelSize > 0)
            customPlot->setAxisLabelSize(axisLabelSize);
        
        if (tickLabelSize > 0)
            customPlot->setTickLabelSize(tickLabelSize);
        
        if (createdWindow)
            QtSLiMMakeWindowVisibleAndExposed(graphWindow);
		
		// BCH 11/16/2025: There is one tricky thing here, which is that in practice the plot window might not be allowed
		// to be the requested size, due to screen constraints.  We don't know that until we try.  Before the call to
		// the QtSLiMMakeWindowVisibleAndExposed() the window is still at the original size we requested (on macOS, at
		// least).  After that call, it has been constrained by whatever factors (screen size, dock/menubar, etc.) exist.
		// We can't do anything about those constraints; but we do want to try to preserve the original aspect ratio
		// requested by the user, and we emit a warning to the console.  See https://github.com/MesserLab/SLiM/issues/567
		double realized_width = customPlot->width(), realized_height = customPlot->height();
		double trim_width = graphWindow->width() - realized_width, trim_height = graphWindow->height() - realized_height;
		
		if ((realized_width != width) || (realized_height != height))
		{
			std::cout << "SLiMgui: the requested graph window size (" << width << ", " << height << ") was not attainable; the realized size was (" <<
						 realized_width << ", " << realized_height << ").  Resizing to try to preserve the requested aspect ratio." << std::endl;
			
			double requested_aspect_ratio = width / height;
			double realized_aspect_ratio = realized_width / realized_height;
			
			if (realized_aspect_ratio > requested_aspect_ratio)
			{
				// the width is, proportionally, larger than requested and needs to be reduced
				double corrected_width = std::round(requested_aspect_ratio * realized_height + trim_width);
				graphWindow->resize(corrected_width, graphWindow->height());
			}
			else if (realized_aspect_ratio < requested_aspect_ratio)
			{
				// the height is, proportionally, larger than requested and needs to be reduced
				double corrected_height = std::round(realized_width / requested_aspect_ratio + trim_height);
				graphWindow->resize(graphWindow->width(), corrected_height);
			}
			
			//std::cout << "   requested aspect ratio " << requested_aspect_ratio << "; final aspect ratio after correction " <<
			//          (customPlot->width() / (double)customPlot->height()) << std::endl;
		}
    }
    else
    {
        qApp->beep();
    }
    
    return customPlot;
}

QtSLiMGraphView_CustomPlot *QtSLiMWindow::eidos_plotWithTitle(QString title)
{
    QtSLiMGraphView *graphView = graphViewWithTitle(title);
    QtSLiMGraphView_CustomPlot *customPlot = nullptr;
    
    if (graphView)
    {
        customPlot = dynamic_cast(graphView);
        
        if (customPlot)
        {
            Plot *plotobj = customPlot->eidosPlotObject();
            
            if (plotobj)
                return customPlot;
        }
        
        EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_plotWithTitle): a plot window exists with the given title, but it is not a custom plot window created with createPlot()." << EidosTerminate(nullptr);
    }
    else
    {
        // when there is no plot window with the given title, it is not an error
        return nullptr;
    }
}

void QtSLiMWindow::plotLogFileData_1D(QString title, QString y_title, double *y_values, int data_count)
{
    // To plot logfile data, we call through to the same APIs as for Eidos-based plotting
    QtSLiMGraphView_CustomPlot *plot = eidos_createPlot(title, nullptr, nullptr, "time", y_title, 0, 0, -1, -1, -1, -1, -1);
    
    double *x_values = (double *)malloc(data_count * sizeof(double));
    for (int i = 0; i < data_count; ++i)
        x_values[i] = i;
    
    std::vector *color = new std::vector;
    color->emplace_back(255, 0, 0, 255);
    
    std::vector *alpha = new std::vector;
    alpha->push_back(1.0);
    
    std::vector *lwd = new std::vector;
    lwd->push_back(1.5);
    
    plot->addLineData(x_values, y_values, data_count, color, alpha, lwd);     // takes buffers from us
}

void QtSLiMWindow::plotLogFileData_2D(QString title, QString x_title, QString y_title, double *x_values, double *y_values, int data_count, bool makeScatterPlot)
{
    // To plot logfile data, we call through to the same APIs as for Eidos-based plotting
    QtSLiMGraphView_CustomPlot *plot = eidos_createPlot(title, nullptr, nullptr, x_title, y_title, 0, 0, -1, -1, -1, -1, -1);
    
    std::vector *color = new std::vector;
    color->emplace_back(0, 0, 0, 255);
    
    std::vector *alpha = new std::vector;
    alpha->push_back(1.0);
    
    std::vector *lwd = new std::vector;
    lwd->push_back(1.0);
    
    if (makeScatterPlot)
    {
        std::vector *symbol = new std::vector;
        symbol->push_back(16);
        
        std::vector *border = new std::vector;
        border->emplace_back(0, 0, 0, 255);
        
        std::vector *size = new std::vector;
        size->push_back(0.5);
        
        plot->addPointData(x_values, y_values, data_count, symbol, color, border, alpha, lwd, size);      // takes buffers from us
    }
    else
    {
        plot->addLineData(x_values, y_values, data_count, color, alpha, lwd);                             // takes buffers from us
    }
}


//
//  change tracking and the recycle button
//

// Do our own tracking of the change count.  We do this so that we know whether the script is in
// the same state it was in when we last recycled, or has been changed.  If it has been changed,
// we add a highlight under the recycle button to suggest to the user that they might want to
// recycle to bring their changes into force.
void QtSLiMWindow::updateChangeCount(void) //:(NSDocumentChangeType)change
{
	//[super updateChangeCount:change];
	
	// Mask off flags in the high bits.  Apple is not explicit about this, but NSChangeDiscardable
	// is 256, and acts as a flag bit, so it seems reasonable to assume this for future compatibility.
//	NSDocumentChangeType maskedChange = (NSDocumentChangeType)(change & 0x00FF);
	
//	if ((maskedChange == NSChangeDone) || (maskedChange == NSChangeRedone))
		slimChangeCount++;
//	else if (maskedChange == NSChangeUndone)
//		slimChangeCount--;
	
    emit controllerChangeCountChanged(slimChangeCount);
}

bool QtSLiMWindow::changedSinceRecycle(void)
{
	return !(slimChangeCount == 0);
}

void QtSLiMWindow::resetSLiMChangeCount(void)
{
    slimChangeCount = 0;
    emit controllerChangeCountChanged(slimChangeCount);
}

// slot receiving the signal QPlainTextEdit::textChanged() from the script textedit
void QtSLiMWindow::scriptTexteditChanged(void)
{
    // Poke the change count.  In SLiMgui we get separate notification types for changes vs. undo/redo,
    // allowing us to know when the document has returned to a checkpoint state due to undo/redo, but
    // there seems to be no way to do that with Qt, so once we register a change, only recycling will
    // bring us back to the unchanged state.
    updateChangeCount();
}


//
//  public slots
//

void QtSLiMWindow::playOneStepClicked(void)
{
    if (!invalidSimulation_)
    {
        if (consoleController)
            consoleController->invalidateSymbolTableAndFunctionMap();
        
        setReachedSimulationEnd(!runSimOneTick());
        
        // BCH 5/7/2021: moved these two lines up here, above validateSymbolTableAndFunctionMap(), so that
        // updateAfterTickFull() calls checkForSimulationTermination() for us before we re-validate the
        // symbol table; this way if the simulation has hit an error the symbol table no longer contains
        // SLiM stuff in it.  I *think* this mirrors what happens when play, rather than step, is used.
        // Nevertheless, it might be a fragile change, so I'm leaving this comment to document the change.
        ui->tickLineEdit->clearFocus();
        updateAfterTickFull(true);
        
        if (consoleController)
            consoleController->validateSymbolTableAndFunctionMap();
    }
}

void QtSLiMWindow::_playOneStep(void)
{
    playOneStepClicked();
    
    if (!reachedSimulationEnd_)
    {
        playOneStepInvocationTimer_.start(350); // milliseconds
    }
    else
    {
        // stop playing
        playOneStepReleased();
    }
}

void QtSLiMWindow::playOneStepPressed(void)
{
    ui->playOneStepButton->qtslimSetHighlight(true);
    _playOneStep();
}

void QtSLiMWindow::playOneStepReleased(void)
{
    ui->playOneStepButton->qtslimSetHighlight(false);
    playOneStepInvocationTimer_.stop();
}

void QtSLiMWindow::tickChanged(void)
{
	if (!tickPlayOn_)
	{
		QString tickString = ui->tickLineEdit->text();
		
		// Special-case initialize(); we can never advance to it, since it is first, so we just validate it
		if (tickString == "initialize()")
		{
			if (community->Tick() != 0)
			{
				qApp->beep();
				updateTickCounter();
                ui->tickLineEdit->selectAll();
			}
			
			return;
		}
		
		// Get the integer value from the textfield, since it is not "initialize()"
		targetTick_ = SLiMClampToTickType(static_cast(tickString.toLongLong()));
		
		// make sure the requested tick is in range
		if (community->Tick() >= targetTick_)
		{
			if (community->Tick() > targetTick_)
            {
                qApp->beep();
                updateTickCounter();
                ui->tickLineEdit->selectAll();
			}
            
			return;
		}
		
		// get the first responder out of the tick textfield
        ui->tickLineEdit->clearFocus();
		
		// start playing
        playOrProfile(PlayType::kTickPlay);
	}
	else
	{
		// stop our recurring perform request; I don't think this is hit any more
        playOrProfile(PlayType::kTickPlay);
	}
}

void QtSLiMWindow::recycleClicked(void)
{
    // If the user has requested autosaves, act on that; these calls run modal, blocking panels
    if (!isZombieWindow_)
    {
        QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance();
        
        if (prefsNotifier.autosaveOnRecyclePref())
        {
            if (!isUntitled)
                saveFile(currentFile);
            else if (prefsNotifier.showSaveIfUntitledPref())
                saveAs();
            //else
            //    qApp->beep();
        }
    }
    
    // Now do the recycle
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    // Converting a QString to a std::string is surprisingly tricky: https://stackoverflow.com/a/4644922/2752221
    std::string utf8_script_string = ui->scriptTextEdit->toPlainText().toUtf8().constData();
    
    if (consoleController)
        consoleController->invalidateSymbolTableAndFunctionMap();
    
    clearOutputClicked();
    if (debugOutputWindow_)
        debugOutputWindow_->clearAllOutput();
    
    setScriptStringAndInitializeSimulation(utf8_script_string);
    
    if (consoleController)
        consoleController->validateSymbolTableAndFunctionMap();
    
    ui->tickLineEdit->clearFocus();
    elapsedCPUClock_ = 0;
    
    updateAfterTickFull(true);
    
    ui->scriptTextEdit->setPalette(ui->scriptTextEdit->qtslimStandardPalette());     // clear any error highlighting
    
    // A bit of playing with undo.  We want to break undo coalescing at the point of recycling, so that undo and redo stop
    // at the moment that we recycled.  Then we reset a change counter that we use to know if we have changed relative to
    // the recycle point, so we can highlight the recycle button to show that the executing script is out of date.
    //[scriptTextView breakUndoCoalescing];
    resetSLiMChangeCount();
    
    emit controllerRecycled();
}

void QtSLiMWindow::playSpeedChanged(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
	// We want our speed to be from the point when the slider changed, not from when play started
    continuousPlayElapsedTimer_.restart();
	continuousPlayTicksCompleted_ = 1;		// this prevents a new tick from executing every time the slider moves a pixel
	
	// This method is called whenever playSpeedSlider changes, continuously; we want to show the chosen speed in a tooltip-ish window
    double speedSliderValue = ui->playSpeedSlider->value() / 100.0;     // scale is 0 to 100, since only integer values are allowed by QSlider
	
	// Calculate frames per second; this equation must match the equation in _continuousPlay:
	double maxTicksPerSecond = static_cast(INFINITY);
	
	if (speedSliderValue < 0.99999)
		maxTicksPerSecond = (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * 839;
	
	// Make a tooltip label string
	QString fpsString("∞ fps");
	
	if (!std::isinf(maxTicksPerSecond))
	{
		if (maxTicksPerSecond < 1.0)
			fpsString = QString::asprintf("%.2f fps", maxTicksPerSecond);
		else if (maxTicksPerSecond < 10.0)
			fpsString = QString::asprintf("%.1f fps", maxTicksPerSecond);
		else
			fpsString = QString::asprintf("%.0f fps", maxTicksPerSecond);
		
		//qDebug() << "fps string: " << fpsString;
	}
    
    // Show the tooltip; wow, that was easy...
    QPoint widgetOrigin = ui->playSpeedSlider->mapToGlobal(QPoint());
    QPoint cursorPosition = QCursor::pos();
    QPoint tooltipPosition = QPoint(cursorPosition.x() - 2, widgetOrigin.y() - ui->playSpeedSlider->rect().height() - 8);
    QToolTip::showText(tooltipPosition, fpsString, ui->playSpeedSlider, QRect(), 1000000);  // 1000 seconds; taken down on mouseup automatically
}

void QtSLiMWindow::showDrawerClicked(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    if (!tablesDrawerController)
        tablesDrawerController = new QtSLiMTablesDrawer(this);
    
    // position it to the right of the main window, with the same height
    QRect windowRect = geometry();
    windowRect.setLeft(windowRect.left() + windowRect.width() + 9);
    windowRect.setRight(windowRect.left() + 200);   // the minimum in the nib is larger
    
    tablesDrawerController->setGeometry(windowRect);
    
    QtSLiMMakeWindowVisibleAndExposed(tablesDrawerController);
}

void QtSLiMWindow::chromosomeDisplayPopupButtonRunMenu(void)
{
    Species *displaySpecies = focalDisplaySpecies();
    
    // When the simulation is not valid and initialized, the context menu is disabled
	if (invalidSimulation_)
        return;
    
    QMenu contextMenu("chromdisplay_menu", this);
    
    if (!displaySpecies || !displaySpecies->HasGenetics() || (displaySpecies->Chromosomes().size() == 0))
	{
        // Just run a dummy menu explaining why the menu is not available
        QString reasonString;
        
        if (!displaySpecies)
            reasonString = "No focal species is selected";
        else if (!displaySpecies->HasGenetics())
            reasonString = "The focal species has no genetics";
        else if (displaySpecies->community_.tick_ == 0)
            reasonString = "The focal species has not initialized";
        else
            reasonString = "No chromosomes to display";     // not sure whether/when this occurs
        
        QAction *no_chroms = contextMenu.addAction(reasonString);
        no_chroms->setEnabled(false);
        
        QPoint mousePos = QCursor::pos();
        contextMenu.exec(mousePos);
        chromosomeDisplayReleased();
        return;
    }
    
    bool disableAll = false;
    QAction *chromDisplayAll = contextMenu.addAction("New Chromosome Display (all)");
    chromDisplayAll->setEnabled(!disableAll);
    
    contextMenu.addSeparator();
    
    const std::vector &chromosomes = displaySpecies->Chromosomes();
    
    for (Chromosome *chrom : chromosomes)
    {
        QString menuItemTitle = QString("New Chromosome Display (symbol '%1')").arg(QString::fromStdString(chrom->Symbol()));
        
        QAction *chromAction = contextMenu.addAction(menuItemTitle);
        
        chromAction->setData(QVariant::fromValue(chrom->ID()));      // we use the ID temporarily, to run the menu, but the symbol will be the actual key
        chromAction->setEnabled(!disableAll);
    }
    
    // Run the context menu synchronously
    QPoint mousePos = QCursor::pos();
    QAction *action = contextMenu.exec(mousePos);
    
    if (action && !invalidSimulation_)
    {
        isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
        displaySpecies = focalDisplaySpecies();     // might change while the menu is running...
        
        int chrom_id;               // not slim_chromosome_index_t since it can also be -1
        std::string chrom_symbol;
        QString windowTitle;
        
        if (action == chromDisplayAll)
        {
            chrom_id = -1;                      // -1 means "all"
            chrom_symbol = "";                  // ditto
            windowTitle = "Chromosome Display";
        }
        else
        {
            chrom_id = action->data().toInt();
            
            Chromosome *chrom = displaySpecies->ChromosomeFromID(chrom_id);
            chrom_symbol = chrom->Symbol();
            
            windowTitle = QString("Chromosome Display ('%1')").arg(QString::fromStdString(chrom_symbol));
        }
        
        QWidget *chromosomeDisplay = newChromosomeDisplay(chrom_symbol, windowTitle);
        
        if (chromosomeDisplay)
            QtSLiMMakeWindowVisibleAndExposed(chromosomeDisplay);
    }
    
    // This is not called by Qt, for some reason (nested tracking loops?), so we call it explicitly
    chromosomeDisplayReleased();
}

void QtSLiMWindow::showConsoleClicked(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    if (!consoleController)
    {
        qApp->beep();
        return;
    }
    
    QtSLiMMakeWindowVisibleAndExposed(consoleController);
}

void QtSLiMWindow::showBrowserClicked(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    if (!consoleController)
    {
        qApp->beep();
        return;
    }
    
    consoleController->showBrowserClicked();
}

void QtSLiMWindow::debugOutputClicked(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    if (!debugOutputWindow_)
    {
        qApp->beep();
        return;
    }
    
    stopDebugButtonFlash();
    
    QtSLiMMakeWindowVisibleAndExposed(debugOutputWindow_);
}

#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
// In some versions of Qt5, such as 5.9.5, QChar::FormFeed did not yet exist
#define Eidos_FormFeed 0x0C
#else
// In Qt6, QChar::FormFeed is the preferred symbol for this
#define Eidos_FormFeed QChar::FormFeed
#endif

void QtSLiMWindow::jumpToPopupButtonRunMenu(void)
{
    QPlainTextEdit *scriptTE = ui->scriptTextEdit;
    QString jumpScriptString = scriptTE->toPlainText();
    QByteArray utf8bytes = jumpScriptString.toUtf8();
	const char *cstr = utf8bytes.constData();
    bool failedParse = true;
    
    // Collect actions, with associated script positions
    std::vector> jumpActions;
    
    // First we scan for comments of the form /** comment */ or /// comment, which are taken to be section headers
    // BCH 10/13/2020: Now we exclude comments of the form /*** or ////, since they are not of the expected form, but are instead just somebody's fancy comment block
    // BCH 11/15/2021: Whoops, the previous change broke /***/ as a separator item; added back in as a special case
    if (cstr)
    {
        SLiMEidosScript script(cstr);
        
        script.Tokenize(true, true);            // make bad tokens as needed, keep nonsignificant tokens
        
        const std::vector &tokens = script.Tokens();
        size_t token_count = tokens.size();
        QString comment;
        
        for (size_t token_index = 0; token_index < token_count; ++token_index)
        {
            const EidosToken &token = tokens[token_index];
            
            if ((token.token_type_ == EidosTokenType::kTokenCommentLong) && (token.token_string_ == "/***/"))
            {
                comment = QString();
            }
            else if ((token.token_type_ == EidosTokenType::kTokenComment) && (token.token_string_.rfind("///", 0) == 0) && (token.token_string_.rfind("////", 0) != 0))
            {
                comment = QString::fromStdString(token.token_string_);
                comment = comment.mid(3);
            }
            else if (token.token_type_ == EidosTokenType::kTokenCommentLong && (token.token_string_.rfind("/**", 0) == 0) && (token.token_string_.rfind("/***", 0) != 0))
            {
                comment = QString::fromStdString(token.token_string_);
                comment = comment.mid(3, comment.length() - 5);
            }
            else
                continue;
            
            // Exclude comments that contain newlines and similar characters
            if ((comment.indexOf(QChar::LineFeed) != -1) ||
                    (comment.indexOf(Eidos_FormFeed) != -1) ||
                    (comment.indexOf(QChar::CarriageReturn) != -1) ||
                    (comment.indexOf(QChar::ParagraphSeparator) != -1) ||
                    (comment.indexOf(QChar::LineSeparator) != -1))
                continue;
            
            comment = comment.trimmed();
            comment = comment.replace("&", "&&");   // quote ampersands since Qt uses them as keyboard shortcut escapes
            
            int32_t comment_start = token.token_UTF16_start_;
            int32_t comment_end = token.token_UTF16_end_ + 1;
            QAction *jumpAction;
            
            if (comment.length() == 0)
            {
                jumpAction = new QAction("", scriptTE);
                jumpAction->setSeparator(true);
            }
            else
            {
                // we cannot handle within-text formatting, since Qt doesn't support it; just an overall style
                // this is supported only on these section header items; we can't do the formatting on script block items
                
                // handle # H1 to ###### H6 headers, used to set the font size; these cannot be nested
                int headerLevel = 3;    // 1/2 are bigger; 3 is "default" and has no effect; 4/5/6 are progressively smaller
                
                if (comment.startsWith("# "))
                {
                    headerLevel = 1;
                    comment = comment.mid(2);
                }
                else if (comment.startsWith("## "))
                {
                    headerLevel = 2;
                    comment = comment.mid(3);
                }
                else if (comment.startsWith("### "))
                {
                    headerLevel = 3;
                    comment = comment.mid(4);
                }
                else if (comment.startsWith("#### "))
                {
                    headerLevel = 4;
                    comment = comment.mid(5);
                }
                else if (comment.startsWith("##### "))
                {
                    headerLevel = 5;
                    comment = comment.mid(6);
                }
                else if (comment.startsWith("###### "))
                {
                    headerLevel = 6;
                    comment = comment.mid(7);
                }
                
                // handle **bold** and _italic_ markdown; these can be nested and all get eaten
                bool isBold = false, isItalic = false;
                bool sawStyleChange = false;
                
                do
                {
                    // loop until this stays false, so we handle nested styles
                    sawStyleChange = false;
                    
                    if (comment.startsWith("__") && comment.endsWith("__"))
                    {
                        isBold = true;
                        sawStyleChange = true;
                        comment = comment.mid(2, comment.length() - 4);
                    }
                    if (comment.startsWith("**") && comment.endsWith("**"))
                    {
                        isBold = true;
                        sawStyleChange = true;
                        comment = comment.mid(2, comment.length() - 4);
                    }
                    if (comment.startsWith("_") && comment.endsWith("_"))
                    {
                        isItalic = true;
                        sawStyleChange = true;
                        comment = comment.mid(1, comment.length() - 2);
                    }
                    if (comment.startsWith("*") && comment.endsWith("*"))
                    {
                        isItalic = true;
                        sawStyleChange = true;
                        comment = comment.mid(1, comment.length() - 2);
                    }
                }
                while (sawStyleChange);
                
                jumpAction = new QAction(comment);
                connect(jumpAction, &QAction::triggered, scriptTE, [scriptTE, comment_start, comment_end]() {
                    QTextCursor cursor = scriptTE->textCursor();
                    cursor.setPosition(comment_start, QTextCursor::MoveAnchor);
                    cursor.setPosition(comment_end, QTextCursor::KeepAnchor);
                    scriptTE->setTextCursor(cursor);
                    scriptTE->centerCursor();
                    QtSLiMFlashHighlightInTextEdit(scriptTE);
                });
                
                QFont action_font = jumpAction->font();
                if (isBold)
                    action_font.setBold(true);
                if (isItalic)
                    action_font.setItalic(true);
                if (headerLevel == 1)
                    action_font.setPointSizeF(action_font.pointSizeF() * 1.50);
                if (headerLevel == 2)
                    action_font.setPointSizeF(action_font.pointSizeF() * 1.25);
                //if (headerLevel == 3)
                //    action_font.setPointSizeF(action_font.pointSizeF() * 1.00);
                if (headerLevel == 4)
                    action_font.setPointSizeF(action_font.pointSizeF() * 0.96);
                if (headerLevel == 5)
                    action_font.setPointSizeF(action_font.pointSizeF() * 0.85);
                if (headerLevel == 6)
                    action_font.setPointSizeF(action_font.pointSizeF() * 0.75);
                jumpAction->setFont(action_font);
            }
            
            jumpActions.emplace_back(comment_start, jumpAction);
        }
    }
    
    // Figure out whether we have multispecies avatars, and thus want to use the "low brightness symbol" emoji for "ticks all" blocks.
    // This emoji provides nicely lined up spacing in the menu, and indicates "ticks all" clearly; seems better than nothing.  It would
    // be even better, perhaps, to have a spacer of emoji width, to make things line up without having a symbol displayed; unfortunately
    // such a spacer does not seem to exist.  https://stackoverflow.com/questions/66496671/is-there-a-blank-unicode-character-matching-emoji-width
    QString ticksAllAvatar;
    
    if (community && community->is_explicit_species_ && (community->all_species_.size() > 0))
    {
        bool hasAvatars = false;
        
        for (Species *species : community->all_species_)
            if (species->avatar_.length() > 0)
                hasAvatars = true;
        
        if (hasAvatars)
            ticksAllAvatar = QString::fromUtf8("\xF0\x9F\x94\x85");     // "low brightness symbol", https://www.compart.com/en/unicode/U+1F505
    }
    
    // Next we parse and get script blocks
    if (cstr)
    {
        SLiMEidosScript script(cstr);
        
        try {
            script.Tokenize(true, false);            // make bad tokens as needed, do not keep nonsignificant tokens
            script.ParseSLiMFileToAST(true);        // make bad nodes as needed (i.e. never raise, and produce a correct tree)
            
            // Extract SLiMEidosBlocks from the parse tree
            const EidosASTNode *root_node = script.AST();
            QString specifierAvatar;
            
            for (EidosASTNode *script_block_node : root_node->children_)
            {
                // handle species/ticks specifiers, which are identifier token nodes at the top level of the AST with one child
                if ((script_block_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (script_block_node->children_.size() == 1))
                {
                    EidosASTNode *specifierChild = script_block_node->children_[0];
                    std::string specifierSpeciesName = specifierChild->token_->token_string_;
                    Species *specifierSpecies = (community ? community->SpeciesWithName(specifierSpeciesName) : nullptr);
                    
                    if (specifierSpecies && specifierSpecies->avatar_.length())
                        specifierAvatar = QString::fromStdString(specifierSpecies->avatar_);
                    else if (!specifierSpecies && (specifierSpeciesName == "all"))
                        specifierAvatar = ticksAllAvatar;
                    
                    continue;
                }
                
                // Create the block and use it to find the string from the start of its declaration to the start of its code
                SLiMEidosBlock *new_script_block = new SLiMEidosBlock(script_block_node);
                int32_t decl_start = new_script_block->root_node_->token_->token_UTF16_start_;
                int32_t code_start = new_script_block->compound_statement_node_->token_->token_UTF16_start_;
                QString decl = jumpScriptString.mid(decl_start, code_start - decl_start);
                
                // Remove everything including and after the first newline
                if (decl.indexOf(QChar::LineFeed) != -1)
                    decl.truncate(decl.indexOf(QChar::LineFeed));
                if (decl.indexOf(Eidos_FormFeed) != -1)
                    decl.truncate(decl.indexOf(Eidos_FormFeed));
                if (decl.indexOf(QChar::CarriageReturn) != -1)
                    decl.truncate(decl.indexOf(QChar::CarriageReturn));
                if (decl.indexOf(QChar::ParagraphSeparator) != -1)
                    decl.truncate(decl.indexOf(QChar::ParagraphSeparator));
                if (decl.indexOf(QChar::LineSeparator) != -1)
                    decl.truncate(decl.indexOf(QChar::LineSeparator));
                
                // Extract a comment at the end and put it after a em-dash in the string
                int simpleCommentStart = decl.indexOf("//");
                int blockCommentStart = decl.indexOf("/*");
                QString comment;
                
                if ((simpleCommentStart != -1) && ((blockCommentStart == -1) || (simpleCommentStart < blockCommentStart)))
                {
                    // extract a simple comment
                    comment = decl.right(decl.length() - simpleCommentStart - 2);
                    decl.truncate(simpleCommentStart);
                }
                else if ((blockCommentStart != -1) && ((simpleCommentStart == -1) || (blockCommentStart < simpleCommentStart)))
                {
                    // extract a block comment
                    comment = decl.right(decl.length() - blockCommentStart - 2);
                    decl.truncate(blockCommentStart);
                    
                    int blockCommentEnd = comment.indexOf("*/");
                    
                    if (blockCommentEnd != -1)
                        comment.truncate(blockCommentEnd);
                }
                
                // Calculate the end of the declaration string; trim off whitespace at the end
                decl = decl.trimmed();
                
                int32_t decl_end = decl_start + decl.length();
                
                // Remove trailing whitespace, replace tabs with spaces, etc.
                decl = decl.simplified();
                comment = comment.trimmed();
                comment = comment.replace("&", "&&");   // quote ampersands since Qt uses them as keyboard shortcut escapes
                
                if (comment.length() > 0)
                    decl = decl + "  —  " + comment;
                
                // If a species/ticks specifier was previously seen that provides us with an avatar, prepend that
                if (specifierAvatar.length())
                {
                    decl = specifierAvatar + " " + decl;
                    specifierAvatar.clear();
                }
                
                // Make a menu item with the final string, and annotate it with the range to select
                QAction *jumpAction = new QAction(decl);
                
                connect(jumpAction, &QAction::triggered, scriptTE, [scriptTE, decl_start, decl_end]() {
                    QTextCursor cursor = scriptTE->textCursor();
                    cursor.setPosition(decl_start, QTextCursor::MoveAnchor);
                    cursor.setPosition(decl_end, QTextCursor::KeepAnchor);
                    scriptTE->setTextCursor(cursor);
                    scriptTE->centerCursor();
                    QtSLiMFlashHighlightInTextEdit(scriptTE);
                });
                
                jumpActions.emplace_back(decl_start, jumpAction);
                
                failedParse = false;
                
                delete new_script_block;
            }
        }
        catch (...)
        {
            // If a raise occurred during parsing, error message junk has been generated
            // that we need to clear out; we don't want it to be shown to the user
            gEidosTermination.clear();
            gEidosTermination.str("");
        }
    }
    
    QMenu contextMenu("jump_to_menu", this);
    
    if (failedParse || (jumpActions.size() == 0))
    {
        QAction *parseErrorItem = contextMenu.addAction("No symbols");
        parseErrorItem->setEnabled(false);
        
        // contextMenu never took ownership, so we need to dispose of allocated actions
        for (auto &jump_pair : jumpActions)
            delete jump_pair.second;
    }
    else
    {
        // sort the actions by position
        std::sort(jumpActions.begin(),
                  jumpActions.end(),
                  [](const std::pair &a1, const std::pair &a2) { return a1.first < a2.first; });
        
        // add them all to contextMenu, and give it ownership
        for (auto &jump_pair : jumpActions)
        {
            contextMenu.addAction(jump_pair.second);
            jump_pair.second->setParent(&contextMenu);
        }
    }
    
    // Run the context menu synchronously
    QPoint mousePos = QCursor::pos();
    contextMenu.exec(mousePos);
    
    // This is not called by Qt, for some reason (nested tracking loops?), so we call it explicitly
    jumpToPopupButtonReleased();
}

void QtSLiMWindow::setScriptBlockLabelTextFromSelection(void)
{
    // this does a subset of the parsing logic of QtSLiMWindow::jumpToPopupButtonRunMenu()
    // it is used to get the label text for the script block label, to the right of the Jump button
    QPlainTextEdit *scriptTE = ui->scriptTextEdit;
    QString curScriptString = scriptTE->toPlainText();
    QByteArray utf8bytes = curScriptString.toUtf8();
    const char *cstr = utf8bytes.constData();
    
    QTextCursor selection_cursor(scriptTE->textCursor());
    int selStart = selection_cursor.selectionStart();
    int selEnd = selection_cursor.selectionEnd();
    
    if (cstr)
    {
        // Figure out whether we have multispecies avatars, and thus want to use the "low brightness symbol" emoji for "ticks all" blocks.
        // This emoji provides nicely lined up spacing in the menu, and indicates "ticks all" clearly; seems better than nothing.  It would
        // be even better, perhaps, to have a spacer of emoji width, to make things line up without having a symbol displayed; unfortunately
        // such a spacer does not seem to exist.  https://stackoverflow.com/questions/66496671/is-there-a-blank-unicode-character-matching-emoji-width
        QString ticksAllAvatar;
        
        if (community && community->is_explicit_species_ && (community->all_species_.size() > 0))
        {
            bool hasAvatars = false;
            
            for (Species *species : community->all_species_)
                if (species->avatar_.length() > 0)
                    hasAvatars = true;
            
            if (hasAvatars)
                ticksAllAvatar = QString::fromUtf8("\xF0\x9F\x94\x85");     // "low brightness symbol", https://www.compart.com/en/unicode/U+1F505
        }
        
        SLiMEidosScript script(cstr);
        
        try {
            script.Tokenize(true, false);            // make bad tokens as needed, do not keep nonsignificant tokens
            script.ParseSLiMFileToAST(true);        // make bad nodes as needed (i.e. never raise, and produce a correct tree)
            
            // Extract SLiMEidosBlocks from the parse tree
            const EidosASTNode *root_node = script.AST();
            QString specifierAvatar;
            
            for (EidosASTNode *script_block_node : root_node->children_)
            {
                // handle species/ticks specifiers, which are identifier token nodes at the top level of the AST with one child
                if ((script_block_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (script_block_node->children_.size() == 1))
                {
                    EidosASTNode *specifierChild = script_block_node->children_[0];
                    std::string specifierSpeciesName = specifierChild->token_->token_string_;
                    Species *specifierSpecies = (community ? community->SpeciesWithName(specifierSpeciesName) : nullptr);
                    
                    if (specifierSpecies && specifierSpecies->avatar_.length())
                        specifierAvatar = QString::fromStdString(specifierSpecies->avatar_);
                    else if (!specifierSpecies && (specifierSpeciesName == "all"))
                        specifierAvatar = ticksAllAvatar;
                    
                    continue;
                }
                
                // Create the block and use it to find the string from the start of its declaration to the start of its code
                SLiMEidosBlock *new_script_block = new SLiMEidosBlock(script_block_node);
                int32_t decl_start = new_script_block->root_node_->token_->token_UTF16_start_;
                int32_t code_end = new_script_block->compound_statement_node_->token_->token_UTF16_end_;
                
                if ((selStart >= decl_start) && (selStart <= code_end) && (selEnd <= code_end + 2))     // +2 allows a selection through the end brace and one more character (typically a newline)
                {
                    int32_t code_start = new_script_block->compound_statement_node_->token_->token_UTF16_start_;
                    QString decl = curScriptString.mid(decl_start, code_start - decl_start);
                    
                    // Remove everything including and after the first newline
                    if (decl.indexOf(QChar::LineFeed) != -1)
                        decl.truncate(decl.indexOf(QChar::LineFeed));
                    if (decl.indexOf(Eidos_FormFeed) != -1)
                        decl.truncate(decl.indexOf(Eidos_FormFeed));
                    if (decl.indexOf(QChar::CarriageReturn) != -1)
                        decl.truncate(decl.indexOf(QChar::CarriageReturn));
                    if (decl.indexOf(QChar::ParagraphSeparator) != -1)
                        decl.truncate(decl.indexOf(QChar::ParagraphSeparator));
                    if (decl.indexOf(QChar::LineSeparator) != -1)
                        decl.truncate(decl.indexOf(QChar::LineSeparator));
                    
                    // Extract a comment at the end and put it after a em-dash in the string
                    int simpleCommentStart = decl.indexOf("//");
                    int blockCommentStart = decl.indexOf("/*");
                    QString comment;
                    
                    if ((simpleCommentStart != -1) && ((blockCommentStart == -1) || (simpleCommentStart < blockCommentStart)))
                    {
                        // extract a simple comment
                        comment = decl.right(decl.length() - simpleCommentStart - 2);
                        decl.truncate(simpleCommentStart);
                    }
                    else if ((blockCommentStart != -1) && ((simpleCommentStart == -1) || (blockCommentStart < simpleCommentStart)))
                    {
                        // extract a block comment
                        comment = decl.right(decl.length() - blockCommentStart - 2);
                        decl.truncate(blockCommentStart);
                        
                        int blockCommentEnd = comment.indexOf("*/");
                        
                        if (blockCommentEnd != -1)
                            comment.truncate(blockCommentEnd);
                    }
                    
                    // Calculate the end of the declaration string; trim off whitespace at the end
                    decl = decl.trimmed();
                    
                    // Remove trailing whitespace, replace tabs with spaces, etc.
                    decl = decl.simplified();
                    comment = comment.trimmed();
                    
                    if (comment.length() > 0)
                        decl = decl + "  —  " + comment;
                               
                               // If a species/ticks specifier was previously seen that provides us with an avatar, prepend that
                               if (specifierAvatar.length())
                        {
                            decl = specifierAvatar + " " + decl;
                            specifierAvatar.clear();
                        }
                    
                    delete new_script_block;
                        
                    ui->scriptBlockLabel->setText(decl);
                    return;
                }
                
                delete new_script_block;
            }
        }
        catch (...)
        {
            // If a raise occurred during parsing, error message junk has been generated
            // that we need to clear out; we don't want it to be shown to the user
            gEidosTermination.clear();
            gEidosTermination.str("");
        }
    }
    
    ui->scriptBlockLabel->setText(QString(""));
}

void QtSLiMWindow::clearOutputClicked(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    ui->outputTextEdit->setPlainText("");
}

void QtSLiMWindow::clearDebugPointsClicked(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    ui->scriptTextEdit->clearDebugPoints();
}

void QtSLiMWindow::dumpPopulationClicked(void)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    try
	{
        // BCH 3/6/2022: Note that the species cycle has been added here for SLiM 4, in keeping with SLiM's native output formats.
        Species *displaySpecies = focalDisplaySpecies();
        
        if (displaySpecies)
        {
            slim_tick_t species_cycle = displaySpecies->Cycle();
            
            // dump the population: output spatial positions and ages and tags if available, but not ancestral sequence or substitutions
			Individual::PrintIndividuals_SLiM(SLIM_OUTSTREAM, nullptr, 0, *displaySpecies, true, true, false, false, true, false, /* p_focal_chromosome */ nullptr);
            
            // dump fixed substitutions also; so the dump in SLiMgui is like outputFull() + outputFixedMutations()
            SLIM_OUTSTREAM << std::endl;
            SLIM_OUTSTREAM << "#OUT: " << community->tick_ << " " << species_cycle << " F " << std::endl;
            SLIM_OUTSTREAM << "Mutations:" << std::endl;
            
            for (unsigned int i = 0; i < displaySpecies->population_.substitutions_.size(); i++)
            {
                SLIM_OUTSTREAM << i << " ";
                displaySpecies->population_.substitutions_[i]->PrintForSLiMOutput_Tag(SLIM_OUTSTREAM);
            }
            
            // now send SLIM_OUTSTREAM to the output textview
            updateOutputViews();
        }
        else
        {
            // With no display species, including when on the "all" species tab, we just beep
            qApp->beep();
        }
	}
	catch (...)
	{
	}
}

void QtSLiMWindow::displayGraphClicked(void)
{
    // see QtSLiMWindow::graphPopupButtonRunMenu() for parallel code for the graph pop-up button
    QObject *object = sender();
    QAction *action = qobject_cast(object);
    
    if (action)
    {
        Species *displaySpecies = focalDisplaySpecies();
        
        if (action == ui->actionCreate_Haplotype_Plot_All)
        {
            if (!continuousPlayOn_ && displaySpecies && displaySpecies->population_.subpops_.size())
            {
                isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
                
                QtSLiMHaplotypeManager::CreateHaplotypePlot(chromosomeConfig, nullptr);
            }
            else
            {
                qApp->beep();
            }
        }
        else if (action == ui->actionCreate_Haplotype_Plot_Selected)
        {
            if (!continuousPlayOn_ && displaySpecies && displaySpecies->population_.subpops_.size() && (focalChromosome() != nullptr))
            {
                isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
                
                QtSLiMHaplotypeManager::CreateHaplotypePlot(chromosomeConfig, focalChromosome());
            }
            else
            {
                qApp->beep();
            }
        }
        else
        {
            QtSLiMGraphView *graphView = nullptr;
            
            if (displaySpecies)
            {
                if (action == ui->actionGraph_1D_Population_SFS)
                    graphView = new QtSLiMGraphView_1DPopulationSFS(this, this);
                else if (action == ui->actionGraph_1D_Sample_SFS)
                    graphView = new QtSLiMGraphView_1DSampleSFS(this, this);
                else if (action == ui->actionGraph_2D_Population_SFS)
                    graphView = new QtSLiMGraphView_2DPopulationSFS(this, this);
                else if (action == ui->actionGraph_2D_Sample_SFS)
                    graphView = new QtSLiMGraphView_2DSampleSFS(this, this);
                else if (action == ui->actionGraph_Mutation_Frequency_Trajectories)
                    graphView = new QtSLiMGraphView_FrequencyTrajectory(this, this);
                else if (action == ui->actionGraph_Mutation_Loss_Time_Histogram)
                    graphView = new QtSLiMGraphView_LossTimeHistogram(this, this);
                else if (action == ui->actionGraph_Mutation_Fixation_Time_Histogram)
                    graphView = new QtSLiMGraphView_FixationTimeHistogram(this, this);
                else if (action == ui->actionGraph_Population_Fitness_Distribution)
                    graphView = new QtSLiMGraphView_PopFitnessDist(this, this);
                else if (action == ui->actionGraph_Subpopulation_Fitness_Distributions)
                    graphView = new QtSLiMGraphView_SubpopFitnessDists(this, this);
                else if (action == ui->actionGraph_Fitness_Time)
                    graphView = new QtSLiMGraphView_FitnessOverTime(this, this);
                else if (action == ui->actionGraph_Age_Distribution)
                    graphView = new QtSLiMGraphView_AgeDistribution(this, this);
                else if (action == ui->actionGraph_Lifetime_Reproduce_Output)
                    graphView = new QtSLiMGraphView_LifetimeReproduction(this, this);
                else if (action == ui->actionGraph_Population_Size_Time)
                    graphView = new QtSLiMGraphView_PopSizeOverTime(this, this);
                else if (action == ui->actionGraph_Population_Visualization)
                    graphView = new QtSLiMGraphView_PopulationVisualization(this, this);
            }
            else if (action == ui->actionGraph_Multispecies_Population_Size_Time)
                graphView = new QtSLiMGraphView_MultispeciesPopSizeOverTime(this, this);
            
            if (graphView)
            {
                QWidget *graphWindow = graphWindowWithView(graphView);
                
                if (graphWindow)
                    QtSLiMMakeWindowVisibleAndExposed(graphWindow);
            }
            else
            {
                qApp->beep();
            }
        }
    }
}

static bool rectIsOnscreen(QRect windowRect)
{
    const QList screens = QGuiApplication::screens();
    
    for (QScreen *screen : screens)
    {
        QRect screenRect = screen->availableGeometry();
        
        if (screenRect.contains(windowRect, true))
            return true;
    }
    
    return false;
}

void QtSLiMWindow::positionNewSubsidiaryWindow(QWidget *p_window)
{
    // If all previous graph windows have been closed, reset our positioning.  This scheme could be much smarter;
    // we could actually try to avoid the specific rectangles covered by existing subsidiary windows.
    if (graphViewCount() == 0)
    {
        openedGraphCount_left = 0;
        openedGraphCount_right = 0;
        openedGraphCount_top = 0;
        openedGraphCount_bottom = 0;
    }
    
    // Force geometry calculation, which is lazy
    p_window->setAttribute(Qt::WA_DontShowOnScreen, true);
    p_window->show();
    p_window->hide();
    p_window->setAttribute(Qt::WA_DontShowOnScreen, false);
    
    // Now get the frame geometry; note that on X11 systems the window frame is often not included in frameGeometry(), even
    // though it's supposed to be, because it is simply not available from X.  We attempt to compensate by adding in the
    // height of the window title bar, although that entails making assumptions about the windowing system appearance.
    QRect windowFrame = p_window->frameGeometry();
    QRect mainWindowFrame = this->frameGeometry();
    bool drawerIsOpen = (!!tablesDrawerController);
    const int titleBarHeight = 30;
    QPoint unadjust;
    
    if (windowFrame == p_window->geometry())
    {
        windowFrame.adjust(0, -titleBarHeight, 0, 0);
        unadjust = QPoint(0, 30);
    }
    if (mainWindowFrame == this->geometry())
    {
        mainWindowFrame.adjust(0, -titleBarHeight, 0, 0);
    }
    
    // try along the bottom first
    {
        QRect candidateFrame = windowFrame;
        
        candidateFrame.moveLeft(mainWindowFrame.left() + openedGraphCount_bottom * (windowFrame.width() + 5));
        candidateFrame.moveTop(mainWindowFrame.bottom() + 5);
        
        if (rectIsOnscreen(candidateFrame) && (candidateFrame.right() <= mainWindowFrame.right()))          // avoid going over to the right, to leave room for the tables drawer window
        {
            p_window->move(candidateFrame.topLeft() + unadjust);
            openedGraphCount_bottom++;
            return;
        }
    }
    
    // try on the left side
    {
        QRect candidateFrame = windowFrame;
        
        candidateFrame.moveRight(mainWindowFrame.left() - 5);
        candidateFrame.moveTop(mainWindowFrame.top() + openedGraphCount_left * (windowFrame.height() + 5));
        
        if (rectIsOnscreen(candidateFrame)) // && (candidateFrame.bottom() <= mainWindowFrame.bottom()))    // doesn't overlap anybody else
        {
            p_window->move(candidateFrame.topLeft() + unadjust);
            openedGraphCount_left++;
            return;
        }
    }
    
    // try along the top
	{
		QRect candidateFrame = windowFrame;
		
		candidateFrame.moveLeft(mainWindowFrame.left() + openedGraphCount_top * (windowFrame.width() + 5));
		candidateFrame.moveBottom(mainWindowFrame.top() - 5);
		
        if (rectIsOnscreen(candidateFrame)) // && (candidateFrame.right() <= mainWindowFrame.right()))    // doesn't overlap anybody else
        {
            p_window->move(candidateFrame.topLeft() + unadjust);
            openedGraphCount_top++;
            return;
        }
	}
    
    // unless the drawer is open, let's try on the right side
	if (!drawerIsOpen)
	{
		QRect candidateFrame = windowFrame;
		
		candidateFrame.moveLeft(mainWindowFrame.right() + 5);
        candidateFrame.moveTop(mainWindowFrame.top() + openedGraphCount_right * (windowFrame.height() + 5));
		
        if (rectIsOnscreen(candidateFrame)) // && (candidateFrame.bottom() <= mainWindowFrame.bottom()))   // doesn't overlap anybody else
        {
            p_window->move(candidateFrame.topLeft() + unadjust);
            openedGraphCount_right++;
            return;
        }
	}
	
    // if the drawer is open, try to the right of it
    if (drawerIsOpen)
    {
        QRect drawerFrame = tablesDrawerController->frameGeometry();
        QRect candidateFrame = windowFrame;
		
		candidateFrame.moveLeft(drawerFrame.right() + 5);
        candidateFrame.moveTop(drawerFrame.top() + openedGraphCount_right * (windowFrame.height() + 5));
		
        if (rectIsOnscreen(candidateFrame)) // && (candidateFrame.bottom() <= drawerFrame.bottom()))    // doesn't overlap anybody else
        {
            p_window->move(candidateFrame.topLeft() + unadjust);
            openedGraphCount_right++;
            return;
        }
    }
	
	// if none of those worked, we just leave the window where it got placed out of the nib
}

QWidget *QtSLiMWindow::imageWindowWithPath(const QString &path)
{
    QImage image(path);
    QFileInfo fileInfo(path);
    
    // We have an image; note that it might be a "null image", however
    bool null = image.isNull();
    int window_width = (null ? 288 : image.width());
    int window_height = (null ? 288 : image.height());
    
    QWidget *image_window = new QWidget(this, Qt::Window | Qt::Tool);    // the image window has us as a parent, but is still a standalone window
    
    image_window->setWindowTitle(fileInfo.fileName());
    image_window->setFixedSize(window_width, window_height);
#ifdef __APPLE__
    // set the window icon only on macOS; on Linux it changes the app icon as a side effect
    image_window->setWindowIcon(qtSLiMAppDelegate->genericDocumentIcon());    // doesn't seem to quite work; we get the SLiM document icon, inherited from parent presumably
#endif
    image_window->setWindowFilePath(path);
    
    // Make the image view
    QLabel *imageView = new QLabel();
    
    imageView->setStyleSheet("QLabel { background-color : white; }");
    imageView->setBackgroundRole(QPalette::Base);
    imageView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
    imageView->setScaledContents(true);
    imageView->setAlignment(Qt::AlignCenter | Qt::AlignVCenter);
    
    if (null)
        imageView->setText("No image data");
    else
        imageView->setPixmap(QPixmap::fromImage(image));
    
    // Install imageView in the window
    QVBoxLayout *topLayout = new QVBoxLayout;
    
    image_window->setLayout(topLayout);
    topLayout->setContentsMargins(0, 0, 0, 0);
    topLayout->setSpacing(0);
    topLayout->addWidget(imageView);
    
    // Make a file system watcher to update us when the image changes
    QFileSystemWatcher *watcher = new QFileSystemWatcher(QStringList(path), image_window);
    
    connect(watcher, &QFileSystemWatcher::fileChanged, imageView, [imageView](const QString &watched_path) {
        QImage watched_image(watched_path);
        
        if (watched_image.isNull()) {
            imageView->setText("No image data");
        } else {
            imageView->setPixmap(QPixmap::fromImage(watched_image));
            imageView->window()->setFixedSize(watched_image.width(), watched_image.height());
        }
    });
    
    // Set up a context menu for copy/open
    QMenu *contextMenu = new QMenu("image_menu", imageView);
    contextMenu->addAction("Copy Image", this, [path]() {
        QImage watched_image(path);     // get the current image from the filesystem
        QClipboard *clipboard = QGuiApplication::clipboard();
        clipboard->setImage(watched_image);
    });
    contextMenu->addAction("Copy File Path", this, [path]() {
        QClipboard *clipboard = QGuiApplication::clipboard();
        clipboard->setText(path);
    });
    
    // Reveal in Finder / Show in Explorer: see https://stackoverflow.com/questions/3490336
    // Note there is no good solution on Linux, so we do "Open File" instead
    #if defined(Q_OS_MACOS)
    contextMenu->addAction("Reveal in Finder", this, [path]() {
        const QFileInfo fileInfo(path);
        QStringList scriptArgs;
        scriptArgs << QLatin1String("-e") << QString::fromLatin1("tell application \"Finder\" to reveal POSIX file \"%1\"").arg(fileInfo.canonicalFilePath());
        QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
        scriptArgs.clear();
        scriptArgs << QLatin1String("-e") << QLatin1String("tell application \"Finder\" to activate");
        QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
    });
    #elif defined(Q_WS_WIN)
    contextMenu->addAction("Show in Explorer", [path]() {
        const QFileInfo fileInfo(path);
        const FileName explorer = Environment::systemEnvironment().searchInPath(QLatin1String("explorer.exe"));
        if (explorer.isEmpty())
            qApp->beep();
        QStringList param;
        if (!fileInfo.isDir())
            param += QLatin1String("/select,");
        param += QDir::toNativeSeparators(fileInfo.canonicalFilePath());
        QProcess::startDetached(explorer.toString() + " " + param);
    });
    #else
    contextMenu->addAction("Open File", [path]() {
        QDesktopServices::openUrl(QUrl::fromLocalFile(path));
    });
    #endif
    
    imageView->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(imageView, &QLabel::customContextMenuRequested, imageView, [imageView, contextMenu](const QPoint &pos) {
        // Run the context menu if we have an image (in which case the text length is zero)
        if (imageView->text().length() == 0)
            contextMenu->exec(imageView->mapToGlobal(pos));
    });
    
    // Position the window nicely
    positionNewSubsidiaryWindow(image_window);
    
    // make window actions for all global menu items
    // we do NOT need to do this, because we use Qt::Tool; Qt will use our parent window's shortcuts
    //qtSLiMAppDelegate->addActionsForGlobalMenuItems(this);
    
    image_window->setAttribute(Qt::WA_DeleteOnClose, true);
    
    return image_window;
}

QWidget *QtSLiMWindow::graphWindowWithView(QtSLiMGraphView *graphView, double windowWidth, double windowHeight)
{
    isTransient = false;    // Since the user has taken an interest in the window, clear the document's transient status
    
    // Make a new window to show the graph
    QWidget *graph_window = new QWidget(this, Qt::Window | Qt::Tool);    // the graph window has us as a parent, but is still a standalone window
    QString title = graphView->graphTitle();
    
    graph_window->setWindowTitle(title);
    graph_window->setMinimumSize(250, 250);
    graph_window->resize(windowWidth, windowHeight);
#ifdef __APPLE__
    // set the window icon only on macOS; on Linux it changes the app icon as a side effect
    graph_window->setWindowIcon(QIcon());
#endif
    
    // Install graphView in the window
    QVBoxLayout *topLayout = new QVBoxLayout;
    
    graph_window->setLayout(topLayout);
    topLayout->setContentsMargins(0, 0, 0, 0);
    topLayout->setSpacing(0);
    topLayout->addWidget(graphView);
    
    // Add a horizontal layout at the bottom, for popup buttons and such added by the graph
    QHBoxLayout *buttonLayout = nullptr;
    
    {
        buttonLayout = new QHBoxLayout;
        
        buttonLayout->setContentsMargins(5, 5, 5, 5);
        buttonLayout->setSpacing(5);
        topLayout->addLayout(buttonLayout);
        
        QLabel *speciesLabel = new QLabel();
        speciesLabel->setText("");
        buttonLayout->addWidget(speciesLabel);
        speciesLabel->setHidden(true);
        
        QSpacerItem *rightSpacer = new QSpacerItem(16, 5, QSizePolicy::Expanding, QSizePolicy::Minimum);
        buttonLayout->addItem(rightSpacer);
        
        // this code is based on the creation of executeScriptButton in ui_QtSLiMEidosConsole.h
        QtSLiMPushButton *actionButton = new QtSLiMPushButton(graph_window);
        actionButton->setObjectName(QString::fromUtf8("actionButton"));
        actionButton->setMinimumSize(QSize(20, 20));
        actionButton->setMaximumSize(QSize(20, 20));
        actionButton->setFocusPolicy(Qt::NoFocus);
        QIcon icon4;
        icon4.addFile(QtSLiMImagePath("action", false), QSize(), QIcon::Normal, QIcon::Off);
        icon4.addFile(QtSLiMImagePath("action", true), QSize(), QIcon::Normal, QIcon::On);
        actionButton->setIcon(icon4);
        actionButton->setIconSize(QSize(20, 20));
        actionButton->qtslimSetBaseName("action");
        actionButton->setCheckable(true);
        actionButton->setFlat(true);
#if QT_CONFIG(tooltip)
        actionButton->setToolTip("

configure graph

"); #endif // QT_CONFIG(tooltip) buttonLayout->addWidget(actionButton); connect(actionButton, &QPushButton::pressed, graphView, [actionButton, graphView]() { actionButton->qtslimSetHighlight(true); graphView->actionButtonRunMenu(actionButton); }); connect(actionButton, &QPushButton::released, graphView, [actionButton]() { actionButton->qtslimSetHighlight(false); }); actionButton->setEnabled(!invalidSimulation() && (community->Tick() > 0)); } // Give the graph view a chance to do something with the window it's now in graphView->addedToWindow(); // force geometry calculation, which is lazy graph_window->setAttribute(Qt::WA_DontShowOnScreen, true); graph_window->show(); graph_window->hide(); graph_window->setAttribute(Qt::WA_DontShowOnScreen, false); // If we added a button layout, give it room so the graph area is still square // Note this has to happen after forcing layout calculations if (buttonLayout) { QSize contentSize = graph_window->size(); QSize minSize = graph_window->minimumSize(); int buttonLayoutHeight = buttonLayout->geometry().height(); contentSize.setHeight(contentSize.height() + buttonLayoutHeight); graph_window->resize(contentSize); minSize.setHeight(minSize.height() + buttonLayoutHeight); graph_window->setMinimumSize(minSize); } // Position the window nicely positionNewSubsidiaryWindow(graph_window); // make window actions for all global menu items // we do NOT need to do this, because we use Qt::Tool; Qt will use our parent window's shortcuts //qtSLiMAppDelegate->addActionsForGlobalMenuItems(graph_window); graph_window->setAttribute(Qt::WA_DeleteOnClose, true); graphView->updateSpeciesBadge(); return graph_window; } void QtSLiMWindow::graphPopupButtonRunMenu(void) { bool disableAll = false; Species *displaySpecies = focalDisplaySpecies(); // When the simulation is not valid and initialized, the context menu is disabled if (invalidSimulation_ || !displaySpecies) disableAll = true; QMenu contextMenu("graph_menu", this); QAction *graph1DFreqSpectrum = contextMenu.addAction("Graph 1D Population SFS"); graph1DFreqSpectrum->setEnabled(!disableAll); QAction *graph1DSampleSFS = contextMenu.addAction("Graph 1D Sample SFS"); graph1DSampleSFS->setEnabled(!disableAll); contextMenu.addSeparator(); QAction *graph2DFreqSpectrum = contextMenu.addAction("Graph 2D Population SFS"); graph2DFreqSpectrum->setEnabled(!disableAll); QAction *graph2DSampleSFS = contextMenu.addAction("Graph 2D Sample SFS"); graph2DSampleSFS->setEnabled(!disableAll); contextMenu.addSeparator(); QAction *graphMutFreqTrajectories = contextMenu.addAction("Graph Mutation Frequency Trajectories"); graphMutFreqTrajectories->setEnabled(!disableAll); QAction *graphMutLossTimeHist = contextMenu.addAction("Graph Mutation Loss Time Histogram"); graphMutLossTimeHist->setEnabled(!disableAll); QAction *graphMutFixTimeHist = contextMenu.addAction("Graph Mutation Fixation Time Histogram"); graphMutFixTimeHist->setEnabled(!disableAll); contextMenu.addSeparator(); QAction *graphPopFitnessDist = contextMenu.addAction("Graph Population Fitness Distribution"); graphPopFitnessDist->setEnabled(!disableAll); QAction *graphSubpopFitnessDists = contextMenu.addAction("Graph Subpopulation Fitness Distributions"); graphSubpopFitnessDists->setEnabled(!disableAll); QAction *graphFitnessVsTime = contextMenu.addAction("Graph Fitness ~ Time"); graphFitnessVsTime->setEnabled(!disableAll); contextMenu.addSeparator(); QAction *graphAgeDistribution = contextMenu.addAction("Graph Age Distribution"); graphAgeDistribution->setEnabled(!disableAll); QAction *graphLifetimeReproduction = contextMenu.addAction("Graph Lifetime Reproductive Output"); graphLifetimeReproduction->setEnabled(!disableAll); QAction *graphPopSizeVsTime = contextMenu.addAction("Graph Population Size ~ Time"); graphPopSizeVsTime->setEnabled(!disableAll); QAction *graphPopVisualization = contextMenu.addAction("Graph Population Visualization"); graphPopVisualization->setEnabled(!disableAll); contextMenu.addSeparator(); QAction *graphMultispeciesPopSizeVsTime = contextMenu.addAction("Multispecies Population Size ~ Time"); graphMultispeciesPopSizeVsTime->setEnabled(!invalidSimulation_); contextMenu.addSeparator(); // the haplotype plot menu items are a bit complicated bool haplotypePlotEnabled = !disableAll && displaySpecies && !continuousPlayOn_ && displaySpecies->population_.subpops_.size(); QAction *createHaplotypePlotAll = contextMenu.addAction("Create Haplotype Plot (all chromosomes)"); createHaplotypePlotAll->setEnabled(haplotypePlotEnabled); QAction *createHaplotypePlotOne = contextMenu.addAction("Create Haplotype Plot (selected chromosome)"); createHaplotypePlotOne->setEnabled(haplotypePlotEnabled && (focalChromosome() != nullptr)); if (haplotypePlotEnabled && (displaySpecies->Chromosomes().size() > 1)) { // enabled with 2+ chromosomes, we show both menu items createHaplotypePlotAll->setText("Create Haplotype Plot (all chromosomes)"); createHaplotypePlotOne->setVisible(true); } else { // disabled or with 0 or 1 chromosomes, we show only createHaplotypePlotAll createHaplotypePlotAll->setText("Create Haplotype Plot"); createHaplotypePlotOne->setVisible(false); } // Run the context menu synchronously QPoint mousePos = QCursor::pos(); QAction *action = contextMenu.exec(mousePos); if (action && !invalidSimulation_) { displaySpecies = focalDisplaySpecies(); // might change while the menu is running... if (action == createHaplotypePlotAll) { if (!continuousPlayOn_ && displaySpecies && displaySpecies->population_.subpops_.size()) { isTransient = false; // Since the user has taken an interest in the window, clear the document's transient status QtSLiMHaplotypeManager::CreateHaplotypePlot(chromosomeConfig, nullptr); } else { qApp->beep(); } } else if (action == createHaplotypePlotOne) { if (!continuousPlayOn_ && displaySpecies && displaySpecies->population_.subpops_.size() && (focalChromosome() != nullptr)) { isTransient = false; // Since the user has taken an interest in the window, clear the document's transient status QtSLiMHaplotypeManager::CreateHaplotypePlot(chromosomeConfig, focalChromosome()); } else { qApp->beep(); } } else { QtSLiMGraphView *graphView = nullptr; if (displaySpecies) { if (action == graph1DFreqSpectrum) graphView = new QtSLiMGraphView_1DPopulationSFS(this, this); if (action == graph1DSampleSFS) graphView = new QtSLiMGraphView_1DSampleSFS(this, this); if (action == graph2DFreqSpectrum) graphView = new QtSLiMGraphView_2DPopulationSFS(this, this); if (action == graph2DSampleSFS) graphView = new QtSLiMGraphView_2DSampleSFS(this, this); if (action == graphMutFreqTrajectories) graphView = new QtSLiMGraphView_FrequencyTrajectory(this, this); if (action == graphMutLossTimeHist) graphView = new QtSLiMGraphView_LossTimeHistogram(this, this); if (action == graphMutFixTimeHist) graphView = new QtSLiMGraphView_FixationTimeHistogram(this, this); if (action == graphPopFitnessDist) graphView = new QtSLiMGraphView_PopFitnessDist(this, this); if (action == graphSubpopFitnessDists) graphView = new QtSLiMGraphView_SubpopFitnessDists(this, this); if (action == graphFitnessVsTime) graphView = new QtSLiMGraphView_FitnessOverTime(this, this); if (action == graphAgeDistribution) graphView = new QtSLiMGraphView_AgeDistribution(this, this); if (action == graphLifetimeReproduction) graphView = new QtSLiMGraphView_LifetimeReproduction(this, this); if (action == graphPopSizeVsTime) graphView = new QtSLiMGraphView_PopSizeOverTime(this, this); if (action == graphPopVisualization) graphView = new QtSLiMGraphView_PopulationVisualization(this, this); } if (action == graphMultispeciesPopSizeVsTime) graphView = new QtSLiMGraphView_MultispeciesPopSizeOverTime(this, this); if (graphView) { QWidget *graphWindow = graphWindowWithView(graphView); if (graphWindow) QtSLiMMakeWindowVisibleAndExposed(graphWindow); } else { qApp->beep(); } } } // This is not called by Qt, for some reason (nested tracking loops?), so we call it explicitly graphPopupButtonReleased(); } QWidget *QtSLiMWindow::newChromosomeDisplay(std::string chromosome_symbol, QString windowTitle) { isTransient = false; // Since the user has taken an interest in the window, clear the document's transient status // Create a chromosome display window for the focal species; if the window is on the "all" tab // in a multispecies model, just return (we shouldn't be called in that case anyway). // FIXME there is a flaw in the whole design here -- none of this updates dynamically if the // user changes the number of chromosomes, the chromosome symbols, etc. The user will need // to close the display window and open a new one, if that happens; not the end of the world. // Fixing this would require a smarter controller object that rebuilds the whole content of // the display after a recycle-and-step to reflect whatever the initialize() process built. Species *species = focalDisplaySpecies(); if (!species) return nullptr; // Make a new window to show the chromosome display QWidget *display_window = new QWidget(this, Qt::Window | Qt::Tool); // the display window has us as a parent, but is still a standalone window display_window->setWindowTitle(windowTitle); #ifdef __APPLE__ // set the window icon only on macOS; on Linux it changes the app icon as a side effect display_window->setWindowIcon(QIcon()); #endif // Make a new layout for chromosome views and build the display inside it QVBoxLayout *topLayout = new QVBoxLayout; display_window->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(0); QtSLiMChromosomeWidgetController *displayController = new QtSLiMChromosomeWidgetController(this, display_window, species, chromosome_symbol); displayController->buildChromosomeDisplay(/* resetWindowSize */ true); // force geometry calculation, which is lazy display_window->setAttribute(Qt::WA_DontShowOnScreen, true); display_window->show(); display_window->hide(); display_window->setAttribute(Qt::WA_DontShowOnScreen, false); // Position the window nicely display_window->move(this->frameGeometry().topLeft() + QPoint(50, 50)); // make window actions for all global menu items // we do NOT need to do this, because we use Qt::Tool; Qt will use our parent window's shortcuts //qtSLiMAppDelegate->addActionsForGlobalMenuItems(display_window); display_window->setAttribute(Qt::WA_DeleteOnClose, true); return display_window; } void QtSLiMWindow::changeDirectoryClicked(void) { isTransient = false; // Since the user has taken an interest in the window, clear the document's transient status QFileDialog dialog(this); dialog.setAcceptMode(QFileDialog::AcceptOpen); dialog.setFileMode(QFileDialog::Directory); dialog.setViewMode(QFileDialog::List); dialog.setDirectory(QString::fromUtf8(sim_working_dir.c_str())); // FIXME could use QFileDialog::open() to get a sheet instead of an app-model panel... if (dialog.exec()) { QStringList fileNames = dialog.selectedFiles(); if (fileNames.size() == 1) { sim_working_dir = fileNames[0].toUtf8().constData(); sim_requested_working_dir = sim_working_dir; } } } void QtSLiMWindow::subpopSelectionDidChange(const QItemSelection & /* selected */, const QItemSelection & /* deselected */) { if (!invalidSimulation_ && !reloadingSubpopTableview) { QItemSelectionModel *selectionModel = ui->subpopTableView->selectionModel(); QModelIndexList selectedRows = selectionModel->selectedRows(); std::vector subpops = listedSubpopulations(); size_t subpopCount = subpops.size(); // first get the state of each row, for algorithmic convenience std::vector rowSelectedState(subpopCount, false); for (QModelIndex &modelIndex : selectedRows) rowSelectedState[static_cast(modelIndex.row())] = true; // then loop through subpops and update their selected state auto subpopIter = subpops.begin(); //bool all_selected = true; bool none_selected = true; for (size_t i = 0; i < subpopCount; ++i) { (*subpopIter)->gui_selected_ = rowSelectedState[i]; if ((*subpopIter)->gui_selected_) none_selected = false; //else // all_selected = false; subpopIter++; } // If the selection has changed, that means that our private mutation tallies need to be recomputed for (Species *species : community->AllSpecies()) { species->population_.InvalidateMutationReferencesCache(); // force a retally species->population_.TallyMutationReferencesAcrossPopulation_SLiMgui(); } // It's a bit hard to tell for sure whether we need to update or not, since a selected subpop might have been removed from the tableview; // selection changes should not happen often, so we can just always update, I think. ui->individualsWidget->update(); for (QtSLiMChromosomeWidget *zoomedWidget : chromosomeZoomedWidgets) zoomedWidget->update(); // was setNeedsDisplayInInterior, which would be more minimal // We don't want to allow an empty selection, maybe; if we are now in that state, and there are subpops to select, select them all // See also updateAfterTickFull() which also needs to do this if (none_selected && subpops.size()) ui->subpopTableView->selectAll(); } } ================================================ FILE: QtSLiM/QtSLiMWindow.h ================================================ // // QtSLiMWindow.h // SLiM // // Created by Ben Haller on 7/11/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIMWINDOW_H #define QTSLIMWINDOW_H #include #include #include #include #include #include #include #include #include #include #include "eidos_globals.h" #include "slim_globals.h" #include "eidos_rng.h" #include "community.h" #include "species.h" #include "QtSLiMExtras.h" #include "QtSLiMPopulationTable.h" class Subpopulation; class QCloseEvent; class QTextCursor; class QtSLiMEidosConsole; class QtSLiMTablesDrawer; class QItemSelection; class SLiMgui; class QtSLiMGraphView; class QtSLiMGraphView_CustomPlot; class Plot; class QtSLiMScriptTextEdit; class QtSLiMTextEdit; class QtSLiMDebugOutputWindow; class QtSLiMChromosomeWidget; class QtSLiMChromosomeWidgetController; class LogFile; namespace Ui { class QtSLiMWindow; } class QtSLiMWindow : public QMainWindow { Q_OBJECT private: // basic file i/o and change count management void init(void); void initializeUI(void); bool maybeSave(void); bool saveFile(const QString &fileName); void setCurrentFile(const QString &fileName); int slimChangeCount = 0; // private change count governing the recycle button's highlight QString lastSavedString; // the last string saved to disk, or initial script string QDateTime lastSavedDate; // the date when we last saved, to detect external changes bool scriptChangeObserved = false; // has a change to the script been observed since last saved? bool isScriptModified(void); // uses scriptChangeObserved / lastSavedString to determine modified status // tracking our interaction with the user about the file on disk, mod dates, external editing, etc. // the goal here is to avoid warning more than once; see https://github.com/MesserLab/SLiM/issues/476 bool warnedAboutUnreadabilityOnDisk = false; // avoid repeating this unless it gets fixed bool warnedAboutNotExistingOnDisk = false; // avoid repeating this unless it gets fixed bool warnedAboutExternalEditing = false; // avoid repeating this unless it gets fixed or changes QString lastExternalChangeString; // the string we last observed on disk and warned about bool currentlyWarningAboutDiskFile = false; // a flag to avoid re-entrancy for this window // state variables that are globals in Eidos and SLiM; we swap these in and out as needed, to provide each sim with its own context bool sim_RNG_initialized = false; Eidos_RNG_State sim_RNG; // QtSLiM never runs multithreaded, so we do not need the _PERTHREAD variant here slim_pedigreeid_t sim_next_pedigree_id = 0; slim_mutationid_t sim_next_mutation_id = 0; bool sim_suppress_warnings = false; std::string sim_working_dir; // the current working dir that we will return to when executing SLiM/Eidos code std::string sim_requested_working_dir; // the last working dir set by the user with the SLiMgui button/menu; we return to it on recycle // play-related variables; note that continuousPlayOn covers profiling play, tick play, and normal play, whereas profilePlayOn, // tickPlayOn_, and nonProfilePlayOn_ cover those cases individually; this is for simplicity in enable bindings in the nib bool invalidSimulation_ = true, continuousPlayOn_ = false, profilePlayOn_ = false, nonProfilePlayOn_ = false; bool tickPlayOn_ = false, reachedSimulationEnd_ = false, hasImported_ = false; slim_tick_t targetTick_ = 0; QElapsedTimer continuousPlayElapsedTimer_; QTimer continuousPlayInvocationTimer_; uint64_t continuousPlayTicksCompleted_ = 0; QTimer continuousProfileInvocationTimer_; QTimer playOneStepInvocationTimer_; int partialUpdateCount_ = 0; std::clock_t elapsedCPUClock_ = 0; // kept even when not profiling, for status bar updates QtSLiMPopulationTableModel *populationTableModel_ = nullptr; QtSLiMEidosConsole *consoleController = nullptr; QtSLiMTablesDrawer *tablesDrawerController = nullptr; QtSLiMDebugOutputWindow *debugOutputWindow_ = nullptr; QTimer debugButtonFlashTimer_; int debugButtonFlashCount_ = 0; int openedGraphCount_left = 0; // used for new graph window positioning int openedGraphCount_right = 0; int openedGraphCount_top = 0; int openedGraphCount_bottom = 0; public: bool isZombieWindow_ = false; // set when the UI is invalidated, to avoid various issues bool isUntitled = false, isRecipe = false, isTransient = false; QString currentFile; std::string scriptString; // the script string that we are running on right now; not the same as the script textview! Community *community = nullptr; // the simulation instance for this window Species *focalSpecies = nullptr; // NOT OWNED: a pointer to the focal species in community; do not use, call focalDisplaySpecies() std::string focalSpeciesName; // the name of the focal species (or "all"), for persistence across recycles SLiMgui *slimgui = nullptr; // the SLiMgui Eidos class instance for this window // display-related variables std::unordered_map genomicElementColorRegistry; bool reloadingSubpopTableview = false; bool reloadingSpeciesBar = false; // chromosome view configuration, applied to all chromosome views in multispecies models QtSLiMChromosomeWidgetController *chromosomeConfig = nullptr; public: typedef enum { WF = 0, nonWF } ModelType; QtSLiMWindow(QtSLiMWindow::ModelType modelType, bool includeComments); // untitled window explicit QtSLiMWindow(const QString &fileName); // window from a file QtSLiMWindow(const QString &recipeName, const QString &recipeScript); // window from a recipe virtual ~QtSLiMWindow() override; void tile(const QMainWindow *previous); void displayStartupMessage(void); void loadFile(const QString &fileName); // loads a file into an existing window void loadRecipe(const QString &recipeName, const QString &recipeScript); // loads a recipe into an existing window QWidget *imageWindowWithPath(const QString &path); // creates an image window subsidiary to the receiver static const QColor &blackContrastingColorForIndex(int index); static const QColor &whiteContrastingColorForIndex(int index); void colorForGenomicElementType(GenomicElementType *elementType, slim_objectid_t elementTypeID, float *p_red, float *p_green, float *p_blue, float *p_alpha); void colorForSpecies(Species *species, float *p_red, float *p_green, float *p_blue, float *p_alpha); QColor qcolorForSpecies(Species *species); std::vector listedSubpopulations(void); std::vector selectedSubpopulations(void); inline bool invalidSimulation(void) { return invalidSimulation_; } void setInvalidSimulation(bool p_invalid); inline bool reachedSimulationEnd(void) { return reachedSimulationEnd_; } void setReachedSimulationEnd(bool p_reachedEnd); inline bool isPlaying(void) { return continuousPlayOn_; } void setContinuousPlayOn(bool p_flag); void setTickPlayOn(bool p_flag); void setProfilePlayOn(bool p_flag); void setNonProfilePlayOn(bool p_flag); QtSLiMScriptTextEdit *scriptTextEdit(void); QtSLiMTextEdit *outputTextEdit(void); QtSLiMEidosConsole *ConsoleController(void) { return consoleController; } QtSLiMTablesDrawer *TablesDrawerController(void) { return tablesDrawerController; } QtSLiMDebugOutputWindow *debugOutputWindow(void) { return debugOutputWindow_; } void flashDebugButton(void); void stopDebugButtonFlash(void); void checkForSimulationTermination(void); void startNewSimulationFromScript(void); void setScriptStringAndInitializeSimulation(std::string string); Species *focalDisplaySpecies(void); Chromosome *focalChromosome(void); void updateOutputViews(void); void updateTickCounter(void); void updateSpeciesBar(void); void updateChromosomeViewSetup(void); void updateAfterTickFull(bool p_fullUpdate); void updatePlayButtonIcon(bool pressed); void updateProfileButtonIcon(bool pressed); void updateRecycleButtonIcon(bool pressed); void updateUIEnabling(void); void updateMenuEnablingACTIVE(QWidget *focusWidget); void updateMenuEnablingINACTIVE(QWidget *focusWidget, QWidget *focusWindow); void updateMenuEnablingSHARED(QWidget *focusWidget); void updateWindowMenu(void); void colorScriptWithProfileCountsFromNode(const EidosASTNode *node, double elapsedTime, int32_t baseIndex, QTextDocument *doc, QTextCharFormat &baseFormat); void displayProfileResults(void); void willExecuteScript(void); void didExecuteScript(void); bool runSimOneTick(void); void _continuousPlay(void); void _continuousProfile(void); void _playOneStep(void); enum PlayType { kNormalPlay = 0, kProfilePlay, kTickPlay, }; void playOrProfile(PlayType playType); bool windowIsReuseable(void); // requires isUntitled, !isRecipe, isTransient, and other conditions void updateChangeCount(void); bool changedSinceRecycle(void); void resetSLiMChangeCount(void); void scriptTexteditChanged(void); void setScriptBlockLabelTextFromSelection(void); bool checkScriptSuppressSuccessResponse(bool suppressSuccessResponse); bool offerAndExecuteAutofix(QTextCursor target, QString replacement, QString explanation, QString terminationMessage); bool checkTerminationForAutofix(QString terminationMessage); // Eidos SLiMgui method forwards EidosValue_SP eidos_logFileData(LogFile *logFile, EidosValue *column_value); void eidos_openDocument(QString path); void eidos_pauseExecution(void); QtSLiMGraphView_CustomPlot *eidos_createPlot(QString title, double *x_range, double *y_range, QString x_label, QString y_label, double width, double height, bool horizontalGrid, bool verticalGrid, bool fullBox, double axisLabelSize, double tickLabelSize); QtSLiMGraphView_CustomPlot *eidos_plotWithTitle(QString title); void plotLogFileData_1D(QString title, QString y_title, double *y_values, int data_count); void plotLogFileData_2D(QString title, QString x_title, QString y_title, double *x_values, double *y_values, int data_count, bool makeScatterPlot); signals: void terminationWithMessage(QString message, EidosErrorContext errorContext); void playStateChanged(void); void controllerChangeCountChanged(int changeCount); void controllerPartialUpdateAfterTick(void); void controllerFullUpdateAfterTick(void); void controllerTickFinished(void); void controllerRecycled(void); public slots: void showTerminationMessage(QString terminationMessage, EidosErrorContext errorContext); void playOneStepClicked(void); void tickChanged(void); void recycleClicked(void); void playSpeedChanged(void); void showDrawerClicked(void); void chromosomeDisplayPopupButtonRunMenu(void); void showConsoleClicked(void); void showBrowserClicked(void); void jumpToPopupButtonRunMenu(void); void clearOutputClicked(void); void clearDebugPointsClicked(void); void dumpPopulationClicked(void); void debugOutputClicked(void); void graphPopupButtonRunMenu(void); void changeDirectoryClicked(void); void displayGraphClicked(void); void selectedSpeciesChanged(void); void subpopSelectionDidChange(const QItemSelection &selected, const QItemSelection &deselected); // // UI glue, defined in QtSLiMWindow_glue.cpp // private slots: void displayFontPrefChanged(void); void applicationPaletteChanged(void); bool save(void); bool saveAs(void); void revert(void); void documentWasModified(void); void appStateChanged(Qt::ApplicationState state); void playOneStepPressed(void); void playOneStepReleased(void); void playPressed(void); void playReleased(void); void profilePressed(void); void profileReleased(void); void recyclePressed(void); void recycleReleased(void); void toggleDrawerPressed(void); void toggleDrawerReleased(void); void chromosomeActionPressed(void); void chromosomeActionReleased(void); void chromosomeDisplayPressed(void); void chromosomeDisplayReleased(void); void clearDebugPressed(void); void clearDebugReleased(void); void checkScriptPressed(void); void checkScriptReleased(void); void prettyprintPressed(void); void prettyprintReleased(void); void scriptHelpPressed(void); void scriptHelpReleased(void); void showConsolePressed(void); void showConsoleReleased(void); void showBrowserPressed(void); void showBrowserReleased(void); void jumpToPopupButtonPressed(void); void jumpToPopupButtonReleased(void); void clearOutputPressed(void); void clearOutputReleased(void); void dumpPopulationPressed(void); void dumpPopulationReleased(void); void debugOutputPressed(void); void debugOutputReleased(void); void graphPopupButtonPressed(void); void graphPopupButtonReleased(void); void changeDirectoryPressed(void); void changeDirectoryReleased(void); void handleDebugButtonFlash(void); void finish_eidos_pauseExecution(void); protected: virtual void closeEvent(QCloseEvent *p_event) override; virtual void moveEvent(QMoveEvent *p_event) override; virtual void resizeEvent(QResizeEvent *p_event) override; virtual void showEvent(QShowEvent *p_event) override; void positionNewSubsidiaryWindow(QWidget *window); QWidget *graphWindowWithView(QtSLiMGraphView *graphView, double windowWidth=300, double windowHeight=300); QtSLiMGraphView *graphViewForGraphWindow(QWidget *window); QWidget *newChromosomeDisplay(std::string chromosome_symbol, QString windowTitle); // pass "" for all chromosomes, or a symbol for one chromosome // used to suppress saving of resize/position info until we are fully constructed bool donePositioning_ = false; // splitter support void interpolateVerticalSplitter(void); QWidget *overallTopWidget = nullptr; QWidget *overallBottomWidget = nullptr; QSplitter *overallSplitter = nullptr; void interpolateHorizontalSplitter(void); QWidget *scriptWidget = nullptr; QWidget *outputWidget = nullptr; QSplitter *bottomSplitter = nullptr; void interpolateSplitters(void); // multispecies chromosome view support std::vector chromosomeWidgetLayouts; std::vector chromosomeOverviewWidgets; std::vector chromosomeZoomedWidgets; void removeExtraChromosomeViews(void); void addChromosomeWidgets(QVBoxLayout *chromosomeLayout, QtSLiMChromosomeWidget *overviewWidget, QtSLiMChromosomeWidget *zoomedWidget); void runChromosomeContextMenuAtPoint(QPoint p_globalPoint); private: void glueUI(void); void invalidateUI(void); QtSLiMGraphView *graphViewWithTitle(QString title); int graphViewCount(void); Ui::QtSLiMWindow *ui; }; #endif // QTSLIMWINDOW_H ================================================ FILE: QtSLiM/QtSLiMWindow.ui ================================================ QtSLiMWindow 0 0 914 691 850 500 10 10 10 10 5 10 270 21 270 21 0 0 0 0 Qt::Orientation::Horizontal 40 10 0 0 300 0 300 130 Qt::FocusPolicy::NoFocus Qt::ScrollBarPolicy::ScrollBarAlwaysOn QAbstractItemView::EditTrigger::NoEditTriggers false false QAbstractItemView::SelectionMode::ExtendedSelection QAbstractItemView::SelectionBehavior::SelectRows false false false false 0 0 16777215 130 QLayout::SizeConstraint::SetDefaultConstraint 60 60 60 60 Qt::FocusPolicy::NoFocus <html><head/><body><p>step one tick</p></body></html> :/buttons/play_step.png :/buttons/play_step_H.png:/buttons/play_step.png 60 60 true 60 60 60 60 Qt::FocusPolicy::NoFocus <html><head/><body><p>play simulation continuously</p></body></html> :/buttons/play.png :/buttons/play_H.png:/buttons/play.png 60 60 true true 30 30 30 30 Qt::FocusPolicy::NoFocus <html><head/><body><p>profile simulation</p></body></html> :/buttons/profile.png :/buttons/profile_H.png:/buttons/profile.png 30 30 true true 60 60 60 60 Qt::FocusPolicy::NoFocus <html><head/><body><p>recycle simulation</p></body></html> :/buttons/recycle.png :/buttons/recycle_H.png:/buttons/recycle.png 60 60 true 60 16777215 Qt::FocusPolicy::NoFocus <html><head/><body><p>simulation playing speed</p></body></html> QSlider::groove:horizontal { border: 1px solid #888888; border-radius: 1px; height: 2px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */ background: #a0a0a0; margin: 2px 0; } QSlider::groove:horizontal:disabled { border: 1px solid #cccccc; border-radius: 1px; height: 2px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */ background: #e0e0e0; margin: 2px 0; } QSlider::handle:horizontal { background: #ffffff; border: 1px solid #909090; width: 8px; margin: -4px 0; border-radius: 4px; } QSlider::handle:horizontal:disabled { background: #ffffff; border: 1px solid #d0d0d0; width: 8px; margin: -4px 0; border-radius: 4px; } 100 100 Qt::Orientation::Horizontal false Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 20 15 QLayout::SizeConstraint::SetDefaultConstraint Qt::Orientation::Horizontal QSizePolicy::Policy::Fixed 2 10 0 0 80 0 80 16777215 Tick: 100 16777215 <html><head/><body><p>the tick that is about to execute</p></body></html> initialize() Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter Qt::Orientation::Horizontal QSizePolicy::Policy::Fixed 2 10 Qt::Orientation::Horizontal QSizePolicy::Policy::Fixed 2 10 80 0 80 16777215 Cycle: 100 16777215 <html><head/><body><p>the cycle that will execute next</p></body></html> initialize() Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter true Qt::Orientation::Horizontal QSizePolicy::Policy::Fixed 2 10 Qt::Orientation::Vertical QSizePolicy::Policy::MinimumExpanding 20 0 8 5 5 4 0 0 0 20 16777215 20 0 0 0 65 16777215 65 5 0 0 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>open object tables window</p></body></html> :/buttons/open_type_drawer.png :/buttons/open_type_drawer_H.png:/buttons/open_type_drawer.png 20 20 true true 0 0 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>chromosome view actions</p></body></html> :/buttons/action.png :/buttons/action_H.png:/buttons/action.png 20 20 true true 0 0 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>make a new chromosome display window</p></body></html> :/buttons/chrom_display.png :/buttons/chrom_display_H.png:/buttons/chrom_display.png 20 20 true true Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 20 14 Qt::Orientation::Horizontal 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>clear debug points (set a debug point by clicking below)</p></body></html> :/buttons/clear_debug.png :/buttons/clear_debug_H.png:/buttons/clear_debug.png 20 20 true 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>check script syntax</p></body></html> :/buttons/check.png :/buttons/check_H.png:/buttons/check.png 20 20 true 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>prettyprint script; option-click/alt-click to do a full reformat</p></body></html> :/buttons/prettyprint.png :/buttons/prettyprint_H.png:/buttons/prettyprint.png 20 20 true 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>scripting help</p></body></html> :/buttons/syntax_help.png :/buttons/syntax_help_H.png:/buttons/syntax_help.png 20 20 true 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>show Eidos console</p></body></html> :/buttons/show_console.png :/buttons/show_console_H.png:/buttons/show_console.png 20 20 true true 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>show Eidos variable browser</p></body></html> :/buttons/show_browser.png :/buttons/show_browser_H.png:/buttons/show_browser.png 20 20 true true Input Script: 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>jump to an event or callback</p></body></html> :/buttons/jump_to.png :/buttons/jump_to_H.png:/buttons/jump_to.png 20 20 true 0 0 1 early() 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>clear output log</p></body></html> :/buttons/delete.png :/buttons/delete_H.png:/buttons/delete.png 20 20 true 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>dump population state</p></body></html> :/buttons/dump_output.png :/buttons/dump_output_H.png:/buttons/dump_output.png 20 20 true Run Output: 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>show the debugging output viewer</p></body></html> :/buttons/debug.png :/buttons/debug_H.png:/buttons/debug.png 20 20 true true 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>show a graph</p></body></html> :/buttons/graph_submenu.png :/buttons/graph_submenu_H.png:/buttons/graph_submenu.png 20 20 true Qt::Orientation::Horizontal 40 10 20 20 20 20 Qt::FocusPolicy::NoFocus <html><head/><body><p>change working directory</p></body></html> :/buttons/change_folder.png :/buttons/change_folder_H.png:/buttons/change_folder.png 20 20 true true 0 0 914 37 File Open Recipe Edit Find Simulation Show Graph Script Help Window New Ctrl+N New (nonWF) Ctrl+Shift+N Open... Ctrl+O Close Ctrl+W Save Ctrl+S Save As... Ctrl+Shift+S Cut Ctrl+X Copy Ctrl+C Paste Ctrl+V Undo Ctrl+Z Redo Ctrl+Shift+Z Delete Select All Ctrl+A Shift Left Ctrl+[ Shift Right Ctrl+] Comment / Uncomment Ctrl+/ Step Ctrl+Shift+P Play Ctrl+P Profile Recycle Ctrl+R Change Working Directory... Dump Population State Check Script Ctrl+= Prettyprint Script Show Script Help Show Variable Browser Ctrl+B Show Eidos Console Ctrl+Shift+E Clear Output Ctrl+K Execute Selection Ctrl+Return Execute All Ctrl+Shift+Return SLiMgui Help Send Feedback on SLiM Mailing List: slim-announce Mailing List: slim-discuss SLiM Home Page SLiM-Extras on GitHub About the Messer Lab QAction::MenuRole::NoRole About Ben Haller QAction::MenuRole::NoRole About Stick Software QAction::MenuRole::NoRole About SLiMgui QAction::MenuRole::AboutRole Quit SLiMgui Ctrl+Q QAction::MenuRole::QuitRole Find Recipe... Ctrl+Shift+O Preferences... Ctrl+, QAction::MenuRole::PreferencesRole Find... Ctrl+F Replace && Find Ctrl+Alt+G Find Next Ctrl+G Find Previous Ctrl+Shift+G Use Selection for Find Ctrl+E Jump to Selection Ctrl+J Revert to Saved Open Recent SLiM Workshops Use Selection for Replace Ctrl+Alt+E Jump to Line Ctrl+L Minimize Ctrl+M Zoom Reformat Script Show Debugging Output Ctrl+Shift+D Clear Debug Points Ctrl+Alt+K Graph 1D Population SFS Graph 1D Sample SFS Graph 2D Population SFS Graph 2D Sample SFS Graph Mutation Frequency Trajectories Graph Mutation Loss Time Histogram Graph Mutation Fixation Time Histogram Graph Population Fitness Distribution Graph Subpopulation Fitness Distributions Graph Fitness ~ Time Graph Age Distribution Graph Lifetime Reproduce Output Graph Population Size ~ Time Graph Population Visualization Create Haplotype Plot (all chromosomes) Show WF Tick Cycle Show nonWF Tick Cycle Multispecies Population Size ~ Time Graph Multispecies Population Size ~ Time Focus on Script Ctrl+1 Focus on Console Ctrl+2 Bigger Font Ctrl++ Smaller Font Ctrl+- Show WF Tick Cycle (Multispecies) Show WF Tick Cycle (Multispecies) Show nonWF Tick Cycle (Multispecies) Show nonWF Tick Cycle (Multispecies) Copy as HTML Copy as HTML Ctrl+Alt+C Create Haplotype Plot (selected chromosome) Show Color Chart Show a chart of the named colors provided by Eidos Show Plot Symbols Show a chart depicting the plot symbols used by Plot's points() method Duplicate Ctrl+D Show SLiMgui Color Scales Show a chart depicting the color scales used by SLiMgui for mutation effects and individual fitness QtSLiMPushButton QPushButton
QtSLiMExtras.h
QtSLiMTextEdit QPlainTextEdit
QtSLiMScriptTextEdit.h
QtSLiMChromosomeWidget QOpenGLWidget
QtSLiMChromosomeWidget.h
QtSLiMIndividualsWidget QOpenGLWidget
QtSLiMIndividualsWidget.h
QtSLiMGenerationLineEdit QLineEdit
QtSLiMExtras.h
QtSLiMStatusBar QStatusBar
QtSLiMExtras.h
QtSLiMScriptTextEdit QPlainTextEdit
QtSLiMScriptTextEdit.h
QTabBar QWidget
QTabBar.h
1
QtSLiMEllipsisLabel QLabel
QtSLiMExtras.h
================================================ FILE: QtSLiM/QtSLiMWindow_glue.cpp ================================================ // // QtSLiMWindow_glue.cpp // SLiM // // Created by Ben Haller on 7/11/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiMWindow.h" #include "ui_QtSLiMWindow.h" #include #include #include #include "QtSLiMScriptTextEdit.h" #include "QtSLiMAppDelegate.h" void QtSLiMWindow::glueUI(void) { // connect all QtSLiMWindow slots //connect(ui->playOneStepButton, &QPushButton::clicked, this, &QtSLiMWindow::playOneStepClicked); // done in playOneStepPressed() now! connect(ui->playButton, &QPushButton::clicked, this, [this]() { playOrProfile(tickPlayOn_ ? PlayType::kTickPlay : PlayType::kNormalPlay); }); connect(ui->profileButton, &QPushButton::clicked, this, [this]() { playOrProfile(PlayType::kProfilePlay); }); connect(ui->tickLineEdit, &QLineEdit::returnPressed, this, &QtSLiMWindow::tickChanged); //connect(ui->cycleLineEdit, &QLineEdit::returnPressed, this, &QtSLiMWindow::cycleChanged); // not editable at the moment connect(ui->recycleButton, &QPushButton::clicked, this, &QtSLiMWindow::recycleClicked); connect(ui->playSpeedSlider, &QSlider::valueChanged, this, &QtSLiMWindow::playSpeedChanged); connect(ui->toggleDrawerButton, &QPushButton::clicked, this, &QtSLiMWindow::showDrawerClicked); //connect(ui->chromosomeActionButton, &QPushButton::clicked, this, &QtSLiMWindow::chromosomeActionClicked); // this button runs when it is pressed //connect(ui->chromosomeDisplayButton, &QPushButton::clicked, this, &QtSLiMWindow::chromosomeDisplayClicked); // this button runs when it is pressed connect(ui->clearDebugButton, &QPushButton::clicked, ui->scriptTextEdit, &QtSLiMScriptTextEdit::clearDebugPoints); connect(ui->checkScriptButton, &QPushButton::clicked, ui->scriptTextEdit, &QtSLiMTextEdit::checkScript); connect(ui->prettyprintButton, &QPushButton::clicked, ui->scriptTextEdit, &QtSLiMTextEdit::prettyprintClicked); connect(ui->scriptHelpButton, &QPushButton::clicked, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_help); connect(ui->consoleButton, &QPushButton::clicked, this, &QtSLiMWindow::showConsoleClicked); connect(ui->browserButton, &QPushButton::clicked, this, &QtSLiMWindow::showBrowserClicked); //connect(ui->jumpToPopupButton, &QPushButton::clicked, this, &QtSLiMWindow::jumpToPopupButtonClicked); // this button runs when it is pressed connect(ui->clearOutputButton, &QPushButton::clicked, this, &QtSLiMWindow::clearOutputClicked); connect(ui->dumpPopulationButton, &QPushButton::clicked, this, &QtSLiMWindow::dumpPopulationClicked); connect(ui->debugOutputButton, &QPushButton::clicked, this, &QtSLiMWindow::debugOutputClicked); //connect(ui->graphPopupButton, &QPushButton::clicked, this, &QtSLiMWindow::graphPopupButtonClicked); // this button runs when it is pressed connect(ui->changeDirectoryButton, &QPushButton::clicked, this, &QtSLiMWindow::changeDirectoryClicked); // set up QtSLiMPushButton "base names" for all buttons ui->playOneStepButton->qtslimSetBaseName("play_step"); ui->playButton->qtslimSetBaseName("play"); ui->profileButton->qtslimSetBaseName("profile"); ui->recycleButton->qtslimSetBaseName("recycle"); ui->toggleDrawerButton->qtslimSetBaseName("open_type_drawer"); ui->chromosomeActionButton->qtslimSetBaseName("action"); ui->chromosomeDisplayButton->qtslimSetBaseName("chrom_display"); ui->clearDebugButton->qtslimSetBaseName("clear_debug"); ui->checkScriptButton->qtslimSetBaseName("check"); ui->prettyprintButton->qtslimSetBaseName("prettyprint"); ui->scriptHelpButton->qtslimSetBaseName("syntax_help"); ui->consoleButton->qtslimSetBaseName("show_console"); ui->browserButton->qtslimSetBaseName("show_browser"); ui->jumpToPopupButton->qtslimSetBaseName("jump_to"); ui->clearOutputButton->qtslimSetBaseName("delete"); ui->dumpPopulationButton->qtslimSetBaseName("dump_output"); ui->debugOutputButton->qtslimSetBaseName("debug"); ui->graphPopupButton->qtslimSetBaseName("graph_submenu"); ui->changeDirectoryButton->qtslimSetBaseName("change_folder"); // set up the "temporary icon" on the debugging button, to support pulsing static QIcon *debug_RED = nullptr; if (!debug_RED) debug_RED = new QIcon(":buttons/debug_RED.png"); ui->debugOutputButton->setTemporaryIcon(*debug_RED); // set up all icon-based QPushButtons to change their icon as they track connect(ui->playOneStepButton, &QPushButton::pressed, this, &QtSLiMWindow::playOneStepPressed); connect(ui->playOneStepButton, &QPushButton::released, this, &QtSLiMWindow::playOneStepReleased); connect(ui->playButton, &QPushButton::pressed, this, &QtSLiMWindow::playPressed); connect(ui->playButton, &QPushButton::released, this, &QtSLiMWindow::playReleased); connect(ui->profileButton, &QPushButton::pressed, this, &QtSLiMWindow::profilePressed); connect(ui->profileButton, &QPushButton::released, this, &QtSLiMWindow::profileReleased); connect(ui->recycleButton, &QPushButton::pressed, this, &QtSLiMWindow::recyclePressed); connect(ui->recycleButton, &QPushButton::released, this, &QtSLiMWindow::recycleReleased); connect(ui->toggleDrawerButton, &QPushButton::pressed, this, &QtSLiMWindow::toggleDrawerPressed); connect(ui->toggleDrawerButton, &QPushButton::released, this, &QtSLiMWindow::toggleDrawerReleased); connect(ui->chromosomeActionButton, &QPushButton::pressed, this, &QtSLiMWindow::chromosomeActionPressed); connect(ui->chromosomeActionButton, &QPushButton::released, this, &QtSLiMWindow::chromosomeActionReleased); connect(ui->chromosomeDisplayButton, &QPushButton::pressed, this, &QtSLiMWindow::chromosomeDisplayPressed); connect(ui->chromosomeDisplayButton, &QPushButton::released, this, &QtSLiMWindow::chromosomeDisplayReleased); connect(ui->clearDebugButton, &QPushButton::pressed, this, &QtSLiMWindow::clearDebugPressed); connect(ui->clearDebugButton, &QPushButton::released, this, &QtSLiMWindow::clearDebugReleased); connect(ui->checkScriptButton, &QPushButton::pressed, this, &QtSLiMWindow::checkScriptPressed); connect(ui->checkScriptButton, &QPushButton::released, this, &QtSLiMWindow::checkScriptReleased); connect(ui->prettyprintButton, &QPushButton::pressed, this, &QtSLiMWindow::prettyprintPressed); connect(ui->prettyprintButton, &QPushButton::released, this, &QtSLiMWindow::prettyprintReleased); connect(ui->scriptHelpButton, &QPushButton::pressed, this, &QtSLiMWindow::scriptHelpPressed); connect(ui->scriptHelpButton, &QPushButton::released, this, &QtSLiMWindow::scriptHelpReleased); connect(ui->consoleButton, &QPushButton::pressed, this, &QtSLiMWindow::showConsolePressed); connect(ui->consoleButton, &QPushButton::released, this, &QtSLiMWindow::showConsoleReleased); connect(ui->browserButton, &QPushButton::pressed, this, &QtSLiMWindow::showBrowserPressed); connect(ui->browserButton, &QPushButton::released, this, &QtSLiMWindow::showBrowserReleased); connect(ui->jumpToPopupButton, &QPushButton::pressed, this, &QtSLiMWindow::jumpToPopupButtonPressed); connect(ui->jumpToPopupButton, &QPushButton::released, this, &QtSLiMWindow::jumpToPopupButtonReleased); connect(ui->scriptBlockLabel, &QtSLiMEllipsisLabel::pressed, this, &QtSLiMWindow::jumpToPopupButtonPressed); //connect(ui->scriptBlockLabel, &QtSLiMEllipsisLabel::released, this, &QtSLiMWindow::jumpToPopupButtonReleased); // seems to be unnecessary connect(ui->clearOutputButton, &QPushButton::pressed, this, &QtSLiMWindow::clearOutputPressed); connect(ui->clearOutputButton, &QPushButton::released, this, &QtSLiMWindow::clearOutputReleased); connect(ui->dumpPopulationButton, &QPushButton::pressed, this, &QtSLiMWindow::dumpPopulationPressed); connect(ui->dumpPopulationButton, &QPushButton::released, this, &QtSLiMWindow::dumpPopulationReleased); connect(ui->debugOutputButton, &QPushButton::pressed, this, &QtSLiMWindow::debugOutputPressed); connect(ui->debugOutputButton, &QPushButton::released, this, &QtSLiMWindow::debugOutputReleased); connect(ui->graphPopupButton, &QPushButton::pressed, this, &QtSLiMWindow::graphPopupButtonPressed); connect(ui->graphPopupButton, &QPushButton::released, this, &QtSLiMWindow::graphPopupButtonReleased); connect(ui->changeDirectoryButton, &QPushButton::pressed, this, &QtSLiMWindow::changeDirectoryPressed); connect(ui->changeDirectoryButton, &QPushButton::released, this, &QtSLiMWindow::changeDirectoryReleased); // this action seems to need to be added to the main window in order to function reliably; // I'm not sure why, maybe it is because it is connected to an object that is not a widget? // adding it as an action here seems to have no visible effect except that the shortcut now works addAction(ui->actionFindRecipe); // menu items that are not visible, for hidden shortcuts QAction *actionNewWF_commentless = new QAction("New WF (Commentless)", this); actionNewWF_commentless->setShortcut(Qt::ControlModifier | Qt::AltModifier | Qt::Key_N); connect(actionNewWF_commentless, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newWF_commentless); addAction(actionNewWF_commentless); QAction *actionNewNonWF_commentless = new QAction("New nonWF (Commentless)", this); actionNewNonWF_commentless->setShortcut(Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier | Qt::Key_N); connect(actionNewNonWF_commentless, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newNonWF_commentless); addAction(actionNewNonWF_commentless); // connect all menu items with existing slots connect(ui->actionPreferences, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_preferences); connect(ui->actionAboutQtSLiM, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_about); connect(ui->actionShowCycle_WF, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_WF); connect(ui->actionShowCycle_nonWF, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_nonWF); connect(ui->actionShowCycle_WF_MS, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_WF_MS); connect(ui->actionShowCycle_nonWF_MS, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showCycle_nonWF_MS); connect(ui->actionShowColorChart, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showColorChart); connect(ui->actionShowPlotSymbols, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showPlotSymbols); connect(ui->actionShowColorScales, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showColorScales); connect(ui->actionQtSLiMHelp, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_help); connect(ui->actionQuitQtSLiM, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_quit); connect(ui->actionNew, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newWF); connect(ui->actionNew_nonWF, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_newNonWF); connect(ui->actionOpen, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_open); connect(ui->actionClose, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_close); connect(ui->actionSave, &QAction::triggered, this, &QtSLiMWindow::save); connect(ui->actionSaveAs, &QAction::triggered, this, &QtSLiMWindow::saveAs); connect(ui->actionRevertToSaved, &QAction::triggered, this, &QtSLiMWindow::revert); connect(ui->actionStep, &QAction::triggered, this, &QtSLiMWindow::playOneStepClicked); connect(ui->actionPlay, &QAction::triggered, this, [this]() { playOrProfile(tickPlayOn_ ? PlayType::kTickPlay : PlayType::kNormalPlay); }); connect(ui->actionProfile, &QAction::triggered, this, [this]() { playOrProfile(PlayType::kProfilePlay); }); connect(ui->actionRecycle, &QAction::triggered, this, &QtSLiMWindow::recycleClicked); connect(ui->actionChangeWorkingDirectory, &QAction::triggered, this, &QtSLiMWindow::changeDirectoryClicked); connect(ui->actionDumpPopulationState, &QAction::triggered, this, &QtSLiMWindow::dumpPopulationClicked); connect(ui->actionMinimize, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_minimize); connect(ui->actionZoom, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_zoom); connect(ui->actionGraph_1D_Population_SFS, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_1D_Sample_SFS, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_2D_Population_SFS, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_2D_Sample_SFS, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Mutation_Frequency_Trajectories, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Mutation_Loss_Time_Histogram, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Mutation_Fixation_Time_Histogram, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Population_Fitness_Distribution, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Subpopulation_Fitness_Distributions, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Fitness_Time, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Age_Distribution, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Lifetime_Reproduce_Output, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Population_Size_Time, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Population_Visualization, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionGraph_Multispecies_Population_Size_Time, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionCreate_Haplotype_Plot_All, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); connect(ui->actionCreate_Haplotype_Plot_Selected, &QAction::triggered, this, &QtSLiMWindow::displayGraphClicked); // connect menu items that can go to either a QtSLiMWindow or a QtSLiMEidosConsole connect(ui->actionFocusOnScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_focusOnScript); connect(ui->actionFocusOnConsole, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_focusOnConsole); connect(ui->actionCheckScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_checkScript); connect(ui->actionPrettyprintScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_prettyprintScript); connect(ui->actionReformatScript, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_reformatScript); connect(ui->actionShowScriptHelp, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_help); connect(ui->actionBiggerFont, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_biggerFont); connect(ui->actionSmallerFont, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_smallerFont); connect(ui->actionShowEidosConsole, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showEidosConsole); connect(ui->actionShowVariableBrowser, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showVariableBrowser); connect(ui->actionClearOutput, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_clearOutput); connect(ui->actionClearDebug, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_clearDebugPoints); connect(ui->actionShowDebuggingOutput, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_showDebuggingOutput); // connect menu items that open a URL connect(ui->actionSLiMWorkshops, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpWorkshops); connect(ui->actionSendFeedback, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpFeedback); connect(ui->actionMailingList_slimdiscuss, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpSLiMDiscuss); connect(ui->actionMailingList_slimannounce, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpSLiMAnnounce); connect(ui->actionSLiMHomePage, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpSLiMHome); connect(ui->actionSLiMExtras, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpSLiMExtras); connect(ui->actionAboutMesserLab, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpMesserLab); connect(ui->actionAboutBenHaller, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpBenHaller); connect(ui->actionAboutStickSoftware, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_helpStickSoftware); // connect custom menu items connect(ui->actionCopyAsHTML, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_copyAsHTML); connect(ui->actionShiftLeft, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_shiftLeft); connect(ui->actionShiftRight, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_shiftRight); connect(ui->actionCommentUncomment, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_commentUncomment); connect(ui->actionExecuteSelection, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_executeSelection); connect(ui->actionExecuteAll, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_executeAll); // standard actions that need to be dispatched (I haven't found a better way to do this; // this is basically implementing the first responder / event dispatch mechanism) connect(ui->actionUndo, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_undo); connect(ui->actionRedo, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_redo); connect(ui->actionCut, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_cut); connect(ui->actionCopy, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_copy); connect(ui->actionPaste, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_paste); connect(ui->actionDuplicate, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_duplicate); connect(ui->actionDelete, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_delete); connect(ui->actionSelectAll, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_selectAll); // Find panel actions; these just get forwarded to QtSLiMFindPanel connect(ui->actionFindShow, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_findShow); connect(ui->actionFindNext, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_findNext); connect(ui->actionFindPrevious, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_findPrevious); connect(ui->actionReplaceAndFind, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_replaceAndFind); connect(ui->actionUseSelectionForFind, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_useSelectionForFind); connect(ui->actionUseSelectionForReplace, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_useSelectionForReplace); connect(ui->actionJumpToSelection, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_jumpToSelection); connect(ui->actionJumpToLine, &QAction::triggered, qtSLiMAppDelegate, &QtSLiMAppDelegate::dispatch_jumpToLine); } // // private slots // void QtSLiMWindow::playPressed(void) { updatePlayButtonIcon(true); } void QtSLiMWindow::playReleased(void) { updatePlayButtonIcon(false); } void QtSLiMWindow::profilePressed(void) { updateProfileButtonIcon(true); } void QtSLiMWindow::profileReleased(void) { updateProfileButtonIcon(false); } void QtSLiMWindow::recyclePressed(void) { updateRecycleButtonIcon(true); } void QtSLiMWindow::recycleReleased(void) { updateRecycleButtonIcon(false); } void QtSLiMWindow::toggleDrawerPressed(void) { ui->toggleDrawerButton->qtslimSetHighlight(true); } void QtSLiMWindow::toggleDrawerReleased(void) { ui->toggleDrawerButton->qtslimSetHighlight(false); } void QtSLiMWindow::chromosomeActionPressed(void) { ui->chromosomeActionButton->qtslimSetHighlight(true); chromosomeConfig->actionButtonRunMenu(ui->chromosomeActionButton); } void QtSLiMWindow::chromosomeActionReleased(void) { ui->chromosomeActionButton->qtslimSetHighlight(false); } void QtSLiMWindow::chromosomeDisplayPressed(void) { ui->chromosomeDisplayButton->qtslimSetHighlight(true); chromosomeDisplayPopupButtonRunMenu(); // this button runs its menu when it is pressed, so make that call here } void QtSLiMWindow::chromosomeDisplayReleased(void) { ui->chromosomeDisplayButton->qtslimSetHighlight(false); } void QtSLiMWindow::clearDebugPressed(void) { ui->clearDebugButton->qtslimSetHighlight(true); } void QtSLiMWindow::clearDebugReleased(void) { ui->clearDebugButton->qtslimSetHighlight(false); } void QtSLiMWindow::checkScriptPressed(void) { ui->checkScriptButton->qtslimSetHighlight(true); } void QtSLiMWindow::checkScriptReleased(void) { ui->checkScriptButton->qtslimSetHighlight(false); } void QtSLiMWindow::prettyprintPressed(void) { ui->prettyprintButton->qtslimSetHighlight(true); } void QtSLiMWindow::prettyprintReleased(void) { ui->prettyprintButton->qtslimSetHighlight(false); } void QtSLiMWindow::scriptHelpPressed(void) { ui->scriptHelpButton->qtslimSetHighlight(true); } void QtSLiMWindow::scriptHelpReleased(void) { ui->scriptHelpButton->qtslimSetHighlight(false); } void QtSLiMWindow::showConsolePressed(void) { ui->consoleButton->qtslimSetHighlight(true); } void QtSLiMWindow::showConsoleReleased(void) { ui->consoleButton->qtslimSetHighlight(false); } void QtSLiMWindow::showBrowserPressed(void) { ui->browserButton->qtslimSetHighlight(true); } void QtSLiMWindow::showBrowserReleased(void) { ui->browserButton->qtslimSetHighlight(false); } void QtSLiMWindow::jumpToPopupButtonPressed(void) { ui->jumpToPopupButton->qtslimSetHighlight(true); jumpToPopupButtonRunMenu(); // this button runs its menu when it is pressed, so make that call here } void QtSLiMWindow::jumpToPopupButtonReleased(void) { ui->jumpToPopupButton->qtslimSetHighlight(false); } void QtSLiMWindow::clearOutputPressed(void) { ui->clearOutputButton->qtslimSetHighlight(true); } void QtSLiMWindow::clearOutputReleased(void) { ui->clearOutputButton->qtslimSetHighlight(false); } void QtSLiMWindow::dumpPopulationPressed(void) { ui->dumpPopulationButton->qtslimSetHighlight(true); } void QtSLiMWindow::dumpPopulationReleased(void) { ui->dumpPopulationButton->qtslimSetHighlight(false); } void QtSLiMWindow::debugOutputPressed(void) { ui->debugOutputButton->qtslimSetHighlight(true); stopDebugButtonFlash(); } void QtSLiMWindow::debugOutputReleased(void) { ui->debugOutputButton->qtslimSetHighlight(false); stopDebugButtonFlash(); } void QtSLiMWindow::graphPopupButtonPressed(void) { ui->graphPopupButton->qtslimSetHighlight(true); graphPopupButtonRunMenu(); // this button runs its menu when it is pressed, so make that call here } void QtSLiMWindow::graphPopupButtonReleased(void) { ui->graphPopupButton->qtslimSetHighlight(false); } void QtSLiMWindow::changeDirectoryPressed(void) { ui->changeDirectoryButton->qtslimSetHighlight(true); } void QtSLiMWindow::changeDirectoryReleased(void) { ui->changeDirectoryButton->qtslimSetHighlight(false); } ================================================ FILE: QtSLiM/QtSLiM_Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName SLiMgui CFBundleName SLiMgui CFBundleExecutable ${EXECUTABLE_NAME} CFBundleVersion ${QMAKE_FULL_VERSION} CFBundleShortVersionString ${QMAKE_FULL_VERSION} CFBundleGetInfoString The SLiM modeling environment. CFBundleIconFile ${ASSETCATALOG_COMPILER_APPICON_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundlePackageType APPL CFBundleSignature ${QMAKE_PKGINFO_TYPEINFO} LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} NSHumanReadableCopyright Copyright © 2016–2025 Benjamin C. Haller, http://messerlab.org/slim/. All rights reserved. NSPrincipalClass NSApplication NSSupportsAutomaticGraphicsSwitching NSRequiresAquaSystemAppearance CFBundleDocumentTypes CFBundleTypeExtensions slim CFBundleTypeIconFile QtSLiM_DocIcon.icns CFBundleTypeMIMETypes text/plain CFBundleTypeName SLiM model CFBundleTypeRole Editor LSHandlerRank Owner LSItemContentTypes edu.messerlab.slim CFBundleTypeExtensions txt CFBundleTypeMIMETypes text/plain CFBundleTypeName NSStringPboardType CFBundleTypeRole Viewer LSHandlerRank Alternate LSItemContentTypes public.plain-text CFBundleTypeExtensions jpg jpeg CFBundleTypeMIMETypes image/jpeg CFBundleTypeName JPEG image CFBundleTypeRole Viewer LSHandlerRank Alternate LSItemContentTypes public.jpeg CFBundleTypeExtensions png CFBundleTypeMIMETypes image/png CFBundleTypeName PNG image CFBundleTypeRole Viewer LSHandlerRank Alternate LSItemContentTypes public.png CFBundleTypeExtensions gif CFBundleTypeMIMETypes image/gif CFBundleTypeName GIF image CFBundleTypeRole Viewer LSHandlerRank Alternate LSItemContentTypes com.compuserve.gif UTExportedTypeDeclarations UTTypeConformsTo public.text UTTypeDescription SLiM model UTTypeIconFile QtSLiM_DocIcon.icns UTTypeIdentifier edu.messerlab.slim UTTypeReferenceURL https://messerlab.org/slim/ UTTypeTagSpecification com.apple.nspboard-type NSStringPboardType com.apple.ostype SLiM public.filename-extension slim public.mime-type text/plain ================================================ FILE: QtSLiM/QtSLiM_Plot.cpp ================================================ // // QtSLiM_Plot.cpp // SLiM // // Created by Ben Haller on 1/30/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiM_Plot.h" #include "QtSLiMGraphView_CustomPlot.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "eidos_class_Image.h" #include "spatial_map.h" #include #include #include #include #include #pragma mark - #pragma mark Plot #pragma mark - Plot::Plot(const std::string &title, QtSLiMGraphView_CustomPlot *p_plotview) : title_(title), plotview_(p_plotview) { } Plot::~Plot(void) { } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - const EidosClass *Plot::Class(void) const { return gSLiM_Plot_Class; } void Plot::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay(); // standard EidosObject behavior (not Dictionary behavior) } EidosValue_SP Plot::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_title: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(title_)); } // variables // all others, including gID_none default: return super::GetProperty(p_property_id); } } void Plot::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP Plot::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_abline: return ExecuteMethod_abline(p_method_id, p_arguments, p_interpreter); case gID_addLegend: return ExecuteMethod_addLegend(p_method_id, p_arguments, p_interpreter); case gID_axis: return ExecuteMethod_axis(p_method_id, p_arguments, p_interpreter); case gID_image: return ExecuteMethod_image(p_method_id, p_arguments, p_interpreter); case gID_legendLineEntry: return ExecuteMethod_legendLineEntry(p_method_id, p_arguments, p_interpreter); case gID_legendPointEntry: return ExecuteMethod_legendPointEntry(p_method_id, p_arguments, p_interpreter); case gID_legendSwatchEntry: return ExecuteMethod_legendSwatchEntry(p_method_id, p_arguments, p_interpreter); case gID_legendTitleEntry: return ExecuteMethod_legendTitleEntry(p_method_id, p_arguments, p_interpreter); case gID_lines: return ExecuteMethod_lines(p_method_id, p_arguments, p_interpreter); case gID_matrix: return ExecuteMethod_matrix(p_method_id, p_arguments, p_interpreter); case gID_mtext: return ExecuteMethod_mtext(p_method_id, p_arguments, p_interpreter); case gID_points: return ExecuteMethod_points(p_method_id, p_arguments, p_interpreter); case gID_rects: return ExecuteMethod_rects(p_method_id, p_arguments, p_interpreter); case gID_segments: return ExecuteMethod_segments(p_method_id, p_arguments, p_interpreter); case gID_setBorderless: return ExecuteMethod_setBorderless(p_method_id, p_arguments, p_interpreter); case gID_text: return ExecuteMethod_text(p_method_id, p_arguments, p_interpreter); case gEidosID_write: return ExecuteMethod_write(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* – (void)abline([Nif a = NULL], [Nif b = NULL], [Nif h = NULL], [Nif v = NULL], [string color = "red"], [numeric lwd = 1.0], [float alpha = 1.0]) // EidosValue_SP Plot::ExecuteMethod_abline(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *a_value = p_arguments[0].get(); EidosValue *b_value = p_arguments[1].get(); EidosValue *h_value = p_arguments[2].get(); EidosValue *v_value = p_arguments[3].get(); EidosValue *color_value = p_arguments[4].get(); EidosValue *lwd_value = p_arguments[5].get(); EidosValue *alpha_value = p_arguments[6].get(); double *a = nullptr, *b = nullptr, *h = nullptr, *v = nullptr; int line_count; if ((a_value->Type() != EidosValueType::kValueNULL) && (b_value->Type() != EidosValueType::kValueNULL) && (h_value->Type() == EidosValueType::kValueNULL) && (v_value->Type() == EidosValueType::kValueNULL)) { // a and b int acount = a_value->Count(); int bcount = b_value->Count(); if (acount == bcount) line_count = acount; else if (acount == 1) line_count = bcount; else if (bcount == 1) line_count = acount; else EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): abline() requires a and b to be the same length, or one of them to be singleton." << EidosTerminate(); if (line_count == 0) return gStaticEidosValueVOID; a = (double *)malloc(line_count * sizeof(double)); b = (double *)malloc(line_count * sizeof(double)); if (!a || !b) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); for (int index = 0; index < line_count; ++index) { a[index] = a_value->NumericAtIndex_NOCAST(index % acount, nullptr); b[index] = b_value->NumericAtIndex_NOCAST(index % bcount, nullptr); } } else if ((a_value->Type() == EidosValueType::kValueNULL) && (b_value->Type() == EidosValueType::kValueNULL) && (h_value->Type() != EidosValueType::kValueNULL) && (v_value->Type() == EidosValueType::kValueNULL)) { // h line_count = h_value->Count(); if (line_count == 0) return gStaticEidosValueVOID; h = (double *)malloc(line_count * sizeof(double)); if (!h) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); for (int index = 0; index < line_count; ++index) h[index] = h_value->NumericAtIndex_NOCAST(index, nullptr); } else if ((a_value->Type() == EidosValueType::kValueNULL) && (b_value->Type() == EidosValueType::kValueNULL) && (h_value->Type() == EidosValueType::kValueNULL) && (v_value->Type() != EidosValueType::kValueNULL)) { // v line_count = v_value->Count(); if (line_count == 0) return gStaticEidosValueVOID; v = (double *)malloc(line_count * sizeof(double)); if (!v) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); for (int index = 0; index < line_count; ++index) v[index] = v_value->NumericAtIndex_NOCAST(index, nullptr); } else { EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): abline() requires one of three usage modes: (1) a and b are non-NULL while h and v are NULL; (2) a, b, and v are NULL while h is non-NULL; or (3) a, b, and h are NULL while v is non-NULL." << EidosTerminate(nullptr); } // color std::vector *colors = new std::vector; int color_count = color_value->Count(); if ((color_count != 1) && (color_count != line_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): abline() requires color to match the number of lines, or be singleton." << EidosTerminate(); for (int index = 0; index < color_count; ++index) { std::string color_string = color_value->StringAtIndex_NOCAST(index, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); colors->emplace_back(colorR, colorG, colorB, 255); } // lwd std::vector *lwds = new std::vector; int lwd_count = lwd_value->Count(); if ((lwd_count != 1) && (lwd_count != line_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): abline() requires lwd to match the number of lines, or be singleton." << EidosTerminate(); for (int index = 0; index < lwd_count; ++index) { double lwd = lwd_value->NumericAtIndex_NOCAST(index, nullptr); if ((lwd < 0.0) || (lwd > 100.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): abline() requires the elements of lwd to be in [0, 100]." << EidosTerminate(nullptr); lwds->push_back(lwd); } // alpha std::vector *alphas = new std::vector; int alpha_count = alpha_value->Count(); if ((alpha_count != 1) && (alpha_count != line_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): abline() requires alpha to match the number of lines, or be singleton." << EidosTerminate(); for (int index = 0; index < alpha_count; ++index) { double alpha = alpha_value->FloatAtIndex_NOCAST(index, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_abline): abline() requires the elements of alpha to be in [0, 1]." << EidosTerminate(nullptr); alphas->push_back(alpha); } plotview_->addABLineData(a, b, h, v, line_count, colors, alphas, lwds); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)addLegend([Ns$ position = NULL], [Ni$ inset = NULL], [Nif$ labelSize = NULL], [Nif$ lineHeight = NULL], [Nif$ graphicsWidth = NULL], [Nif$ exteriorMargin = NULL], [Nif$ interiorMargin = NULL]) // EidosValue_SP Plot::ExecuteMethod_addLegend(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *position_value = p_arguments[0].get(); EidosValue *inset_value = p_arguments[1].get(); EidosValue *labelSize_value = p_arguments[2].get(); EidosValue *lineHeight_value = p_arguments[3].get(); EidosValue *graphicsWidth_value = p_arguments[4].get(); EidosValue *exteriorMargin_value = p_arguments[5].get(); EidosValue *interiorMargin_value = p_arguments[6].get(); // position QtSLiM_LegendPosition position; if (position_value->Type() == EidosValueType::kValueNULL) { position = QtSLiM_LegendPosition::kUnconfigured; } else { QString positionString = QString::fromStdString(position_value->StringAtIndex_NOCAST(0, nullptr)); if (positionString == "topLeft") position = QtSLiM_LegendPosition::kTopLeft; else if (positionString == "topRight") position = QtSLiM_LegendPosition::kTopRight; else if (positionString == "bottomLeft") position = QtSLiM_LegendPosition::kBottomLeft; else if (positionString == "bottomRight") position = QtSLiM_LegendPosition::kBottomRight; else EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() requires position to be 'topLeft', 'topRight', 'bottomLeft', or 'bottomRight' (or NULL)." << EidosTerminate(nullptr); } // inset int inset; if (inset_value->Type() == EidosValueType::kValueNULL) inset = -1; else { inset = inset_value->IntAtIndex_NOCAST(0, nullptr); if ((inset < 0) || (inset > 50)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() requires inset to be in [0, 50]." << EidosTerminate(nullptr); } // labelSize double labelSize; if (labelSize_value->Type() == EidosValueType::kValueNULL) labelSize = -1; else { labelSize = labelSize_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(labelSize) || (labelSize < 5) || (labelSize > 50)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() requires labelSize to be in [5, 50]." << EidosTerminate(nullptr); } // lineHeight double lineHeight; if (lineHeight_value->Type() == EidosValueType::kValueNULL) lineHeight = -1; else { lineHeight = lineHeight_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(lineHeight) || (lineHeight < 5) || (lineHeight > 100)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() requires lineHeight to be in [5, 100]." << EidosTerminate(nullptr); } // graphicsWidth double graphicsWidth; if (graphicsWidth_value->Type() == EidosValueType::kValueNULL) graphicsWidth = -1; else { graphicsWidth = graphicsWidth_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(graphicsWidth) || (graphicsWidth < 5) || (graphicsWidth > 100)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() requires graphicsWidth to be in [5, 100]." << EidosTerminate(nullptr); } // exteriorMargin double exteriorMargin; if (exteriorMargin_value->Type() == EidosValueType::kValueNULL) exteriorMargin = -1; else { exteriorMargin = exteriorMargin_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(exteriorMargin) || (exteriorMargin < 0) || (exteriorMargin > 50)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() requires exteriorMargin to be in [0, 50]." << EidosTerminate(nullptr); } // interiorMargin double interiorMargin; if (interiorMargin_value->Type() == EidosValueType::kValueNULL) interiorMargin = -1; else { interiorMargin = interiorMargin_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(interiorMargin) || (interiorMargin < 0) || (interiorMargin > 50)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() requires interiorMargin to be in [0, 50]." << EidosTerminate(nullptr); } if (plotview_->legendAdded()) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_addLegend): addLegend() has already been called for this plot, and should only be called once." << EidosTerminate(nullptr); plotview_->addLegend(position, inset, labelSize, lineHeight, graphicsWidth, exteriorMargin, interiorMargin); return gStaticEidosValueVOID; } // ********************* – (void)axis(integer$ side, [Nif at = NULL], [ls labels = T]) // EidosValue_SP Plot::ExecuteMethod_axis(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *side_value = p_arguments[0].get(); EidosValue *at_value = p_arguments[1].get(); EidosValue *labels_value = p_arguments[2].get(); // side int side = (int)side_value->IntAtIndex_NOCAST(0, nullptr); if ((side != 1) && (side != 2)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_axis): axis() requires side to be 1 (for the x axis) or 2 (for the y axis)." << EidosTerminate(nullptr); // at std::vector *at = nullptr; int at_length = 0; double last_at = -std::numeric_limits::infinity(); if (at_value->Type() != EidosValueType::kValueNULL) { at_length = at_value->Count(); at = new std::vector; for (int index = 0; index < at_length; ++index) { double pos = at_value->NumericAtIndex_NOCAST(index, nullptr); if (!std::isfinite(pos)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_axis): axis() requires the elements of at to be finite." << EidosTerminate(nullptr); if (pos <= last_at) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_axis): axis() requires the elements of at to be in sorted (increasing) order." << EidosTerminate(nullptr); last_at = pos; at->push_back(pos); } } // labels std::vector *labels; int labels_type; if (labels_value->Type() == EidosValueType::kValueLogical) { // labels can be T, F, or a vector of type string; we need a separate flag to differentiate those cases // T is 1, F is 0, and the string vector case is 2 labels = nullptr; labels_type = (int)labels_value->LogicalAtIndex_NOCAST(0, nullptr); } else { if (at_value->Type() == EidosValueType::kValueNULL) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_axis): axis() requires that when at is NULL, labels be T or F; a vector of labels cannot be supplied without corresponding positions." << EidosTerminate(nullptr); const std::string *string_data = labels_value->StringData(); int labels_length = labels_value->Count(); if (labels_length != at_length) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_axis): axis() requires that labels be the same length as at (if labels is not T or F), to supply a label for each corresponding position." << EidosTerminate(nullptr); labels = new std::vector; labels_type = 2; for (int index = 0; index < labels_length; ++index) labels->emplace_back(QString::fromStdString(string_data[index])); } plotview_->setAxisConfiguration(side, at, labels_type, labels); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)image(object$ image, numeric$ x1, numeric$ y1, numeric$ x2, numeric$ y2, [logical$ flipped = F], [float$ alpha = 1.0]) // EidosValue_SP Plot::ExecuteMethod_image(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *image_value = p_arguments[0].get(); EidosValue *x1_value = p_arguments[1].get(); EidosValue *y1_value = p_arguments[2].get(); EidosValue *x2_value = p_arguments[3].get(); EidosValue *y2_value = p_arguments[4].get(); EidosValue *flipped_value = p_arguments[5].get(); EidosValue *alpha_value = p_arguments[6].get(); // flipped -- handled out of order because we need this to process image bool flipped = flipped_value->LogicalAtIndex_NOCAST(0, nullptr); // image EidosObject *image_object = image_value->ObjectElementAtIndex_NOCAST(0, nullptr); QImage image; if (image_object->Class() == gSLiM_SpatialMap_Class) { // if image is a SpatialImage, plot the map's values; it must be a singleton and have 2D spatiality if (image_value->Count() != 1) EIDOS_TERMINATION << "ERROR (Plot::image): a SpatialMap value passed to image() must be a singleton." << EidosTerminate(nullptr); SpatialMap *spatial_map = (SpatialMap *)image_object; if (spatial_map->image_ && (spatial_map->image_flipped_ == flipped)) { // we have a cached QImage for this SpatialMap, and it matches our flipped flag image = *(QImage *)spatial_map->image_; // make a copy with implicit sharing } else { // cache the spatial map's image if it doesn't already have a matching cache; first delete any existing (unmatching) cache if (spatial_map->image_) { if (spatial_map->image_deleter_) spatial_map->image_deleter_(spatial_map->image_); else std::cout << "Missing SpatialMap image_deleter_; leaking memory" << std::endl; spatial_map->image_ = nullptr; spatial_map->image_deleter_ = nullptr; } if (spatial_map->spatiality_ != 2) EIDOS_TERMINATION << "ERROR (Plot::image): image() only supports plotting of spatial maps of spatiality 2 ('xy', 'xz', or 'yz' maps)." << EidosTerminate(nullptr); int64_t image_width = spatial_map->grid_size_[0]; int64_t image_height = spatial_map->grid_size_[1]; if ((image_width <= 1) || (image_height <= 1)) EIDOS_TERMINATION << "ERROR (Plot::image): image() requires the plotted spatial map to be 2x2 or larger in its grid dimensions." << EidosTerminate(nullptr); // the edge rows/columns get one pixel; interior rows/edges get two pixels // this gives us an image that correctly matches the spatial map in SLiM image_width = image_width * 2 - 2; image_height = image_height * 2 - 2; // make the image buffer to be used by QtSLiMGraphView_CustomPlot; note that it takes ownership of image_data and frees it for us const int bytes_per_pixel = 3; // RGB888 format uint8_t *image_data = (uint8_t *)malloc(image_width * image_height * bytes_per_pixel * sizeof(uint8_t)); // we never interpolate map values, since the correct resolution is ambiguous; we're generating vector graphics here // FIXME: maybe the spatial map display in the individuals view should turn off interpolation too; let the user interpolate the map if they want interpolated display spatial_map->FillRGBBuffer(image_data, image_width, image_height, flipped, /* no_interpolation */ false); QImage *cached_image = new QImage(image_data, image_width, image_height, image_width * bytes_per_pixel, QImage::Format_RGB888, free, image_data); // We give the cached image to the SpatialMap object. Since it doesn't build against Qt, we give it a deletor function. spatial_map->image_ = cached_image; spatial_map->image_flipped_ = flipped; spatial_map->image_deleter_ = Eidos_Deleter; image = *cached_image; // make a copy with implicit sharing } } else if (image_object->Class() == gEidosImage_Class) { // if image is an Image, plot the image's values; it must be a singleton if (image_value->Count() != 1) EIDOS_TERMINATION << "ERROR (Plot::image): an Image value passed to image() must be a singleton." << EidosTerminate(nullptr); EidosImage *eidos_image = (EidosImage *)image_object; if (eidos_image->image_ && (eidos_image->image_flipped_ == flipped)) { // we have a cached QImage for this Image, and it matches our flipped flag image = *(QImage *)eidos_image->image_; // make a copy with implicit sharing } else { // cache the Image's image if it doesn't already have a matching cache; first delete any existing (unmatching) cache if (eidos_image->image_) { if (eidos_image->image_deleter_) eidos_image->image_deleter_(eidos_image->image_); else std::cout << "Missing Image image_deleter_; leaking memory" << std::endl; eidos_image->image_ = nullptr; eidos_image->image_deleter_ = nullptr; } int64_t image_width = eidos_image->width_; int64_t image_height = eidos_image->height_; // make the image buffer to be used by QtSLiMGraphView_CustomPlot; note that it takes ownership of image_data and frees it for us const int bytes_per_pixel = (eidos_image->is_grayscale_ ? 1 : 3); // Grayscale8 or RGB888 format uint8_t *image_data = (uint8_t *)malloc(image_width * image_height * bytes_per_pixel * sizeof(uint8_t)); // optionally flip the rows of the image buffer so it displays in the orientation we want; this could be an option // note that here flipping is done by default, so flipped=true turns off this flip! if (!flipped) { for (int y = 0; y < image_height; ++y) memcpy(image_data + y * (image_width * bytes_per_pixel * sizeof(uint8_t)), eidos_image->pixels_.data() + (image_height - y - 1) * (image_width * bytes_per_pixel * sizeof(uint8_t)), image_width * bytes_per_pixel * sizeof(uint8_t)); } else { memcpy(image_data, eidos_image->pixels_.data(), image_width * image_height * bytes_per_pixel * sizeof(uint8_t)); } QImage *cached_image = new QImage(image_data, image_width, image_height, image_width * bytes_per_pixel, (eidos_image->is_grayscale_ ? QImage::Format_Grayscale8 : QImage::Format_RGB888), free, image_data); // We give the cached image to the EidosImage object. Since it doesn't build against Qt, we give it a deletor function. eidos_image->image_ = cached_image; eidos_image->image_flipped_ = flipped; eidos_image->image_deleter_ = Eidos_Deleter; image = *cached_image; // make a copy with implicit sharing } } else { EIDOS_TERMINATION << "ERROR (Plot::image): unsupported object class in image(); image must be of class SpatialMap or class Image." << EidosTerminate(nullptr); } // x and y double *x = (double *)malloc(2 * sizeof(double)); double *y = (double *)malloc(2 * sizeof(double)); if (!x || !y) EIDOS_TERMINATION << "ERROR (Plot::image): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); x[0] = x1_value->NumericAtIndex_NOCAST(0, nullptr); y[0] = y1_value->NumericAtIndex_NOCAST(0, nullptr); x[1] = x2_value->NumericAtIndex_NOCAST(0, nullptr); y[1] = y2_value->NumericAtIndex_NOCAST(0, nullptr); if (x[0] > x[1]) EIDOS_TERMINATION << "ERROR (Plot::image): image() requires x1 <= x2." << EidosTerminate(nullptr); if (y[0] > y[1]) EIDOS_TERMINATION << "ERROR (Plot::image): image() requires y1 <= y2." << EidosTerminate(nullptr); // alpha double alpha = alpha_value->FloatAtIndex_NOCAST(0, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::image): image() requires the image alpha to be in [0, 1]." << EidosTerminate(nullptr); std::vector *imageAlphas = new std::vector; // we only take a singleton alpha, but the API expects a buffer imageAlphas->push_back(alpha); plotview_->addImageData(x, y, 2, image, imageAlphas); // implicitly shares image; takes the x, y, and imageAlphas buffers from us return gStaticEidosValueVOID; } // ********************* – (void)legendLineEntry(string$ label, [string$ color = "red"], [numeric$ lwd = 1.0]) // EidosValue_SP Plot::ExecuteMethod_legendLineEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *label_value = p_arguments[0].get(); EidosValue *color_value = p_arguments[1].get(); EidosValue *lwd_value = p_arguments[2].get(); // label QString label = QString::fromStdString(label_value->StringAtIndex_NOCAST(0, nullptr)); if (label.length() == 0) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendLineEntry): legendLineEntry() requires a non-empty legend label." << EidosTerminate(); // color std::string color_string = color_value->StringAtIndex_NOCAST(0, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); QColor color(colorR, colorG, colorB, 255); // lwd double lwd = lwd_value->NumericAtIndex_NOCAST(0, nullptr); if ((lwd < 0.0) || (lwd > 100.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendLineEntry): legendLineEntry() requires the line width lwd to be in [0, 100]." << EidosTerminate(nullptr); if (!plotview_->legendAdded()) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendLineEntry): addLegend() must be called before adding legend entries." << EidosTerminate(nullptr); plotview_->addLegendLineEntry(label, color, lwd); return gStaticEidosValueVOID; } // ********************* – (void)legendPointEntry(string$ label, [integer$ symbol = 0], [string$ color = "red"], [string$ border = "black"], [numeric$ lwd = 1.0], [numeric$ size = 1.0]) // EidosValue_SP Plot::ExecuteMethod_legendPointEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *label_value = p_arguments[0].get(); EidosValue *symbol_value = p_arguments[1].get(); EidosValue *color_value = p_arguments[2].get(); EidosValue *border_value = p_arguments[3].get(); EidosValue *lwd_value = p_arguments[4].get(); EidosValue *size_value = p_arguments[5].get(); // label QString label = QString::fromStdString(label_value->StringAtIndex_NOCAST(0, nullptr)); if (label.length() == 0) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendPointEntry): legendPointEntry() requires a non-empty legend label." << EidosTerminate(); // symbol int symbol = symbol_value->IntAtIndex_NOCAST(0, nullptr); if (symbol < 0) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendPointEntry): legendPointEntry() requires the elements of symbol to be >= 0." << EidosTerminate(); // color std::string color_string = color_value->StringAtIndex_NOCAST(0, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); QColor color(colorR, colorG, colorB, 255); // border std::string border_string = border_value->StringAtIndex_NOCAST(0, nullptr); uint8_t borderR, borderG, borderB; Eidos_GetColorComponents(border_string, &borderR, &borderG, &borderB); QColor border(borderR, borderG, borderB, 255); // lwd double lwd = lwd_value->NumericAtIndex_NOCAST(0, nullptr); if ((lwd < 0.0) || (lwd > 100.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendPointEntry): legendPointEntry() requires the elements of lwd to be in [0, 100]." << EidosTerminate(nullptr); // size double size = size_value->NumericAtIndex_NOCAST(0, nullptr); if ((size <= 0.0) || (size > 1000.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendPointEntry): legendPointEntry() requires the elements of size to be in (0, 1000]." << EidosTerminate(nullptr); if (!plotview_->legendAdded()) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendPointEntry): addLegend() must be called before adding legend entries." << EidosTerminate(nullptr); plotview_->addLegendPointEntry(label, symbol, color, border, lwd, size); return gStaticEidosValueVOID; } // ********************* – (void)legendSwatchEntry(string$ label, [string$ color = "red"]) // EidosValue_SP Plot::ExecuteMethod_legendSwatchEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *label_value = p_arguments[0].get(); EidosValue *color_value = p_arguments[1].get(); // label QString label = QString::fromStdString(label_value->StringAtIndex_NOCAST(0, nullptr)); if (label.length() == 0) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendSwatchEntry): legendSwatchEntry() requires a non-empty legend label." << EidosTerminate(); // color std::string color_string = color_value->StringAtIndex_NOCAST(0, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); QColor color(colorR, colorG, colorB, 255); if (!plotview_->legendAdded()) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendSwatchEntry): addLegend() must be called before adding legend entries." << EidosTerminate(nullptr); plotview_->addLegendSwatchEntry(label, color); return gStaticEidosValueVOID; } // ********************* – (void)legendTitleEntry(string$ label) // EidosValue_SP Plot::ExecuteMethod_legendTitleEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *label_value = p_arguments[0].get(); // label QString label = QString::fromStdString(label_value->StringAtIndex_NOCAST(0, nullptr)); if (!plotview_->legendAdded()) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_legendTitleEntry): addLegend() must be called before adding legend entries." << EidosTerminate(nullptr); plotview_->addLegendTitleEntry(label); return gStaticEidosValueVOID; } // ********************* – (void)lines(numeric x, numeric y, [string$ color = "red"], [numeric$ lwd = 1.0], [float$ alpha = 1.0]) // EidosValue_SP Plot::ExecuteMethod_lines(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *x_value = p_arguments[0].get(); EidosValue *y_value = p_arguments[1].get(); EidosValue *color_value = p_arguments[2].get(); EidosValue *lwd_value = p_arguments[3].get(); EidosValue *alpha_value = p_arguments[4].get(); // x and y int xcount = x_value->Count(); int ycount = y_value->Count(); if (xcount != ycount) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_lines): lines() requires x and y to be the same length." << EidosTerminate(); double *x = (double *)malloc(xcount * sizeof(double)); double *y = (double *)malloc(ycount * sizeof(double)); if (!x || !y) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_lines): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); if (x_value->Type() == EidosValueType::kValueFloat) { memcpy(x, x_value->FloatData(), xcount * sizeof(double)); } else { const int64_t *int_data = x_value->IntData(); for (int index = 0; index < xcount; ++index) x[index] = int_data[index]; } if (y_value->Type() == EidosValueType::kValueFloat) { memcpy(y, y_value->FloatData(), ycount * sizeof(double)); } else { const int64_t *int_data = y_value->IntData(); for (int index = 0; index < ycount; ++index) y[index] = int_data[index]; } // color std::string color_string = color_value->StringAtIndex_NOCAST(0, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); std::vector *colors = new std::vector; colors->emplace_back(colorR, colorG, colorB, 255); // lwd double lwd = lwd_value->NumericAtIndex_NOCAST(0, nullptr); if ((lwd < 0.0) || (lwd > 100.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_lines): lines() requires the line width lwd to be in [0, 100]." << EidosTerminate(nullptr); std::vector *lineWidths = new std::vector; // we only take a singleton width, but the API expects a buffer lineWidths->push_back(lwd); // alpha double alpha = alpha_value->FloatAtIndex_NOCAST(0, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_lines): lines() requires the line width alpha to be in [0, 1]." << EidosTerminate(nullptr); std::vector *lineAlphas = new std::vector; // we only take a singleton alpha, but the API expects a buffer lineAlphas->push_back(alpha); plotview_->addLineData(x, y, xcount, colors, lineAlphas, lineWidths); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)matrix(object$ image, numeric$ x1, numeric$ y1, numeric$ x2, numeric$ y2, [logical$ flipped = F], // [Nif valueRange = NULL], [Ns$ colors = NULL], [float$ alpha = 1.0]) // EidosValue_SP Plot::ExecuteMethod_matrix(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *matrix_value = p_arguments[0].get(); EidosValue *x1_value = p_arguments[1].get(); EidosValue *y1_value = p_arguments[2].get(); EidosValue *x2_value = p_arguments[3].get(); EidosValue *y2_value = p_arguments[4].get(); EidosValue *flipped_value = p_arguments[5].get(); EidosValue *valueRange_value = p_arguments[6].get(); EidosValue *colors_value = p_arguments[7].get(); EidosValue *alpha_value = p_arguments[8].get(); // flipped bool flipped = flipped_value->LogicalAtIndex_NOCAST(0, nullptr); // valueRange double range_min = 0.0, range_max = 0.0; if (valueRange_value->Type() == EidosValueType::kValueNULL) { range_min = 0.0; range_max = 1.0; } else { if (valueRange_value->Count() != 2) EIDOS_TERMINATION << "ERROR (Plot::matrix): matrix() requires valueRange to be a vector of length 2 providing a data range, or NULL to use the default data range of [0, 1]." << EidosTerminate(nullptr); range_min = valueRange_value->NumericAtIndex_NOCAST(0, nullptr); range_max = valueRange_value->NumericAtIndex_NOCAST(1, nullptr); if (!std::isfinite(range_min) || !std::isfinite(range_max) || (range_min == range_max)) EIDOS_TERMINATION << "ERROR (Plot::matrix): matrix() requires valueRange to contain finite, unequal values that define a data range." << EidosTerminate(nullptr); } // colors std::string colors_name; if (colors_value->Type() == EidosValueType::kValueNULL) { colors_name = "gray"; std::swap(range_min, range_max); } else { colors_name = colors_value->StringAtIndex_NOCAST(0, nullptr); } EidosColorPalette palette = Eidos_PaletteForName(colors_name); if (palette == EidosColorPalette::kPalette_INVALID) EIDOS_TERMINATION << "ERROR (Plot::matrix): unrecognized color palette name in matrix()." << EidosTerminate(nullptr); // figure out the rescaling factors in effect; note that colors=NULL might have reversed the rescaling range bool rescaling = false; double rescale_offset = 0.0; double rescale_scaling = 1.0; if ((range_min != 0.0) || (range_max != 1.0)) { rescaling = true; rescale_offset = -range_min; rescale_scaling = 1.0 / (range_max - range_min); } // matrix if ((matrix_value->DimensionCount() != 2)) EIDOS_TERMINATION << "ERROR (Plot::matrix): matrix() requires that parameter matrix is actually a matrix (not a vector or array)." << EidosTerminate(nullptr); const int64_t *dims = matrix_value->Dimensions(); int64_t matrix_rows = dims[0]; int64_t matrix_columns = dims[1]; if ((matrix_value->DimensionCount() != 2) || (matrix_rows < 1) || (matrix_columns < 1)) EIDOS_TERMINATION << "ERROR (Plot::matrix): matrix() requires a matrix that is at least 1x1 in its dimensions." << EidosTerminate(nullptr); // make the image buffer to be used by QtSLiMGraphView_CustomPlot; note that it takes ownership of image_data and frees it for us int64_t image_width = matrix_columns; int64_t image_height = matrix_rows; const int bytes_per_pixel = 3; // RGB888 format uint8_t *image_data = (uint8_t *)malloc(matrix_rows * matrix_columns * bytes_per_pixel * sizeof(uint8_t)); uint8_t *image_data_ptr = image_data; // optionally flip the rows of the image buffer so it displays in the orientation we want; this could be an option // note that here flipping is done by default, so flipped=true turns off this flip! if (matrix_value->Type() == EidosValueType::kValueInt) { const int64_t *matrix_data = matrix_value->IntData(); for (int64_t matrix_row = 0; matrix_row < matrix_rows; ++matrix_row) { int64_t matrix_row_to_read = (flipped ? matrix_row : (matrix_rows - 1) - matrix_row); for (int64_t matrix_column = 0; matrix_column < matrix_columns; ++matrix_column) { double original_value = matrix_data[matrix_row_to_read + matrix_column * matrix_rows]; double value = original_value; if (rescaling) value = (value + rescale_offset) * rescale_scaling; if (value < 0.0) value = 0.0; if (value > 1.0) value = 1.0; double red, green, blue; Eidos_ColorPaletteLookup(value, palette, red, green, blue); uint8_t r_i = round(red * 255.0); uint8_t g_i = round(green * 255.0); uint8_t b_i = round(blue * 255.0); *(image_data_ptr++) = r_i; *(image_data_ptr++) = g_i; *(image_data_ptr++) = b_i; //std::cout << "original_value " << original_value << " rescaled to value " << value << ", r " << (uint32_t)r_i << ", g " << (uint32_t)g_i << ", b " << (uint32_t)b_i << std::endl; } } } else { const double *matrix_data = matrix_value->FloatData(); for (int64_t matrix_row = 0; matrix_row < matrix_rows; ++matrix_row) { int64_t matrix_row_to_read = (flipped ? matrix_row : (matrix_rows - 1) - matrix_row); for (int64_t matrix_column = 0; matrix_column < matrix_columns; ++matrix_column) { double original_value = matrix_data[matrix_row_to_read + matrix_column * matrix_rows]; double value = original_value; if (rescaling) value = (value + rescale_offset) * rescale_scaling; if (value < 0.0) value = 0.0; if (value > 1.0) value = 1.0; double red, green, blue; Eidos_ColorPaletteLookup(value, palette, red, green, blue); uint8_t r_i = round(red * 255.0); uint8_t g_i = round(green * 255.0); uint8_t b_i = round(blue * 255.0); *(image_data_ptr++) = r_i; *(image_data_ptr++) = g_i; *(image_data_ptr++) = b_i; //std::cout << "original_value " << original_value << " rescaled to value " << value << ", r " << (uint32_t)r_i << ", g " << (uint32_t)g_i << ", b " << (uint32_t)b_i << std::endl; } } } QImage image(image_data, image_width, image_height, image_width * bytes_per_pixel, QImage::Format_RGB888, free, image_data); // x and y double *x = (double *)malloc(2 * sizeof(double)); double *y = (double *)malloc(2 * sizeof(double)); if (!x || !y) EIDOS_TERMINATION << "ERROR (Plot::image): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); x[0] = x1_value->NumericAtIndex_NOCAST(0, nullptr); y[0] = y1_value->NumericAtIndex_NOCAST(0, nullptr); x[1] = x2_value->NumericAtIndex_NOCAST(0, nullptr); y[1] = y2_value->NumericAtIndex_NOCAST(0, nullptr); if (x[0] > x[1]) EIDOS_TERMINATION << "ERROR (Plot::image): image() requires x1 <= x2." << EidosTerminate(nullptr); if (y[0] > y[1]) EIDOS_TERMINATION << "ERROR (Plot::image): image() requires y1 <= y2." << EidosTerminate(nullptr); // alpha double alpha = alpha_value->FloatAtIndex_NOCAST(0, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::image): image() requires the image alpha to be in [0, 1]." << EidosTerminate(nullptr); std::vector *imageAlphas = new std::vector; // we only take a singleton alpha, but the API expects a buffer imageAlphas->push_back(alpha); plotview_->addImageData(x, y, 2, image, imageAlphas); // implicitly shares image; takes the x, y, and imageAlphas buffers from us return gStaticEidosValueVOID; } // ********************* – (void)mtext(numeric x, numeric y, string labels, [string color = "black"], [numeric size = 10.0], [Nif adj = NULL], [float alpha = 1.0], [numeric angle = 0.0]) // EidosValue_SP Plot::ExecuteMethod_mtext(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *x_value = p_arguments[0].get(); EidosValue *y_value = p_arguments[1].get(); EidosValue *labels_value = p_arguments[2].get(); EidosValue *color_value = p_arguments[3].get(); EidosValue *size_value = p_arguments[4].get(); EidosValue *adj_value = p_arguments[5].get(); EidosValue *alpha_value = p_arguments[6].get(); EidosValue *angle_value = p_arguments[7].get(); // x and y int xcount = x_value->Count(); int ycount = y_value->Count(); int labelscount = labels_value->Count(); if ((xcount != ycount) || (xcount != labelscount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires x, y, and labels to be the same length." << EidosTerminate(); double *x = (double *)malloc(xcount * sizeof(double)); double *y = (double *)malloc(ycount * sizeof(double)); if (!x || !y) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); if (x_value->Type() == EidosValueType::kValueFloat) { memcpy(x, x_value->FloatData(), xcount * sizeof(double)); } else { const int64_t *int_data = x_value->IntData(); for (int index = 0; index < xcount; ++index) x[index] = int_data[index]; } if (y_value->Type() == EidosValueType::kValueFloat) { memcpy(y, y_value->FloatData(), ycount * sizeof(double)); } else { const int64_t *int_data = y_value->IntData(); for (int index = 0; index < ycount; ++index) y[index] = int_data[index]; } // labels std::vector *labels = new std::vector; const std::string *string_data = labels_value->StringData(); for (int index = 0; index < labelscount; ++index) labels->emplace_back(QString::fromStdString(string_data[index])); // color std::vector *colors = new std::vector; int color_count = color_value->Count(); if ((color_count != 1) && (color_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires color to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < color_count; ++index) { std::string color_string = color_value->StringAtIndex_NOCAST(index, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); colors->emplace_back(colorR, colorG, colorB, 255); } // size std::vector *sizes = new std::vector; int size_count = size_value->Count(); if ((size_count != 1) && (size_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires size to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < size_count; ++index) { double size = size_value->NumericAtIndex_NOCAST(index, nullptr); if ((size <= 0.0) || (size > 1000.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires the elements of size to be in (0, 1000]." << EidosTerminate(nullptr); sizes->push_back(size); } // adj double adj[2] = {0.5, 0.5}; if (adj_value->Type() != EidosValueType::kValueNULL) { if (adj_value->Count() != 2) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires adj to be a numeric vector of length 2, or NULL." << EidosTerminate(); adj[0] = adj_value->NumericAtIndex_NOCAST(0, nullptr); adj[1] = adj_value->NumericAtIndex_NOCAST(1, nullptr); } // alpha std::vector *alphas = new std::vector; int alpha_count = alpha_value->Count(); if ((alpha_count != 1) && (alpha_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires alpha to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < alpha_count; ++index) { double alpha = alpha_value->FloatAtIndex_NOCAST(index, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires the elements of alpha to be in [0, 1]." << EidosTerminate(nullptr); alphas->push_back(alpha); } // angle std::vector *angles = new std::vector; int angle_count = angle_value->Count(); if ((angle_count != 1) && (angle_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires angle to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < angle_count; ++index) { double angle = angle_value->NumericAtIndex_NOCAST(index, nullptr); if (!std::isfinite(angle)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): mtext() requires the elements of angle to be finite." << EidosTerminate(nullptr); angles->push_back(angle); } plotview_->addMarginTextData(x, y, labels, xcount, colors, alphas, sizes, adj, angles); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)points(numeric x, numeric y, [integer symbol = 0], [string color = "red"], [string border = "black"], [numeric lwd = 1.0], [numeric size = 1.0], [float alpha = 1.0]) // EidosValue_SP Plot::ExecuteMethod_points(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *x_value = p_arguments[0].get(); EidosValue *y_value = p_arguments[1].get(); EidosValue *symbol_value = p_arguments[2].get(); EidosValue *color_value = p_arguments[3].get(); EidosValue *border_value = p_arguments[4].get(); EidosValue *lwd_value = p_arguments[5].get(); EidosValue *size_value = p_arguments[6].get(); EidosValue *alpha_value = p_arguments[7].get(); // x and y int xcount = x_value->Count(); int ycount = y_value->Count(); if (xcount != ycount) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires x and y to be the same length." << EidosTerminate(); double *x = (double *)malloc(xcount * sizeof(double)); double *y = (double *)malloc(ycount * sizeof(double)); if (!x || !y) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); if (x_value->Type() == EidosValueType::kValueFloat) { memcpy(x, x_value->FloatData(), xcount * sizeof(double)); } else { const int64_t *int_data = x_value->IntData(); for (int index = 0; index < xcount; ++index) x[index] = int_data[index]; } if (y_value->Type() == EidosValueType::kValueFloat) { memcpy(y, y_value->FloatData(), ycount * sizeof(double)); } else { const int64_t *int_data = y_value->IntData(); for (int index = 0; index < ycount; ++index) y[index] = int_data[index]; } // symbol std::vector *symbols = new std::vector; int symbol_count = symbol_value->Count(); if ((symbol_count != 1) && (symbol_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires symbol to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < symbol_count; ++index) { int symbol = symbol_value->IntAtIndex_NOCAST(index, nullptr); if (symbol < 0) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires the elements of symbol to be >= 0." << EidosTerminate(); symbols->push_back(symbol); } // color std::vector *colors = new std::vector; int color_count = color_value->Count(); if ((color_count != 1) && (color_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires color to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < color_count; ++index) { std::string color_string = color_value->StringAtIndex_NOCAST(index, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); colors->emplace_back(colorR, colorG, colorB, 255); } // border std::vector *borders = new std::vector; int border_count = border_value->Count(); if ((border_count != 1) && (border_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires border to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < border_count; ++index) { std::string border_string = border_value->StringAtIndex_NOCAST(index, nullptr); uint8_t borderR, borderG, borderB; Eidos_GetColorComponents(border_string, &borderR, &borderG, &borderB); borders->emplace_back(borderR, borderG, borderB, 255); } // lwd std::vector *lwds = new std::vector; int lwd_count = lwd_value->Count(); if ((lwd_count != 1) && (lwd_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires lwd to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < lwd_count; ++index) { double lwd = lwd_value->NumericAtIndex_NOCAST(index, nullptr); if ((lwd < 0.0) || (lwd > 100.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires the elements of lwd to be in [0, 100]." << EidosTerminate(nullptr); lwds->push_back(lwd); } // size std::vector *sizes = new std::vector; int size_count = size_value->Count(); if ((size_count != 1) && (size_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires size to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < size_count; ++index) { double size = size_value->NumericAtIndex_NOCAST(index, nullptr); if ((size <= 0.0) || (size > 1000.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires the elements of size to be in (0, 1000]." << EidosTerminate(nullptr); sizes->push_back(size); } // alpha std::vector *alphas = new std::vector; int alpha_count = alpha_value->Count(); if ((alpha_count != 1) && (alpha_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires alpha to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < alpha_count; ++index) { double alpha = alpha_value->FloatAtIndex_NOCAST(index, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_points): points() requires the elements of alpha to be in [0, 1]." << EidosTerminate(nullptr); alphas->push_back(alpha); } plotview_->addPointData(x, y, xcount, symbols, colors, borders, alphas, lwds, sizes); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)rects(numeric x1, numeric y1, numeric x2, numeric y2, [string color = "red"], [string border = "black"], [numeric lwd = 1.0], [float alpha = 1.0]) // EidosValue_SP Plot::ExecuteMethod_rects(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *x1_value = p_arguments[0].get(); EidosValue *y1_value = p_arguments[1].get(); EidosValue *x2_value = p_arguments[2].get(); EidosValue *y2_value = p_arguments[3].get(); EidosValue *color_value = p_arguments[4].get(); EidosValue *border_value = p_arguments[5].get(); EidosValue *lwd_value = p_arguments[6].get(); EidosValue *alpha_value = p_arguments[7].get(); // x1, y1, x2, y2 int segment_count = x1_value->Count(); int y1count = y1_value->Count(); int x2count = x2_value->Count(); int y2count = y2_value->Count(); if ((segment_count != y1count) || (segment_count != x2count) || (segment_count != y2count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): rects() requires x1, y1, x2, and y2 to be the same length." << EidosTerminate(); double *x1 = (double *)malloc(segment_count * sizeof(double)); double *y1 = (double *)malloc(segment_count * sizeof(double)); double *x2 = (double *)malloc(segment_count * sizeof(double)); double *y2 = (double *)malloc(segment_count * sizeof(double)); if (!x1 || !y1 || !x2 || !y2) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); if (x1_value->Type() == EidosValueType::kValueFloat) { memcpy(x1, x1_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = x1_value->IntData(); for (int index = 0; index < segment_count; ++index) x1[index] = int_data[index]; } if (y1_value->Type() == EidosValueType::kValueFloat) { memcpy(y1, y1_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = y1_value->IntData(); for (int index = 0; index < segment_count; ++index) y1[index] = int_data[index]; } if (x2_value->Type() == EidosValueType::kValueFloat) { memcpy(x2, x2_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = x2_value->IntData(); for (int index = 0; index < segment_count; ++index) x2[index] = int_data[index]; } if (y2_value->Type() == EidosValueType::kValueFloat) { memcpy(y2, y2_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = y2_value->IntData(); for (int index = 0; index < segment_count; ++index) y2[index] = int_data[index]; } // color std::vector *colors = new std::vector; int color_count = color_value->Count(); if ((color_count != 1) && (color_count != segment_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): rects() requires color to match the length of x1/y1/x2/y2, or be singleton." << EidosTerminate(); for (int index = 0; index < color_count; ++index) { std::string color_string = color_value->StringAtIndex_NOCAST(index, nullptr); if (color_string == "none") { // "none" is a special named color provided by rects() to say you don't want a frame or fill colors->emplace_back(Qt::transparent); } else { uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); colors->emplace_back(colorR, colorG, colorB, 255); } } // border std::vector *borders = new std::vector; int border_count = border_value->Count(); if ((border_count != 1) && (border_count != segment_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): rects() requires border to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < border_count; ++index) { std::string border_string = border_value->StringAtIndex_NOCAST(index, nullptr); if (border_string == "none") { // "none" is a special named color provided by rects() to say you don't want a frame or fill borders->emplace_back(Qt::transparent); } else { uint8_t borderR, borderG, borderB; Eidos_GetColorComponents(border_string, &borderR, &borderG, &borderB); borders->emplace_back(borderR, borderG, borderB, 255); } } // lwd std::vector *lwds = new std::vector; int lwd_count = lwd_value->Count(); if ((lwd_count != 1) && (lwd_count != segment_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): rects() requires lwd to match the length of x1/y1/x2/y2, or be singleton." << EidosTerminate(); for (int index = 0; index < lwd_count; ++index) { double lwd = lwd_value->NumericAtIndex_NOCAST(index, nullptr); if ((lwd < 0.0) || (lwd > 100.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): rects() requires the elements of lwd to be in [0, 100]." << EidosTerminate(nullptr); lwds->push_back(lwd); } // alpha std::vector *alphas = new std::vector; int alpha_count = alpha_value->Count(); if ((alpha_count != 1) && (alpha_count != segment_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): rects() requires alpha to match the length of x1/y1/x2/y2, or be singleton." << EidosTerminate(); for (int index = 0; index < alpha_count; ++index) { double alpha = alpha_value->FloatAtIndex_NOCAST(index, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_rects): rects() requires the elements of alpha to be in [0, 1]." << EidosTerminate(nullptr); alphas->push_back(alpha); } plotview_->addRectData(x1, y1, x2, y2, segment_count, colors, borders, alphas, lwds); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)segments(numeric x1, numeric y1, numeric x2, numeric y2, [string color = "red"], [numeric lwd = 1.0], [float alpha = 1.0]) // EidosValue_SP Plot::ExecuteMethod_segments(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *x1_value = p_arguments[0].get(); EidosValue *y1_value = p_arguments[1].get(); EidosValue *x2_value = p_arguments[2].get(); EidosValue *y2_value = p_arguments[3].get(); EidosValue *color_value = p_arguments[4].get(); EidosValue *lwd_value = p_arguments[5].get(); EidosValue *alpha_value = p_arguments[6].get(); // x1, y1, x2, y2 int segment_count = x1_value->Count(); int y1count = y1_value->Count(); int x2count = x2_value->Count(); int y2count = y2_value->Count(); if ((segment_count != y1count) || (segment_count != x2count) || (segment_count != y2count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_segments): segments() requires x1, y1, x2, and y2 to be the same length." << EidosTerminate(); double *x1 = (double *)malloc(segment_count * sizeof(double)); double *y1 = (double *)malloc(segment_count * sizeof(double)); double *x2 = (double *)malloc(segment_count * sizeof(double)); double *y2 = (double *)malloc(segment_count * sizeof(double)); if (!x1 || !y1 || !x2 || !y2) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_segments): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); if (x1_value->Type() == EidosValueType::kValueFloat) { memcpy(x1, x1_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = x1_value->IntData(); for (int index = 0; index < segment_count; ++index) x1[index] = int_data[index]; } if (y1_value->Type() == EidosValueType::kValueFloat) { memcpy(y1, y1_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = y1_value->IntData(); for (int index = 0; index < segment_count; ++index) y1[index] = int_data[index]; } if (x2_value->Type() == EidosValueType::kValueFloat) { memcpy(x2, x2_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = x2_value->IntData(); for (int index = 0; index < segment_count; ++index) x2[index] = int_data[index]; } if (y2_value->Type() == EidosValueType::kValueFloat) { memcpy(y2, y2_value->FloatData(), segment_count * sizeof(double)); } else { const int64_t *int_data = y2_value->IntData(); for (int index = 0; index < segment_count; ++index) y2[index] = int_data[index]; } // color std::vector *colors = new std::vector; int color_count = color_value->Count(); if ((color_count != 1) && (color_count != segment_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_segments): segments() requires color to match the length of x1/y1/x2/y2, or be singleton." << EidosTerminate(); for (int index = 0; index < color_count; ++index) { std::string color_string = color_value->StringAtIndex_NOCAST(index, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); colors->emplace_back(colorR, colorG, colorB, 255); } // lwd std::vector *lwds = new std::vector; int lwd_count = lwd_value->Count(); if ((lwd_count != 1) && (lwd_count != segment_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_segments): segments() requires lwd to match the length of x1/y1/x2/y2, or be singleton." << EidosTerminate(); for (int index = 0; index < lwd_count; ++index) { double lwd = lwd_value->NumericAtIndex_NOCAST(index, nullptr); if ((lwd < 0.0) || (lwd > 100.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_segments): segments() requires the elements of lwd to be in [0, 100]." << EidosTerminate(nullptr); lwds->push_back(lwd); } // alpha std::vector *alphas = new std::vector; int alpha_count = alpha_value->Count(); if ((alpha_count != 1) && (alpha_count != segment_count)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_segments): segments() requires alpha to match the length of x1/y1/x2/y2, or be singleton." << EidosTerminate(); for (int index = 0; index < alpha_count; ++index) { double alpha = alpha_value->FloatAtIndex_NOCAST(index, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_segments): segments() requires the elements of alpha to be in [0, 1]." << EidosTerminate(nullptr); alphas->push_back(alpha); } plotview_->addSegmentData(x1, y1, x2, y2, segment_count, colors, alphas, lwds); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)setBorderless([numeric marginLeft = 0.0], [numeric marginTop = 0.0], [numeric marginRight = 0.0], [numeric marginBottom = 0.0]) // EidosValue_SP Plot::ExecuteMethod_setBorderless(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *marginLeft_value = p_arguments[0].get(); EidosValue *marginTop_value = p_arguments[1].get(); EidosValue *marginRight_value = p_arguments[2].get(); EidosValue *marginBottom_value = p_arguments[3].get(); double marginLeft = marginLeft_value->NumericAtIndex_NOCAST(0, nullptr); double marginTop = marginTop_value->NumericAtIndex_NOCAST(0, nullptr); double marginRight = marginRight_value->NumericAtIndex_NOCAST(0, nullptr); double marginBottom = marginBottom_value->NumericAtIndex_NOCAST(0, nullptr); if ((marginLeft < 0.0) || (marginTop < 0.0) || (marginRight < 0.0) || (marginBottom < 0.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_setBorderless): setBorderless() requires all margins to be >= 0." << EidosTerminate(nullptr); plotview_->setBorderless(true, marginLeft, marginTop, marginRight, marginBottom); return gStaticEidosValueVOID; } // ********************* – (void)text(numeric x, numeric y, string labels, [string color = "black"], [numeric size = 10.0], [Nif adj = NULL], [float alpha = 1.0], [numeric angle = 0.0]) // EidosValue_SP Plot::ExecuteMethod_text(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *x_value = p_arguments[0].get(); EidosValue *y_value = p_arguments[1].get(); EidosValue *labels_value = p_arguments[2].get(); EidosValue *color_value = p_arguments[3].get(); EidosValue *size_value = p_arguments[4].get(); EidosValue *adj_value = p_arguments[5].get(); EidosValue *alpha_value = p_arguments[6].get(); EidosValue *angle_value = p_arguments[7].get(); // x and y int xcount = x_value->Count(); int ycount = y_value->Count(); int labelscount = labels_value->Count(); if ((xcount != ycount) || (xcount != labelscount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): text() requires x, y, and labels to be the same length." << EidosTerminate(); double *x = (double *)malloc(xcount * sizeof(double)); double *y = (double *)malloc(ycount * sizeof(double)); if (!x || !y) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); if (x_value->Type() == EidosValueType::kValueFloat) { memcpy(x, x_value->FloatData(), xcount * sizeof(double)); } else { const int64_t *int_data = x_value->IntData(); for (int index = 0; index < xcount; ++index) x[index] = int_data[index]; } if (y_value->Type() == EidosValueType::kValueFloat) { memcpy(y, y_value->FloatData(), ycount * sizeof(double)); } else { const int64_t *int_data = y_value->IntData(); for (int index = 0; index < ycount; ++index) y[index] = int_data[index]; } // labels std::vector *labels = new std::vector; const std::string *string_data = labels_value->StringData(); for (int index = 0; index < labelscount; ++index) labels->emplace_back(QString::fromStdString(string_data[index])); // color std::vector *colors = new std::vector; int color_count = color_value->Count(); if ((color_count != 1) && (color_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): text() requires color to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < color_count; ++index) { std::string color_string = color_value->StringAtIndex_NOCAST(index, nullptr); uint8_t colorR, colorG, colorB; Eidos_GetColorComponents(color_string, &colorR, &colorG, &colorB); colors->emplace_back(colorR, colorG, colorB, 255); } // size std::vector *sizes = new std::vector; int size_count = size_value->Count(); if ((size_count != 1) && (size_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): text() requires size to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < size_count; ++index) { double size = size_value->NumericAtIndex_NOCAST(index, nullptr); if ((size <= 0.0) || (size > 1000.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): text() requires the elements of size to be in (0, 1000]." << EidosTerminate(nullptr); sizes->push_back(size); } // adj double adj[2] = {0.5, 0.5}; if (adj_value->Type() != EidosValueType::kValueNULL) { if (adj_value->Count() != 2) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): text() requires adj to be a numeric vector of length 2, or NULL." << EidosTerminate(); adj[0] = adj_value->NumericAtIndex_NOCAST(0, nullptr); adj[1] = adj_value->NumericAtIndex_NOCAST(1, nullptr); } // alpha std::vector *alphas = new std::vector; int alpha_count = alpha_value->Count(); if ((alpha_count != 1) && (alpha_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): text() requires alpha to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < alpha_count; ++index) { double alpha = alpha_value->FloatAtIndex_NOCAST(index, nullptr); if ((alpha < 0.0) || (alpha > 1.0)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_text): text() requires the elements of alpha to be in [0, 1]." << EidosTerminate(nullptr); alphas->push_back(alpha); } // angle std::vector *angles = new std::vector; int angle_count = angle_value->Count(); if ((angle_count != 1) && (angle_count != xcount)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): text() requires angle to match the length of x and y, or be singleton." << EidosTerminate(); for (int index = 0; index < angle_count; ++index) { double angle = angle_value->NumericAtIndex_NOCAST(index, nullptr); if (!std::isfinite(angle)) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_mtext): text() requires the elements of angle to be finite." << EidosTerminate(nullptr); angles->push_back(angle); } plotview_->addTextData(x, y, labels, xcount, colors, alphas, sizes, adj, angles); // takes ownership of buffers return gStaticEidosValueVOID; } // ********************* – (void)write(string$ filePath) // EidosValue_SP Plot::ExecuteMethod_write(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *filePath_value = p_arguments[0].get(); std::string outfile_path = Eidos_ResolvedPath(filePath_value->StringAtIndex_NOCAST(0, nullptr)); if (outfile_path.length() == 0) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_write): write() requires a non-empty path." << EidosTerminate(); QString qpath = QString::fromStdString(outfile_path); bool success = plotview_->writeToFile(qpath); if (!success) EIDOS_TERMINATION << "ERROR (Plot::ExecuteMethod_write): write() could not write to " << outfile_path << "; check the permissions of the enclosing directory." << EidosTerminate(); return gStaticEidosValueVOID; } // // Plot_Class // #pragma mark - #pragma mark Plot_Class #pragma mark - EidosClass *gSLiM_Plot_Class = nullptr; const std::vector *Plot_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_title, true, kEidosValueMaskString | kEidosValueMaskSingleton))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *Plot_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { methods = new std::vector(*super::Methods()); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_abline, kEidosValueMaskVOID)) ->AddNumeric_ON("a", gStaticEidosValueNULL)->AddNumeric_ON("b", gStaticEidosValueNULL) ->AddNumeric_ON("h", gStaticEidosValueNULL)->AddNumeric_ON("v", gStaticEidosValueNULL) ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_addLegend, kEidosValueMaskVOID)) ->AddString_OSN("position", gStaticEidosValueNULL)->AddInt_OSN("inset", gStaticEidosValueNULL) ->AddNumeric_OSN("labelSize", gStaticEidosValueNULL)->AddNumeric_OSN("lineHeight", gStaticEidosValueNULL) ->AddNumeric_OSN("graphicsWidth", gStaticEidosValueNULL)->AddNumeric_OSN("exteriorMargin", gStaticEidosValueNULL) ->AddNumeric_OSN("interiorMargin", gStaticEidosValueNULL))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_axis, kEidosValueMaskVOID)) ->AddInt_S("side")->AddNumeric_ON("at", gStaticEidosValueNULL) ->AddArgWithDefault(kEidosValueMaskLogical | kEidosValueMaskString | kEidosValueMaskOptional, "labels", nullptr, gStaticEidosValue_LogicalT))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_image, kEidosValueMaskVOID)) ->AddObject_S(gStr_image, nullptr)->AddNumeric_S("x1")->AddNumeric_S("y1")->AddNumeric_S("x2")->AddNumeric_S("y2") ->AddLogical_OS("flipped", gStaticEidosValue_LogicalF)->AddFloat_OS("alpha", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_legendLineEntry, kEidosValueMaskVOID)) ->AddString_S("label")->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_OS("lwd", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_legendPointEntry, kEidosValueMaskVOID)) ->AddString_S("label")->AddInt_OS("symbol", gStaticEidosValue_Integer0) ->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddString_OS("border", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_OS("lwd", gStaticEidosValue_Float1)->AddNumeric_OS("size", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_legendSwatchEntry, kEidosValueMaskVOID)) ->AddString_S("label")->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_legendTitleEntry, kEidosValueMaskVOID)) ->AddString_S("label"))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_lines, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_OS("lwd", gStaticEidosValue_Float1)->AddFloat_OS("alpha", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_matrix, kEidosValueMaskVOID)) ->AddNumeric("matrix")->AddNumeric_S("x1")->AddNumeric_S("y1")->AddNumeric_S("x2")->AddNumeric_S("y2") ->AddLogical_OS("flipped", gStaticEidosValue_LogicalF)->AddNumeric_ON("valueRange", gStaticEidosValueNULL) ->AddString_OSN("colors", gStaticEidosValueNULL)->AddFloat_OS("alpha", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_mtext, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddString("labels") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("size", EidosValue_Float_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(10))) ->AddNumeric_ON("adj", gStaticEidosValueNULL)->AddFloat_O("alpha", gStaticEidosValue_Float1) ->AddNumeric_O("angle", gStaticEidosValue_Float0))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_points, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddInt_O("symbol", gStaticEidosValue_Integer0) ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddString_O("border", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddNumeric_O("size", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_rects, kEidosValueMaskVOID)) ->AddNumeric("x1")->AddNumeric("y1")->AddNumeric("x2")->AddNumeric("y2") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddString_O("border", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_segments, kEidosValueMaskVOID)) ->AddNumeric("x1")->AddNumeric("y1")->AddNumeric("x2")->AddNumeric("y2") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_setBorderless, kEidosValueMaskVOID)) ->AddNumeric_OS("marginLeft", gStaticEidosValue_Float0)->AddNumeric_OS("marginTop", gStaticEidosValue_Float0) ->AddNumeric_OS("marginRight", gStaticEidosValue_Float0)->AddNumeric_OS("marginBottom", gStaticEidosValue_Float0))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_text, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddString("labels") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("size", EidosValue_Float_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(10))) ->AddNumeric_ON("adj", gStaticEidosValueNULL)->AddFloat_O("alpha", gStaticEidosValue_Float1) ->AddNumeric_O("angle", gStaticEidosValue_Float0))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gEidosStr_write, kEidosValueMaskVOID)) ->AddString_S(gEidosStr_filePath))); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: QtSLiM/QtSLiM_Plot.h ================================================ // // QtSLiM_Plot.h // SLiM // // Created by Ben Haller on 1/30/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIM_PLOT_H #define QTSLIM_PLOT_H #include #include #include "eidos_value.h" #include "slim_globals.h" class QtSLiMGraphView_CustomPlot; extern EidosClass *gSLiM_Plot_Class; class Plot : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; public: std::string title_; // the title as given to createPlot() QtSLiMGraphView_CustomPlot *plotview_; // We have a reference to the QtSLiMGraphView_CustomPlot that displays us Plot(const Plot&) = delete; // no copying Plot& operator=(const Plot&) = delete; // no copying Plot(void) = delete; // no null construction Plot(const std::string &title, QtSLiMGraphView_CustomPlot *p_plotview); virtual ~Plot(void) override; // // Eidos support // virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_abline(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addLegend(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_axis(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_image(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendLineEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendPointEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendSwatchEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendTitleEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_lines(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_matrix(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_mtext(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_points(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_rects(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_segments(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setBorderless(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_text(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_write(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); }; class Plot_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: Plot_Class(const Plot_Class &p_original) = delete; // no copy-construct Plot_Class& operator=(const Plot_Class&) = delete; // no copying inline Plot_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif // QTSLIM_PLOT_H ================================================ FILE: QtSLiM/QtSLiM_SLiMgui.cpp ================================================ // // QtSLiM_SLiMgui.cpp // SLiM // // Created by Ben Haller on 12/7/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "QtSLiM_SLiMgui.h" #include "QtSLiMWindow.h" #include "QtSLiMGraphView_CustomPlot.h" #include "QtSLiM_Plot.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "log_file.h" #include #include #include #include #pragma mark - #pragma mark SLiMGUI #pragma mark - SLiMgui::SLiMgui(Community &p_community, QtSLiMWindow *p_controller) : community_(p_community), controller_(p_controller), self_symbol_(gID_slimgui, EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_SLiMgui_Class))) { } SLiMgui::~SLiMgui(void) { } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - const EidosClass *SLiMgui::Class(void) const { return gSLiM_SLiMgui_Class; } void SLiMgui::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay(); // standard EidosObject behavior (not Dictionary behavior) } EidosValue_SP SLiMgui::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_pid: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(getpid())); } // variables // all others, including gID_none default: return super::GetProperty(p_property_id); } } void SLiMgui::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP SLiMgui::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_createPlot: return ExecuteMethod_createPlot(p_method_id, p_arguments, p_interpreter); case gID_logFileData: return ExecuteMethod_logFileData(p_method_id, p_arguments, p_interpreter); case gID_openDocument: return ExecuteMethod_openDocument(p_method_id, p_arguments, p_interpreter); case gID_pauseExecution: return ExecuteMethod_pauseExecution(p_method_id, p_arguments, p_interpreter); case gID_plotWithTitle: return ExecuteMethod_plotWithTitle(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* – (No$)createPlot(string$ title, [Nif xrange = NULL], [Nif yrange = NULL], [string$ xlab = "x"], [string$ ylab = "y"], [Nif$ width = NULL], [Nif$ height = NULL] // [logical$ horizontalGrid = F], [logical$ verticalGrid = F], [logical$ fullBox = T]) // EidosValue_SP SLiMgui::ExecuteMethod_createPlot(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *title_value = p_arguments[0].get(); EidosValue *xrange_value = p_arguments[1].get(); EidosValue *yrange_value = p_arguments[2].get(); EidosValue *xlab_value = p_arguments[3].get(); EidosValue *ylab_value = p_arguments[4].get(); EidosValue *width_value = p_arguments[5].get(); EidosValue *height_value = p_arguments[6].get(); EidosValue *horizontalGrid_value = p_arguments[7].get(); EidosValue *verticalGrid_value = p_arguments[8].get(); EidosValue *fullBox_value = p_arguments[9].get(); EidosValue *axisLabelSize_value = p_arguments[10].get(); EidosValue *tickLabelSize_value = p_arguments[11].get(); std::string std_title = title_value->StringAtIndex_NOCAST(0, nullptr); QString title = QString::fromStdString(std_title); if (title.length() == 0) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires a non-empty plot title." << EidosTerminate(); double *x_range = nullptr; double x_range_array[2]; if (xrange_value->Type() != EidosValueType::kValueNULL) { if (xrange_value->Count() != 2) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires xrange to be a numeric vector of length 2, or NULL." << EidosTerminate(); x_range_array[0] = xrange_value->NumericAtIndex_NOCAST(0, nullptr); x_range_array[1] = xrange_value->NumericAtIndex_NOCAST(1, nullptr); x_range = x_range_array; if (x_range[0] >= x_range[1]) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires xrange[0] < xrange[1], when a range is specified (non-NULL)." << EidosTerminate(); } double *y_range = nullptr; double y_range_array[2]; if (yrange_value->Type() != EidosValueType::kValueNULL) { if (yrange_value->Count() != 2) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires yrange to be a numeric vector of length 2, or NULL." << EidosTerminate(); y_range_array[0] = yrange_value->NumericAtIndex_NOCAST(0, nullptr); y_range_array[1] = yrange_value->NumericAtIndex_NOCAST(1, nullptr); y_range = y_range_array; if (y_range[0] >= y_range[1]) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires yrange[0] < yrange[1], when a range is specified (non-NULL)." << EidosTerminate(); } QString xlab = QString::fromStdString(xlab_value->StringAtIndex_NOCAST(0, nullptr)); QString ylab = QString::fromStdString(ylab_value->StringAtIndex_NOCAST(0, nullptr)); double width = 0.0; if (width_value->Type() != EidosValueType::kValueNULL) { width = width_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(width) || (width <= 0.0)) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires width to be > 0.0, or NULL." << EidosTerminate(); } double height = 0.0; if (height_value->Type() != EidosValueType::kValueNULL) { height = height_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(height) || (height <= 0.0)) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires height to be > 0.0, or NULL." << EidosTerminate(); } bool horizontalGrid = horizontalGrid_value->LogicalAtIndex_NOCAST(0, nullptr); bool verticalGrid = verticalGrid_value->LogicalAtIndex_NOCAST(0, nullptr); bool fullBox = fullBox_value->LogicalAtIndex_NOCAST(0, nullptr); double axisLabelSize = axisLabelSize_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(axisLabelSize) || (axisLabelSize < 2.0) || (axisLabelSize > 30.0)) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires axisLabelSize to be in [2, 30]." << EidosTerminate(); double tickLabelSize = tickLabelSize_value->NumericAtIndex_NOCAST(0, nullptr); if (!std::isfinite(tickLabelSize) || (tickLabelSize < 2.0) || (tickLabelSize > 30.0)) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_createPlot): createPlot() requires tickLabelSize to be in [2, 30]." << EidosTerminate(); // make the plot view; note this might return an existing object QtSLiMGraphView_CustomPlot *plotview = controller_->eidos_createPlot(title, x_range, y_range, xlab, ylab, width, height, horizontalGrid, verticalGrid, fullBox, axisLabelSize, tickLabelSize); // plotview owns its Eidos instance of class Plot, and keeps it across recycles Plot *plot = plotview->eidosPlotObject(); if (!plot) { plot = new Plot(std_title, plotview); plotview->setEidosPlotObject(plot); } EidosValue_SP result_SP(EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(plot, gSLiM_Plot_Class))); return result_SP; } // ********************* – (Nfs)logFileData(o$ logFile, is$ column) // EidosValue_SP SLiMgui::ExecuteMethod_logFileData(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue_Object *logFile_value = (EidosValue_Object *)p_arguments[0].get(); EidosValue *column_value = p_arguments[1].get(); LogFile *logFile = (LogFile *)logFile_value->ObjectElementAtIndex_NOCAST(0, nullptr); return controller_->eidos_logFileData(logFile, column_value); } // ********************* – (void)openDocument(string$ path) // EidosValue_SP SLiMgui::ExecuteMethod_openDocument(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *filePath_value = p_arguments[0].get(); std::string file_path = Eidos_ResolvedPath(Eidos_StripTrailingSlash(filePath_value->StringAtIndex_NOCAST(0, nullptr))); QString filePath = QString::fromStdString(file_path); controller_->eidos_openDocument(filePath); return gStaticEidosValueVOID; } // ********************* – (void)pauseExecution(void) // EidosValue_SP SLiMgui::ExecuteMethod_pauseExecution(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) controller_->eidos_pauseExecution(); return gStaticEidosValueVOID; } // ********************* – (No$)plotWithTitle(string$ title) // EidosValue_SP SLiMgui::ExecuteMethod_plotWithTitle(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *title_value = p_arguments[0].get(); QString title = QString::fromStdString(title_value->StringAtIndex_NOCAST(0, nullptr)); if (title.length() == 0) EIDOS_TERMINATION << "ERROR (SLiMgui::ExecuteMethod_plotWithTitle): plotWithTitle() requires a non-empty plot title." << EidosTerminate(); QtSLiMGraphView_CustomPlot *plotview = controller_->eidos_plotWithTitle(title); if (plotview) { Plot *plot = plotview->eidosPlotObject(); EidosValue_SP result_SP(EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(plot, gSLiM_Plot_Class))); return result_SP; } return gStaticEidosValueNULL; } // // SLiMgui_Class // #pragma mark - #pragma mark SLiMgui_Class #pragma mark - EidosClass *gSLiM_SLiMgui_Class = nullptr; const std::vector *SLiMgui_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { properties = new std::vector(*super::Properties()); properties->emplace_back(static_cast((new EidosPropertySignature(gStr_pid, true, kEidosValueMaskInt | kEidosValueMaskSingleton)))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *SLiMgui_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { methods = new std::vector(*super::Methods()); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_createPlot, kEidosValueMaskNULL | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Plot_Class))->AddString_S("title") ->AddNumeric_ON("xrange", gStaticEidosValueNULL)->AddNumeric_ON("yrange", gStaticEidosValueNULL) ->AddString_OS("xlab", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("x"))) ->AddString_OS("ylab", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("y"))) ->AddNumeric_OSN("width", gStaticEidosValueNULL)->AddNumeric_OSN("height", gStaticEidosValueNULL) ->AddLogical_OS("horizontalGrid", gStaticEidosValue_LogicalF)->AddLogical_OS("verticalGrid", gStaticEidosValue_LogicalF) ->AddLogical_OS("fullBox", gStaticEidosValue_LogicalT) ->AddNumeric_OS("axisLabelSize", EidosValue_Int_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(15))) ->AddNumeric_OS("tickLabelSize", EidosValue_Int_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(10))))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_logFileData, kEidosValueMaskNULL | kEidosValueMaskFloat | kEidosValueMaskString)) ->AddObject_S("logFile", gSLiM_LogFile_Class)->AddIntString_S("column"))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_openDocument, kEidosValueMaskVOID))->AddString_S("filePath"))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_pauseExecution, kEidosValueMaskVOID)))); methods->emplace_back(static_cast((new EidosInstanceMethodSignature(gStr_plotWithTitle, kEidosValueMaskNULL | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Plot_Class))->AddString_S("title"))); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: QtSLiM/QtSLiM_SLiMgui.h ================================================ // // QtSLiM_SLiMgui.h // SLiM // // Created by Ben Haller on 12/7/2019. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef QTSLIM_SLIMGUI_H #define QTSLIM_SLIMGUI_H #include #include #include #include "eidos_value.h" #include "eidos_symbol_table.h" #include "slim_globals.h" class QtSLiMWindow; extern EidosClass *gSLiM_SLiMgui_Class; class SLiMgui : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; public: Community &community_; // We have a reference to our community object QtSLiMWindow *controller_; // We have a reference to the SLiMgui window controller for our simulation EidosSymbolTableEntry self_symbol_; // for fast setup of the symbol table SLiMgui(const SLiMgui&) = delete; // no copying SLiMgui& operator=(const SLiMgui&) = delete; // no copying SLiMgui(void) = delete; // no null construction SLiMgui(Community &p_community, QtSLiMWindow *p_controller); virtual ~SLiMgui(void) override; // // Eidos support // inline EidosSymbolTableEntry &SymbolTableEntry(void) { return self_symbol_; } virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_createPlot(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_logFileData(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_openDocument(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_pauseExecution(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_plotWithTitle(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); }; class SLiMgui_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: SLiMgui_Class(const SLiMgui_Class &p_original) = delete; // no copy-construct SLiMgui_Class& operator=(const SLiMgui_Class&) = delete; // no copying inline SLiMgui_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif // QTSLIM_SLIMGUI_H ================================================ FILE: QtSLiM/buttons.qrc ================================================ buttons/play_H.png buttons/play.png buttons/play_step_H.png buttons/play_step.png buttons/recycle_H.png buttons/recycle.png buttons/change_folder_H.png buttons/change_folder.png buttons/check_H.png buttons/check.png buttons/chrom_display_H.png buttons/chrom_display.png buttons/delete_H.png buttons/delete.png buttons/dump_output_H.png buttons/dump_output.png buttons/edit_submenu_H.png buttons/edit_submenu.png buttons/execute_script_H.png buttons/execute_script.png buttons/execute_selection_H.png buttons/execute_selection.png buttons/graph_submenu_H.png buttons/graph_submenu.png buttons/prettyprint_H.png buttons/prettyprint.png buttons/profile_H.png buttons/profile.png buttons/show_browser_H.png buttons/show_browser.png buttons/show_console_H.png buttons/show_console.png buttons/show_execution_H.png buttons/show_execution.png buttons/show_fixed_H.png buttons/show_fixed.png buttons/show_genomicelements_H.png buttons/show_genomicelements.png buttons/show_mutations_H.png buttons/show_mutations.png buttons/show_parse_H.png buttons/show_parse.png buttons/show_recombination_H.png buttons/show_recombination.png buttons/show_tokens_H.png buttons/show_tokens.png buttons/syntax_help_H.png buttons/syntax_help.png buttons/open_type_drawer_H.png buttons/open_type_drawer.png buttons/recycle_G.png buttons/recycle_G_H.png buttons/Qt_sex_ratio.png buttons/Qt_cloning_rate.png buttons/Qt_female_symbol.png buttons/Qt_male_symbol.png buttons/Qt_selfing_rate.png buttons/profile_R.png buttons/profile_R_H.png buttons/action_H.png buttons/action.png buttons/jump_to_H.png buttons/jump_to.png buttons/debug_H.png buttons/debug.png buttons/debug_RED.png buttons/clear_debug_H.png buttons/clear_debug.png ================================================ FILE: QtSLiM/buttons_DARK.qrc ================================================ buttons_DARK/play_H_DARK.png buttons_DARK/play_DARK.png buttons_DARK/play_step_H_DARK.png buttons_DARK/play_step_DARK.png buttons_DARK/recycle_H_DARK.png buttons_DARK/recycle_DARK.png buttons_DARK/change_folder_H_DARK.png buttons_DARK/change_folder_DARK.png buttons_DARK/check_H_DARK.png buttons_DARK/check_DARK.png buttons_DARK/chrom_display_H_DARK.png buttons_DARK/chrom_display_DARK.png buttons_DARK/delete_H_DARK.png buttons_DARK/delete_DARK.png buttons_DARK/dump_output_H_DARK.png buttons_DARK/dump_output_DARK.png buttons_DARK/edit_submenu_H_DARK.png buttons_DARK/edit_submenu_DARK.png buttons_DARK/execute_script_H_DARK.png buttons_DARK/execute_script_DARK.png buttons_DARK/execute_selection_H_DARK.png buttons_DARK/execute_selection_DARK.png buttons_DARK/graph_submenu_H_DARK.png buttons_DARK/graph_submenu_DARK.png buttons_DARK/prettyprint_H_DARK.png buttons_DARK/prettyprint_DARK.png buttons_DARK/profile_H_DARK.png buttons_DARK/profile_DARK.png buttons_DARK/show_browser_H_DARK.png buttons_DARK/show_browser_DARK.png buttons_DARK/show_console_H_DARK.png buttons_DARK/show_console_DARK.png buttons_DARK/show_execution_H_DARK.png buttons_DARK/show_execution_DARK.png buttons_DARK/show_fixed_H_DARK.png buttons_DARK/show_fixed_DARK.png buttons_DARK/show_genomicelements_H_DARK.png buttons_DARK/show_genomicelements_DARK.png buttons_DARK/show_mutations_H_DARK.png buttons_DARK/show_mutations_DARK.png buttons_DARK/show_parse_H_DARK.png buttons_DARK/show_parse_DARK.png buttons_DARK/show_recombination_H_DARK.png buttons_DARK/show_recombination_DARK.png buttons_DARK/show_tokens_H_DARK.png buttons_DARK/show_tokens_DARK.png buttons_DARK/syntax_help_H_DARK.png buttons_DARK/syntax_help_DARK.png buttons_DARK/open_type_drawer_H_DARK.png buttons_DARK/open_type_drawer_DARK.png buttons_DARK/recycle_G_DARK.png buttons_DARK/recycle_G_H_DARK.png buttons_DARK/Qt_sex_ratio_DARK.png buttons_DARK/Qt_cloning_rate_DARK.png buttons_DARK/Qt_female_symbol_DARK.png buttons_DARK/Qt_male_symbol_DARK.png buttons_DARK/Qt_selfing_rate_DARK.png buttons_DARK/profile_R_DARK.png buttons_DARK/profile_R_H_DARK.png buttons_DARK/action_H_DARK.png buttons_DARK/action_DARK.png buttons_DARK/jump_to_H_DARK.png buttons_DARK/jump_to_DARK.png buttons_DARK/debug_DARK.png buttons_DARK/debug_H_DARK.png buttons_DARK/clear_debug_DARK.png buttons_DARK/clear_debug_H_DARK.png ================================================ FILE: QtSLiM/help/EidosHelpClasses.html ================================================

5.1  Class Object

5.1.1  Object properties

5.1.2  Object methods

+ (integer$)length(void)

Returns the size (e.g., length) of the receiving object.  This is equivalent to the length() (or size()) function; in other words, for any object x, the return value of the function call length(x) equals the return value of the class method call x.length().  This method is provided solely for syntactic convenience.  Note that +length() is a synonym for +size().

+ (void)methodSignature([Ns$ methodName = NULL])

Prints the method signature for the method specified by methodName, or for all methods supported by the receiving object if methodName is NULL (the default).

+ (void)propertySignature([Ns$ propertyName = NULL])

Prints the property signature for the property specified by propertyName, or for all properties supported by the receiving object if propertyName is NULL (the default).

+ (integer$)size(void)

Returns the size of the receiving object.  This is equivalent to the size() (or length()) function; in other words, for any object x, the return value of the function call size(x) equals the return value of the class method call x.size().  This method is provided solely for syntactic convenience.  Note that +length() is a synonym for +size().

– (void)str(void)

Prints the internal property structure of the receiving object; in particular, the element type of the object is printed, followed, on successive lines, by all of the properties supported by the object, their types, and a sample of their values.

– (string$)stringRepresentation(void)

Returns a singleton string value that represents the receiving object.  By default, this is simply the name of the class of the receiving object; however, many subclasses of Object provide a different string representation.  The value returned by stringRepresentation() is the same string that would be printed by print() for the object, so stringRepresentation() allows the same representation to be used in other contexts such as paste() and cat().

5.2  Class DataFrame

(object<DataFrame>$)DataFrame(...)

The DataFrame constructor can be called in the same ways as the constructor for Dictionary (its superclass): with no parameters to make an empty DataFrame, with key-value pairs, with a singleton Dictionary (or a subclass of Dictionary, like DataFrame) to make a copy, or with a string in JSON format.  See the Dictionary class for further documentation.  However, note that DataFrame can only use string keys; integer keys are not allowed.

5.2.1  DataFrame properties

colNames => (string)

A vector containing all of the string column names in the DataFrame, in order.  This property is currently an alias for the Dictionary property allKeys.

dim => (integer)

A two-element vector containing the dimensions of the DataFrame.  The 0th element is the number of rows (as provided by nrow), and the 1st element is the number of columns (as provided by ncol).

ncol => (integer$)

The number of columns in the DataFrame; this will be equal to the length of colNames.

nrow => (integer$)

The number of rows in the DataFrame (i.e., the number of elements in a column).  This will be the same for every column, by definition.

5.2.2  DataFrame methods

– (*)asMatrix(void)

Returns a matrix representation of the DataFrame.  The matrix will have the same type as the elements of the DataFrame; if the DataFrame contains more than one type of element, an error will be raised.  The order of the columns of the DataFrame will be preserved.  This method is useful, for example, if you wish to read in a text file as a matrix; you can use readCSV() to read the file as a DataFrame, and then convert it to a matrix with asMatrix().

– (void)cbind(object<Dictionary> source, ...)

Adds all of the columns contained by source (which must be a Dictionary or a subclass of Dictionary such as DataFrame) to the receiver.  This method makes the target DataFrame wider, by adding new columns.  If source contains a column name that is already defined in the target, an error will result.  As always for DataFrame, the columns of the resulting DataFrame must all be the same length.

The source parameter may be a non-singleton vector containing multiple Dictionary objects, and additional Dictionary vectors may be supplied (thus the ellipsis in the signature).  Each Dictionary supplied will be added to the target, in the order supplied.

This method is similar to the Dictionary method addKeysAndValuesFrom(), which may be used instead if replacement of duplicate columns is desired.

– (void)rbind(object<Dictionary> source, ...)

Appends all of the columns contained by source (which must be a Dictionary or a subclass of Dictionary such as DataFrame) to the receiver.  This method makes the DataFrame taller, by adding new rows.  If the source and target do not contain the same column names in the same order, an error will result.  As always for DataFrame, the columns of the resulting DataFrame must all be the same length.

The source parameter may be a non-singleton vector containing multiple Dictionary objects, and additional Dictionary vectors may be supplied (thus the ellipsis in the signature).  Each Dictionary supplied will be appended to the target, in the order supplied.

This method is similar to the Dictionary method appendKeysAndValuesFrom(), which may be used instead if one wishes the append to work even when the columns are in different orders, or other such situations.

– (*)subset([Nli rows = NULL], [Nlis cols = NULL])

Returns the elements in the selected rows and columns of the target DataFrame.  The selection logic is based upon that for subsetRows() and subsetColumns(), respectively; in short, rows may be selected by integer indices or by a logical vector, and columns may be selected by integer indices, by a logical vector, or by a string vector of column names.  In addition, however, NULL may be passed for either rows or cols to select all of the rows or all of the columns, respectively; this is the default for both parameters.  If you want entire rows (rather than selecting particular columns), pass NULL for cols; if you want entire columns (rather than selecting particular rows), pass NULL for rows.

The first step performed by subset() is to produce a DataFrame that contains the selected rows and columns.  If that DataFrame contains more than one column, it is simply returned, and the behavior of subset() is identical to calling subsetRows() and subsetColumns() in sequence (in either order).  If, however, the resulting DataFrame contains only a single column, then subset() will return a vector containing the elements in that column – unlike the behavior of subsetRows() and subsetColumns(), which always return a DataFrame.  This method is therefore a convenient way to get a single value, or multiple values from the same column, from a DataFrame.  (Note that the Dictionary method getValue() can also be used to get all of the values from a given DataFrame column.)

– (object<DataFrame>$)subsetColumns(lis index)

Returns a new DataFrame containing values for the selected columns of the target DataFrame.  The selection logic described below is similar to how the subset operator [] in Eidos works, selecting the columns of the target DataFrame.

The index parameter may be either integer, logical, or string; we will discuss the integer case first.  If index is a singleton integer, the returned DataFrame will contain the index’th column of the target (counting from the left, from 0).  If index is a non-singleton integer vector, the returned DataFrame will contains all of the selected columns, in the order that they are selected by index.  If any index value is out of range for the target DataFrame (such that the DataFrame does not have an index’th column), an error will result.  If the same column is specified more than once, unique column names will be automatically generated for the additional copies of the column.

If index is a string vector, the returned DataFrame will contain copies of the columns in the target named by index.  As with an integer vector, it is an error if a given column does not exist in the target; and unique column names will be generated for additional copies of a column.

Finally, if index is a logical vector, the length of index must be equal to the number of columns in the target.  In this case, the T values in index select the columns which will be included in the returned DataFrame.  The columns in the returned DataFrame will be in the same order as in the target.

– (object<DataFrame>$)subsetRows(li index, [logical$ drop = F])

Returns a new DataFrame containing values for selected rows of the target DataFrame.  The selection logic described below works exactly as the subset operator [] does in Eidos, selecting the rows of the target DataFrame.

The index parameter may be either integer or logical; we will discuss the integer case first.  If index is a singleton integer, the returned DataFrame will contain the index’th element of the value of each key of the target, under the same keys; this is a single row of the target DataFrame.  If index is a non-singleton integer vector, the returned DataFrame will contain the values for all of the selected rows, in the order that they are selected by index.  If any index value in index is out of range for the target DataFrame (such that that DataFrame does not have an index’th row), an error will result.

If index is logical, the length of index must be equal to the number of rows in the target.  In this case, the T values in index select the rows which will be included in the returned DataFrame.  The values of each column in the returned DataFrame will be in the same order as in the target.

If the values of index are such that no value for a given key is selected, the drop parameter controls the resulting behavior.  If drop is F (the default), the key will be included in the returned dictionary with a zero-length value of matching type, such as integer(0) or string(0).  If drop is T, the key will be omitted from the returned dictionary.

5.3  Class Dictionary

(object<Dictionary>$)Dictionary(...)

Creates a new Dictionary object.  Called without arguments, as Dictionary(), this creates a new empty Dictionary.

Alternatively, key-value pairs can be passed to set up the initial state of the new Dictionary.  These are set, sequentially, on the new Dictionary, just as setValue() would do.  For example, calling Dictionary("a", 0:3, "b", c("foo", "bar")) is equivalent to calling Dictionary() and then calling setValue("a", 0:3) and then setValue("b", c("foo", "bar")) on it; it is just a shorthand for convenience.  Keys may be of type string or integer, but must all be of the same type; Dictionary supports using either string or integer keys, but they cannot be mixed in a single Dictionary object.

Another alternative is to call Dictionary() with a singleton Dictionary as its only argument; this creates a new Dictionary that is a copy of the Dictionary passed, containing the same keys and values.  This is equivalent to creating a new empty Dictionary and then calling addKeysAndValuesFrom() to copy key-value pairs over; it is just a shorthand for convenience.

A final alternative is to call Dictionary() with a string vector as its only argument; this creates a new Dictionary from the string, assuming that it is a data archive in JSON format.  If the string value is not a singleton, its elements will be joined together by newlines to make a singleton string value; this allows the result from readFile() to be passed directly to Dictionary() even for a multiline (prettyprinted) JSON file.  Note that a JSON string can be generated from the serialize() method of Dictionary; together with this way of creating a Dictionary, this provides the ability to persist arbitrary information to a string (perhaps a file on disk) and back again.  The recreated Dictionary should be identical to the original, except that zero length vectors such as integer(0), float(0), logical(0), and string(0) will all be serialized as "[]" and recreated as integer(0) since JSON does not provide a way to specify the type of a zero-length array.

5.3.1  Dictionary properties

allKeys => (is)

A vector containing all of the string or integer keys that have been assigned values using setValue(), in sorted (ascending alphabetic or numeric) order.

5.3.2  Dictionary methods

– (void)addKeysAndValuesFrom(object<Dictionary>$ source)

Adds all of the key-value pairs contained by source (which must be a Dictionary or a subclass of Dictionary) to the receiver.  If the target already contains a key that is defined in source, the target’s value for that key will be replaced by the value in source (contrast this with appendKeysAndValuesFrom()).

– (void)appendKeysAndValuesFrom(object<Dictionary> source)

Appends all of the key-value pairs contained by source (which must be a Dictionary or a subclass of Dictionary) to the receiver.  If the target already contains a key that is defined in source, the value from source will be appended to the target’s existing value, which must be of the same type (contrast this with addKeysAndValuesFrom()); if the target does not already contain a key that is defined in source, that key-value pair will simply be added to the target.

In the current implementation, it is an error for either of the values involved in an append to be a matrix or array; values in these Dictionary objects should be simple vectors.  This limitation preserves the future option to expand this method’s functionality to do smart things with matrices and arrays.

– (void)clearKeysAndValues(void)

Removes all key-value pairs from the receiver.

– (integer)compactIndices([logical$ preserveOrder = F])

Compacts the receiver, which must use integer keys.  After this operation, the receiver will contain only values that have a length greater than zero (discarding all key–value pairs for which the value is a zero-length vector).  In addition, the keys used will be compacted down to begin at 0 and count upward sequentially.  If preserveOrder is F (the default), the keys may end up in a different numerical order; this allows the compaction to be performed more efficiently.  If preserveOrder is T, on the other hand, the numerical order of the keys will be preserved.  The returned integer vector contains the original keys that were kept across the compaction operation, in the order in which they were used in the compaction; keys that were not kept (because their value was zero-length) are omitted from this result vector.

For example, with a dictionary that contains key–value pairs -5="a", 17="b", 37="c", 53=integer(0), and 82="d", compactIndices(preserveOrder=T) will transform the dictionary to contain 0="a", 1="b", 2="c", and 3="d", while key 53 (and its zero-length value) is dropped; the returned vector will be (5, 17, 37, 82).  The result from compactIndices(preserveOrder=F) has a non-deterministic order, but one possibility for the same example inout is that it would transform the dictionary to contain key–value pairs 0="c", 1="d", 2="a", and 3="b", with a returned vector of (37, 82, 5, 17); the same key–value pairs are kept, and they are again placed in sequential keys beginning with 0, but their order is no longer preserved across the compaction.

This method is particularly useful when you have a Dictionary d that contains results from some operation on a vector x, such that each key n in d has a value that is the result of processing the n’th element of x.  In this case, order=d.compactIndices(preserveOrder=F) will transmogrify d to contain only the non-zero-length results, in sequential indices counting from 0, and x[order] provides the elements of x that produced those results, in the same order as in d after compaction.  Using preserveOrder=T additionally keeps d in the same order as the original order of x, for cases in which that ordering is important.

– (object<Dictionary>$)getRowValues(li index, [logical$ drop = F])

Returns a new Dictionary containing values for selected “rows” of the target Dictionary, allowing Dictionary to act similarly to a DataFrame.  See the subsetRows() method of class DataFrame for comparison; the main utility of getRowValues() is that it can be used on a Dictionary that has ragged “rows”.  The selection logic described below works similarly to the subset operator [] in Eidos, selecting the “rows” of the target Dictionary.

The index parameter may be either integer or logical; we will discuss the integer case first.  If index is a singleton integer, the returned Dictionary will contain the index’th element of the value of each key of the target, under the same keys; this is a single “row” of the target Dictionary.  If index is a non-singleton integer vector, the returned Dictionary will contain the values for all of the selected rows, in the order that they are selected by index.  If any index value in index is out of range for any key of the target Dictionary (such that that key does not have an index’th value), the returned dictionary will simply not have a value for that “row” of that key.

If index is logical, the T values in index select the “rows” which will be included in the returned Dictionary.  The values within each column in the returned Dictionary will be in the same order as in the target.  The length of index need not match any column of the Dictionary; excess “rows” beyond the length of index will not be selected, and excess values in index beyond the end of the longest “column” will have no effect.

If the values of index are such that no value for a given key is selected, the drop parameter controls the resulting behavior.  If drop is F (the default), the key will be included in the returned dictionary with a zero-length value of matching type, such as integer(0) or string(0).  If drop is T, the key will be omitted from the returned dictionary.

– (*)getValue(is$ key)

Returns the value previously set for the dictionary entry identifier key using setValue(), or NULL if no value has been set.

– (logical$)identicalContents(object<Dictionary>$ x)

Returns T if the target Dictionary is equal to x in all respects – containing the same keys, with values that are identical in the sense defined by the identical() function in Eidos – or returns F otherwise.

Note that if Dictionary objects are contained, as values, by the dictionaries being tested for equality, they will be compared according to the standards of identical(), and must therefore actually be the same Dictionary object, shared by both dictionaries, for isEqual() to return T.

– (string)serialize([string$ format = "slim"])

Returns a serialized form of the dictionary’s contents as a string singleton or vector.  Five formats are supported at present, as chosen with the format parameter: "slim", "pretty", and "json" produce a singleton string, whereas "csv" and "tsv" produce a string vector.  These serializations can be written to disk with writeFile() or writeTempFile(), written to the output stream with cat(), or used in any other way.

The default "slim" format is intended for simple, informal use where a very easily parseable string is desired.  For a simple dictionary containing only keys with singleton non-object values, this will be a semicolon-delimited string like '"string1"=value1;"string2"=value2;' or 'int1=value1;int2=value2;'.  Values of type string will be quoted, and will be escaped with backslash escape sequences, including \\, \", \', \t, \r, and \n.  Values that are not singleton will be separated by spaces, such as '"string1"=1 2 3;', while values that are themselves dictionaries will be delimited by braces, such as '"string1"={int1=value1;int2=value2;};'.  Keys that are of type string will be quoted (always; note that this is a change in behavior starting in SLiM 4.1) and backslash-escaped (as needed, as for string values); keys that are of type integer are not quoted.  No facility for parsing "slim" serializations back into Eidos is presently provided.

For a more extended example, here is an input Dictionary, assigned into a variable x:

x = Dictionary("a", 17, "b", 1:5, "c", c("foo", "bar"),
                  "d", Dictionary("seq", 1.5:5),
                  "e", Dictionary());

and here is the result of x.serialize("json"), omitting the enclosing quotes that would indicate that this is a string value:

"a"=17;"b"=1 2 3 4 5;"c"="foo" "bar";"d"={"seq"=1.5 2.5 3.5 4.5;};"e"={};

The "pretty" format is intended for human-readable output, for purposes such as debugging output.  It is similar to the "slim" format, but (1) it prints an enclosing set of braces at the top level, (2) it adds newlines inside braces, (3) it tracks an indentation level that increments for nested dictionaries, (4) it adds whitespace it some positions for readability, such as around the equals signs that separate keys from values, and (5) it omits the semicolon at the end of a value, adding a newline instead.  No facility for parsing "pretty" serializations back into Eidos is presently provided.

For the same extended example Dictionary as above, here is the result of x.serialize("pretty"), again omitting the enclosing quotes that would indicate that this is a string value:

{
   "a" = 17
   "b" = 1 2 3 4 5
   "c" = "foo" "bar"
   "d" = {
      "seq" = 1.5 2.5 3.5 4.5
   }
   "e" = {}
}

The "json" format, introduced in Eidos 2.7 (SLiM 3.7), provides serialization of the Dictionary into the standard JSON format, which may not be quite as brief or human-readable, but which can be used as a standard interchange format and read by the Dictionary() constructor in Eidos as well as by many other programs.  For example, a Dictionary with a key "key1" with integer value 1:3 and key "key2" with string value "value2" would produce the JSON serialization '{"key1":[1,2,3],"key2":["value2"]}', where the outer single quotes are not part of the serialization itself, but are indicating that the serialization is a string value.  Note that since all Eidos values are vectors, even singleton values are serialized into JSON as arrays by Eidos; the hope is that this will make automated parsing of these JSON strings easier, since the singleton case will not have to be special-cased.  For example, Dictionary("a", 1, "b", Dictionary("x", 2)) would be serialized into JSON as '{"a":[1],"b":[{"x":[2]}]}'.  Note that dictionaries that use integer keys cannot be serialized into JSON, because JSON does not support integer keys.  Documentation on the JSON format can be found online.

The "csv" and "tsv" formats produce standard comma-separated value (CSV) or tab-separated value (TSV) data.  These formats are primarily intended for output from DataFrame, since that class is used to represent the sort of data tables that CSV/TSV are typically used for; but it may be used with Dictionary too, particularly if it is being used to represent a data table with ragged columns (missing values will just be skipped over, producing two commas or two tabs in sequence).  Values of type string will always be quoted, with double quotes (with a repeated double quote used to indicate the presence of a double quote inside a string value, as usual in CSV); values of other types never will.  Decimal points (not decimal commas, regardless of system localization) will always be used for float values, and will never be used for integer values.  Values of logical type will be serialized as TRUE or FALSE, without quotes.  A header line providing the names of the columns (i.e., the keys of the target Dictionary) will always be generated; those column names will also be quoted (if the keys of the dictionary are type string; integer keys are not quoted).  One string element will be generated for each row of the target, plus one string element for the header line; newlines will not be present in the resulting string vector unless newlines were present within the string values in the Dictionary.  The resulting data, if written to a file, should be readable in Eidos using readCSV() (as long as there are no ragged columns or missing values), as well as in other software such as R and Excel.

– (void)setValue(is$ key, * value)

Sets a value for the dictionary entry identifier key.  The key may be a string or an integer; either is allowed, unless the target dictionary has already begun using keys of a given type, in which case it must continue using the same key type (a given dictionary cannot have both string and integer keys).  The value, which may be of any type, can be fetched later using getValue().  Setting a key to a value of NULL removes that key from the dictionary.

If value is of type object, any object class is allowed; all objects may be added as values to a dictionary.  However, additional scoping restrictions may apply if the object class is not under an internal memory-management scheme called “retain-release”; in particular, it may not be legal to keep an object in a dictionary “long term” if it is not under retain-release, where “long term” is a scoping semantic defined by the Context.  All object classes defined by Eidos itself (Dictionary, DataFrame, Image) are under retain-release, so this restriction does not affect pure Eidos code.  See the SLiM manual (section “SLiM scoping rules”) for further discussion of this topic.

+ (void)setValuesVectorized(is$ key, * values)

This class method sets a singleton value from values into each target dictionary, using the same dictionary entry identifier key for each.  The number of elements in values must be equal to the number of target dictionaries, so that the 0th element of values is set as the value for the 0th target object, the 1st element of values is set as the value for the 1st target object, and so forth.

This is a vectorized version of setValue(); dicts.setValuesVectorized("key", values) is equivalent to for (dict in dicts, value in values) dict.setValue("key", value), but is faster since the for loop is vectorized internally.  The speedup is not enormous, however; the larger reason for the existence of this method is convenience.

The values are set into the target dictionaries in exactly the same way as the setValue() method would do; see that method for details about string versus integer keys, scoping restrictions for values of type object, and so forth.  Note that it is not possible to remove values from the target dictionaries, however, since it is not possible to pass NULL as a value here.

5.4  Class Image

(object<Image>$)Image(...)

Creates a new Image object.  This can be called in a few different ways.

Passed a singleton string, as Image(string$ filePath), it creates a new Image from the PNG file at filePath.  If the file represents a grayscale image, an 8-bit grayscale (K) Image will be created; all other PNG files will yield a 24-bit color (RGB) Image.

Passed an integer or float vector, as Image(numeric matrix), it creates a new grayscale Image from the values in matrix, which must be a matrix as its name suggests.  If matrix is integer, its values must be in [0, 255], and will be used directly as 8-bit pixel values without translation; if matrix is float, its values must be in [0.0, 1.0], and will be translated into 8-bit pixel values.  The dimensions of the image, in pixels, will be equal to the dimensions of the matrix.  The orientation of the image will match that of the matrix, in the sense that the image will appear as the matrix does when printed in the Eidos console; internally this requires a transposition of values, as discussed further below.  For the integer case, the integerK property of the resulting image will recover the original matrix exactly; for the float case, the floatK property will only approximately recover the original matrix since the translation into 8-bit pixel values involves quantization, but values of 0.0 and 1.0 will be recovered exactly.

5.4.1  Image properties

width => (integer$)

The width of the image, in pixels.

height => (integer$)

The height of the image, in pixels.

isGrayscale => (logical$)

This flag is T if the image is grayscale, with only a K channel; it is F if the image is color, with R/G/B channels.

bitsPerChannel => (integer$)

The number of bits used to represent a single pixel, in one channel of the image.  At present this is always 8; grayscale (K) images are 8-bit, color (RGB) images are 24-bit.  It could be extended to support 16-bit channels in future.

integerR => (integer)

The red (R) channel of the image, represented as a 2D integer matrix.  Values will be in [0,255].  See the floatR property for an alternative representation.  If the image is grayscale, this property is unavailable.

integerG => (integer)

The green (G) channel of the image, represented as a 2D integer matrix.  Values will be in [0,255].  See the floatG property for an alternative representation.  If the image is grayscale, this property is unavailable.

integerB => (integer)

The blue (R) channel of the image, represented as a 2D integer matrix.  Values will be in [0,255].  See the floatB property for an alternative representation.  If the image is grayscale, this property is unavailable.

integerK => (integer)

The gray (K) channel of the image, represented as a 2D integer matrix.  Values will be in [0,255].  See the floatK property for an alternative representation.  If the image is color, this property is unavailable.

floatR => (float)

The red (R) channel of the image, represented as a 2D float matrix.  Values will be in [0,1], obtained by dividing the integerR layer by 255.  See the integerR property for an alternative representation.  If the image is grayscale, this property is unavailable.

floatG => (float)

The green (G) channel of the image, represented as a 2D float matrix.  Values will be in [0,1], obtained by dividing the integerG layer by 255.  See the integerG property for an alternative representation.  If the image is grayscale, this property is unavailable.

floatB => (float)

The blue (B) channel of the image, represented as a 2D float matrix.  Values will be in [0,1], obtained by dividing the integerB layer by 255.  See the integerB property for an alternative representation.  If the image is grayscale, this property is unavailable.

floatK => (float)

The gray (K) channel of the image, represented as a 2D float matrix.  Values will be in [0,1], obtained by dividing the integerK layer by 255.  See the integerK property for an alternative representation.  If the image is color, this property is unavailable.

5.4.2  Image methods

– (void)write(string$ filePath)

Writes the image to the given filesystem path filePath as PNG data.  It is suggested, but not required, that filePath should end in a .png or .PNG filename extension.  If the file cannot be written, an error will result.  At present, since bitsPerChannel is always 8, grayscale data will be written as an 8-bit grayscale PNG while color (RGB) data will be written as a 24-bit color PNG without alpha.

================================================ FILE: QtSLiM/help/EidosHelpFunctions.html ================================================

3.1.  Math functions

(numeric)abs(numeric x)

Returns the absolute value of x.  If x is integer, the C++ function llabs() is used and an integer vector is returned; if x is float, the C++ function fabs() is used and a float vector is returned.

(float)acos(numeric x)

Returns the arc cosine of x using the C++ function acos().

(float)asin(numeric x)

Returns the arc sine of x using the C++ function asin().

(float)atan(numeric x)

Returns the arc tangent of x using the C++ function atan().

(float)atan2(numeric x, numeric y)

Returns the arc tangent of y/x using the C++ function atan2(), which uses the signs of both x and y to determine the correct quadrant for the result.

(float)ceil(float x)

Returns the ceiling of x: the smallest integral value greater than or equal to x.  Note that the return value is float even though integral values are guaranteed, because values could be outside of the range representable by integer.

(float)cos(numeric x)

Returns the cosine of x using the C++ function cos().

(numeric)cumProduct(numeric x)

Returns the cumulative product of x: a vector of equal length as x, in which the element at index i is equal to the product of the elements of x across the range 0:i.  The return type will match the type of x.  If x is of type integer, but all of the values of the cumulative product vector cannot be represented in that type, an error condition will result.

(numeric)cumSum(numeric x)

Returns the cumulative sum of x: a vector of equal length as x, in which the element at index i is equal to the sum of the elements of x across the range 0:i.  The return type will match the type of x.  If x is of type integer, but all of the values of the cumulative sum vector cannot be represented in that type, an error condition will result.

(float)exp(numeric x)

Returns the base-e exponential of x, ex,  using the C++ function exp().  This may be somewhat faster than E^x for large vectors.

(float)floor(float x)

Returns the floor of x: the largest integral value less than or equal to x.  Note that the return value is float even though integral values are guaranteed, because values could be outside of the range representable by integer.

(integer)integerDiv(integer x, integer y)

Returns the result of integer division of x by y.  The / operator in Eidos always produces a float result; if you want an integer result you may use this function instead.  If any value of y is 0, an error will result.  The parameters x and y must either be of equal length, or one of the two must be a singleton.  The precise behavior of integer division, in terms of how rounding and negative values are handled, may be platform dependent; it will be whatever the C++ behavior of integer division is on the given platform.  Eidos does not guarantee any particular behavior, so use this function with caution.

(integer)integerMod(integer x, integer y)

Returns the result of integer modulo of x by y.  The % operator in Eidos always produces a float result; if you want an integer result you may use this function instead.  If any value of y is 0, an error will result.  The parameters x and y must either be of equal length, or one of the two must be a singleton.  The precise behavior of integer modulo, in terms of how rounding and negative values are handled, may be platform dependent; it will be whatever the C++ behavior of integer modulo is on the given platform.  Eidos does not guarantee any particular behavior, so use this function with caution.

(logical)isFinite(float x)

Returns the finiteness of x: T if x is not INF or NAN, F if x is INF or NAN.  INF and NAN are defined only for type float, so x is required to be a float.  Note that isFinite() is not the opposite of isInfinite(), because NAN is considered to be neither finite nor infinite.

(logical)isInfinite(float x)

Returns the infiniteness of x: T if x is INF, F otherwise.  INF is defined only for type float, so x is required to be a float.  Note that isInfinite() is not the opposite of isFinite(), because NAN is considered to be neither finite nor infinite.

(logical)isNAN(float x)

Returns the undefinedness of x: T if x is not NAN, F if x is NAN.  NAN is defined only for type float, so x is required to be a float.

(float)log(numeric x)

Returns the base-e logarithm of x using the C++ function log().

(float)log10(numeric x)

Returns the base-10 logarithm of x using the C++ function log10().

(float)log2(numeric x)

Returns the base-2 logarithm of x using the C++ function log2().

(numeric$)product(numeric x)

Returns the product of x: the result of multiplying all of the elements of x together.  If x is float, the result will be float.  If x is integer, things are a bit more complex; the result will be integer if it can fit into the integer type without overflow issues (including during intermediate stages of the computation), otherwise it will be float.

(float)round(float x)

Returns the round of x: the integral value nearest to x, rounding half-way cases away from 0 (different from the rounding policy of R, which rounds halfway cases toward the nearest even number).  Note that the return value is float even though integral values are guaranteed, because values could be outside of the range representable by integer.

(*)setDifference(* x, * y)

Returns the set-theoretic (asymmetric) difference of x and y, denoted x y: a vector containing all elements that are in x but are not in y.  Duplicate elements will be stripped out, in the same manner as the unique() function.  The order of elements in the returned vector is arbitrary and should not be relied upon.  The returned vector will be of the same type as x and y, and x and y must be of the same type.

(*)setIntersection(* x, * y)

Returns the set-theoretic intersection of x and y, denoted x y: a vector containing all elements that are in both x and y (but not in only x or y).  Duplicate elements will be stripped out, in the same manner as the unique() function.  The order of elements in the returned vector is arbitrary and should not be relied upon.  The returned vector will be of the same type as x and y, and x and y must be of the same type.

(*)setSymmetricDifference(* x, * y)

Returns the set-theoretic symmetric difference of x and y, denoted x y: a vector containing all elements that are in x or y, but not in both.  Duplicate elements will be stripped out, in the same manner as the unique() function.  The order of elements in the returned vector is arbitrary and should not be relied upon.  The returned vector will be of the same type as x and y, and x and y must be of the same type.

(*)setUnion(* x, * y)

Returns the set-theoretic union of x and y, denoted x y: a vector containing all elements that are in x and/or y.  Duplicate elements will be stripped out, in the same manner as the unique() function.  This function is therefore roughly equivalent to unique(c(x, y)), but this function will probably be faster.  The order of elements in the returned vector is arbitrary and should not be relied upon.  The returned vector will be of the same type as x and y, and x and y must be of the same type.

(numeric)sign(numeric x)

Returns the sign of x, meaning that for each element of x, a value of either -1, 0, or 1 will be returned as the corresponding element in the returned vector depending upon whether the original element was (respectively) negative, zero, or positive.  If x is integer, an integer vector is returned; if x is float, a float vector is returned.

(float)sin(numeric x)

Returns the sine of x using the C++ function sin().

(float)sqrt(numeric x)

Returns the square root of x using the C++ function sqrt().  This may be somewhat faster than x^0.5 for large vectors.

(numeric$)sum(lif x)

Returns the sum of x: the result of adding all of the elements of x together.  The unusual parameter type signature lif indicates that x can be logical, integer, or float.  If x is float, the result will be float.  If x is logical, the result will be integer (the number of T values in x, since the integer values of T and F are 1 and 0 respectively).  If x is integer, things are a bit more complex; in this case, the result will be integer if it can fit into the integer type without overflow issues (including during intermediate stages of the computation), otherwise it will be float.  Note that floating-point roundoff issues can cause this function to return inexact results when x is float type; this is rarely an issue, but see the sumExact() function for an alternative.

(float$)sumExact(float x)

Returns the exact sum of x: the exact result of adding all of the elements of x together.  Unlike the sum() function, sumExact() accepts only type float, since the sum() function is already exact for other types.  When summing floating-point values – particularly values that vary across many orders of magnitude – the precision limits of floating-point numbers can lead to roundoff errors that cause the sum() function to return an inexact result.  This function does additional work to ensure that the final result is exact within the possible limits of the float type; some roundoff may still inevitably occur, in other words, but a more exact result could not be represented with a value of type float.  The disadvantage of using this function instead of sum() is that it is much slower – about 35 times slower, according to one test on macOS, but that will vary across operating systems and hardware.  This function is rarely truly needed, but apart from the performance consequences there is no disadvantage to using it.

(float)tan(numeric x)

Returns the tangent of x using the C++ function tan().

(float)trunc(float x)

Returns the truncation of x: the integral value nearest to, but no larger in magnitude than, x.  Note that the return value is float even though integral values are guaranteed, because values could be outside of the range representable by integer.

3.2.  Statistics functions

(float)cor(numeric x, [Nif y = NULL])

Returns the sample Pearson’s correlation coefficient between vectors x and y, usually denoted r.  If y is NULL, it is considered to have the same value as x; for vector x this is not very useful (since the correlation of x with itself is 1.0 by definition), but it is more useful for calculating a correlation matrix using the columns of x (see below).  The sizes of x and y must be identical.  If x and y have a size of 0 or 1, NAN will be returned (a change in behavior from Eidos 4.0; it used to return NULL).  The return value will be a singleton float.

It is also legal to call cor() with matrix x and/or y.  In this case the return value will be a correlation matrix between x and y.  Each column of x will be represented by one row of the result (or if x is a vector, the result will simply have one row representing x), and each column of y will be represented by one column of the result (or if y is a vector, the result will simply have one column representing y).  Each element in the result matrix will therefore represent the correlation between a column of matrix x (or the entirety of vector x) and a column of matrix y (or the entirety of vector y).  Calling cor(x, x), or equivalently cor(x), thus produces a symmetric correlation matrix among the columns of x.

(float)cov(numeric x, [Nif y = NULL])

Returns the corrected sample covariance between vectors x and y.  If y is NULL, it is considered to have the same value as x; for vector x this is equivalent to calling var(x), but it is more useful for calculating a variance-covariance matrix using the columns of x (see below).  The sizes of x and y must be identical.  If x and y have a size of 0 or 1, NAN will be returned (a change in behavior from Eidos 4.0; it used to return NULL).  The return value will be a singleton float.

It is also legal to call cov() with matrix x and/or y.  In this case the return value will be a covariance matrix between x and y.  Each column of x will be represented by one row of the result (or if x is a vector, the result will simply have one row representing x), and each column of y will be represented by one column of the result (or if y is a vector, the result will simply have one column representing y).  Each element in the result matrix will therefore represent the covariance between a column of matrix x (or the entirety of vector x) and a column of matrix y (or the entirety of vector y).  Calling cov(x, x), or equivalently cov(x), thus produces a symmetric variance-covariance matrix among the columns of x.

(float)filter(numeric x, float filter, [lif$ outside = F])

Returns the result of convolving x with filter.  The returned vector will be the same length as x.  The convolution is performed by centering filter on each position of x to produce a corresponding result element that is the sum over the products of each filter value with each x value within the filter’s range.  The length of filter is required to be odd, so that the filter has a central value (and can thus be centered over each value of x).

If the filter, centered over a given value of x, extends beyond the end of x then the calculation of the corresponding element of the result is governed by the outside parameter.  When outside is F (the default), the corresponding element in the result will be NAN; this matches the behavior of the R filter() function (except that R uses NA).  If outside is T, values outside x will be excluded from the calculation (the filter value covering that position will be considered to be 0), and the other values in the filter will be adjusted so that the sum of the absolute values of the filter weights used is unchanged, to compensate for the excluded values by giving the positions inside x more weight. Finally, if outside is integer or float, that value will be used as the value of x for all positions outside x; one might pass an expected value or mean value in this way, to be used for all outside positions.

This function is useful for computing running means and similar transformations of an input vector.  For a simple running mean of width w, pass rep(1/w, w) for filter.  That case is automatically detected and handled efficiently; otherwise, the runtime of this function is proportional to the length of x times the length of filter, and so will be slow for long filters.

(+$)max(+ x, ...)

Returns the maximum of x and the other arguments supplied: the single greatest value contained by all of them.  All of the arguments must be the same type as x, and the return type will match that of x.  If all of the arguments have a size of 0, the return value will be NULL; note that this means that max(x, max(y)) may produce an error, if max(y) is NULL, in cases where max(x, y) does not.

(float$)mean(lif x)

Returns the arithmetic mean of x: the sum of x divided by the number of values in x.  If x has a size of 0, the return value will be NULL.  The unusual parameter type signature lif indicates that x can be logical, integer, or float; if x is logical, it is coerced to integer internally (with F being 0 and T being 1, as always), allowing mean() to calculate the average truth value of a logical vector.

(+$)min(+ x, ...)

Returns the minimum of x and the other arguments supplied: the single smallest value contained by all of them.  All of the arguments must be the same type as x, and the return type will match that of x.  If all of the arguments have a size of 0, the return value will be NULL; note that this means that min(x, min(y)) may produce an error, if min(y) is NULL, in cases where min(x, y) does not.

(+)pmax(+ x, + y)

Returns the parallel maximum of x and y: the element-wise maximum for each corresponding pair of elements in x and y.  The type of x and y must match, and the returned value will have the same type.  In one usage pattern the size of x and y match, in which case the returned value will have the same size.  In the other usage pattern either x and y is a singleton, in which case the returned value will match the size of the non-singleton argument, and pairs of elements for comparison will be formed between the singleton’s element and each of the elements in the non-singleton.

(+)pmin(+ x, + y)

Returns the parallel minimum of x and y: the element-wise minimum for each corresponding pair of elements in x and y.  The type of x and y must match, and the returned value will have the same type.  In one usage pattern the size of x and y match, in which case the returned value will have the same size.  In the other usage pattern either x and y is a singleton, in which case the returned value will match the size of the non-singleton argument, and pairs of elements for comparison will be formed between the singleton’s element and each of the elements in the non-singleton.

(float)quantile(numeric x, [Nf probs = NULL])

Returns sample quantiles of x for the given probabilities.  The smallest value in x corresponds to a probability of 0, and the largest value in x to a probability of 1.  The probs vector should be a vector of probabilities in [0, 1], or NULL, which is equivalent to c(0.0, 0.25, 0.5, 0.75, 1.0), requesting sample quartiles.

The quantile function linearly interpolates between the points of the empirical cumulative distribution function.  In other words, if x is a vector of length n+1, then the quantiles with probs equal to (0, 1/n, 2/n, ..., (n−1)/n, 1) are equal to the sorted values of x, and the quantile is a linear function of probs otherwise.  Note that there are many ways to compute quantiles; this algorithm corresponds to R’s default “type 7” algorithm.

(numeric)range(numeric x, ...)

Returns the range of x and the other arguments supplied: a vector of length 2 composed of the minimum and maximum values contained by all of them, at indices 0 and 1 respectively.  All of the arguments must be the same type as x, and the return type will match that of x.  If all of the arguments have a size of 0, the return value will be NULL; note that this means that range(x, range(y)) may produce an error, if range(y) is NULL, in cases where range(x, y) does not.

(numeric)rank(numeric x, [string$ tiesMethod = "average"])

Returns the ranks of the elements of x: a vector of length L (the length of x), composed of the relative ranks, from 1 to L, of each corresponding element of x.  The tiesMethod parameter may be any of "average" (the default), "first", "last", "max", or "min" ("random", supported by R, is not supported by Eidos at this time but could be added if needed).  For "average", the return value is of type float; for all others, it is of type integer.  (Note that the return type does not depend upon the type of x.)

The result for all of these tiesMethod values is identical (except for type) if the elements of x are unique; the difference between these methods is in how ties are resolved.  Suppose that n elements of x are tied (because they are equal), corresponding to ranks k through k+n−1.  For tiesMethod "average", all n tied elements receive the same rank, (k + (n−1)/2), which is the average of the ranks.  For "first", the first tied element receives rank k, upward to the last tied element receiving rank k+n−1.  For "last", the last tied element receives rank k, downward to the first tied element receiving rank k+n−1.  For "max", all n tied element receive the maximum rank, k+n−1.  For "min", all n tied element receive the minimum rank, k.

(float$)sd(numeric x)

Returns the corrected sample standard deviation of x.  If x has a size of 0 or 1, NAN will be returned (a change in behavior from Eidos 4.0; it used to return NULL).  Matrix/array dimensions are ignored by sd(); it simply uses all of the elements of x for its calculation.

(float$)ttest(float x, [Nf y = NULL], [Nf$ mu = NULL])

Returns the p-value resulting from running a t-test with the supplied data.  Two types of t-tests can be performed.  If x and y are supplied (i.e., y is non-NULL), a two-sample unpaired two-sided Welch’s t-test is conducted using the samples in x and y, each of which must contain at least two elements.  The null hypothesis for this test is that the two samples are drawn from populations with the same mean.  Other options, such as pooled-variance t-tests, paired t-tests, and one-sided t-tests, are not presently available.  If x and mu are supplied (i.e., mu is non-NULL), a one-sample t-test is conducted in which the null hypothesis is that the sample is drawn from a population with mean mu.

Note that the results from this function are substantially different from those produced by R.  The Eidos ttest() function uses uncorrected sample statistics, which means they will be biased for small sample sizes, whereas R probably uses corrected, unbiased sample statistics.  This is an Eidos bug, and might be fixed if anyone complains.  If large sample sizes are used, however, the bias is likely to be small, and uncorrected statistics are simpler and faster to compute.

(float$)var(numeric x)

Returns the corrected sample variance of x.  If x has a size of 0 or 1, NAN will be returned (a change in behavior from Eidos 4.0; it used to return NULL).  This is the square of the standard deviation calculated by sd().  It is illegal to call var() with a matrix or array argument; use cov() to calculate a variance-covariance matrix.

3.3.  Distribution drawing and density functions

(float)dmvnorm(float x, numeric mu, numeric sigma)

Returns a vector of probability densities for a k-dimensional multivariate normal distribution with a length k mean vector mu and a k × k variance-covariance matrix sigma.  The mu and sigma parameters are used for all densities.  The quantile values, x, should be supplied as a matrix with one row per vector of quantile values and k columns (one column per dimension); for convenience, a single quantile may be supplied as a vector rather than a matrix with just one row.  The number of dimensions k must be at least two; for k=1, use dnorm().

Cholesky decomposition of the variance-covariance matrix sigma is involved as an internal step, and this requires that sigma be positive-definite; if it is not, an error will result.  When more than one density is needed, it is much more efficient to call dmvnorm() once to generate all of the densities, since the Cholesky decomposition of sigma can then be done just once.

(float)dbeta(float x, numeric alpha, numeric beta)

Returns a vector of probability densities for a beta distribution at quantiles x with parameters alpha and beta.  The alpha and beta parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of the same length as x, specifying a value for each density computation.  The probability density function is P(s | α,β) = [Γ(α+β)/Γ(α)Γ(β)]sα−1(1−s)β−1, where α is alpha and β is beta.  Both parameters must be greater than 0.

(float)dexp(float x, [numeric mu = 1])

Returns a vector of probability densities for an exponential distribution at quantiles x with mean mu (i.e. rate 1/mu).  The mu parameter may either be a singleton, specifying a single value to be used for all of the draws, or they may be vectors of the same length as x, specifying a value for each density computation.

(float)dgamma(float x, numeric mean, numeric shape)

Returns a vector of probability densities for a gamma distribution at quantiles x with mean mean and shape parameter shape.  The mean and shape parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of the same length as x, specifying a value for each density computation.  The probability density function is P(s | α,β) = [Γ(α)βα]−1sα−1exp(−s/β), where α is the shape parameter shape, and the mean of the distribution given by mean is equal to αβ.

(float)dnorm(float x, [numeric mean = 0], [numeric sd = 1])

Returns a vector of probability densities for a normal distribution at quantiles x with mean mean and standard deviation sd.  The mean and sd parameters may either be singletons, specifying a single value to be used for all of the densities, or they may be vectors of the same length as x, specifying a value for each density computation.

(integer)findInterval(numeric x, numeric vec, [logical$ rightmostClosed = F], [logical$ allInside = F])

Returns a vector of interval indices for the values in x within a vector of non-decreasing breakpoints vec.  The returned integer vector contains, for each corresponding element of x, the index of the interval in vec within which that element of x is contained.

More precisely, if i is the returned integer vector from findInterval(x, v), and N is length(v), then for each index j in x, it will be true that v[i[j]]x[j] < v[i[j]+1], treating v[-1] as -INF and v[N] as INF, assuming that the two flags rightmostClosed and allInside have their default value of F.  The effects of the flags will be discussed below.  Note that vec must be non-decreasing; in other words, it must be sorted in ascending order, although it may have duplicate values.  The returned vector will thus be equal in length to x, and each of its elements will be in the interval [-1, N-1].

The rightmostClosed flag, if T, alters the above behavior to treat the rightmost interval, vec[N-2] .. vec[N-1], as closed.  This means that if x[j]==vec[N-1] (i.e., equals max(vec)), the corresponding result i[j] will be N-2 as for all other values in the last interval.

The allInside flag, if T, alters the above behavior to coerce returned indices into 0 .. N-2.  In other words, -1 is mapped to 0, and N-1 is mapped to N-2.

(float)pnorm(float q, [numeric mean = 0], [numeric sd = 1])

Returns a vector of cumulative distribution function values for a normal distribution at quantiles q with mean mean and standard deviation sd.  The mean and sd parameters may either be singletons, specifying a single value to be used for all of the quantiles, or they may be vectors of the same length as q, specifying a value for each quantile.

(float)qnorm(float p, [numeric mean = 0], [numeric sd = 1])

Returns a vector of quantiles for a normal distribution with lower tail probabilities less than p, with mean mean and standard deviation sd. The mean and sd parameters may either be singletons, specifying a single value to be used for all of the quantiles, or they may be vectors of the same length as p, specifying a value for each quantile computation.

(float)rbeta(integer$ n, numeric alpha, numeric beta)

Returns a vector of n random draws from a beta distribution with parameters alpha and beta.  The alpha and beta parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.  Draws are made from a beta distribution with probability density P(s | α,β) = [Γ(α+β)/Γ(α)Γ(β)]sα−1(1−s)β−1, where α is alpha and β is beta.  Both parameters must be greater than 0.  The values drawn are in the interval [0, 1].

(integer)rbinom(integer$ n, integer size, float prob)

Returns a vector of n random draws from a binomial distribution with a number of trials specified by size and a probability of success specified by prob.  The size and prob parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.

(float)rcauchy(integer$ n, [numeric location = 0], [numeric scale = 1])

Returns a vector of n random draws from a Cauchy distribution with location location and scale scale.  The location and scale parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.

(float)rdirichlet(integer$ n, numeric alpha)

Returns a matrix of n random draws from a Dirichlet distribution with vector of shape parameters alpha.  The Dirichlet distribution is a multidimensional generalization of the beta distribution, sometimes called the multivariate beta distribution; see also rbeta().  All values in alpha must be positive and finite, and alpha must be of length >= 2.  The return value is a matrix with n rows and size(alpha) columns, each row containing a single Dirichlet random deviate.

(integer)rdunif(integer$ n, [integer min = 0], [integer max = 1])

Returns a vector of n random draws from a discrete uniform distribution from min to max, inclusive.  The min and max parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.  See runif() for draws from a continuous uniform distribution, and runif64() for uniform draws from the full 64-bit integer range.

(integer)rdunif64(integer$ n)

Returns a vector of n random draws from a discrete uniform distribution spanning the full 64-bit integer range.  See rdunif() for draws from a discrete uniform distribution with a specified range.

(float)rexp(integer$ n, [numeric mu = 1])

Returns a vector of n random draws from an exponential distribution with mean mu (i.e. rate 1/mu).  The mu parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length n, specifying a value for each draw.

(float)rf(integer$ n, numeric d1, numeric d2)

Returns a vector of n random draws from an F-distribution with degrees of freedom d1 and d2.  The d1 and d2 parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.

(float)rgamma(integer$ n, numeric mean, numeric shape)

Returns a vector of n random draws from a gamma distribution with mean mean and shape parameter shape.  The mean and shape parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.  Draws are made from a gamma distribution with probability density P(s | α,β) = [Γ(α)βα]−1exp(−s/β), where α is the shape parameter shape, and the mean of the distribution given by mean is equal to αβ.  Values of mean less than zero are allowed, and are equivalent (in principle) to the negation of a draw from a gamma distribution with the same shape parameter and the negation of the mean parameter.

(integer)rgeom(integer$ n, float p)

Returns a vector of n random draws from a geometric distribution with parameter p.  The p parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length n, specifying a value for each draw.  Eidos follows R in using the geometric distribution with support on the set {0, 1, 2, …}, where the drawn value indicates the number of failures prior to success.  There is an alternative definition, based upon the number of trial required to get one success, so beware.

(float)rlaplace(integer$ n, [numeric b = 1])

Returns a vector of n random draws from a Laplace distribution with shape parameter b.  The b parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length n, specifying a value for each draw.

(float)rlnorm(integer$ n, [numeric meanlog = 0], [numeric sdlog = 1])

Returns a vector of n random draws from a lognormal distribution with mean meanlog and standard deviation sdlog, specified on the log scale.  The meanlog and sdlog parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.

(integer)rmultinom(integer$ n, integer$ size, numeric prob)

Returns a matrix of n random vectors from the specified multinomial distribution.  For each random vector drawn from the multinomial distribution, size objects will be put into k boxes, where prob is a vector of probabilities of length k specifying the probability for each box.  If prob is not normalized to sum to 1.0, its entries are treated as weights and normalized appropriately.  The draws are returned as a matrix with k rows (one row per box) and n columns (one column per drawn random vector).

(float)rmvnorm(integer$ n, numeric mu, numeric sigma)

Returns a matrix of n random draws from a k-dimensional multivariate normal distribution with a length k mean vector mu and a k × k variance-covariance matrix sigma.  The mu and sigma parameters are used for all n draws.  The draws are returned as a matrix with n rows (one row per draw) and k columns (one column per dimension).  The number of dimensions k must be at least two; for k=1, use rnorm().

Cholesky decomposition of the variance-covariance matrix sigma is involved as an internal step, and this requires that sigma be positive-definite; if it is not, an error will result.  When more than one draw is needed, it is much more efficient to call rmvnorm() once to generate all of the draws, since the Cholesky decomposition of sigma can then be done just once.

(integer)rnbinom(integer$ n, numeric size, float prob)

Returns a vector of n random draws from a negative binomial distribution representing the number of failures which occur in a sequence of Bernoulli trials before reaching a target number of successful trials specified by size, given a probability of success specified by prob.  The mean of this distribution for size s and prob p is s(1−p)/p, with variance s(1−p)/p2.  The size and prob parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.

(float)rnorm(integer$ n, [numeric mean = 0], [numeric sd = 1])

Returns a vector of n random draws from a normal distribution with mean mean and standard deviation sd.  The mean and sd parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.

(integer)rpois(integer$ n, numeric lambda)

Returns a vector of n random draws from a Poisson distribution with parameter lambda (not to be confused with the language concept of a “lambda”; lambda here is just the name of a parameter, because the symbol typically used for the parameter of a Poisson distribution is the Greek letter λ).  The lambda parameter may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length n, specifying a value for each draw.

(float)runif(integer$ n, [numeric min = 0], [numeric max = 1])

Returns a vector of n random draws from a continuous uniform distribution from min to max, inclusive.  The min and max parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.  See rdunif() for draws from a discrete uniform distribution.

(float)rweibull(integer$ n, numeric lambda, numeric k)

Returns a vector of n random draws from a Weibull distribution with scale parameter lambda and shape parameter k, both greater than zero.  The lambda and k parameters may either be singletons, specifying a single value to be used for all of the draws, or they may be vectors of length n, specifying a value for each draw.  Draws are made from a Weibull distribution with probability distribution P(s | λ,k) = (k / λksk−1 exp(-(s/λ)k).

(integer)rztpois(integer$ n, numeric lambda)

Returns a vector of n random draws from a zero-truncated Poisson distribution with parameter lambda (not to be confused with the language concept of a “lambda”; lambda here is just the name of a parameter, because the symbol typically used for the parameter of a Poisson distribution is the Greek letter λ).  The zero-truncated Poisson distribution is the conditional probability distribution of a Poisson-distributed random variable, given that the value of the random variable is not zero.  The values returned by rztpois() will therefore never be zero.

The lambda parameter, λ, may either be a singleton, specifying a single value to be used for all of the draws, or it may be a vector of length n, specifying a value for each draw.  It is important to note that for rpois() the expected mean of the distribution is λ, but for rztpois() the expected mean of the distribution is λ / (1 − exp(−λ)), precisely because the zero-truncated Poisson distribution is conditional upon being non-zero.

3.4.  Vector construction functions

(*)c(...)

Returns the concatenation of all of its parameters into a single vector, stripped of all matrix/array dimensions (see rbind() and cbind() for concatenation that does not strip this information).  The parameters will be promoted to the highest type represented among them, and that type will be the return type.  NULL values are ignored; they have no effect on the result.

(float)float(integer$ length)

Returns a new float vector of the length specified by length, filled with 0.0 values.  This can be useful for pre-allocating a vector which you then fill with values by subscripting.

(integer)integer(integer$ length, [integer$ fill1 = 0], [integer$ fill2 = 1], [Ni fill2Indices = NULL])

Returns a new integer vector of the length specified by length, filled with 0 values by default.  This can be useful for pre-allocating a vector which you then fill with values by subscripting.

If a value is supplied for fill1, the new vector will be filled with that value instead of the default of 0.  Additionally, if a non-NULL vector is supplied for fill2Indices, the indices specified by fill2Indices will be filled with the value provided by fill2.  For example, given the default values of 0 and 1 for fill1 and fill2, the returned vector will contain 1 at all positions specified by fill2Indices, and will contain 0 at all other positions.

(logical)logical(integer$ length)

Returns a new logical vector of the length specified by length, filled with F values.  This can be useful for pre-allocating a vector which you then fill with values by subscripting.

(object<Object>)object(void)

Returns a new empty object vector.  Unlike float(), integer(), logical(), and string(), a length cannot be specified and the new vector contains no elements.  This is because there is no default value for the object type.  Adding to such a vector is typically done with c().  Note that the return value is of type object<Object>; this method creates an object vector that does not know what element type it contains.  Such object vectors may be mixed freely with other object vectors in c() and similar contexts; the result of such mixing will take its object-element type from the object vector with a defined object-element type (if any).

(*)rep(* x, integer$ count)

Returns the repetition of x: the entirety of x is repeated count times.  The return type matches the type of x.

(*)repEach(* x, integer count)

Returns the repetition of elements of x: each element of x is repeated.  If count is a singleton, it specifies the number of times that each element of x will be repeated.  Otherwise, the length of count must be equal to the length of x; in this case, each element of x is repeated a number of times specified by the corresponding value of count.

(*)sample(* x, integer$ size, [logical$ replace = F], [Nif weights = NULL])

Returns a vector of size containing a sample from the elements of x.  If replace is T, sampling is conducted with replacement (the same element may be drawn more than once); if it is F (the default), then sampling is done without replacement.  A vector of weights may be supplied in the optional parameter weights; if not NULL, it must be equal in size to x, all weights must be non-negative, and the sum of the weights must be greater than 0.  If weights is NULL (the default), then equal weights are used for all elements of x.  An error occurs if sample() runs out of viable elements from which to draw; most notably, if sampling is done without replacement then size must be at most equal to the size of x, but if weights of zero are supplied then the restriction on size will be even more stringent.  The draws are obtained from the standard Eidos random number generator, which might be shared with the Context.

(numeric)seq(numeric$ from, numeric$ to, [Nif$ by = NULL], [Ni$ length = NULL])

Returns a sequence, starting at from and proceeding in the direction of to until the next value in the sequence would fall beyond to.  If the optional parameters by and length are both NULL (the default), the sequence steps by values of 1 or -1 (as needed to proceed in the direction of to).  A different step value may be supplied with by, but must have the necessary sign.  Alternatively, a sequence length may be supplied in length, in which case the step magnitude will be chosen to produce a sequence of the requested length (with the necessary sign, as before).  If from and to are both integer then the return type will be integer when possible (but this may not be possible, depending upon values supplied for by or length), otherwise it will be float.

(integer)seqAlong(* x)

Returns an index sequence along x, from 0 to size(x) - 1, with a step of 1.  This is a convenience function for easily obtaining a set of indices to address or iterate through a vector.  Any matrix/array dimension information is ignored; the index sequence is suitable for indexing into x as a vector.

(integer)seqLen(integer$ length)

Returns an index sequence of length, from 0 to length - 1, with a step of 1; if length is 0 the sequence will be zero-length.  This is a convenience function for easily obtaining a set of indices to address or iterate through a vector.  Note that when length is 0, using the sequence operator with 0:(length-1) will produce 0 -1, and calling seq(a, b, length=length) will raise an error, but seqLen(length) will return integer(0), making seqLen() particularly useful for generating a sequence of a given length that might be zero.

(string)string(integer$ length)

Returns a new string vector of the length specified by length, filled with "" values.  This can be useful for pre-allocating a vector which you then fill with values by subscripting.

3.5.  Value inspection & manipulation functions

(logical$)all(logical x, ...)

Returns T if all values are T in x and in any other arguments supplied; if any value is F, returns F.  All arguments must be of logical type.  If all arguments are zero-length, T is returned.

(logical$)allClose(float x, float y, [float$ rtol = 1.0e-05], [float$ atol = 1.0e-08], [logical$ equalNAN = F])

Returns T if all pairs of values between x and y are “close”; if any pair of values is not close, returns F.  The definition of “close” matches that used in the isClose() function; see that documentation for all further details, including the way that values in x and y are paired, as well as the meaning of the rtol, atol, and equalNAN parameters.  This function is essentially equivalent to all(isClose(x, y, rtol, atol, equalNAN)), but is more efficient.

(logical$)any(logical x, ...)

Returns T if any value is T in x or in any other arguments supplied; if all values are F, returns F.  All arguments must be of logical type.  If all arguments are zero-length, F is returned.

(void)cat(* x, [string$ sep = " "], [logical$ error = F])

Concatenates output to Eidos’s output stream, joined together by sep.  The value x that is output may be of any type.  A newline is not appended to the output, unlike the behavior of print(); if a trailing newline is desired, you can use "\n" (or use the catn() function).  Also unlike print(), cat() tends to emit very literal output; print(logical(0)) will emit “logical(0)”, for example – showing a semantic interpretation of the value – whereas cat(logical(0)) will emit nothing at all, since there are no elements in the value (it is zero-length).  Similarly, print(NULL) will emit “NULL”, but cat(NULL) will emit nothing.

By default (when error is F), the output is sent to the standard Eidos output stream.  When running at the command line, this sends it to stdout; when running in SLiMgui, this sends it to the simulation window’s output textview.  If error is T, the output is instead sent to the Eidos error stream.  When running at the command line, this sends it to stderr; when running in SLiMgui, the output is routed to the simulation’s debugging output window.

(void)catn([* x = ""], [string$ sep = " "], [logical$ error = F])

Concatenates output (with a trailing newline) to Eidos’s output stream, joined together by sep.  The behavior of catn() is identical to that of cat(), except that a final newline character is appended to the output for convenience.  For catn() a default value of "" is supplied for x, to allow a simple catn() call with no parameters to emit a newline.

By default (when error is F), the output is sent to the standard Eidos output stream.  When running at the command line, this sends it to stdout; when running in SLiMgui, this sends it to the simulation window’s output textview.  If error is T, the output is instead sent to the Eidos error stream.  When running at the command line, this sends it to stderr; when running in SLiMgui, the output is routed to the simulation’s debugging output window.

(string)format(string$ format, numeric x)

Returns a vector of formatted strings generated from x, based upon the formatting string format.  The format parameter may be any string value, but must contain exactly one escape sequence beginning with the % character.  This escape sequence specifies how to format a single value from the vector x.  The returned vector contains one string value for each element of x; each string value is identical to the string supplied in format, except with a formatted version of the corresponding value from x substituted in place of the escape sequence.

The syntax for format is a subset of the standard C/C++ printf()-style format strings (e.g., http://en.cppreference.com/w/c/io/fprintf).  The escape sequence used to format each value of x is composed of several elements:

  •   A % character at the beginning, initiating the escape sequence (if an actual % character is desired, rather than an escape sequence, %% may be used)
  •   Optional flags that modify the style of formatting:
  • - : The value is left-justified with the field (as opposed to the default of right-justification).
  • + : The sign of the value is always prepended, even if the value is positive (as opposed to the default of appending the sign only if the value is negative).
  • space : The value is prepended by a space when a sign is not prepended.  This is ignored if the + flag is present, since values are then always prepended by a sign.
  • # : An alternative format is used.  For %o, at least one leading zero is always produced.  For %x and %X, 0x or 0X (respectively) is prepended if the value is nonzero.  For %f, %F, %e, %E, %g, and %G, a decimal point is forced even if no zeros follow.
  • 0 : Leading zeros are used to pad the field instead of spaces.  This flag is ignored if the left-justification flag, -, is present.  It is also ignored for integer values, if a precision is specified.
  •   An optional minimum field width, specified as an integer value.  Fields will be padded out to this minimum width.  Padding will be done with space characters by default (or with zeros, if the 0 flag is used), on the left by default (or on the right, if the - flag is used).
  •   An optional precision, given as an integer value preceded by a . character.  If no integer value follows the . character, a precision of zero will be used.  For integer values of x (formatted with %d, %i, %o, %x, or %X) the precision specifies the minimum number of digits that will appear (with extra zeros on the left if necessary), with a default precision of 1.  For float values of x formatted with %f, %F, %e, %E, %g, or %G, the precision specifies the minimum number of digits that will appear to the right of the decimal point (with extra zeros on the right if necessary), with a default precision of 6.
  •   A format specifier.  For integer values, this may be %d or %i (producing base-10 output; there is no difference between the two), %o (producing base-8 or octal output), %x (producing base-16 hexadecimal output using lowercase letters), or %X (producing base-16 hexadecimal output using uppercase letters).  For float values, this may be %f or %F to produce decimal notation (of the form [−]ddd.ddd; there is no difference between the two), %e or %E to produce scientific notation (of the form [−]d.ddde±dd or [−]d.dddE±dd, respectively), or %g or %G to produce either decimal notation or scientific notation (using the formatting of %f / %e or %F / %E, respectively) on a per-value basis, depending upon the range of the value.

Note that relative to the standard C/C++ printf()-style behavior, there are a few differences: (1) only a single escape sequence may be present in the format string, (2) the use of * to defer field width and precision values to a passed parameter is not supported, (3) only integer and float values of x are supported, (4) only the %d, %i, %o, %x, %X, %f, %F, %e, %E, %g, and %G format specifiers are supported, and (5) no length modifiers may be supplied, since Eidos does not support different sizes of the integer and float types.  Note also that the Eidos conventions of emitting INF and NAN for infinities and Not-A-Number values respectively is not honored by this function; the strings generated for such values are platform-dependent, following the implementation definition of the C++ compiler used to build Eidos, since format() calls through to snprintf() to assemble the final string values.

For example, format("A number: %+7.2f", c(-4.1, 15.375, 8)) will produce a vector with three elements: "A number:   -4.10" "A number:  +15.38" "A number:   +8.00".  The precision of .2 results in two digits after the decimal point, the minimum field width of 7 results in padding of the values on the left (with spaces) to a minimum of seven characters, the flag + causes a sign to be shown on positive values as well as negative values, and the format specifier f leads to the float values of x being formatted in base-10 decimal.  One string value is produced in the result vector for each value in the parameter x.  These values could then be merged into a single string with paste(), for example, or printed with print() or cat().

(logical$)identical(* x, * y, ...)

Returns a logical value indicating whether two or more values are identical.  For two values x and y, this will return T if x and y have exactly the same type and size, and all of their corresponding elements are exactly the same, and (for matrices and arrays) their dimensions are identical; otherwise it will return F.  Additional parameters beyond x and y are compared to x in the same manner, and T is returned only if all of the parameters are identical.

The test here is for exact equality; an integer value of 1 is not considered identical to a float value of 1.0, for example.  Elements in object values must be literally the same element, not simply identical in all of their properties.  Type promotion is never done.  For testing whether two values are the same, this is generally preferable to the use of operator == or operator !=; see the discussion at section 2.5.1.  Note that identical(NULL,NULL) and identical(NAN, NAN) are both T.

(*)ifelse(logical test, * trueValues, * falseValues)

Returns the result of a vector conditional operation: a vector composed of values from trueValues, for indices where test is T, and values from falseValues, for indices where test is F.  The lengths of trueValues and falseValues must either be equal to 1 or to the length of test; however, trueValues and falseValues don’t need to be the same length as each other.  Furthermore, the type of trueValues and falseValues must be the same (including, if they are object type, their element type).  The return will be of the same length as test, and of the same type as trueValues and falseValues.  Each element of the return vector will be taken from the corresponding element of trueValues if the corresponding element of test is T, or from the corresponding element of falseValues if the corresponding element of test is F; if the vector from which the value is to be taken (i.e., trueValues or falseValues) has a length of 1, that single value is used repeatedly, recycling the vector.  If test, trueValues, and/or falseValues are matrices or arrays, that will be ignored by ifelse() except that the result will be of the same dimensionality as test.

This is quite similar to a function in R of the same name; note, however, that Eidos evaluates all arguments to functions calls immediately, so trueValues and falseValues will be evaluated fully regardless of the values in test, unlike in R.  Value expressions without side effects are therefore recommended.

(logical)isClose(float x, float y, [float$ rtol = 1.0e-05], [float$ atol = 1.0e-08], [logical$ equalNAN = F])

Returns a logical vector indicating whether each pair of values in x and y are “close”; if any pair of values is not close, returns F.  See the allClose() function for an efficient way to test whether all pairs of values in x and y are close.

A pair of values a and b is considered “close” according to the following criteria.  If both a and b are finite, they are close if abs(a − b) <= (atol + rtol * abs(b)).  If both a and b are infinite, they are close if they have the same sign.  If both a and b are NAN, they are close if equalNAN is T.  In all other cases, a and b are not close.  For finite values, rtol thus defines a relative tolerance, and atol an absolute tolerance; the relative difference rtol * abs(b) and the absolute difference atol are added together and compared against the absolute difference between a and b to determine closeness.

Note that the default value for atol is not appropriate when comparing numbers with magnitudes much smaller than one; be sure to select atol for the use case at hand, especially for defining the threshold below which a non-zero value a will be considered “close” to a very small or zero value b.  Note also that isClose() is not symmetric in a and b; it assumes that b is the reference value for calculating the relative difference.

Regarding how values in x and y are paired, three cases are supported.  If x and y are the same length, then x and y are paired element-wise; if x is singleton, the single x value is paired with each value in y; or if y is singleton, each value in x is paired with the single y value.

(integer$)length(* x)

Returns the size (e.g., length) of x: the number of elements contained in x.  Note that length() is a synonym for size().

(integer)match(* x, * table)

Returns a vector of the positions of (first) matches of x in table.  Type promotion is not performed; x and table must be of the same type.  For each element of x, the corresponding element in the result will give the position of the first match for that element of x in table; if the element has no match in table, the element in the result vector will be -1.  The result is therefore a vector of the same length as x.  If a logical result is desired, with T indicating that a match was found for the corresponding element of x, use (match(x, table) >= 0).

(integer)order(+ x, [logical$ ascending = T])

Returns a vector of sorting indices for x: a new integer vector of the same length as x, containing the indices into x that would sort x.  In other words, x[order(x)]==sort(x).  This can be useful for more complex sorting problems, such as sorting several vectors in parallel by a sort order determined by one of the vectors.  If the optional logical parameter ascending is T (the default), then the sorted order will be ascending; if it is F, the sorted order will be descending.  The ordering is determined according to the same logic as the < and > operators in Eidos.  To easily sort vectors in a single step, use sort() or sortBy(), for non-object and object vectors respectively.

(string$)paste(..., [string$ sep = " "])

Returns a joined string composed from the string representations of the elements of the parameters passed in, taken in order, joined together by sep.  Although this function is based upon the R paste() function of the same name, note that it is much simpler and less powerful; in particular, the result is always a singleton string, rather than returning a non-singleton string vector when one of the parameters is a non-singleton.  The string representation used by paste() is the same as that emitted by cat().

(string$)paste0(...)

Returns a joined string composed from the string representations of the elements of the parameters passed in, taken in order, joined together with no separator.  This function is identical to paste(), except that no separator is used.  Note that this differs from the semantics of paste0() in R.

(void)print(* x, [logical$ error = F])

Prints output to Eidos’s output stream.  The value x that is output may be of any type.  A newline is appended to the output.  See cat() for a discussion of the differences between print() and cat().

By default (when error is F), the output is sent to the standard Eidos output stream.  When running at the command line, this sends it to stdout; when running in SLiMgui, this sends it to the simulation window’s output textview.  If error is T, the output is instead sent to the Eidos error stream.  When running at the command line, this sends it to stderr; when running in SLiMgui, the output is routed to the simulation’s debugging output window.

(*)rev(* x)

Returns the reverse of x: a new vector with the same elements as x, but in the opposite order.

(integer$)size(* x)

Returns the size of x: the number of elements contained in x.  Note that length() is a synonym for size().

(+)sort(+ x, [logical$ ascending = T])

Returns a sorted copy of x: a new vector with the same elements as x, but in sorted order.  If the optional logical parameter ascending is T (the default), then the sorted order will be ascending; if it is F, the sorted order will be descending.  The ordering is determined according to the same logic as the < and > operators in Eidos.  To sort an object vector, use sortBy().  To obtain indices for sorting, use order().

(object)sortBy(object x, string$ property, [logical$ ascending = T])

Returns a sorted copy of x: a new vector with the same elements as x, but in sorted order.  If the optional logical parameter ascending is T (the default), then the sorted order will be ascending; if it is F, the sorted order will be descending.  The ordering is determined according to the same logic as the < and > operators in Eidos.  The property argument gives the name of the property within the elements of x according to which sorting should be done.  This must be a simple property name; it cannot be a property path.  For example, to sort a Mutation vector by the selection coefficients of the mutations, you would simply pass "selectionCoeff", including the quotes, for property.  To sort a non-object vector, use sort().  To obtain indices for sorting, use order().

(void)str(* x, [logical$ error = F])

Prints the structure of x: a summary of its type and the values it contains.  If x is an object, note that str() produces different results from the str() method of x; the str() function prints the external structure of x (the fact that it is an object, and the number and type of its elements), whereas the str() method prints the internal structure of x (the external structure of all the properties contained by x).

By default (when error is F), the output is sent to the standard Eidos output stream.  When running at the command line, this sends it to stdout; when running in SLiMgui, this sends it to the simulation window’s output textview.  If error is T, the output is instead sent to the Eidos error stream.  When running at the command line, this sends it to stderr; when running in SLiMgui, the output is routed to the simulation’s debugging output window.

(integer)tabulate(integer bin, [Ni$ maxbin = NULL])

Returns occurrence counts for each non-negative integer in bin.  Occurrence counts are tabulated into bins for each value 0:maxbin in bin; values outside that range are ignored.  The default value of maxbin, NULL, is equivalent to passing maxbin=max(0, bin); in other words, by default the result vector will be exactly large enough to accommodate counts for every integer in bin.  In any case, the result vector will contain maxbin+1 elements (some or all of which might be zero, if the occurrence count of that integer in bin is zero).

Note that the semantics of this function differ slightly from the tabulate() function in R, because R is 1-based and Eidos is 0-based.

(*)unique(* x, [logical$ preserveOrder = T])

Returns the unique values in x.  In other words, for each value k in x that occurs at least once, the vector returned will contain k exactly once.  If preserveOrder is T (the default), the order of values in x is preserved, taking the first instance of each value; this is relatively slow, with O(n^2) performance.  If preserveOrder if F instead, the order of values in x is not preserved, and no particular ordering should be relied upon; this is relatively fast, with O(n log n) performance.  This performance difference will only matter for large vectors, however; for most applications the default behavior can be retained whether the order of the result matters or not.

(integer)which(logical x)

Returns the indices of T values in x.  In other words, if an index k in x is T, then the vector returned will contain k; if index k in x is F, the vector returned will omit k.  One way to look at this is that it converts from a logical subsetting vector to an integer (index-based) subsetting vector, without changing which subset positions would be selected.

(integer$)whichMax(+ x)

Returns the index of the (first) maximum value in x.  In other words, if k is equal to the maximum value in x, then the vector returned will contain the index of the first occurrence of k in x.  If the maximum value is unique, the result is the same as (but more efficient than) the expression which(x==max(x)), which returns the indices of all of the occurrences of the maximum value in x.

(integer$)whichMin(+ x)

Returns the index of the (first) minimum value in x.  In other words, if k is equal to the minimum value in x, then the vector returned will contain the index of the first occurrence of k in x.  If the minimum value is unique, the result is the same as (but more efficient than) the expression which(x==min(x)), which returns the indices of all of the occurrences of the minimum value in x.

3.6.  Value type testing and coercion functions

(float)asFloat(+ x)

Returns the conversion to float of x.  If x is string and cannot be converted to float, Eidos will throw an error.

(integer)asInteger(+ x)

Returns the conversion to integer of x.  If x is of type string or float and cannot be converted to integer, Eidos will throw an error.

(logical)asLogical(+ x)

Returns the conversion to logical of x.  Recall that in Eidos the empty string "" is considered F, and all other string values are considered T.  Converting INF or -INF to logical yields T (since those values are not equal to zero); converting NAN to logical throws an error.

(string)asString(+ x)

Returns the conversion to string of x.  Note that asString(NULL) returns "NULL" even though NULL is zero-length.

(string$)elementType(* x)

Returns the element type of x, as a string.  For the non-object types, the element type is the same as the type: "NULL", "logical", "integer", "float", or "string".  For object type, however, elementType() returns the name of the type of element contained by the object, such as "Species" or "Mutation" in the Context of SLiM.  Contrast this with type().

(logical$)isFloat(* x)

Returns T if x is float type, F otherwise.

(logical$)isInteger(* x)

Returns T if x is integer type, F otherwise.

(logical$)isLogical(* x)

Returns T if x is logical type, F otherwise.

(logical$)isNULL(* x)

Returns T if x is NULL type, F otherwise.

(logical$)isObject(* x)

Returns T if x is object type, F otherwise.

(logical$)isString(* x)

Returns T if x is string type, F otherwise.

(string$)type(* x)

Returns the type of x, as a string: "NULL", "logical", "integer", "float", "string", or "object".  Contrast this with elementType().

3.7.  String manipulation functions

(lis)grep(string$ pattern, string x, [logical$ ignoreCase = F], [string$ grammar = "ECMAScript"], [string$ value = "indices"], [logical$ fixed = F], [logical$ invert = F])

Searches for regular expression matches in the string-elements of x.  Regular expressions (regexes) express patterns that strings can either match or not match; they are very widely used in programming languages and terminal shells.  The topic of regexes is very complex, and a great deal of information about them can be found online, including examples and tutorials; this manual will not attempt to document the topic in detail.

The grep() function uses a regex supplied in pattern, looking for matches for the regex in each element of x.  If ignoreCase is F (the default), the pattern matching will be case sensitive (i.e., uppercase versus lowercase will matter); if it is T, the pattern matching will be case-insensitive.

The grammar parameter determines the regex grammar used to find matches.  Several options are available.  The default, "ECMAScript", is a straightforward regex grammar, the specification for which can be found at https://www.cplusplus.com/reference/regex/ECMAScript/ among many other links.  The "basic" grammar uses POSIX basic regular expressions, often called BRE; this is documented at https://en.wikibooks.org/wiki/Regular_Expressions/POSIX_Basic_Regular_Expressions.  The "extended" grammar uses POSIX extended regular expressions, often called ERE; this is documented at https://en.wikibooks.org/wiki/Regular_Expressions/POSIX-Extended_Regular_Expressions.  The "awk" grammar is based upon the "extended" grammar, with more escapes for non-printing characters.  The "grep" and "egrep" grammars are based upon the "basic" and "extended" grammars, respectively, but also allow newline characters ("\n") to separate alternations.  If you are not sure which grammar you want to use, "ECMAScript" is recommended.  All of these grammars are implemented internally in Eidos using the C++ <regex> library, so if you need clarification on the details of a grammar, you can search for related C++ materials online.

Information about the matches found is returned in one of four ways.  If value is "indices" (the default), an integer vector is returned containing the index in x for each match.  If value is "elements", a string vector is returned containing the actual string-elements of x for each match.  If value is "matches", a string vector is returned containing only the substring that matched, within each string-element in x that matched (if more than one substring in a given element matched, the first match is returned).  Finally, if value is "logical" a logical vector is returned, of the same length as x, containing T where the corresponding element of x matched, or F where it did not match.  This function therefore encapsulates the functionality of both the grep() and grepl() functions of R; use value="logical" for functionality like that of R’s grepl().

If fixed is F (the default), matching is determined using pattern following the specified regex grammar as described above.  If fixed is T, matching is instead determined using pattern as a string value to be matched “as is”, rather than as a regular expression; the grammar specified does not matter in this case, but ignoreCase still applies.  This could be thought of as another grammar value, really, meaning “no grammar”, but it is supplied as a separate flag following R.

Finally, if invert if F (the default) matching proceeds as normal for the chosen regex grammar, whereas if invert if T matching is inverted: indices, elements, or logical values are returned for the elements of x that did not match.  If invert is T, the value parameter may not be "matches".

Note that there is not presently any way to extract subpattern matches, nor is there any way to perform replacements of matches.

(integer)nchar(string x)

Returns a vector of the number of characters in the string-elements of x.

(logical)strcontains(string x, string$ s, [integer$ pos = 0])

Returns the occurrence of a string specified by s in each of the elements of x, starting at position pos.  Position 0, the default, is the beginning of x; a position of 0 means the entire string is searched.  A starting search position that is at or beyond the end of a given element of x is not an error; it just implies that a match will not be found in that element.  The existences of matches are returned as a logical vector; if a match was found in a given element, the corresponding value in the returned vector is T, otherwise it is F.  This function is a simplified version of strfind(), which returns the positions of matches.  The strprefix() and strsuffix() functions are also related.

(integer)strfind(string x, string$ s, [integer$ pos = 0])

Returns the first occurrence of a string specified by s in each of the elements of x, starting at position pos.  Position 0, the default, is the beginning of x; a position of 0 means the entire string is searched.  A starting search position that is at or beyond the end of a given element of x is not an error; it just implies that a match will not be found in that element.  The positions of matches are returned as an integer vector; if no match was found in a given element, the corresponding value in the returned vector is -1.  The strcontains() function may be used when a logical value (found / not found) is desired.

(logical)strprefix(string x, string$ s)

Returns the occurrence of a prefix string specified by s at the beginning of each of the elements of x.  The existences of prefixes are returned as a logical vector; if a given element begins with the prefix, the corresponding value in the returned vector is T, otherwise it is F.

(string)strsplit(string$ x, [string$ sep = " "])

Returns substrings of x that were separated by the separator string sep.  Every substring defined by an occurrence of the separator is included, and thus zero-length substrings may be returned.  For example, strsplit(".foo..bar.", ".") returns a string vector containing "", "foo", "", "bar", "".  In that example, the empty string between "foo" and "bar" in the returned vector is present because there were two periods between foo and bar in the input string – the empty string is the substring between those two separators.  If sep is "", a vector of single characters will be returned, resulting from splitting x at every position.  Note that paste() performs the inverse operation of strsplit().

(logical)strsuffix(string x, string$ s)

Returns the occurrence of a suffix string specified by s at the end of each of the elements of x.  The existences of suffixes are returned as a logical vector; if a given element ends with the suffix, the corresponding value in the returned vector is T, otherwise it is F.

(string)substr(string x, integer first, [Ni last = NULL])

Returns substrings extracted from the elements of x, spanning character position first to character position last (inclusive).  Character positions are numbered from 0 to nchar(x)-1.  Positions that fall outside of that range are legal; a substring range that encompasses no characters will produce an empty string.  If first is greater than last, an empty string will also result.  If last is NULL (the default), then the substring will extend to the end of the string.  The parameters first and last may either be singletons, specifying a single value to be used for all of the substrings, or they may be vectors of the same length as x, specifying a value for each substring.

3.8.  Matrix and array functions

(*)apply(* x, integer margin, string$ lambdaSource)

Prior to Eidos 1.6 / SLiM 2.6, sapply() was named apply(), and this function did not yet exist

Applies a block of Eidos code to margins of x.  This function is essentially an extension of sapply() for use with matrices and arrays; it is recommended that you fully understand sapply() before tackling this function.  As with sapply(), the lambda specified by lambdaSource will be executed for subsets of x, and the results will be concatenated together with type-promotion in the style of c() to produce a result.  Unlike sapply(), however, the subsets of x used might be rows, columns, or higher-dimensional slices of x, rather than just single elements, depending upon the value of margin.  For apply(), x must be a matrix or array.  The apply() function in Eidos is patterned directly after the apply() function in R, and should behave identically, except that dimension indices in Eidos are zero-based whereas in R they are one-based.

The margin parameter gives the indices of dimensions of x that will be iterated over when assembling values to supply to lambdaSource.  If x is a matrix it has two dimensions: rows, of dimension index 0, and columns, of dimension index 1.  These are the indices of the dimension sizes returned by dim(); dim(x)[0] gives the number of rows of x, and dim(x)[1] gives the number of columns.  These dimension indices are also apparent when subsetting x; a subset index in position 0, such as x[m,], gives row m of x, whereas a subset index in position 1, such as x[,n], gives column n of x.  In the same manner, supplying 0 for margin specifies that subsets of x from x[0,] to x[m,] should be “passed” to lambdaSource, through the applyValue “parameter”; dimension 0 is iterated over, whereas dimension 1 is taken in aggregate since it is not included in margin.  The final effect of this is that whole rows of x are passed to lambdaSource through applyValue.  Similarly, margin=1 would specify that subsets of x from x[,0] to x[,n] should be passed to lambdaSource, resulting in whole columns being passed.  Specifying margin=c(0,1) would indicate that dimensions 0 and 1 should both be iterated over (dimension 0 more rapidly), so for a matrix each each individual value of x would be passed to lambdaSource.  Specifying margin=c(1,0) would similarly iterate over both dimensions, but dimension 1 more rapidly; the traversal order would therefore be different, and the dimensionality of the result would also differ (see below).  For higher-dimensional arrays dimension indices beyond 1 exist, and so margin=c(0,1) or margin=c(1,0) would provide slices of x to lambdaSource, each slice having a specific row and column index.  Slices are generated by subsetting in the same way as operator [], but additionally, redundant dimensions are dropped as by drop().

The return value from apply() is built up from the type-promoted concatenated results, as if by the c() function, from the iterated execution of lambdaSource; the only question is what dimensional structure is imposed upon that vector of values.  If the results from lambdaSource are not of a consistent length, or are of length zero, then the concatenated results are returned as a plain vector.  If all results are of length n > 1, the return value is an array of dimensions c(n, dim(x)[margin]); in other words, each n-vector provides the lowest dimension of the result, and the sizes of the marginal dimensions are imposed upon the data above that.  If all results are of length n == 1, then if a single margin was specified the result is a vector (of length equal to the size of that marginal dimension), or if more than one margin was specified the result is an array of dimension dim(x)[margin]; in other words, the sizes of the marginal dimensions are imposed upon the data.  Since apply() iterates over the marginal dimensions in the same manner, these structures follows the structure of the data.

The above explanation may not be entirely clear, so let’s look at an example.  If x is a matrix with two rows and three columns, such as defined by x = matrix(1:6, nrow=2);, then executing apply(x, 0, "sum(applyValue);"); would cause each row of x to be supplied to the lambda through applyValue, and the values in each row would thus be summed to produce 9 12 as a result.  The call apply(x, 1, "sum(applyValue);"); would instead sum columns of x, producing 3 7 11 as a result.  Now consider using range() rather than sum() in the lambda, thus producing two values for each row or column.  The call apply(x, 0, "range(applyValue);"); produces a result of matrix(c(1,5,2,6), nrow=2), with the range of the first row of x, 1–5, in the first column of the result, and the range of the second row of x, 2–6, in the second column.  Although visualization becomes more difficult, these same patterns extend to higher dimensions and arbitrary margins of x.

For efficiently obtaining the sums of the rows or columns of a matrix, see rowSums() and colSums().

(*)array(* data, integer dim)

Creates a new array from the data specified by data, with the dimension sizes specified by dim.  The first dimension size in dim is the number of rows, and the second is the number of columns; further entries specify the sizes of higher-order dimensions.  As many dimensions may be specified as desired, but with a minimum of two dimensions.  An array with two dimensions is a matrix (by definition); note that matrix() may provide a more convenient way to make a new matrix.  Each dimension must be of size 1 or greater; 0-size dimensions are not allowed.

The elements of data are used to populate the new array; the size of data must therefore be equal to the size of the new array, which is the product of all the values in dim.  The new array will be filled in dimension order: one element in each row until a column is filled, then on to the next column in the same manner until all columns are filled, and then onward into the higher-order dimensions in the same manner.

(*)asVector(* x)

Creates a new vector from the elements of x, stripping off any dimensional information associated with x being a vector or array.  The values of the resulting vector are read out from x in dimension order: one element from each row until a column is completed, then on to the next column in the same manner until all columns are completed, and then onward into the higher-order dimensions in the same manner.  If x is already a vector, it is returned unmodified.  See drop() for a similar method that drops only matrix/array dimensions that are redundant.

(*)cbind(...)

Combines vectors or matrices by column to produce a single matrix.  The parameters must be vectors (which are interpreted by cbind() as if they were one-column matrices) or matrices.  They must be of the same type, of the same class if they are of type object, and have the same number of rows.  If these conditions are met, the result is a single matrix with the parameters joined together, left to right.  Parameters may instead be NULL, in which case they are ignored; or if all parameters are NULL, the result is NULL.  A sequence of vectors, matrices, and NULLs may thus be concatenated with the NULL values removed, analogous to c().  Calling cbind(x) is an easy way to create a one-column matrix from a vector.

To combine vectors or matrices by row instead, see rbind().

(numeric)colSums(lif x)

Returns the sums of the columns of x, which must be a matrix.  The result is a vector of elements, each providing the sum of the corresponding column of x.  If x is of type logical or integer the result will be of type integer; unlike the sum() function, colSums() does not promote the return type to float if integer overflow occurs, but instead throws an error.  If x is of type float the result will be of type float.  Except for the change in the treatment of integer overflow noted above, this is equivalent to using apply() with sum() to sum the columns of x, but is much faster.

(numeric$)det(numeric x)

Returns the determinant of x, which must be a square matrix (otherwise an error is raised).  The determinant is a scalar-valued function of the entries of the matrix, and characterizes some properties of the matrix.  In particular, the determinant is nonzero if and only if the matrix is invertible.  If the determinant is zero, the matrix does not have an inverse and is referred to as “singular”.  In Eidos the determinant is calculated from the LU decomposition of the matrix.  The return type will match the type of x.

(*)diag([* x = 1], [Ni$ nrow = NULL], [Ni$ ncol = NULL])

Returns the diagonal of x.  This function has four distinct usage patterns (matching R).  First, if x is a matrix of any type, it returns the diagonal elements of x as a vector; in this case, nrow and ncol must be NULL.  Second, if x is 1 (the default) and nrow is non-NULL, it returns an identity matrix with the requested number of rows (and, if ncol is also non-NULL, the requested number of columns, otherwise the matrix will be square).  Third, if x is a singleton integer value and nrow and ncol are NULL, it returns a square identity matrix of size x.  Fourth, if x is a logical, integer, or float vector of length at least 2, it returns a matrix that uses the values of x as its diagonal (without recycling or truncation, unlike R) and has F, 0, or 0.0 off-diagonal entries as appropriate.

Note that using diag(x), without nrow or ncol, can have unexpected effects if x is a vector that could be of length one.  Use diag(x, nrow=length(x)) for consistent behavior.

(integer)dim(* x)

Returns the dimensions of matrix or array x.  The first dimension value is the number of rows, the second is the number of columns, and further values indicate the sizes of higher-order dimensions, identically to how dimensions are supplied to array().  NULL is returned if x is not a matrix or array.

(*)drop(* x)

Returns the result of dropping redundant dimensions from matrix or array x.  Redundant dimensions are those with a size of exactly 1.  Non-redundant dimensions are retained.  If only one non-redundant dimension is present, the result is a vector; if more than one non-redundant dimension is present, the result will be a matrix or array.  If x is not a matrix or array, it is returned unmodified.  See asVector() for a way to drop all dimensions of a matrix or array, whether redundant or not.

(float)inverse(numeric x)

Returns the (multiplicative) inverse of x, which must be a square non-singular matrix (otherwise an error is raised).  If matrix B is the inverse of n-by-n matrix A, then AB = BA = In, where In denotes the n-by-n identity matrix and the multiplication used is ordinary matrix multiplication as performed by matrixMult().  If x might be singular (and thus non-invertible), and you wish to avoid the possibility of an error, you can call det() first to find the determinant of the matrix; if the determinant is zero, the matrix is singular and does not have an inverse, and so inverse() should not be called.  In Eidos the inverse is calculated from the LU decomposition of the matrix.

(logical)lowerTri(* x, [logical$ diag = F])

Returns the lower triangle of x, which must be a matrix.  The return value will be a logical matrix of the same dimensions as x, with elements T in the lower triangle, F elsewhere.  If diag is F (the default), the diagonal is not included in the lower triangle; if diag is T, the diagonal is included in the lower triangle (i.e., its elements will be T).

(*)matrix(* data, [Ni$ nrow = NULL], [Ni$ ncol = NULL], [logical$ byrow = F])

Creates a new matrix from the data specified by data.  By default this creates a one-column matrix.  If non-NULL values are supplied for nrow and/or ncol, a matrix will be made with the requested number of rows and/or columns if possible; if the length of data is not compatible with the requested dimensions, an error will result.  By default, values from data will populate the matrix by columns, filling each column sequentially before moving on to the next column; if byrow is T the matrix will be populated by rows instead.

(numeric)matrixMult(numeric x, numeric y)

Returns the result of matrix multiplication of x with y.  In Eidos (as in R), with two matrices A and B the simple product A * B multiplies the corresponding elements of the matrices; in other words, if X is the result of A * B, then Xij = Aij * Bij.  This is parallel to the definition of other operators; A + B adds the corresponding elements of the matrices (Xij = Aij + Bij), etc.  In R, true matrix multiplication is achieved with a special operator, %*%; in Eidos, the matrixMult() function is used instead.

Both x and y must be matrices, and must be conformable according to the standard definition of matrix multiplication (i.e., if x is an n × m matrix then y must be a m × p matrix, and the result will be a n × p matrix).  Vectors will not be promoted to matrices by this function, even if such promotion would lead to a conformable matrix.

(numeric)matrixPow(numeric x, integer$ power)

Returns the result of raising matrix x to an integer power.  The parameter x must be a square matrix (or an error will be raised).  This operation is performed by repeated matrix multiplication with matrixMult(), and uses inverse() to compute the inverse of the matrix if power is negative.

(integer$)nrow(* x)

Returns the number of rows in matrix or array x.  For vector x, nrow() returns NULL; size() should be used.  An equivalent of R’s NROW() function, which treats vectors as 1-column matrices, is not provided but would be trivial to implement as a user-defined function.

(integer$)ncol(* x)

Returns the number of columns in matrix or array x.  For vector x, ncol() returns NULL; size() should be used.  An equivalent of R’s NCOL() function, which treats vectors as 1-column matrices, is not provided but would be trivial to implement as a user-defined function.

(numeric)outerProduct(numeric x, numeric y)

Returns the outer product of vectors x and y.  The outer product, x y, is the result of matrix multiplication of x with the transpose of y, or xyT.  It will be a matrix with a number of rows equal to the length of x, and a number of columns equal to the length of y.  It is required that x and y be vectors, not matrices or arrays, that they have non-zero lengths, and that they be the same type – both integer or both float.  The return value will be of the same type as x and y.

(*)rbind(...)

Combines vectors or matrices by row to produce a single matrix.  The parameters must be vectors (which are interpreted by rbind() as if they were one-row matrices) or matrices.  They must be of the same type, of the same class if they are of type object, and have the same number of columns.  If these conditions are met, the result is a single matrix with the parameters joined together, top to bottom.  Parameters may instead be NULL, in which case they are ignored; or if all parameters are NULL, the result is NULL.  A sequence of vectors, matrices, and NULLs may thus be concatenated with the NULL values removed, analogous to c().  Calling rbind(x) is an easy way to create a one-row matrix from a vector.

To combine vectors or matrices by column instead, see cbind().

(numeric)rowSums(lif x)

Returns the sums of the rows of x, which must be a matrix.  The result is a vector of elements, each providing the sum of the corresponding row of x.  If x is of type logical or integer the result will be of type integer; unlike the sum() function, rowSums() does not promote the return type to float if integer overflow occurs, but instead throws an error.  If x is of type float the result will be of type float.  Except for the change in the treatment of integer overflow noted above, this is equivalent to using apply() with sum() to sum the rows of x, but is much faster.

(*)t(* x)

Returns the transpose of x, which must be a matrix.  This is the matrix reflected across its diagonal; or alternatively, the matrix with its columns written out instead as rows in the same order.

(numeric$)tr(numeric x)

Returns the trace of x, which must be a square matrix (otherwise an error is raised).  The trace is the sum of the diagonal elements of the matrix.  The return type will match the type of x.

(logical)upperTri(* x, [logical$ diag = F])

Returns the upper triangle of x, which must be a matrix.  The return value will be a logical matrix of the same dimensions as x, with elements T in the upper triangle, F elsewhere.  If diag is F (the default), the diagonal is not included in the upper triangle; if diag is T, the diagonal is included in the upper triangle (i.e., its elements will be T).

3.9.  Filesystem access functions

(logical$)createDirectory(string$ path)

Creates a new filesystem directory at the path specified by path and returns a logical value indicating if the creation succeeded (T) or failed (F).  If the path already exists, createDirectory() will do nothing to the filesystem, will emit a warning, and will return T to indicate success if the existing path is a directory, or F to indicate failure if the existing path is not a directory.

(logical$)deleteFile(string$ filePath)

Deletes the file specified by filePath and returns a logical value indicating if the deletion succeeded (T) or failed (F).

This function might also be able to delete a directory at filePath, but only if it is empty (apart from the . and .. directory entries that exist on Un*x filesystems).  If other files (including invisible files) exist in the directory, deleteFile() will probably fail as a safety measure, in which case the contained files must be deleted individually first.  This is vague because the actual policy regarding deletion of directories will depend upon the operating system, since Eidos achieves the deletion by calling an operating-system function.

(logical$)fileExists(string$ filePath)

Checks the existence of the file specified by filePath and returns a logical value indicating if it exists (T) or does not exist (F).  This also works for directories.

(string)filesAtPath(string$ path, [logical$ fullPaths = F])

Returns a string vector containing the names of all files in a directory specified by path.  If the optional parameter fullPaths is T, full filesystem paths are returned for each file; if fullPaths is F (the default), then only the filenames relative to the specified directory are returned.  This list includes directories (i.e. subfolders), including the "." and ".." directories on Un*x systems.  The list also includes invisible files, such as those that begin with a "." on Un*x systems.  This function does not descend recursively into subdirectories.  If an error occurs during the read, NULL will be returned.

(logical$)flushFile(string$ filePath)

Flushes buffered content to a file specified by filePath.  Normally, written data is buffered by writeFile() if the compress option of that function is T, holding the data in memory rather than writing it to disk immediately.  This buffering improves both performance and file size; however, sometimes it is desirable to flush the buffered data to disk with flush() so that the filesystem is up to date.  Note that flushing after every write is not recommended, since it will lose all of the benefits of buffering.  Calling flushFile() for a path that has not been written to, or is not being buffered, will do nothing.  If the flush is successful, T will be returned; if not, F will be returned (but at present, an error will result instead).

(string$)getwd(void)

Gets the current filesystem working directory.  The filesystem working directory is the directory which will be used as a base path for relative filesystem paths.  For example, if the working directory is "~/Desktop" (the Desktop subdirectory within the current user’s home directory, as represented by ~), then the filename "foo.txt" would correspond to the filesystem path "~/Desktop/foo.txt", and the relative path "bar/baz/" would correspond to the filesystem path “~/Desktop/bar/baz/“.

Note that the path returned may not be identical to the path previously set with setwd(), if for example symbolic links are involved; but it ought to refer to the same actual directory in the filesystem.

The initial working directory is – as is generally the case on Un*x – simply the directory given to the running Eidos process by its parent process (the operating system, a shell, a job scheduler, a debugger, or whatever the case may be).  If you launch Eidos (or SLiM) from the command line in a Un*x shell, it is typically the current directory in that shell.  Before relative filesystem paths are used, you may therefore wish check what the initial working directory is on your platform, with getwd(), if you are not sure.  Alternatively, you can simply use setwd() to set the working directory to a known path.

(object<DataFrame>$)readCSV(string$ filePath, [ls colNames = T], [Ns$ colTypes = NULL], [string$ sep = ","], [string$ quote = '"'], [string$ dec = "."], [string$ comment = ""])

Reads data from a CSV or other delimited file specified by filePath and returns a DataFrame object containing the data in a tabular form.  CSV (comma-separated value) files use a somewhat standard file format in which a table of data is provided, with values within a row separated by commas, while rows in the table are separated by newlines.  Software from R to Excel (and Eidos; see the serialize() method of Dictionary) can export data in CSV format.  This function can actually also read files that use a delimiter other than commas; TSV (tab-separated value) files are a popular alternative.  Since there is substantial variation in the exact file format for CSV files, this documentation will try to specify the precise format expected by this function.  Note that CSV files represent values differently that Eidos usually does, and some of the format options allowed by readCSV(), such as decimal commas, are not otherwise available in Eidos.

If colNames is T (the default), the first row of data is taken to be a header, containing the string names of the columns in the data table; those names will be used by the resulting DataFrame.  If colNames is F, a header row is not expected and column names are auto-generated as X1, X2, etc.  If colNames is a string vector, a header row is not expected and colNames will be used as the column names; if additional columns exist beyond the length of colNames their names will be auto-generated.  Duplicate column names will generate a warning and be made unique.

If colTypes is NULL (the default), the value type for each column will be guessed from the values it contains, as described below.  If colTypes is a singleton string, it should contain single-letter codes indicating the desired type for each column, from left to right.  The letters lifs have the same meaning as in Eidos signatures (logical, integer, float, and string); in addition, ? may be used to indicate that the type for that column should be guessed as by default, and _ or - may be used to indicate that that column should be skipped – omitted from the returned DataFrame.  Other characters in colTypes will result in an error.  If additional columns exist beyond the end of the colTypes string their types will be guessed as by default.

The separator between values is supplied by sep; it is a comma by default, but a tab can be used instead by supplying tab ("\t" in Eidos), or another character may also be used.  If sep is the empty string "", the separator between values is “whitespace”, meaning one or more spaces or tabs.  When the separator is whitespace, whitespace at the beginning or the end of a line will be ignored.

Similarly, the character used to quote string values is a double quote ('"' in Eidos), by default, but another character may be supplied in quote.  When the string delimiter is encountered, all following characters are considered to be part of the string until another string delimiter is encountered, terminating the string; this includes spaces, comment characters, newlines, and everything else.  Within a string value, the string delimiter itself is used twice in a row to indicate that the delimiter itself is present within the string; for example, if the string value (shown without the usual surrounding quotes to try to avoid confusion) is she said "hello", and the string delimiter is the double quote as it is by default, then in the CSV file the value would be given as "she said ""hello""".  The usual Eidos style of escaping characters using a backslash is not part of the CSV standard followed here.  (When a string value is provided without using the string delimiter, all following characters are considered part of the string except a newline, the value separator sep, the quote separator quote, and the comment separator comment; if none of those characters are present in the string value, the quote delimiter may be omitted.)

The character used to indicate a decimal delimiter in numbers may be supplied with dec; by default this is "." (and so 10.0 would be ten, written with a decimal point), but "," is common in European data files (and so 10,0 would be ten, written with a decimal comma).  Note that dec and sep may not be the same, so that it is unambiguous whether 10,0 is two numbers (10 and 0) or one number (10.0).  For this reason, European CSV files that use a decimal comma typically use a semicolon as the value separator, which may be supplied with sep=";" to readCSV().

Finally, the remainder of a line following a comment character will be ignored when the file is read; by default comment is the empty string, "", indicating that comments do not exist at all, but "#" is a popular comment prefix.

To translate the CSV data into a DataFrame, it is necessary for Eidos to guess what value type each column is unless a column type is specified by colTypes.  Quotes surrounding a value are irrelevant to this guess; for example, 1997 and "1997" are both candidates to be integer values (because some programs generate CSV output in which every value is quoted regardless of type).  If every value in a column is either true, false, TRUE, FALSE, T, or F, the column will be taken to be logical.  Otherwise, if every value in a column is an integer (here defined as an optional + or -, followed by nothing but decimal digits 0123456789), the column will be taken to be integer.  Otherwise, if every value in a column is a floating-point number (here defined as an optional + or -, followed by decimal digits 0123456789, optionally a decimal separator and then optionally more decimal digits, and ending with an optional exponent like e7, E+05, or e-2), the column will be taken to be float; the special values NAN, INF, INFINITY, -INF, and -INFINITY (not case-sensitive) are also candidates to be float (if the rest of the column is also convertible to float), representing the corresponding float constants.  Otherwise, the column will be taken to be string.  NULL and NA are not recognized by readCSV() in CSV files and will be read as strings.  Every line in a CSV file must contain the same number of values (forming a rectangular data table); missing values are not allowed by readCSV() since there is no way to represent them in DataFrame (since Eidos has no equivalent of R’s NA value).  Spaces are considered part of a data field and are not trimmed, following the RFC 4180 standard.  These choices are an attempt to provide optimal behavior for most clients, but given the lack of any universal standard for CSV files, and the lack of any type information in the CSV format, they will not always work as desired; in such cases, it should be reasonably straightforward to preprocess input files using standard Unix text-processing tools like sed and awk.

(string)readFile(string$ filePath)

Reads in the contents of a file specified by filePath and returns a string vector containing the lines (separated by \n and \r characters) of the file.  Reading files other than text files is not presently supported.  If an error occurs during the read, NULL will be returned.

(string$)setwd(string$ path)

Sets the current filesystem working directory.  The filesystem working directory is the directory which will be used as a base path for relative filesystem paths (see getwd() for further discussion).  An error will result if the working directory cannot be set to the given path.

The current working directory prior to the change will be returned as an invisible string value; the value returned is identical to the value that would have been returned by getwd(), apart from its invisibility.

See getwd() for discussion regarding the initial working directory, before it is set with setwd().

(string$)tempdir(void)

Returns a path to a directory appropriate for saving temporary files.  The path returned by tempdir() is platform-specific, and is not guaranteed to be the same from one run of SLiM to the next.  It is guaranteed to end in a slash, so further path components should be appended without a leading slash.  At present, on macOS and Linux systems, the path will be "/tmp/"; this may change in future Eidos versions without warning.

(logical$)writeFile(string$ filePath, string contents, [logical$ append = F], [logical$ compress = F])

Writes or appends to a file specified by filePath with contents specified by contents, a string vector of lines.  If append is T, the write will be appended to the existing file (if any) at filePath; if it is F (the default), then the write will replace an existing file at that path.  If the write is successful, T will be returned; if not, F will be returned (but at present, an error will result instead).

If compress is T, the contents will be compressed with zlib as they are written, and the standard .gz extension for gzip-compressed files will be appended to the filename in filePath if it is not already present.  If the compress option is used in conjunction with append==T, Eidos will buffer data to append and flush it to the file in a delayed fashion (for performance reasons), and so appended data may not be visible in the file until later – potentially not until the process ends (i.e., the end of the SLiM simulation, for example).  If that delay if undesirable, buffered data can be explicitly flushed to the filesystem with flushFile().  The compress option was added in Eidos 2.4 (SLiM 3.4).  Note that readFile() does not currently support reading in compressed data.

Note that newline characters will be added at the ends of the lines in contents.  If you do not wish to have newlines added, you should use paste() to assemble the elements of contents together into a singleton string.

(string$)writeTempFile(string$ prefix, string$ suffix, string contents, [logical$ compress = F])

Writes to a unique temporary file with contents specified by contents, a string vector of lines.  The filename used will begin with prefix and end with suffix, and will contain six random characters in between; for example, if prefix is "plot1_" and suffix is ".pdf", the generated filename might look like "plot1_r5Mq0t.pdf".  It is legal for prefix, suffix, or both to be the empty string, "", but supplying a file extension is usually advisable at minimum.  The file will be created inside the /tmp/ directory of the system, which is provided by Un*x systems as a standard location for temporary files; the /tmp/ directory should not be specified as part of prefix (nor should any other directory information).  The filename generated is guaranteed not to already exist in /tmp/.  The file is created with Un*x permissions 0600, allowing reading and writing only by the user for security.  If the write is successful, the full path to the temporary file will be returned; if not, "" will be returned.

If compress is T, the contents will be compressed with zlib as they are written, and the standard .gz extension for gzip-compressed files will be appended to the filename suffix in suffix if it is not already present.  The compress option was added in Eidos 2.4 (SLiM 3.4).  Note that readFile() does not currently support reading in compressed data.

Note that newline characters will be added at the ends of the lines in contents.  If you do not wish to have newlines added, you should use paste() to assemble the elements of contents together into a singleton string.

3.10.  Color manipulation functions

(string)cmColors(integer$ n)

This method has been deprecated, and may be removed in a future release of Eidos.  In SLiM 3.5 and later, use colors(n, "cm") instead.

Generate colors in a “cyan-magenta” color palette.

(string)colors(numeric x, string$ name)

Generate colors in a standard color palette.  If x is a singleton integer, the returned vector will contain x color strings representing x colors equidistant along the named palette, spanning its full extent.  Alternatively, if x is a float vector of values in [0,1], the returned vector will contain one color string for each value in x, representing the color at the corresponding fraction along the named palette (values outside [0,1] will be clamped to that range).  (Note that the function signature states the type of x as numeric, but in this function the integer and float cases have completely different semantic meanings.)

The color palette specified by name may be any of the following color palettes based upon color palettes in R: "cm", "heat", and "terrain".

It may also be one of the following color palettes based on color palettes in MATLAB (and the Turbo palette from Anton Mikhailov of the Google AI group, based upon the Jet palette provided by MATLAB): "parula", "hot", "jet", "turbo", and "gray".

Finally, it may be one of the following color palettes based upon color palettes in Matplotlib, also available in the viridis R package: "magma", "inferno", "plasma", "viridis", and "cividis".  These color palettes are designed to be perceptually uniform, changing continuously and linearly.  They are also designed to perform well even for users with red-green colorblindness; the "cividis" palette, in particular, is designed to look nearly identical to those with and without red-green colorblindness, to be perceptually uniform in both hue and brightness, and to increase linearly in brightness.

This function replaces the deprecated cmColors(), heatColors(), and terrainColors() functions, and adds several several additional color palettes to Eidos.  See rainbow() for another color palette function.

(float)color2rgb(string color)

Converts a color string to RGB.  The color string specified in color may be either a named color or a color in hexadecimal format such as "#007FC0".  The equivalent RGB color is returned as a float vector of length three (red, green, blue).  Returned RGB values will be in the interval [0, 1].

This function can also be called with a non-singleton vector of color strings in color.  In this case, the returned float value will be a matrix of RGB values, with three columns (red, green, blue) and one row per element of color.

(string)heatColors(integer$ n)

This method has been deprecated, and may be removed in a future release of Eidos.  In SLiM 3.5 and later, use colors(n, "heat") instead.

Generate colors in a “heat map” color palette.

(float)hsv2rgb(float hsv)

Converts an HSV color to RGB.  The HSV color is specified in hsv as a float vector of length three (hue, saturation, value), and the equivalent RGB color is returned as a float vector of length three (red, green, blue).  HSV values will be clamped to the interval [0, 1], and returned RGB values will also be in the interval [0, 1].

This function can also be called with a matrix of HSV values, with three columns (hue, saturation, value).  In this case, the returned float value will be a matrix of RGB values, with three columns (red, green, blue) and one row per row of hsv.

(string)rainbow(integer$ n, [float$ s = 1.0], [float$ v = 1.0], [float$ start = 0.0], [Nf$ end = NULL], [logical$ ccw = T])

Generate colors in a “rainbow” color palette.  The number of colors desired is passed in n, and the returned vector will contain n color strings.  Parameters s and v control the saturation and value of the rainbow colors generated.  The color sequence begins with the hue start, and ramps to the hue end, in a counter-clockwise direction around the standard HSV color wheel if ccw is T (the default, following R), otherwise in a clockwise direction.  If end is NULL (the default), a value of (n-1)/n is used, producing a complete rainbow around the color wheel when start is also the default value of 0.0.  See colors() for other color palettes.

(string)rgb2color(float rgb)

Converts an RGB color to a color string.  The RGB color is specified in rgb as a float vector of length three (red, green, blue).  The equivalent color string is returned as singleton string specifying the color in the format "#RRGGBB", such as "#007FC0".  RGB values will be clamped to the interval [0, 1].

This function can also be called with a matrix of RGB values, with three columns (red, green, blue).  In this case, the returned string value will be a vector of color strings, with one element per row of rgb.

(float)rgb2hsv(float rgb)

Converts an RGB color to HSV.  The RGB color is specified in rgb as a float vector of length three (red, green, blue), and the equivalent HSV color is returned as a float vector of length three (hue, saturation, value).  RGB values will be clamped to the interval [0, 1], and returned HSV values will also be in the interval [0, 1].

This function can also be called with a matrix of RGB values, with three columns (red, green, blue).  In this case, the returned float value will be a matrix of HSV values, with three columns (hue, saturation, value) and one row per row of rgb.

(string)terrainColors(integer$ n)

This method has been deprecated, and may be removed in a future release of Eidos.  In SLiM 3.5 and later, use colors(n, "terrain") instead.

Generate colors in a “terrain” color palette.

3.11.  Miscellaneous functions

(void)assert(logical assertions, [Ns$ message = NULL])

Assert that a condition or conditions are true.  If any element of assertions is F, execution will be stopped.  A message, “assertion failed”, will be printed before stopping; if message is not NULL; its value will then be printed.

(void)beep([Ns$ soundName = NULL])

Plays a sound or beeps.  On macOS in a GUI environment (i.e., in EidosScribe or SLiMgui), the optional parameter soundName can be the name of a sound file to play; in other cases (if soundName is NULL, or at the command line, or on platforms other than OS X) soundName is ignored and a standard system beep is played.

When soundName is not NULL, a sound file in a supported format (such as .aiff or .mp3) is searched for sequentially in four standard locations, in this order: ~/Library/Sounds, /Library/Sounds, /Network/Library/Sounds, and finally /System/Library/Sounds.  Standard OS X sounds located in /System/Library/Sounds include "Basso", "Blow", "Bottle", "Frog", "Funk", "Glass", "Hero", "Morse", "Ping", "Pop", "Purr", "Sosumi", "Submarine", and "Tink".  Do not include the file extension, such as .aiff or .mp3, in soundName.

CAUTION: When not running in EidosScribe or SLiMgui, it is often the case that the only simple means available to play a beep is to send a BEL character (ASCII 7) to the standard output.  Unfortunately, when this is the case, it means that (1) no beep will be audible if output is being redirected into a file, and (2) a control character, ^G, will occur in the output at the point when the beep was requested.  It is therefore recommended that beep() be used only when doing interactive work in a terminal shell (or in a GUI), not when producing output files.  However, this issue is platform-specific; on some platforms beep() may result in a beep, and no emitted ^G, even when output is redirected.  When a ^G must be emitted to the standard output to generate the beep, a warning message will also be emitted to make any associated problems easier to diagnose.

(void)citation(void)

Prints citation information for Eidos to Eidos’s output stream.

(float$)clock([string$ type = "cpu"])

Returns the value of a system clock.  If type is "cpu", this returns the current value of the CPU usage clock.  This is the amount of CPU time used by the current process, in seconds; it is unrelated to the current time of day (for that, see the time() function).  This is useful mainly for determining how much processor time a given section of code takes; clock() can be called before and after a block of code, and the end clock minus the start clock gives the elapsed CPU time consumed in the execution of the block of code.  See also the timed parameter of executeLambda(), which automates this procedure.  Note that if multiple cores are utilized by the process, the CPU usage clock will be the sum of the CPU usage across all cores, and may therefore run faster than the wall clock.

If type is "mono", this returns the value of the system’s monotonic clock.  This represents user-perceived (“wall clock”) elapsed time from some arbitrary timebase (which will not change during the execution of the program), but it will not jump if the time zone or the wall clock time are changed for the system.  This clock is useful for measuring user-perceived elapsed time, as described above, and may provide a more useful metric for performance than CPU time if multiple cores are being utilized.

(string$)date(void)

Returns a standard date string for the current date in the local time of the executing machine.  The format is %d-%m-%Y (day in two digits, then month in two digits, then year in four digits, zero-padded and separated by dashes) regardless of the localization of the executing machine, for predictability and consistency.

(string$)debugIndent(void)

Returns the indentation string currently being used to start lines in the debugging output stream.  In a pure Eidos context this will currently be the empty string, "".  In specific Contexts, such as SLiM, the debugging output stream may be structured with nested indentation, in which case this string will typically be a series of spaces or tabs.  To make your debugging output (such as from cat(), catn(), or print() with the error=T optional argument set) line up with other output at the current level of execution nesting, you can start your new lines of output with this string if you wish.

(void)defineConstant(string$ symbol, * value)

Defines a new constant with the name symbol and the value specified by value.  The name cannot previously be defined in any way (i.e., as either a variable or a constant).  The defined constant acts identically to intrinsic Eidos constants such as T, NAN, and PI, and will remain defined for as long as the Eidos context lives even if it is defined inside a block being executed by executeLambda(), apply(), sapply(), or a Context-defined script block.

Syntactically, value may be any value at all; semantically, however, if value is of object type then value’s class must be under an internal memory-management scheme called “retain-release”.  Objects that are not under retain-release can cease to exist whenever the Context is finished using them, and thus a defined constant referencing such an object could become invalid, which must be prevented.  Objects that are under retain-release will not cease to exist if they are referenced by a global constant; the reference to them from the global constant “retains” them and keeps them in existence.  All object classes built into Eidos are under retain-release; see the SLiM manual (section “SLiM scoping rules”) for discussion of which SLiM object classes are under retain-release.

(void)defineGlobal(string$ symbol, * value)

Defines a new global variable with the name symbol and the value specified by value.  The name cannot previously be defined as a constant.  The result is similar to a standard variable assignment with operator =, except that the variable is always defined in the global scope (even if the defineGlobal() call is made inside a user-defined function or other locally-scoped block, such as a SLiM event or callback).  This means that the variable will remain defined even after the current scope is exited.  Note that global variables can be hidden by local variables with the same name; unlike defined constants, such scoped masking is allowed.

Syntactically, value may be any value at all; semantically, however, if value is of object type then value’s class must be under an internal memory-management scheme called “retain-release”.  Objects that are not under retain-release can cease to exist whenever the Context is finished using them, and thus a global variable referencing such an object could become invalid, which must be prevented.  Objects that are under retain-release will not cease to exist if they are referenced by a global variable; the reference to them from the global variable “retains” them and keeps them in existence.  All object classes built into Eidos are under retain-release; see the SLiM manual (section “SLiM scoping rules”) for discussion of which SLiM object classes are under retain-release.

(vNlifso)doCall(string$ functionName, ...)

Returns the results from a call to a specified function.  The function named by the parameter functionName is called, and the remaining parameters to doCall() are forwarded on to that function verbatim.  This can be useful for calling one of a set of similar functions, such as sin(), cos(), etc., to perform a math function determined at runtime, or one of the as...() family of functions to convert to a type determined at runtime.  Note that named arguments and default arguments, beyond the functionName argument, are not supported by doCall(); all arguments to the target function must be specified explicitly, without names.

(vNlifso)executeLambda(string$ lambdaSource, [ls$ timed = F])

Executes a block of Eidos code defined by lambdaSource.  Eidos allows you to execute lambdas: blocks of Eidos code which can be called directly within the same scope as the caller.  Eidos lambdas do not take arguments; for this reason, they are not first-class functions.  (Since they share the scope of the caller, however, you may effectively pass values in and out of a lambda using variables.)  The string argument lambdaSource may contain one or many Eidos statements as a single string value.  Lambdas are represented, to the caller, only as the source code string lambdaSource; the executable code is not made available programmatically.  If an error occurs during the tokenization, parsing, or execution of the lambda, that error is raised as usual; executing code inside a lambda does not provide any additional protection against exceptions raised.  The return value produced by the code in the lambda is returned by executeLambda().  If the optional parameter timed is T, the total (CPU clock) execution time for the lambda will be printed after the lambda has completed (see clock()); if it is F (the default), no timing information will be printed.  The timed parameter may also be "cpu" or "mono" to specifically request timing with the CPU clock (which will count the usage across all cores, and may thus run faster than wall clock time if multiple cores are being utilized) or the monotonic clock (which will correspond, more or less, to elapsed wall clock time regardless of multithreading); see the documentation for clock() for further discussion of these timing options.

The current implementation of executeLambda() caches a tokenized and parsed version of lambdaSource, so calling executeLambda() repeatedly on a single source string is much more efficient than calling executeLambda() with a newly constructed string each time.  If you can use a string literal for lambdaSource, or reuse a constructed source string stored in a variable, that will improve performance considerably.

(logical)exists(string symbol)

Returns a logical vector indicating whether symbols exist.  If a symbol has been defined as an intrinsic Eidos constant like T, INF, and PI, or as a Context-defined constant like sim in SLiM, or as a user-defined constant using defineConstant(), or as a variable by assignment, this function returns T.  Otherwise, the symbol has not been defined, and exists() returns F.  This is commonly used to check whether a user-defined constant already exists, with the intention of defining the constant if it has not already been defined.  A vector of symbols may be passed, producing a vector of corresponding results.

(void)functionSignature([Ns$ functionName = NULL])

Prints function signatures for all functions (if functionName is NULL, the default), or for the function named by functionName, to Eidos’s output stream.

(void)functionSource(string$ functionName)

Prints the Eidos source code for the function specified by functionName, or prints a diagnostic message if the function is implemented in C++ rather than Eidos.

(integer$)getSeed(void)

Returns the random number seed.  This is the last seed value set using setSeed(); if setSeed() has not been called, it will be a seed value chosen based on the process-id and the current time when Eidos was initialized, unless the Context has set a different seed value.

(void)license(void)

Prints Eidos’s license terms to Eidos’s output stream.

(void)ls([logical$ showSymbolTables = F])

Prints all currently defined variables to Eidos’s output stream.

Beginning in Eidos 2.5 (SLiM 3.5), the showSymbolTables optional argument can be set to T to request full information on the current symbol table chain.  This will show which symbol table a given symbol is defined in, as well as revealing whether there are other symbols with the same name that have been masked by a local definition.  This is mostly useful for debugging.

(integer$)parallelGetNumThreads(void)

Gets the number of threads that is requested be used in subsequent parallel (i.e., multithreaded) regions, as set with parallelSetNumThreads().  If Eidos is not configured to run multithreaded, this function will return 1.  See also parallelGetMaxThreads(), which returns the maximum number of threads that can be used.  Note that if this function returns the maximum number of threads, as returned by parallelGetMaxThreads(), then there are two possible semantic meanings of that return value, which cannot be distinguished using this function; see parallelSetNumThreads() for discussion.

(integer$)parallelGetMaxThreads(void)

Gets the maximum number of threads that can be used in parallel (i.e., multithreaded) regions.  This is configured externally; it may be OpenMP’s default number of threads for the hardware platform being used, or may be set by an environment variable or command-line option.  If Eidos is not configured to run multithreaded, this function will return 1.

(object<Dictionary>$)parallelGetTaskThreadCounts(void)

Gets the number of threads that is requested to be used for specific tasks in Eidos and SLiM.  Returns a new Dictionary containing values for all of the tasks for which a number of threads can be specified; see parallelSetTaskThreadCounts() for a list of all such tasks.  Note that the specified number of threads will not necessarily be used in practice; in particular, a thread count set by parallelSetNumThreads() will override these per-task counts.  Also, if the task size is below a certain task-specific threshold the task will not be executed in parallel regardless of these settings.

(void)parallelSetNumThreads([Ni$ numThreads = NULL])

Sets the number of threads that is requested to be used in subsequent parallel (i.e., multithreaded) regions.  If Eidos is not configured to run multithreaded, this function will have no effect.  The requested number of threads will be clamped to the interval [1, maxThreads], where maxThreads is the maximum number of threads configured externally (either by OpenMP’s default, or by an environment variable or command-line option).  That maximum number of threads (the value of maxThreads) can be obtained from parallelGetMaxThreads().

There is an important wrinkle in the semantics of this method that must be explained.  Passing NULL (the default) resets Eidos to the default number of threads for which it is configured to run.  In this configuration, parallelGetNumThreads() will return maxThreads, but the number of threads used for any given parallel operation might not, in fact, be equal to maxThreads; Eidos might use fewer threads if it determines that that would improve performance.  Passing the value of maxThreads explicitly, on the other hand, sets Eidos to always use maxThreads threads, even if it may result in lower performance; but in this configuration, too, parallelGetNumThreads() will return maxThreads.  For example, suppose maxThreads is 16.  Passing NULL requests that Eidos use up to 16 threads, as it sees fit; in contrast, explicitly passing 16 requests that Eidos use exactly 16 threads.  In both cases, however, parallelGetNumThreads() will return 16.

If you wish to temporarily change the number of threads used, the standard pattern is to call parallelSetNumThreads() with the number of threads you want to use, do the operation you wish to control, and then call parallelSetNumThreads(NULL) to return to the default behavior of Eidos.

Note that the number of threads requested here overrides any per-task request set with parallelSetTaskThreadCounts().  Also, if the task size is below a certain task-specific threshold the task will not be executed in parallel regardless of these settings.

(void)parallelSetTaskThreadCounts(No<Dictionary>$ dict)

Sets the number of threads that is requested to be used for specific tasks in Eidos and SLiM.  The dictionary dict should contain string keys that identify tasks, and integer values that provide the number of threads to be used when performing those tasks.  For example, a key of "LOG10_FLOAT" identifies the task of performing the log10() function on a float vector, and a value of 8 for that key would tell Eidos to use eight threads when performing that task.  The number of threads actually used will never be greater than the maximum thread count as returned by parallelGetMaxThreads().  Furthermore, a thread count set with parallelSetNumThreads() overrides the per-task setting, so if you wish to set specific per-task thread counts you should not set an overall thread count with parallelSetNumThreads().  If dict is NULL, all task thread counts will be reset to their default values.

The currently requested thread counts for all tasks can be obtained with parallelGetTaskThreadCounts().  Note that the counts returned by that function may not match the counts requested with parallelSetTaskThreadCounts(); in particular, they may be clipped to the maximum number of threads as returned by parallelGetMaxThreads().

The task keys recognized, and the tasks they govern, are:

"ABS_FLOAT" abs(float x)
"CEIL" ceil()
"EXP_FLOAT" exp(float x)
"FLOOR" floor()
"LOG_FLOAT" log(float x)
"LOG10_FLOAT" log10(float x)
"LOG2_FLOAT" log2(float x)
"ROUND" round()
"SQRT_FLOAT" sqrt(float x)
"SUM_INTEGER" sum(integer x)
"SUM_FLOAT" sum(float x)
"SUM_LOGICAL" sum(logical x)
"TRUNC" trunc()

"MAX_INT" max(integer x)
"MAX_FLOAT" max(float x)
"MIN_INT" min(integer x)
"MIN_FLOAT" min(float x)
"PMAX_INT_1" pmax(i$ x, i y) / pmax(i x, i$ y)
"PMAX_INT_2" pmax(integer x, integer y)
"PMAX_FLOAT_1" pmax(f$ x, f y) / pmax(f x, f$ y)
"PMAX_FLOAT_2" pmax(float x, float y)
"PMIN_INT_1" pmin(i$ x, i y) / pmax(i x, i$ y)
"PMIN_INT_2" pmin(integer x, integer y)
"PMIN_FLOAT_1" pmin(f$ x, f y) / pmin(f x, f$ y)
"PMIN_FLOAT_2" pmin(float x, float y)

"MATCH_INT" match(integer x, integer table)
"MATCH_FLOAT" match(float x, float table)
"MATCH_STRING" match(string x, string table)
"MATCH_OBJECT" match(object x, object table)
"SAMPLE_INDEX" sample() index buffer generation (internal)
"SAMPLE_R_INT" sample(integer x, weights=NULL)
"SAMPLE_R_FLOAT" sample(float x, weights=NULL)
"SAMPLE_R_OBJECT" sample(object x, weights=NULL)
"SAMPLE_WR_INT" sample(integer x, if weights)
"SAMPLE_WR_FLOAT" sample(float x, if weights)
"SAMPLE_WR_OBJECT" sample(object x, if weights)
"TABULATE_MAXBIN" tabulate() determination of maxbin (if not supplied)
"TABULATE" tabulate() main loop

"DNORM_1" dnorm(numeric$ mean, numeric$ sd)
"DNORM_2" dnorm() other cases
"RBINOM_1" rbinom(i$ size = 1, f$ prob = 0.5)
"RBINOM_2" rbinom(i$ size, f$ prob) other cases
"RBINOM_3" rbinom() other cases
"RDUNIF_1" rdunif(i$ min = 0, i$ max = 1) and similar
"RDUNIF_2" rdunif(i$ min, i$ max) other cases
"RDUNIF_3" rdunif() other cases
"REXP_1" rexp(numeric$ mu)
"REXP_2" rexp() other cases
"RNORM_1" rnorm(numeric$ mean, numeric$ sd)
"RNORM_2" rnorm(numeric$ sigma)
"RNORM_3" rnorm() other cases
"RPOIS_1" rpois(numeric$ lambda)
"RPOIS_2" rpois() other cases
"RUNIF_1" runif(numeric$ min = 0, numeric$ max = 1)
"RUNIF_2" runif(numeric$ min, numeric$ max) other cases
"RUNIF_3" runif() other cases

"SORT_INT" sort(integer x)
"SORT_FLOAT" sort(float x)
"SORT_STRING" sort(string x)

"CLIPPEDINTEGRAL_1S" clippedIntegral() for "x", "y", "z"
"CLIPPEDINTEGRAL_2S" clippedIntegral() for "xy", "xz", "yz"
"DRAWBYSTRENGTH" drawByStrength(returnDict=T)
"INTNEIGHCOUNT" interactingNeighborSount()
"LOCALPOPDENSITY" localPopulationDensity()
"NEARESTINTNEIGH" nearestInteractingNeighbors(returnDict=T)
"NEARESTNEIGH" nearestNeighbors(returnDict=T)
"NEIGHCOUNT" neighborCount()
"TOTNEIGHSTRENGTH" totalOfNeighborsStrengths()

"POINT_IN_BOUNDS_1D" pointInBounds(), 1D case
"POINT_IN_BOUNDS_2D" pointInBounds(), 2D case
"POINT_IN_BOUNDS_3D" pointInBounds(), 3D case
"POINT_PERIODIC_1D" pointPeriodic(), 1D case
"POINT_PERIODIC_2D" pointPeriodic(), 2D case
"POINT_PERIODIC_3D" pointPeriodic(), 3D case
"POINT_REFLECTED_1D" pointReflected(), 1D case
"POINT_REFLECTED_2D" pointReflected(), 2D case
"POINT_REFLECTED_3D" pointReflected(), 3D case
"POINT_STOPPED_1D" pointStopped(), 1D case
"POINT_STOPPED_2D" pointStopped(), 2D case
"POINT_STOPPED_3D" pointStopped(), 3D case
"POINT_UNIFORM_1D" pointUniform(), 1D case
"POINT_UNIFORM_2D" pointUniform(), 2D case
"POINT_UNIFORM_3D" pointUniform(), 3D case
"SET_SPATIAL_POS_1_1D" setSpatialPosition() with one point, 1D case
"SET_SPATIAL_POS_1_2D" setSpatialPosition() with one point, 2D case
"SET_SPATIAL_POS_1_3D" setSpatialPosition() with one point, 3D case
"SET_SPATIAL_POS_2_1D" setSpatialPosition() with N points, 1D case
"SET_SPATIAL_POS_2_2D" setSpatialPosition() with N points, 2D case
"SET_SPATIAL_POS_2_3D" setSpatialPosition() with N points, 3D case
"SPATIAL_MAP_VALUE" spatialMapValue()

"CONTAINS_MARKER_MUT" containsMarkerMutation(returnMutation = F)
"I_COUNT_OF_MUTS_OF_TYPE" countOfMutationsOfType() (Individual)
"H_COUNT_OF_MUTS_OF_TYPE" countOfMutationsOfType() (Haplosome)
"INDS_W_PEDIGREE_IDS" individualsWithPedigreeIDs()
"RELATEDNESS" relatedness()
"SAMPLE_INDIVIDUALS_1" sampleIndividuals() simple case with replace=T
"SAMPLE_INDIVIDUALS_2" sampleIndividuals() base case with replace=T
"SET_FITNESS_SCALE_1" Individual.fitness = one value
"SET_FITNESS_SCALE_2" Individual.fitness = N values
"SUM_OF_MUTS_OF_TYPE" sumOfMutationsOfType()

"AGE_INCR" incrementing Individual age values
"DEFERRED_REPRO"
deferred nonWF reproduction
"WF_REPRO"
WF reproduction (no callbacks)
"FITNESS_ASEX_1"
fitness eval, asex, individual fitnessScaling
"FITNESS_ASEX_2"
fitness eval, asex, no fitnessScaling or mutations
"FITNESS_ASEX_3"
fitness eval, asex, fitnessScaling and mutations
"FITNESS_SEX_1"
fitness eval, sexual, individual fitnessScaling
"FITNESS_SEX_2"
fitness eval, sexual, no fitnessScaling or mutations
"FITNESS_SEX_3"
fitness eval, sexual, fitnessScaling and mutations
"MIGRANT_CLEAR"
clearing the migrant property at tick end
"SIMPLIFY_SORT_PRE"
preparation for simplification sorting (internal)
"SIMPLIFY_SORT"
simplification sorting
"SIMPLIFY_SORT_POST"
cleanup after simplification sorting (internal)
"PARENTS_CLEAR"
clearing parental haplosomes at tick end in WF models
"UNIQUE_MUTRUNS"
uniquing mutation runs (internal bookkeeping)
"SURVIVAL"
survival evaluation (no callbacks)

Typically, a dictionary of task keys and thread counts is read from a file and set up with this function at initialization time, but it is also possible to change new task thread counts dynamically.  If Eidos is not configured to run multithreaded, this function has no effect.

(string$)readLine(void)

Reads a line of input from the “standard input” file.  This function is intended to allow for communication between a running Eidos script (such as a SLiM simulation) and an external process that is sending input to it.  It reads one line from the standard input and returns it as a singleton string.  This is done with the C++ function std::getline(), using the std::cin file.  If that call returns false (typically because the end-of-file was reached or there was a read error of some kind), the empty string "" will be returned.  The system() function can also be helpful for related situations.

This function will raise an error if called in a GUI environment such as EidosScribe, SLiMgui, or SLiMguiLegacy, since there is no standard input set up for the running script in those environments.

(void)rm([Ns variableNames = NULL])

Removes variables from the Eidos namespace; in other words, it causes the variables to become undefined.  Variables are specified by their string name in the variableNames parameter.  If the optional variableNames parameter is NULL (the default), all variables will be removed (be careful!).

In SLiM 3, there was an optional parameter removeConstants that, if T, allowed you to remove defined constants (and then potentially redefine them to have a different value).  The removeConstants parameter was removed in SLiM 4, since the defineGlobal() function now provides the ability to define (and redefine) global variables that are not constant.

(*)sapply(* x, string$ lambdaSource, [string$ simplify = "vector"])

Named apply() prior to Eidos 1.6 / SLiM 2.6

Applies a block of Eidos code to the elements of x.  This function is sort of a hybrid between c() and executeLambda(); it might be useful to consult the documentation for both of those functions to better understand what sapply() does.  For each element in x, the lambda defined by lambdaSource will be called.  For the duration of that callout, a variable named applyValue will be defined to have as its value the element of x currently being processed.  The expectation is that the lambda will use applyValue in some way, and will return either NULL or a new value (which need not be a singleton, and need not be of the same type as x).  The return value of sapply() is generated by concatenating together all of the individual vectors returned by the lambda, in exactly the same manner as the c() function (including the possibility of type promotion).

Since this function can be hard to understand at first, here is an example:

sapply(1:10, "if (applyValue % 2) applyValue ^ 2; else NULL;");

This produces the output 1 9 25 49 81.  The sapply() operation begins with the vector 1:10.  For each element of that vector, the lambda is called and applyValue is defined with the element value.  In this respect, sapply() is actually very much like a for loop.  If applyValue is even (as evaluated by the modulo operator, %), the condition of the if statement is F and so NULL is returned by the lambda; this must be done explicitly, since a void return is not allowed by sapply().  If applyValue is odd, on the other hand, the lambda returns its square (as calculated by the exponential operator, ^).  Just as with the c() function, NULL values are dropped during concatenation, so the final result contains only the squares of the odd values.

This example illustrates that the lambda can “drop” values by returning NULL, so sapply() can be used to select particular elements of a vector that satisfy some condition, much like the subscript operator, [].  The example also illustrates that input and result types do not have to match; the vector passed in is integer, whereas the result vector is float.

Beginning in Eidos 1.6, a new optional parameter named simplify allows the result of sapply() to be a matrix or array in certain cases, better organizing the elements of the result.  If the simplify parameter is "vector", the concatenated result value is returned as a plain vector in all cases; this is the default behavior, for backward compatibility.  Two other possible values for simplify are presently supported.  If simplify is "matrix", the concatenated result value will be turned into a matrix with one column for each non-NULL value returned by the lambda, as if the values were joined together with cbind(), as long as all of the lambda’s return values are either (a) NULL or (b) the same length as the other non-NULL values returned.  If simplify is "match", the concatenated result value will be turned into a vector, matrix, or array that exactly matches the dimensions as x, with a one-to-one correspondence between x and the elements of the return value just like a unary operator, as long as all of the lambda’s return values are singletons (with no NULL values).  Both "matrix" and "match" will raise an error if their preconditions are not met, to avoid unexpected behavior, so care should be taken that the preconditions are always met when these options are used.

As with executeLambda(), all defined variables are accessible within the lambda, and changes made to variables inside the lambda will persist beyond the end of the sapply() call; the lambda is executing in the same scope as the rest of your code.

The sapply() function can seem daunting at first, but it is an essential tool in the Eidos toolbox.  It combines the iteration of a for loop, the ability to select elements like operator [], and the ability to assemble results of mixed type together into a single vector like c(), all with the power of arbitrary Eidos code execution like executeLambda().  It is relatively fast, compared to other ways of achieving similar results such as a for loop that accumulates results with c().  Like executeLambda(), sapply() is most efficient if it is called multiple times with a single string script variable, rather than with a newly constructed string for lambdaSource each time.

Prior to Eidos 1.6 (SLiM 2.6), sapply() was instead named apply(); it was renamed to sapply() in order to more closely match the naming of functions in R.  This renaming allowed a new apply() function to be added to Eidos that operates on the margins of matrices and arrays, similar to the apply() function of R (see apply(), above).

(void)setSeed(integer$ seed)

Set the random number seed.  Future random numbers will be based upon the seed value set, and the random number sequence generated from a particular seed value is guaranteed to be reproducible.  The last seed set can be recovered with the getSeed() function.

(void)source(string$ filePath, [logical$ chdir = F])

Executes the contents of an Eidos source file found at the filesystem path filePath.  This is essentially shorthand for calling readFile(), joining the read lines with newlines to form a single string using paste(), and then passing that string to executeLambda().  The source file must consist of complete Eidos statements.  Regardless of what the last executed source line evaluates to, source() has no return value.  If no file exists at filePath, an error will be raised.

The chdir parameter controls the current working directory in effect while the source file is executed.  If chdir is F (the default), the current working directory will remain unchanged.  If chdir is T, the current working directory will be temporarily changed to the filesystem path at which the source file is located, and restored after execution of the source file is complete.

(void)stop([Ns$ message = NULL])

Stops execution of Eidos (and of the Context, such as the running SLiM simulation, if applicable), in the event of an error.  If the optional message parameter is not NULL, it will be printed to Eidos’s output stream prior to stopping.

(logical$)suppressWarnings(logical$ suppress)

Turns suppression of warning messages on or off.  The suppress flag indicates whether suppression of warnings should be enabled (T) or disabled (F).  The previous warning-suppression value is returned by suppressWarnings(), making it easy to suppress warnings from a given call and then return to the previous suppression state afterwards.  It is recommended that warnings be suppressed only around short blocks of code (not all the time), so that unexpected but perhaps important warnings are not missed.  And of course warnings are generally emitted for good reasons; before deciding to disregard a given warning, make sure that you understand exactly why it is being issued, and are certain that it does not represent a serious problem.

(*)sysinfo(string$ key)

Returns information about the system.  The information returned by tempdir() depends upon the value of key, which selects one of the pieces of information listed:

key value

os the name of the OS; "macOS" or "Windows", or "Unix" for all others

sysname the name of the kernel

release the operating system (kernel) release

version the operating system (kernel) version

nodename the name by which the machine is known on the network

machine the hardware type; often the CPU type (e.g., "x86_64")

The value "unknown" will be returned for a key if the correct value cannot be ascertained.  Note that the values of keys that refer to the kernel may not be what you expect; for example, on one particular macOS 10.15.7 system, sysname returns "Darwin", release returns "19.6.0", and version returns "Darwin Kernel Version 19.6.0: Thu Sep 16 20:58:47 PDT 2021; root:xnu-6153.141.40.1~1/RELEASE_X86_64".

Further keys can be added if there is information that would be useful, particularly if a cross-platform way to obtain the information can be found.

(string)system(string$ command, [string args = ""], [string input = ""], [logical$ stderr = F], [logical$ wait = T])

Runs a Un*x command in a /bin/sh shell with optional arguments and input, and returns the result as a vector of output lines.  The args parameter may contain a vector of arguments to command; they will be passed directly to the shell without any quoting, so applying the appropriate quoting as needed by /bin/sh is the caller’s responsibility.  The arguments are appended to command, separated by spaces, and the result is passed to the shell as a single command string, so arguments may simply be given as part of command instead, if preferred.  By default no input is supplied to command; if input is non-empty, however, it will be written to a temporary file (one line per string element) and the standard input of command will be redirected to that temporary file (using standard /bin/sh redirection with <, appended to the command string passed to the shell).  By default, output sent to standard error will not be captured (and thus may end up in the output of the SLiM process, or may be lost); if stderr is T, however, the standard error stream will be redirected into standard out (using standard /bin/sh redirection with 2>&1, appended to the command string passed to the shell).

Arbitrary command strings involving multiple commands, pipes, redirection, etc., may be used with system(), but may be incompatible with the way that args, input, and stderr are handled by this function, so in this case supplying the whole command string in command may be the simplest course.  You may redirect standard error into standard output yourself in command with 2>&1.  Supplying input to a complex command line can often be facilitated by the use of parentheses to create a subshell; for example,

system("(wc -l | sed 's/ //g')", input=c('foo', 'bar', 'baz'));

will supply the input lines to wc courtesy of the subshell started for the () operator.  If this strategy doesn’t work for the command line you want to execute, you can always write a temporary file yourself using writeFile() or writeTempFile() and redirect that file to standard input in command with <.

If wait is T (the default), system() will wait for the command to finish, and return the output generated as a string vector, as described above.  If wait is F, system() will instead append " &" to the end of the command line to request that it be run in the background, and it will not collect and return the output from the command; instead it will return string(0) immediately.  If the output from the command is needed, it could be redirected to a file, and that file could be checked periodically in Eidos for some indication that the command had completed; if output is not redirected to a file, it may appear in SLiM’s output stream.  If the final command line executed by system() ends in " &", the behavior of system() should be just as if wait=T had been supplied, but it is recommended to use wait=T instead to ensure that the command line is correctly assembled.

There is an example at https://github.com/MesserLab/SLiM-Extras/blob/master/functions/rgnorm.slim that demonstrates the use of system(), calling out to Python, to obtain draws from a generalized normal distribution (which is not supported intrinsically by Eidos).  That example even includes internal buffering of a large number of draws, making it a reasonably efficient solution.

(string$)time(void)

Returns a standard time string for the current time in the local time of the executing machine.  The format is %H:%M:%S (hour in two digits, then minute in two digits, then seconds in two digits, zero-padded and separated by dashes) regardless of the localization of the executing machine, for predictability and consistency.  The 24-hour clock time is used (i.e., no AM/PM).

(float$)usage([ls$ type = "rss"])

Returns the memory usage.  This is the amount of memory used by the current process, in MB (megabytes); multiply by 1024*1024 to get the usage in bytes.

Memory usage is a surprisingly complex topic.  One metric reported by usage() is the resident set size, or RSS, which includes memory usage from shared libraries, but does not include memory that is swapped out or has never been used.  For most purposes, RSS is a useful metric of memory usage from a practical perspective.  On some platforms (AIX, BSD, Solaris) the memory usage reported may be zero, but it should be correct on both macOS and Linux platforms.  On macOS, memory pages that have not been used for a while may get compressed by the kernel to reduce the RSS of the process; the RSS metric reported by usage() will reflect the compressed size of such pages, not their original size, so surprising decreases in memory usage may be observed when the kernel decides to compress some memory pages.  The RSS is requested with a type of "rss", which is the default; for historical reasons, it can also be requested with a type of F.

Another metric reported by usage() is the peak RSS.  This is just the highest RSS value that has ever been recorded by the kernel.  It should generally mirror the behavior of RSS, except that it ratchets upward monotonically.  The peak RSS is requested with a type of "rss_peak"; for historical reasons, it can also be requested with a type of T.

The third metric currently reported by usage() is the virtual memory usage.  This is essentially the amount of memory used by pages that have been assigned to the process, whether those pages are resident, compressed, or swapped.  It is typically much larger than the RSS, because it includes various types of memory that are not counted in the RSS; indeed, for some system configurations the virtual memory usage can be reported as being the entire memory space of the computer.  Whether it is a useful metric will be platform-dependent; caveat emptor.

This function can be useful for documenting the memory usage of long runs as they are in progress.  In SLiM, the RSS could also be used to trigger tree-sequence simplification with a call to treeSeqSimplify(), to reduce memory usage when it becomes too large, but keep in mind that the simplification process itself may cause a substantial spike in memory usage, and that page compression and swaps may reduce the RSS even though the memory actually used by tree-sequence recording continues to increase.

When running under SLiM, other tools for monitoring memory usage include the slim command-line options -m[em] and -M[emhist], and the usage() and outputUsage() methods of Community; see the SLiM manual for more information.

(float)version([logical$ print = T])

Get Eidos’s version.  There are two ways to use this function.  If print is T, the default, then the version number is printed to the Eidos output stream in a formatted manner, like “Eidos version 2.1”.  If Eidos is attached to a Context that provides a version number, that is also printed, like “SLiM version 3.1”.  In this case, the Eidos version number, and the Context version number if available, are returned as an invisible float vector.  This is most useful when using Eidos interactively.  If print is F, on the other hand, nothing is printed, but the returned float vector of version numbers is not invisible.  This is useful for scripts that need to test the Eidos or Context version they are running against.

In both cases, in the float version numbers returned, a version like 2.4.2 would be returned as 2.42; this would not scale well to subversions greater than nine, so that will be avoided in our versioning.

================================================ FILE: QtSLiM/help/EidosHelpOperators.html ================================================

2.2.2  ITEM: 1. Sequences: operator :

The : operator is used to construct vectors with (usually) more than one value.  In particular, it is used to construct sequences, and so it is called the sequence operator.  Given operands x and y (standing for any two numbers), the sequence operator starts at x and counts, by 1 (or -1, as appropriate) toward y without passing it.  It yields a vector containing all of the numbers it encounters along the way.

Note that the sequence operator can count down as well as up, that it can handle float as well as integer operands, and that negative numbers are allowed.

2.2.4  ITEM: 2. Subsets: operator []

The [] operator selects a subset of the vector upon which it operates; it is thus often called the subset operator.  It can work in one of two different ways, depending upon whether it is given an integer vector of indices, or is given a logical vector of selectors.

First of all, a subset can be selected with an integer vector of indices.  These indices are zero-based, like C but unlike R; the first value in a vector is thus at index 0, not index 1.  Note that a given index can be used multiple times.

Second, a subset can be selected with a logical vector of selectors.  In this case, the logical vector must be the same length as the vector being selected; each logical value indicates whether the corresponding vector value should be selected (T) or not (F).

2.3.1  ITEM: 3. Arithmetic operators: +, -, *, /, %, ^

These are the standard operators of arithmetic; + performs addition, - performs subtraction, * performs multiplication, / performs division, % performs a modulo operation (more on that below), and ^ performs exponentiation.  Not a great deal needs to be said about these operators, which behave according to the standard rules of mathematics.  They also follow the standard rules of “precedence”; exponentiation is the highest precedence, addition and subtraction are the lowest precedence, and the other three are in the middle, so 4^2+5*6^7 is grouped as (4^3)+(5*(6^7)), as expected if you remember your grade-school math.

There are only a few minor twists to be discussed.  One is the meaning of the % operator, which many people have not previously encountered.  This computes the “modulo” from a division, which is the remainder left behind after division.  For example, 13%6 is 1, because after 13 is divided evenly by 6 (taking care of 12 of the 13), 1 is left as a remainder.  Probably the most common use of % is in determining whether a number is even or odd by looking at the result of a %2 operation; 5%2 is 1, indicating that 5 is odd, whereas 6%2 is 0, indicating that 6 is even.

Another twist is that both the division and modulo operators in Eidos operate on float values – even if integer values are passed – and return float results.  (For those who care, division is performed internally using the C++ division operator /, and modulo is performed using the C++ fmod() function).  This policy was chosen because the definitions of integer division and modulo vary widely among programming languages and are contested and unclear (see Bantchev 2006, http://www.math.bas.bg/bantchev/articles/divmod.pdf).  If you are sure that you want integer division or modulo, and understand the issues involved, Eidos provides the functions integerDiv() and integerMod() for this purpose.  Besides side-stepping the vague definitions of the integer operator, this policy also avoids rather common bugs involving the accidental use of integer division when float division was desired – a much more common occurrence than vice versa.

A third twist is that + and - can both act as “unary” operators, meaning that they are happy to take just a single operand.  This is standard math notation, as in the expressions -6+3 or 7*-5; but it can sometimes look a bit strange, as in the expression 5--6 (more easily read as 5 - -6).

A fourth twist is that the ^ operator is right-associative, whereas all other binary Eidos operators are left-associative.  For example, 2-3-4 is evaluated as (2-3)-4, not as 2-(3-4); this is left-associativity.  However, 2^3^4 is evaluated as 2^(3^4), not (2^3)^4; this is right-associativity.  Since this follows the standard associativity for these operators, in both mathematics and most other programming languages, the result should generally be intuitive, but if you have never explicitly thought about associativity before you might be taken by surprise.

A fifth twist is that the arithmetic operators and functions in Eidos are guaranteed to handle overflows safely.  The float type is safe because it uses IEEE-standard arithmetic, including the use of INF to indicate infinities and the use of NAN to represent not-a-number results; this is the same as in most languages.  In Eidos, however, the integer type is also safe, unlike in C, C++, and many other languages.  All operations on integer values in Eidos either (1) will always produce float results, as the / and % operators do; (2) will produce float results when needed to avoid overflow, as the product() and sum() functions do; or (3) will raise an error condition on an overflow, as the Eidos operators +, -, and * do, as well as the abs() and asInteger() functions.  This means that the integer type in Eidos can be used without fear that overflows might cause results to be incorrect.

The final twist is really a reminder: everything is a vector.  These operators are designed to do something smart, when possible, with vectors of any length, not just with single-valued vectors as shown above.  In general, the operands of these arithmetic operators must either be the same length (in which case the elements in the operand vectors are paired off and the operation is performed between each pair), or one or the other vector must be of length 1 (in which case the operation is performed using that single value, paired with each value in the other operand vector).

2.3.2  ITEM: 4. Logical operators: |, &, !

The |, &, and ! operators act upon logical values.  If they are given operands of other types, those operands will be “coerced” to logical values following the rule mentioned above: zero is F, non-zero is T (and for string operands, a string that is zero characters long – the empty string, "" – is considered F, while all other string values are considered T).

As to what they do: | is the “or” operation, & is the “and” operation, and ! is the “not” operation.  As in common parlance, “or” is T if either of its operands is T, whereas “and” is T only if both of its operands are T.  The “not” operator is unary (it takes only one operand), and it negates its operand; T becomes F, F becomes T.  As with the arithmetic operators, these operators work with vector operands, too – either matching up values pairwise between the two operands, or applying a single value across a multivalued operand.

Those familiar with programming might wish to know that the | and & operators do not “short-circuit” – they can’t, because they are vector operators. If the & operator first sees an operand that evaluates to F, for example, it knows that it will produce F value(s) as a result; but it does not know what size result vector to make. If a later operand is a multivalued vector, the & operator will produce a result vector of matching length; if all later operands are also length 1, however, & will produce a result vector of length 1.  To know this for sure (and to make sure that there are no illegal length mismatches between later operands), it must evaluate all of its operands; it cannot short-circuit.  Similarly for the | operator.

These semantics match those in R, for its | and & operators, but they might seem a little strange to those used to C and other scalar-based languages.  For those used to R, on the other hand, it should be noted here that Eidos does not support the && and || operators of R, for reasons of simplicity; it is safer to use the any() or all() functions to simplify multivalued logical vectors before using & or |.  If this is gibberish to you, it is not important; the point here is only to prevent confusion among users accustomed to R.

2.3.3  ITEM: 5. Comparative operators: ==, !=, <, <=, >, >=

These operators compare their left and right operand.  The operators test for equality (==), inequality (!=), less-than (<), less-than-or-equality (<=), greater-than (>), and greater-than-or-equality (>=) relationships.  As seen above with the arithmetic and logical operators, this can work in two different ways: if the operands are the same length, their elements are paired up and the comparison is done between each pair, whereas if the operands are not the same length then one operand must be of length one, and its value is compared against all of the values of the other operand.

Regardless of the types of the operands, these operators all produce a logical result vector.  If the operands are of different types, promotion will be used to coerce them to be the same type (i.e. logical will be coerced to integer, integer to float, and float to string).  Note that this is often not what you want!  You might not want the automatic type promotion that makes 5=="5" evaluate as T, or the vectorized comparison that makes 1:5==4 evaluate as something other than simply F.  You might really want to ask: are two values identical?  For such purposes, the identical() function is a better choice.

2.3.4  ITEM: 6. String concatenation: operator +

The + operator is often used as an arithmetic operator, but it can also act as a concatenation operator for string operands. Concatenation is pasting together; the + operator simply pastes its string operands together, end to end.

In fact, this works with non-string operands too, as long as a string operand is nearby; the interpretation of + as a concatenation operator is preferred by Eidos, and wins out over its arithmetic interpretation, as long as a string operand is present to suggest doing so. The other non-string operands will be coerced to string.  However, this does not work retroactively; if Eidos has already done arithmetic addition on some operands, it will not go back and perform concatenation instead.  To force concatenation in such situations, you can simply begin the expression with an empty string, "".

The concatenation operator also works with vectors, as usual.

Beginning with Eidos 2.2, string concatenation involving NULL concatenates the string value "NULL", just as if NULL were a singleton string vector containing that value.

2.4.1  ITEM: 7. Assignment: operator =

The results of expressions can be saved in variables.  As in many languages, this is done with the = operator, often called the assignment operator.

The assignment operator, =, is different from the equality comparison operator, ==.  In many languages, confusing the two can cause bugs that are hard to find; in C, for example, it is legal to write:

if (x=y) ...

In C, this would assign the value of y to x, and then the expression x=y would evaluate to the value that was assigned, and that value would be tested by the if statement.  This can be useful as a way of writing extremely compact code; but it is also a very common source of bugs, especially for inexperienced programmers.  In Eidos using assignment in this way is simply illegal; assignment is allowed only in the context of a statement like x=y; to prevent these issues.  (This point is mostly of interest to experienced programmers, so if it is unclear, don’t worry.)

Variable names are fairly unrestricted.  They may begin with a letter (uppercase or lowercase) or an underscore, and subsequently may contain all of those characters, and numerical digits as well.  So x_23, fooBar, and MyVariable23 are all legal variable names (although not good ones – good variable names explain what the variable represents, such as selection_coeff).  However, 4by4 would not be a legal variable name, since it begins with a digit.

2.3.5  ITEM: 8. The ternary conditional: operator ? else

Eidos, like many languages, has an if statement that can be used to specify conditional execution of statements, and an if-else construct can be used to provide an alternative code path.  Sometimes, however, one wishes to have conditional execution of an expression, rather than an entire statement.  The if-else construct is particularly inconvenient with assignments involving complex lvalues, such as:

if (condition)

x[index].property = a;

else

x[index].property = b;

It is desirable to provide a way for the user to specify that the choice of rvalue, a or b, should depend upon condition without having to duplicate the lvalue and the assignment.  The R language provides this functionality by making if-else statements result in an rvalue, like an expression.  The C language, on the other hand, provides a ternary conditional operator, ?:, that can be used in expressions to much the same effect.  Eidos straddles the gap with a ternary conditional operator, ? else, that uses the ? initiator of C, but the else token as a continuation as in R.  In the syntax of Eidos, the above conditional assignment can be rewritten as:

x[index].property = condition ? a else b;

This will evaluate condition and result in a if condition is T, or b if condition is F.  That result is then assigned into the lvalue.  Note that, as in C, the precedence of the ternary conditional operator is very low, but higher than operator =, so that parentheses are often not needed to group statements of this type.  The else clause of the ternary conditional is required; there is no equivalent of an if statement without an else, since an rvalue must be produced.

Just as with if-else statements, only the selected subexpression, as determined by the condition, is evaluated; the other subexpression will not be evaluated, so any side effects it might have will not occur.  For example, with the statement:

x = condition ? f1() else f2();

here f1() will be called if condition is T, f2() if condition is F; only the subexpression selected by the condition is evaluated, and so it is never the case that both f1() and f2() are called.

Ternary conditionals may be nested.  Because the operator is right-associative, an expression such as:

z = (a == b ? a else b ? c else d);

is grouped as:

z = (a == b ? a else (b ? c else d));

rather than

z = ((a == b ? a else b) ? c else d);

This is generally desirable, since it provides a flow similar to chaining of if-else if-else statements.  In any case, parentheses may be used to change the order to evaluation as usual.

2.3.6  ITEM: 9. Grouping: operator ()

All of the discussion above involved simple expressions that allowed the standard precedence rules of mathematics to determine the order of operations; 1+2*3 is evaluated as 1+(2*3) rather than (1+2)*3 because the * operator is higher precedence than the + operator.  For the record, here is the full precedence hierarchy for operators in Eidos, from highest to lowest precedence:

[], (), . subscript, function call, and member access

^ exponentiation (right-associative)

+, -, ! unary plus, unary minus, logical (Boolean) negation (right-associative)

: sequence construction

*, /, % multiplication, division, and modulo

+, - addition and subtraction

<, >, <=, >= less-than, greater-than, less-than-or-equality, greater-than-or-equality

==, != equality and inequality

& logical (Boolean) and

| logical (Boolean) or

= assignment

Operators at the same precedence level are generally evaluated in the order in which they are encountered.  Put more technically, Eidos operators are generally left-associative; 3*5%2 evaluates as (3*5)%2, which is 1, not as 3*(5%2), which is 3.  The only binary operator in Eidos that is an exception to this rule is the ^ operator, which (following standard mathematical convention) is right-associative; 2^3^4 is evaluated as 2^(3^4), not (2^3)^4.  The unary +, unary -, and ! operators are also technically right-associative; for unary operators this is of little practical import, however (it basically just implies that the unary operators must occur to the left of their operand; you write -x, not x-, to express the negation of x).

In any case, parentheses can be used to modify the order of operations, just as in math.  This works just as you would expect.

Note that this use of parentheses is distinct from the () operator as used in making function calls.

Finally, note that Eidos 2.4 and earlier (SLiM 3.4 and earlier) had an operator precedence bug: exponentiation was given a lower precedence than unary minus and its siblings, and so the expression -2^2 would evaluate to 4, as (-2)^2, rather than -4, as -(2^2).  This violated standard mathematical precedence rules, and was fixed in Eidos 2.5 (SLiM 3.5).

2.7.1  ITEM: 10. Function calls: operator ()

A function is simply a block of code which has been given a name.  Using that name, you can then cause the execution of that block of code whenever you wish.  That is the first major purpose of functions: the reuseability of a useful chunk of code.  A function can be supplied with the particular variables upon which it should act, called the function’s “parameters” or “arguments”; you can execute a function with the sequence 5:15 as an argument in one place, and with the string "foo" as an argument in another.  That is the second major purpose of functions: the generalization of a useful chunk of code to easily act on different inputs.

In Eidos, you may define your own functions, or you may execute a lambda (i.e., a snippet of code represented as a string value) directly in the Eidos interpreter.  However, a fairly large set of built-in functions are supplied for your use, and the hope is that they will suffice for most purposes.

Functions are called using the () operator.  Function arguments go between the parentheses of the () operator, separated by commas.  Most functions expect an exact number of arguments; many functions, in fact, are even fussier than that, requiring each parameter to be of a particular type, a particular size, or both.  But some, such as c(), are more flexible.

Many functions provide a return value.  In other words, a function call like c(5,6) can evaluate to a particular value, just as an expression like 5+6 evaluates to a particular value.  The result from a function call can be used in an expression or assigned to a variable, as you might expect.

2.8.3  ITEM: 11. Properties: operator .

Objects encapsulate behaviors as well as elements.  One type of behavior is called a property.  A property is a simple attribute of each element in an object.  Properties can be read using the member-access operator, written as . (a period).  The name of a particular property can be used with . to get that property’s value.  Operations on object are vectorized just as they are for all other types in Eidos; the result of the . operator is a vector containing the value of the property for all of the elements of the object operand.

You can also use the member-access operator to write new values to properties that are not read-only, using the = operator to do the assignment into the property selected by the . operator.

2.8.6  ITEM: 12. Method calls: operator () and operator .

Objects encapsulate behaviors as well as elements.  In addition to properties, another type of behavior is called a method.  Methods are very much like functions; they are chunks of code that you can call to perform tasks.  However, each type of object has its own particular methods – unlike functions, which are defined globally.  Methods are more heavyweight than properties; they might involve quite a lot of computation, they might create a completely new object as their result, and they might even modify the object upon which they are called.  Not all methods are heavyweight in this sort of way, however; anything that one might want an object to do, but that does not feel like a simple property of the object, can be a method.  Methods can also take arguments, just like functions, and they can return whole vectors as their result, unlike (read-write) properties, which must refer to singleton values so that multiplexed assignment can work.  Methods are therefore much more powerful than properties.

Methods are called using the member-access operator, ., with a syntax that looks a lot like accessing a property, but combined with the function call operator, ().  That might look like:

object.method()

Naturally, method calls are also vector operations.  For a multi-element object, a single method call will result in the method call being multiplexed out to all of the elements of the object, and the results from all of those method calls will be concatenated together in the same way that the c() function performs concatenation (including dropping of NULLs and type promotion, potentially).

================================================ FILE: QtSLiM/help/EidosHelpStatements.html ================================================

2.5.1  ITEM: 1. if and if–else statements

As in many languages, conditional execution is provided by the if statement.  This statement is supplied with a logical condition; if the condition is T, the rest of the if statement is executed, whereas if the condition is F, the rest of the if statement is ignored.  An example:


> if (2^2^2^2^2 > 10000) "exponentiation is da bomb!"


"exponentiation is da bomb!"


The only twist here, really, is that the condition must evaluate to a single value, i.e. a vector of size() == 1.  The if statement, in other words, is essentially a scalar operator, not a vector operator.  If you have a multivalued logical vector, you can use the any() or all() functions to simplify it to a single logical value.  Alternatively, the ifelse() function provides a vector conditional operation, similar to that in R.

It is worth exploring this twist with an example.  Suppose you have a variable x which ought to be equal to 3, and a variable y which ought to contain two values, 7 and 8.  You might expect to be able to write:


> if (x == 3 & y == c(7,8)) "yes!"


ERROR (EidosInterpreter::Evaluate_If): condition has size() != 1.


The error informs you that the size of condition is not equal to 1 (and that that is a problem).  The expression y == c(7,8) produces a logical vector with two values, the result of testing the first and second values respectively.  The & operator thus produces a two-valued logical vector as its result, and if is not happy about that.  To resolve this, you could use the all() function, or in many cases more appropriately, the identical() function.  See the Eidos manual for further discussion of this issue.

It is also worth noting that the condition for if does not need to be a logical value; a value of a different type will be converted to logical by coercion if possible.

Often you want to perform an alternative action when the condition of an if statement is F; the if–else statement allows this.  It is simplest to just show this with an example:


> if (2/2/2/2/2 > 10000) "division is da bomb!"; else "not so much."


"not so much."


Super simple, right?

2.5.3  ITEM: 3. semicolons and "null statements", ;

Every statement in Eidos must end with a semicolon (except compound statements, which end with a closing brace).  However, when you’re working interactively in EidosScribe, EidosScribe will add a trailing semicolon to your statements if necessary, just to make your life simpler.  So when you type:

> 1+1==2

what is really being evaluated behind the scenes is:

> 1+1==2;

When you’re not working interactively, semicolons are required, and if you forget, you will get an error, like this:


> 1+1==2


ERROR (Parse): unexpected token 'EOF' in statement; expected ';'


EOF stands for End Of File; it’s a standard way of referring to the end of an input buffer, in this case the line of input provided by the user for execution.

The simplest and shortest possible statement in Eidos is the "null statement", which consists of nothing but a semicolon:

;

This is not terribly useful, since it does nothing.

2.5.4  ITEM: 4. compound statements with { }

The other thing you might wonder about, regarding if statements, is: what if I want to perform more than one action in response to the condition being T or F?  This, then, is an opportune moment to introduce the concept of compound statements.  A compound statement is a series of statements (zero or more) enclosed by braces.  An example is worth a thousand words:


> if (1+1==2)

{

   x = 1;

   x = x + 1;

   x;

}

else

{

   "whoah, I'm confused";

}


2


Note that the input here is spread across multiple lines for clarity; all of this could be typed on a single line instead.  If entered as multiple lines, it cannot presently be entered in EidosScribe’s interactive mode because the if statement would stand on its own and be evaluated as soon as it was completed; instead, the full text would need to be entered in the script area on the left, selected, and executed.  All of the blue lines are user input, whereas the final line in black, 2, shows the output of the execution of the whole if–else statement; the if clause is executed, the calculations involving x are performed, and the final statement x; produces a result which is printed to the console as usual.

The way that x; results in output here might seem a bit surprising at first, but it is a consequence of the fact that the value of a compound statement is the value of the last statement executed within the compound statement; the values of the previous statements are discarded.

You can use a compound statement in any context in which a single statement would be allowed.  For example, compound statements are very commonly used with looping constructs.

2.6.1  ITEM: 5. while statements

A while loop repeats a statement as long as a given condition is true.  The condition is tested before the first time that the statement is executed, so the statement will be executed zero or more times.  Here is a code snippet to compute the first twenty numbers of the Fibonacci sequence:


> fib = c(1, 1);

while (size(fib) < 20)

{

   next_fib = fib[size(fib) - 1] + fib[size(fib) - 2];

   fib = c(fib, next_fib);

}

fib;


1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765


Its use of a while loop is optimal, because it ensures that if the fib vector is already long enough to satisfy the length condition size(fib) < 20, no further values of fib will be computed.  You could use this while loop to lengthen the fib vector on demand within a larger block of code that used the fib vector repeatedly.

2.6.2  ITEM: 6. do–while statements

A do–while loop repeats a statement as long as a given condition is true.  Unlike while loops, in this case the condition is tested at the end of the loop, and thus the loop statement is always executed at least once.  Here is a code snippet to compute a factorial:


> counter = 5;

factorial = 1;

do

{

   factorial = factorial * counter;

   counter = counter - 1;

}

while (counter > 0);

"The factorial of 5 is " + factorial;


"The factorial of 5 is 120"


Note that this example could be rewritten using a while loop instead, but it might be a bit less intuitive in its operation since it would no longer embody the formal definition of the factorial as explicitly.  Note also that computing a factorial could be done much more trivially (and efficiently) using the sequence operator : and the product() function, but the code here is useful for the purpose of illustration.

2.6.3  ITEM: 7. for statements (with in)

The for loop is used to loop through all of the elements in a vector.  For each value in the given vector, a given variable is set to the value, and a given statement is then executed.  For example, the following code computes squares by setting element to each value of my_sequence, one by one, and then executing the print() function for each value:


> my_sequence = 1:4;

for (element in my_sequence)

   print("The square of " + element + " is " + element^2);


"The square of 1 is 1"

"The square of 2 is 4"

"The square of 3 is 9"

"The square of 4 is 16"


This looping construct is called by various names in other languages, such as the “for each” statement (PHP), the “range-based for” (C++), “fast enumeration” (Objective-C), and so forth.  It is different from the traditional for loop of C and related languages, which entails an initializer expression, a condition expression, and an increment/decrement expression.  That type of for loop does not exist in Eidos (following R); the iterator for of R and Eidos is a more natural and efficient choice for vector-based languages.

2.6.4  ITEM: 8. next statements

Sometimes you might wish to cut short the execution of a given iteration of a loop, skipping the rest of the work that would normally be done and proceeding directly to the next iteration.  This is the function of the next statement.  The next statement can be used within for, while, and do–while loops.

2.6.5  ITEM: 9. break statements

Often it is necessary to stop the execution of a loop altogether, not just to cut short the current iteration of the loop as next does.  To achieve this – to break out of a loop completely – use the break statement.  The break statement can be used within for, while, and do–while loops.

2.6.6  ITEM: 10. return statements

The return statement returns a value from a block of code, as in other languages such as C and R.  In one common case, when defining a user-defined function, return is used to stop execution of the function and return a given value to the caller.  Otherwise, a return is useful mostly when the Context within which you’re using Eidos uses the returned value.  When using Eidos in SLiM, for example, SLiM uses the value returned by Eidos scripts such as mutationEffect() callbacks and mateChoice() callbacks, making return very useful in that Context.  Apart from such Context-dependent uses, return is mainly useful as a way to break out of nested loops regardless of the depth of nesting, as illustrated below.

The return statement is very simple: the keyword return, and then, optionally, an expression.  When the return statement is executed, the expression is evaluated and its value is immediately returned as the value of the largest enclosing statement.  The return statement therefore breaks out of all conditionals, loops, and compound statements, regardless of the depth of nesting.

In some circumstances a return statement is not necessary, because compound statements evaluate to the value of the last statement evaluated within them, and if statements behave similarly; as in R, therefore, a return statement can often be omitted.  However, using return makes the intentions of the programmer more explicit, and so its use is encouraged.

If the expression for the return statement is omitted, the return value used is NULL.  In situations where the return value will not be used, such as Eidos events in SLiM, the return value should be omitted to make the intent of the code clear.

2.9.1  ITEM: 11. single-line comments with //

Technically, comments are actually a type of whitespace; comments in Eidos code are completely ignored and have no effect whatsoever on the results of the execution of code, just like other kinds of whitespace in most respects.  Single-line comments begin with // and then may consist of any text whatsoever, up to the end of the current line of code.  A comment may occur by itself on a line, or it may follow other Eidos code.  So for example, you could write:

1 + 1 == 2;    // this is true

Comments are never required in Eidos, but using them to annotate your code is nevertheless a very good idea, both so that you remember what your intentions were when you come back to the code weeks or months later, and so that others who might need to understand or maintain your code have a helping hand.

2.9.2  ITEM: 12. block comments with /* */

It is possible to comment out whole blocks of script, instead of just single lines.  This can be useful for writing longer comments that describe a section of code in more detail.  In Eidos (as in C and C++), such block comments can be written with a beginning /* and a terminating */.  Here’s an example of this style of comment:

/*

   This computes the factorial x!, which is

   the product of all values from 1 to x, for

   any positive integer x.

*/

x_factorial = product(1:x);

A nice feature of Eidos is that block comments nest properly, making it possible to use them to comment out stretches of code that already contain block comments.  For example, if the code above was no longer needed, but you didn’t want to delete it entirely because you might need it again later, you could use a block comment to disable it:

/* ******* NOT NEEDED ************

/*

   This computes the factorial x!, which is

   the product of all values from 1 to x, for

   any positive integer x.

*/

x_factorial = product(1:x);

*/

The outer block comment is not terminated by the first */ because Eidos recognizes that that belongs to the inner block comment; the outer block comment continues until the second */ is encountered.

4.1  ITEM: 13. user-defined functions

Suppose we wish to define a function that doubles whatever float value is passed to it.  This is very easy to do:

function (float)double(float x)

{

return 2 * x;

}

The function keyword initiates the declaration of a new function.  It is followed by the full signature for the new function; here the signature declares that the function is named double, takes a parameter named x that is of type float, and returns type float.  This signature is then followed by the definition of the new function, in the form of a compound statement; here, the double() function is defined as returning two times the value it was passed.  Note that a return statement is used here to return a specified value from the function; if no return statement is encountered, the value of the last statement evaluated is automatically returned to the caller (as in R), but generally it is clearer to explicitly use return.

Calling such functions works in exactly the same way as calling built-in functions:

> double(5.35)

10.7

Functions may be recursive; a simple factorial() function might be defined recursively as:

function (integer)factorial(integer x)

{

if (x <= 1)

return 1;

else

return x * factorial(x - 1);

}

This works well enough, as you can see:

> factorial(13)

6227020800

As with the built-in Eidos functions, user-defined functions may take multiple parameters, each of which may be allowed to be one of several different possible types.  Parameters to user-defined functions may also be optional, with a default value if left unsupplied.  Finally, functions are scoped; the code inside them executes in a private namespace in which only the parameters to the function are available, and variables defined inside a function will not persist beyond the end of the function’s execution.


================================================ FILE: QtSLiM/help/EidosHelpTypes.html ================================================

2.1.1  ITEM: 1. type integer

The integer type is used in Eidos to represent integers – whole numbers, with no fractional component.  Unlike in many languages, exponential notation may be used to specify integer literals (“literals” means values stated literally in the script, rather than derived through calculations).

The integer type is advantageous primarily because it is exact; it does not suffer from any sort of roundoff error. Exact comparison with integer constants is therefore safe; roundoff error will not lead to problems caused by 0.999999999 being deemed to be unequal to 1.  However, integer is disadvantageous because it can only represent a limited range of values, and beyond that range, results will be unpredictable.  Eidos uses 64 bits to store integer values, so that range is quite wide; to −9223372036854775806 to 9223372036854775807, to be exact.  That is broad, but it is still enormously narrower than the range of numbers representable with float.

2.1.2  ITEM: 2. type float

The float type is used in Eidos to represent all non-integer numbers – fractions and real numbers.  Exponential notation may be used to specify float literals; in particular; literals with a decimal point or a negative exponent are taken to be of type float.

Note that this rule means that some literals are represented using float even though they could also be represented using integer.

The float type is advantageous primarily because it can represent an enormously wide range of values.  Eidos uses C++’s double type to represent its float values; the range of values allowed will depend upon your computer’s settings, but it will be vast.  If that range is exceeded, or if numerical problems occur, type float can also represent values as infinity or as “Not A Number” (INF and NAN, respectively, in Eidos).  The float type is thus more robust for operations that might produce such values.  The disadvantage of float is that it is inexact; some values cannot be represented exactly (just as 1/3 in base 10 cannot be represented exactly, and must be written as 0.3333333...).  Roundoff can thus cause comparison errors, overflow and underflow errors, and the accumulation of numerical error.

Several float constants are defined in Eidos; besides INF and NAN, PI is defined as π (3.14159...), and E is defined as e (2.71828...).

2.1.3  ITEM: 3. type logical

The logical type represents true and false values, such as those from comparisons.  In many languages this type is called something like boolean or BOOL; Eidos follows R in using the name logical instead.

There are no logical literals in Eidos.  However, there are defined constants that behave in essentially the same way as literals.  In particular, T is defined as true, and F is defined as false.  These are the only two values that the logical type can take.  As in a great many other languages, these logical values have equivalent numerical values; F is 0, and T is 1 (and in fact any non-zero value is considered to be true if converted to logical type).  Values of type integer or float may therefore be converted to logical, and vice-versa.

2.1.4  ITEM: 4. type string

The string type represents a string of characters – a word, a sentence, a paragraph, the complete works of Shakespeare.  There is no formatting on a string – no font, no point size, no bold or italic.  Instead, it is just a character stream.  A string literal must be enclosed by either single or double quotation marks, ' or ".  This choice simplifies writing Eidos strings that themselves contain quote characters, because you can delimit the string with the opposite kind of quote.  For example, 'You say, "Ere thrice the sun done salutation to the dawn"' is a string that contains double quotes, whereas "Quoth the Raven, 'nevermore'.” is a string that contains single quotes.  Apart from this consideration, it does not matter whether you use single or double quotes; the internal representation is the same.  The suggested convention is to prefer double quotes, all else being equal, since they are more universally used in other programming languages.

A complication arises if one wishes to include both single and double quotation marks within a string; whichever delimiter you choose, one or the other quote character will terminate the string literal.  In this case, the quotation mark must be “escaped” by preceding it with a backslash, \.  The backslash can be used to “escape” various other characters; to include a newline in a string, for example, use \n, and to include a tab, use \t.  Since the backslash has this special meaning, backslashes themselves must be escaped as \\.  An alternative to dealing with escape sequences is to use the “here document” style of string literal; see the Eidos manual for details on this.

2.7.2  ITEM: 5. type NULL

The NULL type two primary uses: as a return value, and as a parameter.

As a return value, NULL is used to indicate that a function had nothing useful to return.  Some functions always return NULL, such as print(); print() sends its output directly to the Eidos console.  It has nothing useful to return, so it returns NULL.  (That NULL value does not normally get printed out by Eidos because it is marked as an “invisible” return, a side topic not really worth getting into here; invisible returns work much as they do in R).

Some functions will return a useful value if they can, but will return NULL if they can’t. Often a NULL return is a result of passing NULL in as an argument; garbage in, garbage out, as they say.  For example, the readFile() function will return NULL if an error occurs that prevents the file read operation from completing.  The calling code could then detect that NULL return and act accordingly – it might try to read from a different path, print an error, or terminate execution with stop(), or it might just ignore the problem, if reading the file was optional anyway (such as an optional configuration file to modify the default behavior of a script).

The other use of NULL, as mentioned above, is as an argument to a function. Passing NULL is occasionally a way of signaling that you don’t want to supply a value for an argument, or that you want a default behavior from the function rather than telling it more specifically what to do.

NULL cannot be an element of a vector of some other type; it cannot be used to mark missing or unknown values, for example.  Instead, NULL is its own type of vector in Eidos, always of zero length.  (There is also no NA value in Eidos like the one in R, while we’re on the topic of marking missing values.  Not having to worry about missing values makes Eidos substantially simpler and faster, and Eidos – unlike R – is not designed to be used for doing statistical analysis, so marking missing values is not expected to be important.  Eidos does support NAN – Not A Number – values in float vectors, however, which could conceivably be used to mark missing values if necessary.)

The basic philosophy of how Eidos handles NULL values in expressions and computations is that NULL in such situations represents a non-fatal error or an unknown value.  If using the NULL value in some meaningful way could lead to potentially misleading or incorrect results, Eidos will generate a fatal error.  The idea is to give Eidos code an opportunity to detect a NULL, and thus to catch and handle the non-fatal error; but if the code does not handle the NULL, using the NULL in further operations will result in a fatal error before the functioning of the code is seriously compromised.  NULL values are thus a sort of third rail; there’s a good reason they exist, but you have to be very careful around them.  They are a bit like zero-valued pointers in C (NULL), C++ (nullptr), Objective-C (nil), and similar languages; they are widely used, but if you ever use one the wrong way it is an immediate and fatal error.  For further details, please consult the Eidos manual.

2.8.1  ITEM: 6. type object

In addition to logical, integer, float, string, and NULL, there is one more type in Eidos left to discuss: object.  A variable of type object is a vector that contains elements; it is a container, a bag of stuff.  In this way, it is similar to Eidos’s other types; a float vector in Eidos contains floating-point elements, whereas an object vector contains object-elements (often just called “objects”; whether one is referring to a single object-element or a vector of type object is generally clear from context).  An object vector can also embody behavior: it has operations that it can perform using the elements it contains, which all belong to a class that defines the available behaviors.  The object type in Eidos is thus similar to objects in other languages such as Java, C++, or R – except much more limited.  In Eidos you cannot define your own object classes; you work only with the predefined object classes supplied by SLiM or whatever other Context you might be using Eidos within.  These predefined object classes generally define Context-dependent object-elements related to the task performed by the Context; in SLiM, the classes are things such as mutations, genomic elements, and mutation types (described in SLiM’s documentation).  Eidos itself also supplies a few built-in object classes, notably Dictionary and Image.

The behaviors of objects in Eidos manifest in two ways: objects can have properties (also called instance variables or member variables, in other languages) that can be read from and written to, and they can have methods (also called member functions, in other languages).  The behavior of an object vector in Eidos is determined by the class of element the object contains; an Eidos object will always contain only one class of element (just as a float cannot contain string-elements, for example).

Instances of particular object classes – particular kinds of objects – are obtained via built-in functions and/or global constants and variables.  For example, in SLiM there is a global constant called sim that represents the simulated species as an instance of the Species class.

================================================ FILE: QtSLiM/help/SLiMHelpCallbacks.html ================================================

5.13.0  ITEM: 1. initialize() callbacks

Before a SLiM simulation can be run, the various classes underlying the simulation need to be set up with an initial configuration.  Simulation configuration in SLiM is done in initialize() callbacks that run prior to the beginning of simulation execution.  For our present purposes, the idea is very simple; in your input file, you can write something like this:

initialize()
{
...
}

The initialize() declaration specifies that the script block is to be executed as an initialize() callback before the simulation starts.  The script between the braces {} would set up various aspects of the simulation by calling initialization functions.  These are SLiM functions that may be called only in an initialize() callback, and their names begin with initialize to mark them clearly as such.  You may also use other Eidos functionality in these callbacks; for example, you might automate generating a complex genetic structure containing many genes by using a for loop.

In general, it is required for a species to set up its genetic structure in an initialize() callback with calls to initializeMutationRate(), initializeRecombinationRate(), initializeMutationType(), initializeGenomicElementType(), and initializeGenomicElement(); species must call all of these, setting up at least one mutation type, at least one genomic element type, and at least one genomic element.  The exception to this general rule is for species that have no genetics at all – species that are modeled purely on an ecological/behavioral level.  Such species may be defined by calling none of those initialization functions; in this case, SLiM will default to a zero-length chromosome with mutation and recombination rates of zero.  A middle ground between these two configuration paths is not allowed; either a species has no genetics, or it fully defines its genetics.

One thing worth mentioning is that in the context of an initialize() callback, the sim global representing the species being simulated is not defined.  This is because the state of the simulation is not yet constructed fully, and accessing partially constructed state would not be safe.  (Similarly, in multispecies models, the community object and the objects representing individual species are not yet defined.)

The above initialize() callback syntax implicitly declares a single species, with the default name of sim, and therefore sets up a single-species model.  It is also possible to explicitly declare a species, which is done with this extended syntax (using a species name of fox as an example):

species fox initialize() { ... }

This sets up a multispecies model (although it might, in fact, declare only a single species, fox; the term “multispecies”, in SLiM parlance, really means “explicitly declared species”, but multispecies models almost always do contain multiple species, so the distinction is unimportant).  In most respects multispecies models work identically to single-species models, so we will tend to focus on the single-species case in the reference documentation, with a species name of sim, for simplicity and clarity.

In single-species models all initialization can be done in a single initialize() callback (or you can have more than one, if you wish).  In multispecies models, each species must be initialized with its own callback(s), as shown above.  In addition, multispecies models also support an optional community-level initialization callback that is declared as follows:

species all initialize() { ... }

These callbacks, technically called non-species-specific initialize() callbacks, provide a place for community-level initialization to occur.  They are run before any species-specific initialize() callbacks are run, so you might wish to set up all of your model parameters in one, providing a single location for all parameters.  In multispecies models, the initializeModelType() and initializeInteractionType() functions may only be called from a non-species-specific initialize() callback, since those aspects of model configuration span the entire community.  In single-species models, these functions may be called from an ordinary initialize() callback for simplicity and backward compatibility.

Once all initialize() callbacks have executed, in the order in which they are specified in the SLiM input file, the simulation will begin.  The tick number at which it starts is determined by the Eidos events you have defined; the first tick in which an Eidos event is scheduled to execute is the tick at which the simulation starts.  Similarly, the simulation will terminate after the last tick for which a script block (either an event or a callback) is registered to execute, unless the stop() function or the simulationFinished() method of Community or Species are called to end the simulation earlier.

5.13.1  ITEM: 2. Eidos events

An Eidos event is a block of Eidos code that is executed every tick, within a tick range, to perform a desired task.  The syntax of an Eidos event declaration looks like one of these:

[id] [t1 [: t2]] first() { ... }
[id] [t1 [: t2]] early() { ... }
[id] [t1 [: t2]] late() { ... }

The first declaration declares a first() event that executes first thing in the tick cycle.  The second declaration declares an early() event that executes relatively early in the tick cycle.  The third declaration declares a late() event that executes near the end of the tick cycle.  Exactly when these events run depends upon whether the model is a WF model or a nonWF model; see the tick cycle diagrams for those model types.

The id is an optional identifier like s1 (or more generally, sX, where X is an integer greater than or equal to 0) that defines an identifier that can be used to refer to the script block.  In most situations it can be omitted, in which case the id is implicitly defined as -1, a placeholder value that essentially represents the lack of an identifier value.  Supplying an id is only useful if you wish to manipulate your script blocks programmatically.

Then comes a tick or a range of ticks, and then a block of Eidos code enclosed in braces to form a compound statement.  A trivial example might look like this:

1000:5000 early() {
catn(community.tick);
}

This would print the tick number in every tick in the specified range, which is obviously not very exciting.  The broader point is that the Eidos code in the braces {} is executed early in every tick within the specified range of ticks.  In this case, the tick range is 1000 to 5000, and so the Eidos event will be executed 4001 times (not 4000!).  A range of ticks can be given, as in the example above, or a single tick can be given with a single integer:

100 late() {
print("Finished tick 100!");
}

The tick range may also be incompletely specified, with a somewhat idiosyncratic syntax.  A range of 1000: would specify that the event should run in tick 1000 and every subsequent tick until the model finishes; a range of :1000 would similarly specify that the event should run in the first tick executed, and every subsequent tick, up to and including tick 1000.

In fact, you can omit specifying a tick altogether, in which case the Eidos event runs every tick.  Since it takes a little time to set up the Eidos interpreter and interpret a script, it is advisable to use the narrowest range of ticks possible; however, that is more of a concern with callbacks, since they might be called many time in every tick, whereas first(), early(), and late() events will just be called once per tick.

The ticks specified for a Eidos event block can be any positive integer.  All blocks that apply to a given time point will be run in definition order; blocks specified higher in the input file will run before those specified lower.  Sometimes it is desirable to have a script block execute in a tick which is not fixed, but instead depends upon some parameter, defined constant, or calculation; this may be achieved by rescheduling the script block with the Community method rescheduleScriptBlock().

In multispecies models, one can optionally provide a ticks specifier before the definition of an Eidos event, specifying that the event should only run in ticks in which a particular species is active.  That extended syntax looks like this:

[ticks species_name] [id] [t1 [: t2]] first() { ... }
[ticks species_name] [id] [t1 [: t2]] early() { ... }
[ticks species_name] [id] [t1 [: t2]] late() { ... }

The species_name should be the name of a species that was explicitly declared in the multispecies model.  If the ticks specifier is omitted, the event will run in every tick (within the specified tick range).

When Eidos events are executed, several global variables are defined by SLiM for use by the Eidos code.  Here is a summary of those SLiM globals:

community The Community object for the overall simulation

sim A Species object for the simulated species (in single-species simulations)

g1, ... GenomicElementType objects for defined genomic element types

i1, ... InteractionType objects for defined interaction types

m1, ... MutationType objects representing defined mutation types

p1, ... Subpopulation objects for existing subpopulations

s1, ... SLiMEidosBlock objects for named events and callbacks

self A SLiMEidosBlock object for the script block currently executing

In multispecies models, symbols for each species will be defined instead of sim.  Note that species symbols such as sim are not available in initialize() callbacks, since the species objects have not yet been initialized.  Similarly, the globals for subpopulations, mutation types, and genomic element types are only available after the point at which those objects have been defined by an initialize() callback.

5.13.2  ITEM: 3. mutationEffect() callbacks

An Eidos callback is a block of Eidos code that is called by SLiM in specific circumstances, to allow the customization of particular actions taken by SLiM while running the simulation of a species.  Nine types of callbacks are presently supported (in addition to initialize() callbacks): mutationEffect() callbacks, discussed here, and fitnessEffect(), mateChoice(), modifyChild(), recombination(), interaction(), reproduction() , mutation(), and survival() callbacks.

A mutationEffect() callback is called by SLiM when it is determining the fitness effect of a mutation carried by an individual.  Normally, the fitness effect of a mutation is determined by the selection coefficient s of the mutation and the dominance coefficient h of the mutation (the latter used only if the individual is heterozygous for the mutation).  More specifically, the standard calculation for the fitness effect of a mutation takes one of two forms.  If the individual is homozygous, then the fitness effect is (1+s), or:

w = w * (1.0 + selectionCoefficient),

where w is the relative fitness of the individual carrying the mutation.  If the individual is heterozygous, then the dominance coefficient enters the picture, and the fitness effect is (1+hs) or:

w = w * (1.0 + dominanceCoeff * selectionCoeff).

The dominance coefficient usually comes from the dominanceCoeff property of the mutation’s MutationType; if the focal individual has only one non-null haplosome, however, such that the mutation is paired with a null haplosome (i.e., is actually hemizygous, not heterozygous), the hemizygousDominanceCoeff property of the MutationType is used instead.

That is the standard behavior of SLiM, reviewed here to provide a conceptual baseline.  Supplying a mutationEffect() callback allows you to substitute any calculation you wish for the relative fitness effect of a mutation; the new relative fitness effect computation becomes:

w = w * mutationEffect()

where mutationEffect() is the value returned by your callback.  This value is a multiplicative fitness effect, so 1.0 is neutral, unlike the selection coefficient scale where 0.0 is neutral; be careful with this distinction!

Like Eidos events, mutationEffect() callbacks are defined as script blocks in the input file, but they use a variation of the syntax for defining an Eidos event:

[id] [t1 [: t2]] mutationEffect(<mut-type-id> [, <subpop-id>]) { ... }

For example, if the callback were defined as:

1000:2000 mutationEffect(m2, p3) { 1.0; }

then a relative fitness of 1.0 (i.e. neutral) would be used for all mutations of mutation type m2 in subpopulation p3 from tick 1000 to tick 2000.  The very same mutations, if also present in individuals in other subpopulations, would preserve their normal selection coefficient and dominance coefficient in those other subpopulations; this callback would therefore establish spatial heterogeneity in selection, in which mutation type m2 was neutral in subpopulation p3 but under selection in other subpopulations, for the range of ticks given.

In multispecies models, callbacks must be defined with a species specifier that states the species with which species the callback is associated.  Such a definition looks like this:

species species_name [id] [t1 [: t2]] mutationEffect(...) { ... }

It is the same syntax, in other words, except for the species specifier at the beginning, with the name of the species that the callback will modify.  As with the ticks specifier for events, this means the callback will only be called in ticks when the species is active; but the species specifier goes further, making that species the focal species for the callback.

In addition to the standard SLiM globals, a mutationEffect() callback is supplied with some additional information passed through “pseudo-parameters”, variables that are defined by SLiM within the context of the callback’s code to supply the callback with relevant information:

mut A Mutation object, the mutation whose relative fitness is being evaluated

homozygous A value of T (the mutation is homozygous), F (heterozygous), or NULL (it is
paired with a null haplosome, and is thus hemizygous or haploid)

effect The default relative fitness value calculated by SLiM

individual The individual carrying this mutation (an object of class Individual)

subpop The subpopulation in which that individual lives

These may be used in the mutationEffect() callback to compute a fitness value.  To implement the standard fitness functions used by SLiM for an autosomal simulation with no null haplosomes involved, for example, you could do something like this:

mutationEffect(m1) {
if (homozygous)
return 1.0 + mut.selectionCoeff;
else
return 1.0 + mut.mutationType.dominanceCoeff * mut.selectionCoeff;
}

As mentioned above, a relative fitness of 1.0 is neutral (whereas a selection coefficient of 0.0 is neutral); the 1.0 + in these calculations converts between the selection coefficient scale and the relative fitness scale, and is therefore essential.  However, the effect global variable mentioned above would already contain this value, precomputed by SLiM, so you could simply return effect to get that behavior when you want it:

mutationEffect(m1) {
if (<conditions>)
<custom fitness calculations...>;
else
return effect;
}

This would return a modified fitness value in certain conditions, but would return the standard fitness value otherwise.

More than one mutationEffect() callback may be defined to operate in the same tick.  As with Eidos events, multiple callbacks will be called in the order in which they were defined in the input file.  Furthermore, each callback will be given the effect value returned by the previous callback – so the value of effect is not necessarily the default value, in fact, but is the result of all previous mutationEffect() callbacks for that individual in that tick.  In this way, the effects of multiple callbacks can “stack”.

One caveat to be aware of in WF models is that mutationEffect() callbacks are called at the end of the tick, just before the next tick begins.  If you have a mutationEffect() callback defined for tick 10, for example, it will actually be called at the very end of tick 10, after child generation has finished, after the new children have been promoted to be the next parental generation, and after late() events have been executed.  The fitness values calculated will thus be used during tick 11; the fitness values used in tick 10 were calculated at the end of tick 9.  (This is primarily so that SLiMgui, which refreshes its display in between ticks, has computed fitness values at hand that it can use to display the new parental individuals in the proper colors.)  This is not an issue in nonWF models, since fitness values are used in the same tick in which they are calculated.

If the randomizeCallbacks parameter to initializeSLiMOptions() is T (the default), the order in which the fitness of individuals is evaluated will be randomized within each subpopulation.  This partially mitigates order-dependency issues, although such issues can still arise whenever the effects of a mutationEffect() callback are not independent.  If randomizeCallbacks is F, the fitness of individuals will be evaluated in sequential order within each subpopulation, greatly increasing the risk of order-dependency problems.

Many other possibilities can be implemented with mutationEffect() callbacks.  However, since mutationEffect() callbacks involve Eidos code being executed for the evaluation of fitness of every mutation of every individual (within the tick range, mutation type, and subpopulation specified), they can slow down a simulation considerably, so use them as sparingly as possible.

5.13.3  ITEM: 4. fitnessEffect() callbacks

We have already seen mutationEffect() callbacks, which modify the effect of a given mutation in a focal individual.  Sometimes it is desirable to model effects upon individual fitness that are not governed by particular mutations (or not directly, at least); fitness effects due to spatial position, or resource acquisition, or behavior such as competitive or altruistic interactions, for example.  Another situation of this type is when fitness depends upon the overall phenotype of an individual – the height of a tree, say – which might be influenced by genetics, but also by environmental effects, climate, and so forth.  For these sorts of situations, SLiM provides fitnessEffect() callbacks.

A fitnessEffect() callback is called by SLiM when it is determining the fitness of an individual – typically, but not always, once per tick during the fitness calculation tick cycle stage.  Normally, the fitness of a given individual is determined by multiplying together the fitness effects of all mutations possessed by that individual.  Supplying a fitnessEffect() callback allows you to add another multiplicative fitness effect into that calculation.  As with mutationEffect() callbacks, the value returned by fitnessEffect() callbacks is a fitness effect, so 1.0 is neutral.

The syntax for declaring fitnessEffect() callbacks is similar to that for mutationEffect() callbacks, but simpler since no mutation type is needed:

[id] [t1 [: t2]] fitnessEffect([<subpop-id>]) { ... }

(In multispecies models, the definition must be preceded by a species specification as usual.)

For example, if the callback were defined as:

1000:2000 fitnessEffect(p3) { 0.75; }

then a fitness effect of 0.75 would be multiplied into the fitness values of all individuals in subpopulation p3 from tick 1000 to tick 2000.

Much more interesting, of course, are fitnessEffect() callbacks that return different fitness effects for different individuals, depending upon their state!  In addition to the standard SLiM globals, a fitnessEffect() callback is supplied with some additional information passed through “pseudo-parameters”, variables that are defined by SLiM within the context of the callback’s code to supply the callback with relevant information:

individual The focal individual (an object of class Individual)

subpop The subpopulation in which that individual lives

These may be used in the fitnessEffect() callback to compute a fitness effect that depends upon the state of the focal individual.  The fitness effect for the callback is simply returned as a singleton float value, as usual.

More than one fitnessEffect() callback may be defined to operate in the same tick.  Each such callback will provide an independent fitness effect for the focal individual; the results of each fitnessEffect() callback will be multiplied in to the individual’s fitness.  These callbacks will generally be called once per individual in each tick, in an order that is formally undefined.

Beginning in SLiM 3.0, it is also possible to set the fitnessScaling property on a subpopulation to scale the fitness values of every individual in the subpopulation by the same constant amount, or to set the fitnessScaling property on an individual to scale the fitness value of that specific individual.  These scaling factors are multiplied together with all other fitness effects for an individual to produce the individual’s final fitness value.  The fitnessScaling properties of Subpopulation and Individual can often provide similar functionality to fitnessEffect() callbacks with greater efficiency and simplicity.  They are reset to 1.0 in every tick for which a given species is active, immediately after fitness values are calculated, so they only need to be set when a value other than 1.0 is desired.

As with mutationEffect() callbacks, fitnessEffect() callbacks are called at the end of the tick, just before the next tick begins.  Also, as with mutationEffect() callbacks, the order in which fitnessEffect() callbacks are called will be shuffled when randomizeCallbacks is enabled, as it is by default, partially mitigating order-dependency issues.

The fitnessEffect() callback mechanism is quite flexible and useful, although it has been considerably eclipsed by the modern modern and efficient fitnessScaling property mentioned above.  When efficiency is not at a premium, it remains a clear and expressive paradigm for modeling individual-level fitness effects.  The performance penalty paid is often not large, since these callbacks are called only once per individual per tick, whereas a mutationEffect() for a type of mutation that is common in the simulation might be called thousands of times per individual per tick (once per mutation of that type possessed by the focal individual).  The performance penalty typically becomes severe only when the fitnessEffect() callback needs to perform calculations, once per focal individual, that would vectorize well if performed across a whole vector of individuals.  In such cases, fitnessScaling should be used.

5.13.4  ITEM: 5. mateChoice() callbacks

Normally, WF models in SLiM regulate mate choice according to fitness; individuals of higher fitness are more likely to be chosen as mates.  However, one might wish to simulate more complex mate-choice dynamics such as assortative or disassortative mating, mate search algorithms, and so forth.  Such dynamics can be handled in WF models with the mateChoice() callback mechanism.  (In nonWF models mating is arranged by the script, so there is no need for a callback.)

A mateChoice() callback is established in the input file with a syntax very similar to that of fitnessEffect() callbacks:

[id] [t1 [: t2]] mateChoice([<subpop-id>]) { ... }

(In multispecies models, the definition must be preceded by a species specification as usual.)

Note that if a subpopulation is given to which the mateChoice() callback is to apply, the callback is used for all matings that will generate a child in the stated subpopulation (as opposed to all matings of parents in the stated subpopulation); this distinction is important when migration causes children in one subpopulation to be generated by matings of parents in a different subpopulation.

When a mateChoice() callback is defined, the first parent in a mating is still chosen proportionally according to fitness (if you wish to influence that choice, you can use a mutationEffect() or fitnessEffect() callback).  In a sexual (rather than hermaphroditic) simulation, this will be the female parent; SLiM does not currently support males as the choosy sex.  The second parent – the male parent, in a sexual simulation – will then be chosen based upon the results of the mateChoice() callback.

More specifically, the callback must return a vector of weights, one for each individual in the subpopulation; SLiM will then choose a parent with probability proportional to weight.  The mateChoice() callback could therefore modify or replace the standard fitness-based weights depending upon some other criterion such as assortativeness.  A singleton object of type Individual may be returned instead of a weights vector to indicate that that specific individual has been chosen as the mate (beginning in SLiM 2.3); this could also be achieved by returned a vector of weights in which the chosen mate has a non-zero weight and all other weights are zero, but returning the chosen individual directly is much more efficient.  A zero-length return vector – as generated by float(0), for example – indicates that a suitable mate was not found; in that event, a new first parent will be drawn from the subpopulation.  Finally, if the callback returns NULL, that signifies that SLiM should use the standard fitness-based weights to choose a mate; the mateChoice() callback did not wish to alter the standard behavior for the current mating (this is equivalent to returning the unmodified vector of weights, but returning NULL is much faster since it allows SLiM to drop into an optimized case).  Apart from the special cases described above – a singleton Individual, float(0), and NULL – the returned vector of weights must contain the same number of values as the size of the subpopulation, and all weights must be non-negative.  Note that the vector of weights is not required to sum to 1, however; SLiM will convert relative weights on any scale to probabilities for you.

If the sum of the returned weights vector is zero, SLiM treats it as meaning the same thing as a return of float(0) – a suitable mate could not be found, and a new first parent will thus be drawn.  (This is a change in policy beginning in SLiM 2.3; prior to that, returning a vector of sum zero was considered a runtime error.)  There is a subtle difference in semantics between this and a return of float(0): returning float(0) immediately short-circuits mate choice for the current first parent, whereas returning a vector of zeros allows further applicable mateChoice() callbacks to be called, one of which might “rescue” the first parent by returning a non-zero weights vector or an individual.  In most models this distinction is irrelevant, since chaining mateChoice() callbacks is uncommon.  When the choice is otherwise unimportant, returning float(0) will be handled more quickly by SLiM.

In addition to the standard SLiM globals, a mateChoice() callback is supplied with some additional information passed through “pseudo-parameters”:

individual The parent already chosen (the female, in sexual simulations)

subpop The subpopulation into which the offspring will be placed

sourceSubpop The subpopulation from which the parents are being chosen

weights The standard fitness-based weights for all individuals

If sex is enabled, the mateChoice() callback must ensure that the appropriate weights are zero and nonzero to guarantee that all eligible mates are male (since the first parent chosen is always female, as explained above).  In other words, weights for females must be 0.  The weights vector given to the callback is guaranteed to satisfy this constraint.  If sex is not enabled – in a hermaphroditic simulation, in other words – this constraint does not apply.

For example, a simple mateChoice() callback might look like this:

1000:2000 mateChoice(p2) {
return weights ^ 2;
}

This defines a mateChoice() callback for ticks 1000 to 2000 for subpopulation p2.  The callback simply transforms the standard fitness-based probabilities by squaring them.  Code like this could represent a situation in which fitness and mate choice proceed normally in one subpopulation (p1, here, presumably), but are altered by the effects of a social dominance hierarchy or male-male competition in another subpopulation (p2, here), such that the highest-fitness individuals tend to be chosen as mates more often than their (perhaps survival-based) fitness values would otherwise suggest.  Note that by basing the returned weights on the weights vector supplied by SLiM, the requirement that females be given weights of 0 is finessed; in other situations, care would need to be taken to ensure that.

More than one mateChoice() callback may be defined to operate in the same tick.  As with Eidos events, multiple callbacks will be called in the order in which they were defined.  Furthermore, each callback will be given the weights vector returned by the previous callback – so the value of weights is not necessarily the default fitness-based weights, in fact, but is the result of all previous weights() callbacks for the current mate-choice event.  In this way, the effects of multiple callbacks can “stack”.  If any mateChoice() callback returns float(0), however – indicating that no eligible mates exist, as described above – then the remainder of the callback chain will be short-circuited and a new first parent will immediately be chosen.

Note that matings in SLiM do not proceed in random order.  Offspring are generated for each subpopulation in turn, and within each subpopulation the order of offspring generation is also non-random with respect to both the source subpopulation and the sex of the offspring.  It is important, therefore, that mateChoice() callbacks are not in any way biased by the offspring generation order; they should not treat matings early in the process any differently than matings late in the process.  Any failure to guarantee such invariance could lead to large biases in the simulation outcome.  In particular, it is usually dangerous to activate or deactivate mateChoice() callbacks while offspring generation is in progress.

A wide variety of mate choice algorithms can easily be implemented with mateChoice() callbacks.  However, mateChoice() callbacks can be particularly slow since they are called for every proposed mating, and the vector of mating weights can be large and slow to process.

5.13.5  ITEM: 6. modifyChild() callbacks

Normally, a SLiM simulation defines child generation with its rules regarding selfing versus crossing, recombination, mutation, and so forth.  However, one might wish to modify these rules in particular circumstances – by preventing particular children from being generated, by modifying the generated children in particular ways, or by generating children oneself.  All of these dynamics can be handled in SLiM with the modifyChild() callback mechanism.

A modifyChild() callback is established in the input file with a syntax very similar to that of other callbacks:

[id] [t1 [: t2]] modifyChild([<subpop-id>]) { ... }

The modifyChild() callback may optionally be restricted to the children generated to occupy a specified subpopulation.  (In multispecies models, the definition must be preceded by a species specification as usual.)

When a modifyChild() callback is called, a parent or parents have already been chosen, and a candidate child has already been generated.  The parent or parents are provided to the callback, as is the generated child.  The callback may accept the generated child, modify it, substitute completely different genetic information for it, or reject it (causing a new parent or parents to be selected and a new child to be generated, which will again be passed to the callback).

In addition to the standard SLiM globals, a modifyChild() callback is supplied with additional information passed through “pseudo-parameters”:

child The generated child (an object of class Individual)

isCloning T if the child is the result of cloning

isSelfing T if the child is the result of selfing (but see note below)

parent1 The first parent (an object of class Individual)

parent2 The second parent (an object of class Individual)

subpop The subpopulation in which the child will live

sourceSubpop The subpopulation of the parents (==subpop if not a migration mating)

These may be used in the modifyChild() callback to decide upon a course of action.  The haplosomes of child (available as child.haplosomes) may be modified by the callback; whatever mutations they contain on exit will be used for the new child.  Alternatively, they may be left unmodified (to accept the generated child as is).  The child’s haplosomes may be thought of as the two gametes that will fuse to produce the fertilized egg that results in a new offspring; for a biparental cross involving diploid autosomes, child.haploidGenome1 is the gamete contributed by the first parent (the female, if sex is turned on), and child.haploidGenome2 is the gamete contributed by the second parent (the male, if sex is turned on).  The child object itself may also be modified – for example, to set the spatial position of the child.

Importantly, a logical singleton return value is required from modifyChild() callbacks.  Normally this should be T, indicating that generation of the child may proceed (with whatever modifications might have been made to the child’s haplosomes).  A return value of F indicates that generation of this child should not continue; this will cause new parent(s) to be drawn, a new child to be generated, and a new call to the modifyChild() callback.  A modifyChild() callback that always returns F can cause SLiM to hang, so be careful that it is guaranteed that your callback has a nonzero probability of returning T for every state your simulation can reach.

Note that isSelfing is T only when a mating was explicitly set up to be a selfing event by SLiM; an individual may also mate with itself by chance (by drawing itself as a mate) even when SLiM did not explicitly set up a selfing event, which one might term incidental selfing.  If you need to know whether a mating event was an incidental selfing event, you can compare the parents; self-fertilization will always entail parent1==parent2, even when isSelfing is F.  Since selfing is enabled only in non-sexual simulations, isSelfing will always be F in sexual simulations (and incidental selfing is also impossible in sexual simulations).

Note that matings in SLiM do not proceed in random order.  Offspring are generated for each subpopulation in turn, and within each subpopulation the order of offspring generation is also non-random with respect to the source subpopulation, the sex of the offspring, and the reproductive mode (selfing, cloning, or autogamy).  It is important, therefore, that modifyChild() callbacks are not in any way biased by the offspring generation order; they should not treat offspring generated early in the process any differently than offspring generated late in the process.  Similar to mateChoice() callbacks, any failure to guarantee such invariance could lead to large biases in the simulation outcome.  In particular, it is usually dangerous to activate or deactivate modifyChild() callbacks while offspring generation is in progress.  When SLiM sees that mateChoice() or modifyChild() callbacks are defined, it randomizes the order of child generation within each subpopulation, so this issue is mitigated somewhat.  However, offspring are still generated for each subpopulation in turn.  Furthermore, in ticks without active callbacks offspring generation order will not be randomized (making the order of parents nonrandom in the next generation), with possible side effects.  In short, order-dependency issues are possible and must be handled very carefully.

As with the other callback types, multiple modifyChild() callbacks may be registered and active.  In this case, all registered and active callbacks will be called for each child generated, in the order that the callbacks were registered.  If a modifyChild() callback returns F, however, indicating that the child should not be generated, the remaining callbacks in the chain will not be called.

There are many different ways in which a modifyChild() callback could be used in a simulation.  In nonWF models, modifyChild() callbacks are often unnecessary since each generated child is available to the script in the models’ reproduction() callback anyway; but they may be used if desired.

5.13.6  ITEM: 7. recombination() callbacks

Typically, a simulation sets up a recombination map at the beginning of the run with initializeRecombinationRate(), and that map is used for the duration of the run.  Less commonly, the recombination map is changed dynamically from tick to tick, with Chromosome’s method setRecombinationRate(); but still, a single recombination map applies for all individuals of a species in a given tick.  However, in unusual circumstances a simulation may need to modify the way that recombination works on an individual basis; for this, the recombination() callback mechanism is provided.  This can be useful for models involving chromosomal inversions that prevent recombination within a region for some individuals, for example, or for models of the evolution of recombination.

A recombination() callback is defined with a syntax much like that of other callbacks:

[id] [t1 [: t2]] recombination([<subpop-id> [, <chromosome-id>]]) { ... }

The recombination() callback will be called during the generation of every gamete during the tick(s) in which it is active.  It may optionally be restricted to apply only to gametes generated by parents in a specified subpopulation, using the <subpop-id> specifier.  In addition, in multi-chromosome models it may optionally be restricted to apply only to a specified chromosome, using the <chromosome-id> specifier, which may be either the id or the symbol of a chromosome defined in the species.  (In multispecies models, the definition must be preceded by a species specification as usual.)

When a recombination() callback is called, a parent has already been chosen to generate a gamete, and candidate recombination breakpoints for use in recombining the parental haplosomes have been drawn.  The relevant haplosomes of the focal parent are provided to the callback, as is the focal parent itself (as an Individual object) and the subpopulation in which it resides.  Furthermore, the proposed breakpoints are provided to the callback.  The callback may modify these breakpoints in order to change the breakpoints used, in which case it must return T to indicate that changes were made, or it may leave the proposed breakpoints unmodified, in which case it must return F.  (The behavior of SLiM is undefined if the callback returns the wrong logical value.)

In addition to the standard SLiM globals, then, a recombination() callback is supplied with additional information passed through “pseudo-parameters”:

individual The focal parent that is generating a gamete

haplosome1 One haplosome of the focal parent; this is the initial copy strand

haplosome2 The other haplosome of the focal parent

subpop The subpopulation to which the focal parent belongs

breakpoints An integer vector of crossover breakpoints

These may be used in the recombination() callback to determine the final recombination breakpoints used by SLiM.  If values are set into breakpoints, the new values must be of type integer.  If breakpoints is modified by the callback, T should be returned, otherwise F should be returned (this is a speed optimization, so that SLiM does not have to spend time checking for changes when no changes have been made).

The positions specified in breakpoints mean that a crossover will occur immediately before the specified base position (between the preceding base and the specified base, in other words).  The haplosome specified by haplosome1 will be used as the initial copy strand when SLiM executes the recombination; this cannot presently be changed by the callback.  (Note that haplosome1 and haplosome2 will be haplosomes from individual, but their order may be swapped, depending on which is the initial copy strand!)

In this design, the recombination callback does not specify a custom recombination map.  Instead, the callback can add or remove breakpoints at specific locations.  To implement a chromosomal inversion, for example, if the parent is heterozygous for the inversion mutation then crossovers within the inversion region are removed by the callback.  As another example, to implement a model of the evolution of the overall recombination rate, a model could (1) set the global recombination rate to the highest rate attainable in the simulation, (2) for each individual, within the recombination() callback, calculate the fraction of that maximum rate that the focal individual would experience based upon its genetics, and (3) probabilistically remove proposed crossover points based upon random uniform draws compared to that threshold fraction, thus achieving the individual effective recombination rate desired.  Other similar treatments could actually vary the effective recombination map, not just the overall rate, by removing proposed crossovers with probabilities that depend upon their position, allowing for the evolution of localized recombination hot-spots and cold-spots.  Crossovers may also be added, not just removed, by recombination() callbacks.

In SLiM 3.3 the recombination model in SLiM was redesigned.  This required a corresponding redesign of recombination() callbacks.  In particular, the gcStarts and gcEnds pseudo-parameters to recombination() callbacks were removed.  In the present design, the callback receives “crossover breakpoints” information only, in the breakpoints pseudo-parameter; it receives no information about gene conversion.  However, recombination() callbacks can still be used with the “DSB” recombination model; at the point when the callback is called, the pattern of gene conversion tracts will have been simplified down to a vector of crossover breakpoints.  “Complex” gene conversion tracts, however, involving heteroduplex mismatch repair, are not compatible with recombination() callbacks, since there is presently no way for them to be specified to the callback.

Note that the positions in breakpoints are not, in the general case, guaranteed to be sorted or uniqued; in other words, positions may appear out of order, and the same position may appear more than once.  After all recombination() callbacks have completed, the positions from breakpoints will be sorted, uniqued, and used as the crossover points in generating the prospective gamete haplosome.  The essential point here is that if the same position occurs more than once, across breakpoints, the multiple occurrences of the position do not cancel; SLiM does not cross over and then “cross back over” given a pair of identical positions.  Instead, the multiple occurrences of the position will simply be uniqued down to a single occurrence.

As with the other callback types, multiple recombination() callbacks may be registered and active.  In this case, all registered and active callbacks will be called for each gamete generated, in the order that the callbacks were registered.

5.13.7  ITEM: 8. interaction() callbacks

The InteractionType class provides various built-in interaction functions that translate from distances to interaction strengths.  However, it may sometimes be useful to define a custom function for that purpose; for that reason, SLiM allows interaction() callbacks to be defined that modify the standard interaction strength calculated by InteractionType.  In particular, this mechanism allows the strength of interactions to depend upon not only the distance between individuals, but also the genetics and other state of the individuals, the spatial position of the individuals, and other environmental variables.

An interaction() callback is called by SLiM when it is determining the strength of the interaction between one individual (the receiver of the interaction) and another individual (the exerter of the interaction).  This generally occurs when an interaction query is made to InteractionType, as a side effect of serving that query.  This means that interaction() callbacks may be called at a variety of points in the tick cycle, unlike the other callback types in SLiM, which are each called at a specific point.  If you write an interaction() callback, you need to take this into account; assuming that the tick cycle is at a particular stage, or even that the tick or cycle is the same as it was when evaluate() was called, may be dangerous.

When an interaction strength is needed, the first thing SLiM does is calculate the default interaction strength using the interaction function that has been defined for the InteractionType.  If the receiver is the same as the exerter, the interaction strength is always zero; and in spatial simulations if the distance between the receiver and the exerter is greater than the maximum distance set for the InteractionType, the interaction strength is also always zero.  In these cases, interaction() callbacks will not be called, and there is no way to redefine these interaction strengths.

Otherwise, SLiM will then call interaction() callbacks that apply to the interaction type and exerter subpopulation for the interaction being evaluated.  An interaction() callback is defined with a variation of the syntax used for other callbacks:

[id] [t1 [: t2]] interaction(<int-type-id> [, <subpop-id>]) { ... }

For example, if the callback were defined as:

1000:2000 interaction(i2, p3) { 1.0; }

then an interaction strength of 1.0 would be used for all interactions of interaction type i2, for exerters in subpopulation p3, from tick 1000 to tick 2000.

Beginning in SLiM 4, the receiver and exerter may be in different subpopulations from each other – or even, in multispecies models, in different species altogether.  For the subpopulation id in the interaction() callback declaration, it does not matter which subpopulation the receiver is in; if the exerter is in p3, for the above example, then the interaction() callback will be called regardless of the receiver’s subpopulation (assuming other preconditions are also met, such as the tick range and the interaction type id).  This means that interaction() callbacks are not species-specific, unlike other callback types; even if an interaction() callback is declared to be specific to exerters in p3, as above, receivers can still be in a different species.  With no subpopulation id specified, interaction() callbacks are even more general: the InteractionType can then be evaluated and queried for receivers and exerters belonging to any species.  For this reason, in multispecies models interaction() callbacks must be declared using a species specifier of species all, unlike all other SLiM callback types; it is not legal to declare an interaction() callback as species-specific.  Note that there is no way to declare an interaction() callback as applying only to receivers in a given subpopulation; if that functionality is desired, you can test receiver.subpopulation in your callback code and act accordingly.

In addition to the standard SLiM globals, an interaction() callback is supplied with some additional information passed through “pseudo-parameters”:

distance The distance from receiver to exerter, in spatial simulations; NAN otherwise

strength The default interaction strength calculated by the interaction function

receiver The individual receiving the interaction (an object of class Individual)

exerter The individual exerting the interaction (an object of class Individual)

These may be used in the interaction() callback to compute an interaction strength.  To simply use the default interaction strength that SLiM would use if a callback had not been defined for interaction type i1, for example, you could do this:

interaction(i1) {
return strength;
}

Usually an interaction() callback will modify that default strength based upon factors such as the genetics of the receiver and/or the exerter, the spatial positions of the two individuals, or some other simulation state.  Any finite float value greater than or equal to 0.0 may be returned.  The value returned will be not be cached by SLiM; if the interaction strength between the same two individuals is needed again later, the interaction() callback will be called again (something to keep in mind if the interaction strength includes a stochastic component).  Note that the provided distance and strength values are based upon the spatial positions of the exerter and receiver when evaluate() was called, not their current spatial positions, if they have moved since the interaction was evaluated.

More than one interaction() callback may be defined to operate in the same tick.  As with other callbacks, multiple callbacks will be called in the order in which they were defined in the input file.  Furthermore, each callback will be given the strength value returned by the previous callback – so the value of strength is not necessarily the default value, in fact, but is the result of all previous interaction() callbacks for the interaction in question.  In this way, the effects of multiple callbacks can “stack”.

The interaction() callback mechanism is extremely powerful and flexible, allowing any sort of user-defined interactions whatsoever to be queried dynamically using the methods of InteractionType.  However, in the general case a simulation may call for the evaluation of the interaction strength between each individual and every other individual, making the computation of the full interaction network an O(N2) problem.  Since interaction() callbacks may be called for each of those N2 interaction evaluations, they can slow down a simulation considerably, so it is recommended that they be used sparingly.  This is the reason that the various interaction functions of InteractionType were provided; when an interaction does not depend upon individual state, the intention is to avoid the necessity of an interaction() callback altogether.  Furthermore, constraining the number of cases in which interaction strengths need to be calculated – using a short maximum interaction distance, querying the nearest neighbors of the focal individual rather than querying all possible interactions with that individual, and specifying the reciprocality and sex segregation of the InteractionType, for example – may greatly decrease the computational overhead of interaction evaluation.

5.13.8  ITEM: 9. reproduction() callbacks

In WF models (the default model type in SLiM), the SLiM core manages the reproduction of individuals in each tick.  In nonWF models, however, reproduction is managed by the model script, in reproduction() callbacks.  These callbacks may only be defined in nonWF models.

A reproduction() callback is defined with a syntax much like that of other callbacks:

[id] [t1 [: t2]] reproduction([<subpop-id> [, <sex>]]) { ... }

The reproduction() callback will be called once for each individual during the tick(s) in which it is active.  It may optionally be restricted to apply only to individuals in a specified subpopulation, using the <subpop-id> specifier; this may be a subpopulation specifier such as p1, or NULL indicating no restriction.  It may also optionally be restricted to apply only to individuals of a specified sex (in sexual models), using the <sex> specifier; this may be "M" or "F", or NULL indicating no restriction.  (In multispecies models, the definition must be preceded by a species specification as usual.)

When a reproduction() callback is called, SLiM’s expectation is that the callback will trigger the reproduction of a focal individual by making method calls to add new offspring individuals.  Typically the offspring added are the offspring of the focal individual, and typically they are added to the subpopulation to which the focal individual belongs, but neither of these is required; a reproduction() callback may add offspring generated by any parent(s), to any subpopulation in the focal species.  The focal individual is provided to the callback (as an Individual object), as is the subpopulation in which it resides.

A common alternative pattern is for a reproduction() callback to ignore the focal individual and generate all of the offspring for a species for the current tick, from all parents.  The callback then sets self.active to 0, preventing itself from being called again in the current tick; this callback design therefore executes once per tick.  This can be useful if individuals influence each other’s offspring generation (as in a monogamous-mating model, for example); it can also simply be more efficient when producing offspring in bulk.

In addition to the usual SLiM globals, then, a reproduction() callback is supplied with additional information passed through global variables:

individual The focal individual that is expected to reproduce

subpop The subpopulation to which the focal individual belongs

At present, the return value from reproduction() callbacks is not used, and must be void (i.e., a value may not be returned).  It is possible that other return values will be defined in future.

It is possible, of course, to do actions unrelated to reproduction inside reproduction() callbacks, but it is not recommended.  The first() event phase of the current tick provides an opportunity for actions immediately before reproduction, and the early() event phase of the current tick provides an opportunity for actions immediately after reproduction, so only actions that are intertwined with reproduction itself should occur in reproduction() callbacks.  Besides providing conceptual clarity, following this design principle will also decrease the probability of bugs, since actions that are unrelated to reproduction should usually not influence or be influenced by the dynamics of reproduction.

If the randomizeCallbacks parameter to initializeSLiMOptions() is T (the default), the order in which individuals are given an opportunity to reproduce with a call to reproduction() callbacks will be randomized within each subpopulation.  This partially mitigates order-dependency issues, although such issues can still arise whenever the effects of a reproduction() callback are not independent.  If randomizeCallbacks is F, individuals will be given their opportunity to reproduce in sequential order within each subpopulation, greatly increasing the risk of order-dependency problems.

As with the other callback types, multiple reproduction() callbacks may be registered and active.  In this case, all registered and active callbacks will be called for each individual, in the order that the callbacks were registered.

5.13.9  ITEM: 10. mutation() callbacks

SLiM auto-generates new mutations according to the current mutation rate (or rate map) and the genetic structure defined by genomic elements, their genomic element types, the mutation types those genomic element types draw from, and the distribution of fitness effects defined by those mutation types.  In nucleotide-based models, the nucleotide sequence and the mutation matrix also play a role in determining both the rate of mutation and the nucleotide mutated to.  In some models it can be desirable to modify these dynamics in some way – altering the selection coefficients of new mutations in some way, changing the mutation type used, dictating the nucleotide to be used, replacing the proposed mutation with a pre-existing mutation at the same position, or even suppressing the proposed mutation altogether.  To achieve this, one may define a mutation() callback.

A mutation() callback is defined as:

[id] [t1 [: t2]] mutation([<mut-type-id> [, <subpop-id>]]) { ... }

The mutation() callback will be called once for each new auto-generated mutation during the tick(s) in which the callback is active.  It may optionally be restricted to apply only to mutations of a particular mutation type, using the <mut-type-id> specifier; this may be a mutation type specifier such as m1, or NULL indicating no restriction.  It may also optionally be restricted to individuals generated by a specified subpopulation (usually – see below for discussion), using the <subpop-id> specifier; this should be a subpopulation specifier such as p1.  (In multispecies models, the definition must be preceded by a species specification as usual.)

When a mutation() callback is called, a focal mutation (provided to the callback as an object of type Mutation) has just been created by SLiM, referencing a particular position in a parental haplosome (also provided, as an object of type Haplosome).  The mutation will not be added to that parental haplosome; rather, the parental haplosome is being copied, during reproduction, to make a gamete or an offspring haplosome, and the mutation is, conceptually, a copying error made during that process.  It will be added to the offspring haplosome that is the end result of the copying process (which may also involve recombination with another haplosome).  At the point that the mutation() callback is called, the offspring haplosome is not yet created, however, and so it cannot be accessed from within the mutation() callback; the mutation() callback can affect only the mutation itself, not the haplosome to which the mutation will be added.

In addition to the standard SLiM globals, then, a mutation() callback is supplied with additional information passed through global variables:

mut The focal mutation that is being modified or reviewed

haplosome The parental haplosome that is being copied

element The genomic element that controls the mutation site

originalNuc The nucleotide (0/1/2/3 for A/C/G/T) originally at the mutating position

parent The parent which is generating the offspring haplosome

subpop The subpopulation to which the parent belongs

The mutation() callback has three possible returns: T, F, or (beginning in SLiM 3.5) a singleton object of type Mutation.  A return of T indicates that the proposed mutation should be used in generating the offspring haplosome (perhaps with modifications made by the callback).  Conversely, a return of F indicates that the proposed mutation should be suppressed.  If a proposed mutation is suppressed, SLiM will not try again; one fewer mutations will be generated during reproduction than would otherwise have been true.  Returning F will therefore mean that the realized mutation rate in the model will be lower than the expected mutation rate.  Finally, a return of an object of type Mutation replaces the proposed mutation (mut) with the mutation returned; the offspring haplosomes being generated will contain the returned mutation.  The position of the returned mutation must match that of the proposed mutation.  This provides a mechanism for a mutation() callback to make SLiM re-use existing mutations instead of generating new mutations, which can be useful.

The callback may perform a variety of actions related to the generated mutation.  The selection coefficient of the mutation can be changed with setSelectionCoefficient(), and the mutation type of the mutation can be changed with setMutationType(); the drawSelectionCoefficient() method of MutationType may also be useful here.  A tag property value may be set for the mutation, and named values may be attached to the mutation with setValue().  In nucleotide-based models, the nucleotide (or nucleotideValue) property of the mutation may also be changed; note that the original nucleotide at the focal position in the parental haplosome is provided through originalNuc (it could be retrieved with haplosome.nucleotides(), but SLiM already has it at hand anyway).  All of these modifications to the new mutation may be based upon the state of the parent, including its genetic state, or upon any other model state.

It is possible, of course, to do actions unrelated to mutation inside mutation() callbacks, but it is not recommended; first(), early(), and late() events should be used for general-purpose scripting.  Besides providing conceptual clarity, following this design principle will also decrease the probability of bugs, since actions that are unrelated to mutation should not influence or be influenced by the dynamics of mutation.

The proposed mutation will not appear in the sim.mutations vector of segregating mutations until it has been added to a haplosome; it will therefore not be visible in that vector within its own mutation() callback invocation, and indeed, may not be visible in subsequent callbacks during the reproduction tick cycle stage until such time as the offspring individual being generated has been completed.  If that offspring is ultimately rejected, in particular by a modifyChild() callback, the proposed mutation may not be used by SLiM at all.  It may therefore be unwise to assume, in a mutation() callback, that the focal mutation will ultimately be added to the simulation, depending upon the rest of the model’s script.

There is one subtlety to be mentioned here, having to do with subpopulations.  The subpop pseudo-parameter discussed above is always the subpopulation of the parent which possesses the haplosome that is being copied and is mutating; there is no ambiguity about that whatsoever.  The <subpop-id> specified in the mutation() callback declaration, however, is a bit more subtle; above it was said that it restricts the callback “to individuals generated by a specified subpopulation”, and that is usually true but requires some explanation.  In WF models, recall that migrants are generated in a source subpopulation and placed in a target subpopulation, as a model of juvenile migration; in that context, the <subpop-id> specifies the source subpopulation to which the mutation() callback will be restricted.  In nonWF models, offspring are generated by the add...() family of Subpopulation methods, which can cross individuals from two different subpopulations and place the result in a third target subpopulation; in that context, in general, the <subpop-id> specifies the source subpopulation that is generating the particular gamete that is sustaining a mutation during its production.  The exception to this rule is addRecombinant() and addMultiRecombinant(); since there are four different source subpopulations potentially in play there per mutation, it was deemed simpler in that case for the <subpop-id> to specify the target subpopulation to which the mutation() callback will be restricted.  If restriction to the source subpopulation is needed with addRecombinant() or addMultiRecombinant(), the subpop pseudo-parameter may be consulted rather than using <subpop-id>.

Note that mutation() callbacks are only called for mutations that are auto-generated by SLiM, as a consequence of the mutation rate and the genetic structure defined.  Mutations that are created in script, using addNewMutation() or addNewDrawnMutation(), will not trigger mutation() callbacks; but of course the script may modify or tailor such added mutations in whatever way is desired, so there is no need for callbacks in that situation.

As with the other callback types, multiple mutation() callbacks may be registered and active.  In this case, all registered and active callbacks will be called for each generated mutation to which they apply, in the order that the callbacks were registered.

5.13.10  ITEM: 11. survival() callbacks

In nonWF models, a selection phase in the tick cycle results in mortality; individuals survive or die based upon their fitness.  In most cases this standard behavior is sufficient; but occasionally it can be useful to observe the survival decisions SLiM makes (to log out information about dying individuals, for example), to modify those decisions (influencing which individuals live and which die, perhaps based upon factors other than genetics), or even to short-circuit mortality completely (moving dead individuals into a “cold storage” subpopulation for later use, perhaps).  To accomplish such goals, one can the survival() callback mechanism to override SLiM’s default behavior.  Note that in WF models, since they always model non-overlapping generations, the entire parental generation dies in each tick regardless of fitness; survival() callbacks therefore apply only to nonWF models.

A survival() callback is defined with a syntax much like that of other callbacks:

[id] [t1 [: t2]] survival([<subpop-id>]) { ... }

The survival() callback will be called during the selection phase of the tick cycle of nonWF models, during the tick(s) in which it is active.  By default it will be called once per individual in the entire population (whether slated for survival or not); it may optionally be restricted to apply only to individuals in a specified subpopulation, using the <subpop-id> specifier.  (In multispecies models, the definition must be preceded by a species specification as usual.)

When a survival() callback is called, a focal individual has already been evaluated by SLiM regarding its survival; a final fitness value for the individual has been calculated, and a random uniform draw in [0,1] has been generated that determines whether the individual is to survive (a draw less than the individual’s fitness) or die (a draw greater than or equal to the individual’s fitness).  The focal individual is provided to the callback, as is the subpopulation in which it resides.  Furthermore, the preliminary decision (whether the focal individual will survive or not), the focal individual’s fitness, and the random draw made by SLiM to determine survival are also provided to the callback.  The callback may return NULL to accept SLiM’s decision, or may return T to indicate that the individual should survive, or F to indicate that it should die, regardless of its fitness and the random deviate drawn.  The callback may also return a singleton Subpopulation object to indicate the individual should remain alive but should be moved to that subpopulation (note that calling takeMigrants() during the survival phase is illegal, because SLiM is busy modifying the population’s internal state).

In addition to the standard SLiM globals, then, a survival() callback is supplied with additional information passed through “pseudo-parameters”:

individual The focal individual that will live or die

subpop The subpopulation to which the focal individual belongs

surviving A logical value indicating SLiM’s preliminary decision (T == survival)

fitness The focal individual’s fitness

draw SLiM’s random uniform deviate, which determined the preliminary decision

These may be used in the survival() callback to determine the final decision.

While survival() callbacks are still being called, no decisions are put into effect; no individuals actually die, and none are moved to a new Subpopulation if that was requested.  In effect, SLiM pre-plans the fate of every individual completely without modifying the model state at all.  After all survival() callbacks have completed for every individual, the planned fates for every individual will then be executed, without any opportunity for further intervention through callbacks.  It is therefore legal to inspect subpopulations and individuals inside a survival() callback, but it should be understood that previously made decisions about the fates of other individuals will not yet have any visible effect.  It is generally a good idea for the decisions rendered by survival() callbacks to be independent anyway, to avoid biases due to order-dependency.  If the randomizeCallbacks parameter to initializeSLiMOptions() is T (the default), the order in which survival() callbacks are called on individuals will be randomized within each subpopulation; nevertheless, order-dependency issues can occur if callback effects are not independent.  If randomizeCallbacks is F, the order in which individuals are evaluated within each subpopulation is not guaranteed to be random, and order-dependency problems are thus even more likely.

It is worth noting that if survival() callbacks are used, “fitness” in the model is then no longer really fitness; the model is making its own decisions about which individuals live and die, and those decisions are the true determinant of fitness in the biological sense.  A survival() callback that makes its own decisions regarding survival with no regard for SLiM’s calculated fitness values can completely alter the pattern of selection in a population, rendering all of SLiM’s fitness machinery – selection and dominance coefficients, fitnessScaling values, etc. – completely irrelevant.  To avoid highly counterintuitive and confusing effects, it is thus generally a good idea to use of survival() callbacks only when it is strictly necessary to achieve a desired outcome.

As with the other callback types, multiple survival() callbacks may be registered and active.  In this case, all registered and active callbacks will be called for each individual evaluated, in the order that the callbacks were registered.


================================================ FILE: QtSLiM/help/SLiMHelpClasses.html ================================================

5.2  Class Chromosome

5.2.1  Chromosome properties

colorSubstitution <–> (string$)

The color used to display substitutions in SLiMgui when both mutations and substitutions are being displayed in the chromosome view.  Outside of SLiMgui, this property still exists, but is not used by SLiM.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB".  If colorSubstitution is the empty string, "", SLiMgui will defer to the color scheme of each MutationType, just as it does when only substitutions are being displayed.  The default, "3333FF", causes all substitutions to be shown as dark blue when displayed in conjunction with mutations, to prevent the view from becoming too noisy.  Note that when substitutions are displayed without mutations also being displayed, this value is ignored by SLiMgui and the substitutions use the color scheme of each MutationType.

geneConversionEnabled => (logical$)

When gene conversion has been enabled by calling initializeGeneConversion(), switching to the DSB recombination model, this property is T; otherwise, when using the crossover breakpoints model, it is F.

geneConversionGCBias => (float$)

The gene conversion bias coefficient, which expresses a bias in the resolution of heteroduplex mismatches in complex gene conversion tracts.  When gene conversion has not been enabled by calling initializeGeneConversion(), this property will be unavailable.

geneConversionNonCrossoverFraction => (float$)

The fraction of double-stranded breaks that result in non-crossover events.  When gene conversion has not been enabled by calling initializeGeneConversion(), this property will be unavailable.

geneConversionMeanLength => (float$)

The mean length of a gene conversion tract (in base positions).  When gene conversion has not been enabled by calling initializeGeneConversion(), this property will be unavailable.

geneConversionSimpleConversionFraction => (float$)

The fraction of gene conversion tracts that are “simple” (i.e., not involving resolution of heteroduplex mismatches); the remainder will be “complex”.  When gene conversion has not been enabled by calling initializeGeneConversion(), this property will be unavailable.

genomicElements => (object<GenomicElement>)

All of the GenomicElement objects that comprise the chromosome, in sorted order (not necessarily in the order in which they were defined).

hotspotEndPositions => (integer)

The end positions for hotspot map regions along the chromosome.  Each hotspot map region is assumed to start at the position following the end of the previous hotspot map region; in other words, the regions are assumed to be contiguous.  When using sex-specific hotspot maps, this property will unavailable; see hotspotEndPositionsF and hotspotEndPositionsM.

hotspotEndPositionsF => (integer)

The end positions for hotspot map regions for females, when using sex-specific hotspot maps; unavailable otherwise.  See hotspotEndPositions for further explanation.

hotspotEndPositionsM => (integer)

The end positions for hotspot map regions for males, when using sex-specific hotspot maps; unavailable otherwise.  See hotspotEndPositions for further explanation.

hotspotMultipliers => (float)

The hotspot multiplier for each of the hotspot map regions specified by hotspotEndPositions.  When using sex-specific hotspot maps, this property will be unavailable; see hotspotMultipliersF and hotspotMultipliersM.

hotspotMultipliersF => (float)

The hotspot multiplier for each of the hotspot map regions specified by hotspotEndPositionsF, when using sex-specific hotspot maps; unavailable otherwise.

hotspotMultipliersM => (float)

The hotspot multiplier for each of the hotspot map regions specified by hotspotEndPositionsM, when using sex-specific hotspot maps; unavailable otherwise.

id => (integer$)

The id for the chromosome, as given to initializeChromosome().  For an implicitly defined chromosome, the id will be 1.  The id can be used to refer to the chromosome; see also symbol.

intrinsicPloidy => (integer$)

The intrinsic ploidy of the chromosome, meaning the number of haplosome objects that are allocated in each individual, associated with the chromosome (even if some of those haplosomes are null haplosomes acting as placeholders).  This is a consequence of the chromosome’s type.  Chromosome types "A", "X", and "Z" are intrinsically diploid (and thus this property would have the value 2), as are the backwards-compatibility chromosome types "H-" and "-Y".  All other chromosome types are intrinsically haploid (and thus this property would have the value 1).

isSexChromosome => (logical$)

Indicates whether the chromosome is a sex chromosome (T) or not (F).  This is a consequence of the chromosome’s type.  Chromosome types "X", "Y", "Z", and "W" are considered sex chromosomes, as is the backwards-compatibility type "-Y"; all other chromosome types are not.  See also the sexChromosomes property of Species.

lastPosition => (integer$)

The last valid position in the chromosome; equal to length-1, where length is the length as given to initializeChromosome().  For an implicitly defined chromosome, the chromosome’s last position is determined by the maximum of the end of the last genomic element, the end of the last recombination region, and the end of the last mutation map region (or hotspot map region).  See also length.

length => (integer$)

The length of the chromosome (meaning the number of valid base positions it contains), as given to initializeChromosome().  The length is simply equal to the last position plus 1, since the chromosome always starts at 0.  See also lastPosition.

mutationEndPositions => (integer)

The end positions for mutation rate regions along the chromosome.  Each mutation rate region is assumed to start at the position following the end of the previous mutation rate region; in other words, the regions are assumed to be contiguous.  When using sex-specific mutation rate maps, this property will unavailable; see mutationEndPositionsF and mutationEndPositionsM.

This property is unavailable in nucleotide-based models.

mutationEndPositionsF => (integer)

The end positions for mutation rate regions for females, when using sex-specific mutation rate maps; unavailable otherwise.  See mutationEndPositions for further explanation.

This property is unavailable in nucleotide-based models.

mutationEndPositionsM => (integer)

The end positions for mutation rate regions for males, when using sex-specific mutation rate maps; unavailable otherwise.  See mutationEndPositions for further explanation.

This property is unavailable in nucleotide-based models.

mutationRates => (float)

The mutation rate for each of the mutation rate regions specified by mutationEndPositions.  When using sex-specific mutation rate maps, this property will be unavailable; see mutationRatesF and mutationRatesM.

This property is unavailable in nucleotide-based models.

mutationRatesF => (float)

The mutation rate for each of the mutation rate regions specified by mutationEndPositionsF, when using sex-specific mutation rate maps; unavailable otherwise.

This property is unavailable in nucleotide-based models.

mutationRatesM => (float)

The mutation rate for each of the mutation rate regions specified by mutationEndPositionsM, when using sex-specific mutation rate maps; unavailable otherwise.

This property is unavailable in nucleotide-based models.

name <–> (string$)

The name of the chromosome, as given to initializeChromosome().  The chromosome name is not used by SLiM, and may be whatever you wish.

overallMutationRate => (float$)

The overall mutation rate across the whole chromosome determining the overall number of mutation events that will occur anywhere in the chromosome, as calculated from the individual mutation ranges and rates as well as the coverage of the chromosome by genomic elements (since mutations are only generated within genomic elements, regardless of the mutation rate map).  When using sex-specific mutation rate maps, this property will unavailable; see overallMutationRateF and overallMutationRateM.

This property is unavailable in nucleotide-based models.

overallMutationRateF => (float$)

The overall mutation rate for females, when using sex-specific mutation rate maps; unavailable otherwise.  See overallMutationRate for further explanation.

This property is unavailable in nucleotide-based models.

overallMutationRateM => (float$)

The overall mutation rate for males, when using sex-specific mutation rate maps; unavailable otherwise.  See overallMutationRate for further explanation.

This property is unavailable in nucleotide-based models.

overallRecombinationRate => (float$)

The overall recombination rate across the whole chromosome determining the overall number of recombination events that will occur anywhere in the chromosome, as calculated from the individual recombination ranges and rates.  When using sex-specific recombination maps, this property will unavailable; see overallRecombinationRateF and overallRecombinationRateM.

overallRecombinationRateF => (float$)

The overall recombination rate for females, when using sex-specific recombination maps; unavailable otherwise.  See overallRecombinationRate for further explanation.

overallRecombinationRateM => (float$)

The overall recombination rate for males, when using sex-specific recombination maps; unavailable otherwise.  See overallRecombinationRate for further explanation.

recombinationEndPositions => (integer)

The end positions for recombination regions along the chromosome.  Each recombination region is assumed to start at the position following the end of the previous recombination region; in other words, the regions are assumed to be contiguous.  When using sex-specific recombination maps, this property will unavailable; see recombinationEndPositionsF and recombinationEndPositionsM.

recombinationEndPositionsF => (integer)

The end positions for recombination regions for females, when using sex-specific recombination maps; unavailable otherwise.  See recombinationEndPositions for further explanation.

recombinationEndPositionsM => (integer)

The end positions for recombination regions for males, when using sex-specific recombination maps; unavailable otherwise.  See recombinationEndPositions for further explanation.

recombinationRates => (float)

The recombination rate for each of the recombination regions specified by recombinationEndPositions.  When using sex-specific recombination maps, this property will unavailable; see recombinationRatesF and recombinationRatesM.

recombinationRatesF => (float)

The recombination rate for each of the recombination regions specified by recombinationEndPositionsF, when using sex-specific recombination maps; unavailable otherwise.

recombinationRatesM => (float)

The recombination rate for each of the recombination regions specified by recombinationEndPositionsM, when using sex-specific recombination maps; unavailable otherwise.

species => (object<Species>$)

The species to which the target object belongs.

symbol => (string$)

The symbol for the chromosome, as given to initializeChromosome(); see the documentation for that function.  For an implicitly defined chromosome, the symbol is "A" for non-sexual models, and for sexual models (for historical reasons) is determined by the model type passed to initializeSex() as documented there.  The symbol can be used to refer to the chromosome; see also id.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.

type => (string$)

The type of the chromosome, as given to initializeChromosome(); see the documentation for that function for a list of the supported chromosome types.  For an implicitly defined chromosome, the type is "A" for non-sexual models, and for sexual models (for historical reasons) is determined by the model type passed to initializeSex() as documented there.

5.2.2  Chromosome methods

– (is)ancestralNucleotides([Ni$ start = NULL], [Ni$ end = NULL], [string$ format = "string"])

Returns the ancestral nucleotide sequence originally supplied to initializeAncestralNucleotides(), including any sequence changes due to nucleotide mutations that have fixed and substituted.  This nucleotide sequence is the reference sequence for positions in a haplosome that do not contain a nucleotide-based mutation.  The range of the returned sequence may be constrained by a start position given in start and/or an end position given in end; nucleotides will be returned from start to end, inclusive.  The default value of NULL for start and end represent the first and last base positions of the chromosome, respectively.

The format of the returned sequence is controlled by the format parameter.  A format of "string" will return the sequence as a singleton string (e.g., "TATA").  A format of "char" will return a string vector with one element per nucleotide (e.g., "T", "A", "T", "A").  A format of "integer" will return an integer vector with values A=0, C=1, G=2, T=3 (e.g., 3, 0, 3, 0).  If the sequence returned is likely to be long, the "string" format will be the most memory-efficient, and may also be the fastest (but may be harder to work with).

For purposes related to interpreting the nucleotide sequence as a coding sequence, a format of "codon" is also supported.  This format will return an integer vector with values from 0 to 63, based upon successive nucleotide triplets in the sequence (which, for this format, must have a length that is a multiple of three).  The codon value for a given nucleotide triplet XYZ is 16X + 4Y + Z, where X, Y, and Z have the usual values A=0, C=1, G=2, T=3.  For example, the triplet AAA has a codon value of 0, AAC is 1, AAG is 2, AAT is 3, ACA is 4, and on upward to TTT which is 63.  If the nucleotide sequence AACACATTT is requested in codon format, the codon vector 1 4 63 will therefore be returned.  These codon values can be useful in themselves; they can also be passed to codonToAminoAcid() to translate them into the corresponding amino acid sequence if desired.

– (integer)drawBreakpoints([No<Individual>$ parent = NULL], [Ni$ n = NULL])

Draw recombination breakpoints, using the chromosome’s recombination rate map, the current gene conversion parameters, and (in some cases – see below) any active and applicable recombination() callbacks.  The number of breakpoints to generate, n, may be supplied; if it is NULL (the default), the number of breakpoints will be drawn based upon the overall recombination rate and the chromosome length (following the standard procedure in SLiM).  Note that if the double-stranded breaks model has been chosen, the number of breakpoints generated will probably not be equal to the number requested, because most breakpoints will entail gene conversion tracts, which entail additional crossover breakpoints.

It is generally recommended that the parent individual be supplied to this method, but parent is NULL by default.  The individual supplied in parent is used for two purposes.  First, in sexual models that define separate recombination rate maps for males versus females, the sex of parent will be used to determine which map is used; in this case, a non-NULL value must be supplied for parent, since the choice of recombination rate map must be determined.  Second, in models that define recombination() callbacks, parent is used to determine the various pseudo-parameters that are passed to recombination() callbacks (individual, haplosome1, haplosome2, subpop), and the subpopulation to which parent belongs is used to select which recombination() callbacks are applicable; given the necessity of this information, recombination() callbacks will not be called as a side effect of this method if parent is NULL.  Apart from these two uses, parent is not used, and the caller does not guarantee that the generated breakpoints will actually be used to recombine the haplosomes of parent in particular.  If a recombination() callback is called, haplosome1 for that callback will always be the first haplosome of parent for the chromosome; in other words, drawBreakpoints() will always treat the first haplosome of a homologous pair as the initial copy strand.  If the caller wishes to randomly choose an initial copy strand (which is usually desirable), they should do that themselves (note that the addRecombinant() and addMultiRecombinant() methods have a flag to facilitate this).

– (object<GenomicElement>)genomicElementForPosition(integer positions)

Returns a vector of GenomicElement objects corresponding to the given vector positions, which contains base positions along the chromosome.  If every position lies within a defined genomic element, the returned vector will have the same length as positions, and will correspond one-to-one with it.  However, if a position in positions is not within a genomic element, no GenomicElement object will be present for it in the returned vector, and so the returned vector will no longer have the same length as positions, and will no longer correspond one-to-one with it.  The method hasGenomicElementForPosition() can be used to detect this circumstance.

– (logical)hasGenomicElementForPosition(integer positions)

Returns a logical vector corresponding to the given vector positions, which contains base positions along the chromosome.  The returned vector will have the same length as positions, and will correspond one-to-one with it, containing T if the corresponding position lies inside a genomic element, or F if it does not.  The method genomicElementForPosition() can be used to look up the GenomicElement objects themselves.

– (integer$)setAncestralNucleotides(is sequence)

This method, which may be called only in nucleotide-based models, replaces the ancestral nucleotide sequence for the model.  The sequence parameter is interpreted exactly as it is in the initializeAncestralSequence() function; see that documentation for details.  The length of the ancestral sequence is returned.

It is unusual to replace the ancestral sequence in a running simulation, since the nucleotide states of segregating and fixed mutations will depend upon the original ancestral sequence.  It can be useful when loading a new population state with readHaplosomesFromMS() or readHaplosomesFromVCF(), such as when resetting the simulation state to an earlier state in a conditional simulation; however, that is more commonly done using readFromPopulationFile() with a SLiM or .trees file.

– (void)setGeneConversion(numeric$ nonCrossoverFraction, numeric$ meanLength, numeric$ simpleConversionFraction, [numeric$ bias = 0])

This method switches the recombination model to the “double-stranded break (DSB)” model (if it is not already set to that), and configures the details of the gene conversion tracts that will therefore be modeled.  The meanings and effects of the parameters exactly mirror the initializeGeneConversion() function.

– (void)setHotspotMap(numeric multipliers, [Ni ends = NULL], [string$ sex = "*"])

In nucleotide-based models, set the mutation rate multiplier along the chromosome.  There are two ways to call this method.  If the optional ends parameter is NULL (the default), then multipliers must be a singleton value that specifies a single multiplier to be used along the entire chromosome.  If, on the other hand, ends is supplied, then multipliers and ends must be the same length, and the values in ends must be specified in ascending order.  In that case, multipliers and ends taken together specify the multipliers to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in ends should extend to the end of the chromosome (as previously determined, during simulation initialization).  See the initializeHotspotMap() function for further discussion of precisely how these multipliers and positions are interpreted.

If the optional sex parameter is "*" (the default), then the supplied hotspot map will be used for both sexes (which is the only option for hermaphroditic simulations).  In sexual simulations sex may be "M" or "F" instead, in which case the supplied hotspot map is used only for that sex.  Note that whether sex-specific hotspot maps will be used is set by the way that the simulation is initially configured with initializeHotspot(), and cannot be changed with this method; so if the simulation was set up to use sex-specific hotspot maps then sex must be "M" or "F" here, whereas if it was set up not to, then sex must be "*" or unsupplied here.  If a simulation needs sex-specific hotspot maps only some of the time, the male and female maps can simply be set to be identical the rest of the time.

The hotspot map is normally constant in simulations, so be sure you know what you are doing.

– (void)setMutationRate(numeric rates, [Ni ends = NULL], [string$ sex = "*"])

Set the mutation rate per base position per gamete.  There are two ways to call this method.  If the optional ends parameter is NULL (the default), then rates must be a singleton value that specifies a single mutation rate to be used along the entire chromosome.  If, on the other hand, ends is supplied, then rates and ends must be the same length, and the values in ends must be specified in ascending order.  In that case, rates and ends taken together specify the mutation rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in ends should extend to the end of the chromosome (as previously determined, during simulation initialization).  See the initializeMutationRate() function for further discussion of precisely how these rates and positions are interpreted.

If the optional sex parameter is "*" (the default), then the supplied mutation rate map will be used for both sexes (which is the only option for hermaphroditic simulations).  In sexual simulations sex may be "M" or "F" instead, in which case the supplied mutation rate map is used only for that sex.  Note that whether sex-specific mutation rate maps will be used is set by the way that the simulation is initially configured with initializeMutationRate(), and cannot be changed with this method; so if the simulation was set up to use sex-specific mutation rate maps then sex must be "M" or "F" here, whereas if it was set up not to, then sex must be "*" or unsupplied here.  If a simulation needs sex-specific mutation rate maps only some of the time, the male and female maps can simply be set to be identical the rest of the time.

The mutation rate intervals are normally a constant in simulations, so be sure you know what you are doing.

In nucleotide-based models, setMutationRate() may not be called.  If variation in the mutation rate along the chromosome is desired, setHotspotMap() should be used.

– (void)setRecombinationRate(numeric rates, [Ni ends = NULL], [string$ sex = "*"])

Set the recombination rate per base position per gamete.  All rates must be in the interval [0.0, 0.5].  There are two ways to call this method.  If the optional ends parameter is NULL (the default), then rates must be a singleton value that specifies a single recombination rate to be used along the entire chromosome.  If, on the other hand, ends is supplied, then rates and ends must be the same length, and the values in ends must be specified in ascending order.  In that case, rates and ends taken together specify the recombination rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in ends should extend to the end of the chromosome (as previously determined, during simulation initialization).  See the initializeRecombinationRate() function for further discussion of precisely how these rates and positions are interpreted.

If the optional sex parameter is "*" (the default), then the supplied recombination rate map will be used for both sexes (which is the only option for hermaphroditic simulations).  In sexual simulations sex may be "M" or "F" instead, in which case the supplied recombination map is used only for that sex.  Note that whether sex-specific recombination maps will be used is set by the way that the simulation is initially configured with initializeRecombinationRate(), and cannot be changed with this method; so if the simulation was set up to use sex-specific recombination maps then sex must be "M" or "F" here, whereas if it was set up not to, then sex must be "*" or unsupplied here.  If a simulation needs sex-specific recombination maps only some of the time, the male and female maps can simply be set to be identical the rest of the time.

The recombination intervals are normally a constant in simulations, so be sure you know what you are doing.

5.3  Class Community

5.3.1  Community properties

allGenomicElementTypes => (object<GenomicElementType>)

All of the GenomicElementType objects defined in the simulation.  These are guaranteed to be in sorted order, by their id property.

allInteractionTypes => (object<InteractionType>)

All of the InteractionType objects defined in the simulation.  These are guaranteed to be in sorted order, by their id property.

allMutationTypes => (object<MutationType>)

All of the MutationType objects defined in the simulation.  These are guaranteed to be in sorted order, by their id property.

allScriptBlocks => (object<SLiMEidosBlock>)

All registered SLiMEidosBlock objects in the simulation.  These are guaranteed to be in sorted order, by their id property.

allSpecies => (object<Species>)

All of the Species objects defined in the simulation (in species declaration order).

allSubpopulations => (object<Subpopulation>)

All of the Subpopulation objects defined in the simulation.

cycleStage => (string$)

The current cycle stage, as a string.  The values of this property essentially mirror the cycle stages of WF and nonWF models.  Common values include "first" (during execution of first() events), "early" (during execution of early() events), "reproduction" (during offspring generation), "fitness" (during fitness evaluation), "survival" (while applying selection and mortality in nonWF models), and "late" (during execution of late() events).

Other possible values include "begin" (during internal setup before each cycle), "tally" (while tallying mutation reference counts and removing fixed mutations), "swap" (while swapping the offspring generation into the parental generation in WF models), "end" (during internal bookkeeping after each cycle), and "console" (during the in-between-ticks state in which commands in SLiMgui’s Eidos console are executed).  It would probably be a good idea not to use this latter set of values; they are probably not user-visible during ordinary model execution anyway.

During execution of initialize() callbacks, no Community object yet exists and so this property cannot be accessed.  To detect this state, use exists("community"); if that is F, community does not exist, and therefore your code is executing during initialize() callbacks (or outside of SLiM entirely, in some other Eidos-based context).

logFiles => (object<LogFile>)

The LogFile objects being used in the simulation.

modelType => (string$)

The type of model being simulated, as specified in initializeSLiMModelType().  This will be "WF" for WF models (Wright-Fisher models, the default), or "nonWF" for nonWF models (non-Wright-Fisher models).  This must be the same for all species in the community; it is therefore a property on Community, not Species.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to the simulation.

tick <–> (integer$)

The current tick number.

verbosity <–> (integer$)

The verbosity level, for SLiM’s logging of information about the simulation.  This is 1 by default, but can be changed at the command line with the -l[ong] option.  It is provided here so that scripts can consult it to govern the level of verbosity of their own output, or set the verbosity level for particular sections of their code.  A verbosity level of 0 suppresses most of SLiM’s optional output; 2 adds some extra output beyond SLiM’s standard output.

5.3.2  Community methods

– (object<LogFile>$)createLogFile(string$ filePath, [Ns initialContents = NULL], [logical$ append = F], [logical$ compress = F], [string$ sep = ","], [Ni$ logInterval = NULL], [Ni$ flushInterval = NULL], [logical$ header = T])

Creates and returns a new LogFile object that logs data from the simulation (see the documentation for the LogFile class for details).  Logged data will be written to the file at filePath, overwriting any existing file at that path by default, or appending to it instead if append is T (successive rows of the log table will always be appended to the previously written content, of course).  Before the header line for the log is written out, any string elements in initialContents will be written first, separated by newlines, allowing for a user-defined file header.  If compress is T, the contents will be compressed with zlib as they are written, and the standard .gz extension for gzip-compressed files will be appended to the filename in filePath if it is not already present.

The sep parameter specifies the separator between data values within a row.  The default of "," will generate a “comma-separated value” (CSV) file, while passing sep="\t" will use a tab separator instead to generate a “tab-separated value” (TSV) file.  Other values for sep may also be used, but are less standard.

LogTable supports periodic automatic logging of a new row of data, enabled by supplying a non-NULL value for logInterval.  In this case, a new row will be logged (as if logRow() were called on the LogFile) at the end of every logInterval ticks (just before the tick counter increments, in both WF and nonWF models), starting at the end of the tick in which the LogFile was created.  A logInterval of 1 will cause automatic logging at the end of every tick, whereas a logInterval of NULL disables automatic logging.  Automatic logging can always be disabled or reconfigured later with the LogFile method setLogInterval(), or logging can be triggered manually by calling logRow().

When compression is enabled, LogFile flushes new data lazily by default, for performance reasons, buffering data for multiple rows before writing to disk.  Passing a non-NULL value for flushInterval requests a flush every flushInterval rows (with a value of 1 providing unbuffered operation).  Note that flushing very frequently will likely result in both lower performance and a larger final file size (in one simple test, 48943 bytes instead of 4280 bytes, or more than a 10× increase in size).  Alternatively, passing a very large value for flushInterval will effectively disable automatic flushing, except at the end of the simulation (but be aware that this may use a large amount of memory for large log files).  In any case, the log file will be created immediately, with its requested initial contents; the initial write is not buffered.  When compression is not enabled, the flushInterval setting is ignored.

The header parameter controls whether a header line is written out at the beginning of logging.  If it is T (the default), a header line is written out; if F, no header is written.  Suppressing the header output can be useful if you are using LogFile to append data to an existing file that already has a header line.

The LogFile documentation discusses how to configure and use LogFile to write out the data you are interested in from your simulation.

– (integer$)estimatedLastTick(void)

Returns SLiM’s current estimate of the last tick in which the model will execute.  Because script blocks can be added, removed, and rescheduled, and because the simulation may end prematurely (due to a call to simulationFinished(), for example), this is only an estimate, and may change over time.

– (void)deregisterScriptBlock(io<SLiMEidosBlock> scriptBlocks)

All SLiMEidosBlock objects specified by scriptBlocks (either with SLiMEidosBlock objects or with integer identifiers) will be scheduled for deregistration.  The deregistered blocks remain valid, and may even still be executed in the current stage of the current tick; the blocks are not actually deregistered and deallocated until sometime after the currently executing script block has completed.  To immediately prevent a script block from executing, even when it is scheduled to execute in the current stage of the current tick, use the active property of the script block.

– (object<GenomicElementType>)genomicElementTypesWithIDs(integer ids)

Find and return the GenomicElementType objects with id values matching the values in ids.  If no matching GenomicElementType object can be found with a given id, an error results.

– (object<InteractionType>)interactionTypesWithIDs(integer ids)

Find and return the InteractionType objects with id values matching the values in ids.  If no matching InteractionType object can be found with a given id, an error results.

– (object<MutationType>)mutationTypesWithIDs(integer ids)

Find and return the MutationType objects with id values matching the values in ids.  If no matching MutationType object can be found with a given id, an error results.

– (void)outputUsage(void)

Output the current memory usage of the simulation to Eidos’s output stream.  The specifics of what is printed, and in what format, should not be relied upon as they may change from version to version of SLiM.  This method is primarily useful for understanding where the memory usage of a simulation predominantly resides, for debugging or optimization.  Note that it does not capture all memory usage by the process; rather, it summarizes the memory usage by SLiM and Eidos in directly allocated objects and buffers.  To get the same memory usage reported by outputUsage(), but as a float$ value, use the Community method usage().  To get the total memory usage of the running process (either current or peak), use the Eidos function usage().

– (object<SLiMEidosBlock>$)registerEarlyEvent(Nis$ id, string$ source, [Ni$ start = NULL], [Ni$ end = NULL], [No<Species>$ ticksSpec = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos early() event in the current simulation, with optional start and end ticks (and, for multispecies models, optional ticks specifier ticksSpec) limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered event is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerFirstEvent(Nis$ id, string$ source, [Ni$ start = NULL], [Ni$ end = NULL], [No<Species>$ ticksSpec = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos first() event in the current simulation, with optional start and end ticks (and, for multispecies models, optional ticks specifier ticksSpec) limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered event is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerInteractionCallback(Nis$ id, string$ source, io<InteractionType>$ intType, [Nio<Subpopulation>$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos interaction() callback in the current simulation (global to the community), with a required interaction type intType (which may be an integer identifier), optional exerter subpopulation subpop (which may also be an integer identifier, or NULL, the default, to indicate all subpopulations), and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it will be eligible to execute the next time an InteractionType is evaluated.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerLateEvent(Nis$ id, string$ source, [Ni$ start = NULL], [Ni$ end = NULL], [No<Species>$ ticksSpec = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos late() event in the current simulation, with optional start and end ticks (and, for multispecies models, optional ticks specifier ticksSpec) limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered event is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)rescheduleScriptBlock(io<SLiMEidosBlock>$ block, [Ni$ start = NULL], [Ni$ end = NULL], [Ni ticks = NULL])

Reschedule the target script block given by block to execute in a specified set of ticks.  The block parameter may be either an integer representing the ID of the desired script block, or a SLiMScriptBlock specified directly.  The target script block, block, is returned.

The first way to specify the tick set is with start and end parameter values; block will then execute from start to end, inclusive.

The second way to specify the tick set is using the ticks parameter, specifying each tick in which the block should execute.  The vector supplied for ticks does not need to be in sorted order, but it must not contain any duplicates.

It can sometimes be better to handle script block scheduling in other ways.  If an early() event needs to execute every tenth tick over the whole duration of a long model run, for example, it might not be advisable to use a call like community.rescheduleScriptBlock(s1, ticks=seq(10, 100000, 10)) for that purpose, since that would make things complicated for SLiM’s scheduler.  Instead, it might be preferable to add a test such as if (community.tick % 10 != 0) return; at the beginning of the event.  It is legal to reschedule a script block while the block is executing; a call like community.rescheduleScriptBlock(self, community.tick + 10, community.tick + 10); made inside a given block would therefore also cause the block to execute every tenth tick, although this sort of self-rescheduling code is probably harder to read, maintain, and debug.

Whichever way of specifying the tick set is used, block may continue to be executed during the current tick cycle stage even after it has been rescheduled, unless it is made inactive using its active property, and similarly, the block may not execute during the current tick cycle stage if it was not already scheduled to do so.  Rescheduling script blocks during the tick and tick cycle stage in which they are executing, or in which they are intended to execute, should be avoided.  Also, note that script blocks which are open-ended (i.e., with no specified end tick), are not used in determining whether the end of the simulation has been reached (because then the simulation would run forever).

Note that new script blocks can also be created and scheduled using the register...() methods of Community and Species; by using the same source as a template script block, the template can be duplicated and scheduled for different ticks, perhaps with modifications or variations.  In multispecies models, note that blocks may not run due to their species or ticks specifier, even in ticks in which they are scheduled to run.

– (object<SLiMEidosBlock>)scriptBlocksWithIDs(integer ids)

Find and return the SLiMEidosBlock objects with id values matching the values in ids.  If no matching SLiMEidosBlock object can be found with a given id, an error results.

– (void)simulationFinished(void)

Declare the current simulation finished.  Normally SLiM ends a simulation when, at the end of a tick, there are no script events or callbacks registered for any future tick (excluding scripts with no declared end tick).  If you wish to end a simulation before this condition is met, a call to simulationFinished() will cause the current simulation to end at the end of the current tick.  For example, a simulation might self-terminate if a test for a dynamic equilibrium condition is satisfied.  Note that the current tick will finish executing; if you want the simulation to stop immediately, you can use the Eidos method stop(), which raises an error condition.

– (object<Species>)speciesWithIDs(integer ids)

Find and return the Species objects with id values matching the values in ids.  If no matching Species object can be found with a given id, an error results.

– (object<Subpopulation>)subpopulationsWithIDs(integer ids)

Find and return the Subpopulation objects with id values matching the values in ids.  If no matching Subpopulation object can be found with a given id, an error results.

– (object<Subpopulation>)subpopulationsWithNames(string names)

Find and return the Subpopulation objects with name values matching the values in names.  If no matching Subpopulation object can be found with a given name, an error results.

– (float$)usage(void)

Return the current memory usage of the simulation.  The specifics of what is totalled up should not be relied upon as it may change from version to version of SLiM.  This method is primarily useful for understanding where the memory usage of a simulation predominantly resides, for debugging or optimization.  Note that it does not capture all memory usage by the process; rather, it summarizes the memory usage by SLiM and Eidos in directly allocated objects and buffers.  To see details of this internal memory usage, use the Community method outputUsage().  To get the total memory usage of the running process (either current or peak), use the Eidos function usage().

5.4  Class Haplosome

5.4.1  Haplosome properties

chromosome => (object<Chromosome>$)

The Chromosome object which this haplosome represents; for example, if this haplosome represents the X sex chromosome in a particular individual, then this property provides the Chromosome object that defines the genetic structure of the X sex chromosome in that individual’s species.

chromosomeSubposition => (integer$)

The position index of the haplosome, within the set of haplosomes associated with the Chromosome object which this haplosome represents.  For example, if an individual in a multi-chromosome model has two haplosomes that represent a given chromosome within its haplosomes vector, the first of those haplosomes will have a chromosomeSubposition value of 0, the second will have a chromosomeSubposition value of 1.  For an intrinsically diploid chromosome in individuals generated by a standard biparental cross, the first haplosome (at subposition 0) came from its first parent (the female parent, in sexual models), and the second haplosome (at subposition 1) came from its second parent (the male parent, in sexual models).

haplosomePedigreeID => (integer$)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T) or tree-sequence recording is turned on with initializeTreeSeq(), haplosomePedigreeID is a “semi-unique” non-negative identifier for each haplosome in a simulation, never re-used throughout the duration of the simulation run.  The haplosomePedigreeID of a given haplosome will be equal to either (2*pedigreeID) or (2*pedigreeID + 1) of the individual that the haplosome belongs to (the former for a first haplosome of the individual, the latter for a second haplosome of the individual if one exists); this invariant relationship is guaranteed.

This value is “semi-unique” in the sense that it is shared by all of the first haplosomes of an individual, or by all of the second haplosomes of an individual.  In a single-chromosome model, a given individual will have just one first haplosome, and perhaps (depending on the chromosome type) one second haplosome, and so the value of haplosomePedigreeID for each of those haplosomes will be truly unique.  In a multi-chromosome model, however, an individual has a first haplosome for each chromosome, and perhaps (depending on the chromosome types) a second haplosome for each chromosome.  In that case, the value of haplosomePedigreeID is unique in the sense that it is different for each individual, but it is not unique in the sense that it will be shared by other haplosomes within the same individual – shared by all the first haplosomes, or shared by all the second haplosomes.  This “semi-uniqueness” is intentional; it allows haplosomePedigreeID to be used as a “key” that associates the haplosomes of an individual across disparate datasets, such as across the different tree sequences for each chromosome that are produced by tree-sequence recording in a multi-chromosome model.  See sections 1.5.1 and 8.3 for further discussion of multi-chromosome models.

If neither pedigree tracking nor tree-sequence recording is enabled, this property is unavailable.

individual => (object<Individual>$)

The Individual object to which this haplosome belongs.

isNullHaplosome => (logical$)

T if the haplosome is a “null” haplosome, F if it is an ordinary haplosome object.  Null haplosomes are used as placeholders when a real haplosome doesn’t exist in a particular slot, allowing SLiM’s code to operate in the same way across a wide variety of genomic configurations.  For example, when simulating chromosome type "X" (an X chromosome), the second haplosome for that chromosome in males will be a null haplosome, preserving a constant number of haplosomes for all individuals even though males have one X while females have two.  Null haplosomes should not be accessed or manipulated.

mutationCount => (integer$)

The number of Mutation objects present in this haplosome.  This property is provided as a convenience, and for efficiency; the result of haplosome.mutationCount will be equal to size(haplosome.mutations).

mutations => (object<Mutation>)

All of the Mutation objects present in this haplosome.  If you only need the number of mutations present, use the mutationCount property.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  Note that the Haplosome objects used by SLiM are new with every new individual, so the tag value of each new offspring haplosome generated in each tick will be initially undefined.

5.4.2  Haplosome methods

+ (void)addMutations(object<Mutation> mutations)

Add the existing mutations in mutations to the target haplosomes, if they are not already present (if they are already present, they will be ignored), and if the addition is not prevented by the mutation stacking policy (see the mutationStackPolicy property of MutationType).  All target haplosomes and all mutations in mutations must be associated with the same Chromosome object; attempting to add a mutation to a haplosome associated with a different chromosome will raise an error.

Calling this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

Note that in nonWF models that use tree-sequence recording, mutations cannot be added to an individual after the tick in which the individual is created (i.e., when the age of the individual is greater than 0), to prevent the possibility of inconsistencies in the recorded tree sequence.

+ (object<Mutation>)addNewDrawnMutation(io<MutationType> mutationType, integer position, [Nio<Subpopulation> originSubpop = NULL], [Nis nucleotide = NULL])

Add new mutations to the target haplosomes with the specified mutationType (specified by the MutationType object or by integer identifier), position, originTick (which may be NULL, the default, to specify the current tick; otherwise, beginning in SLiM 3.5, it must be equal to the current tick anyway, as other uses of this property have been deprecated), and originSubpop (specified by the Subpopulation object or by integer identifier, or by NULL, the default, to specify the subpopulation to which the first target haplosome belongs).  If originSubpop is supplied as an integer, it is intentionally not checked for validity; you may use arbitrary values of originSubpop to “tag” the mutations that you create.  The selection coefficients of the mutations are drawn from their mutation types; addNewMutation() may be used instead if you wish to specify selection coefficients.  All of the target haplosomes must be associated with the same Chromosome object, since each new mutation is added to all of the target haplosomes.

In non-nucleotide-based models, mutationType will always be a non-nucleotide-based mutation type, and so nucleotide must be NULL (the default).  In a nucleotide-based model, mutationType might still be non-nucleotide-based (in which case nucleotide must still be NULL), or mutationType might be nucleotide-based, in which case a non-NULL value must be supplied for nucleotide, specifying the nucleotide(s) to be associated with the new mutation(s).  Nucleotides may be specified with string values ("A", "C", "G", or "T"), or with integer values (A=0, C=1, G=2, T=3).  If a nucleotide mutation already exists at the mutating position, it is replaced automatically in accordance with the stacking policy for nucleotide-based mutation types.  No check is performed that a new mutation’s nucleotide differs from the ancestral sequence, or that its selection coefficient is consistent with other mutations that may already exist at the given position with the same nucleotide; model consistency is the responsibility of the model.

Beginning in SLiM 2.5 this method is vectorized, so all of these parameters may be singletons (in which case that single value is used for all mutations created by the call) or non-singleton vectors (in which case one element is used for each corresponding mutation created).  Non-singleton parameters must match in length, since their elements need to be matched up one-to-one.

The new mutations created by this method are returned, even if their actual addition is prevented by the mutation stacking policy (see the mutationStackPolicy property of MutationType).  However, the order of the mutations in the returned vector is not guaranteed to be the same as the order in which the values are specified in parameter vectors, unless the position parameter is specified in ascending order.  In other words, pre-sorting the parameters to this method into ascending order by position, using order() and subsetting, will guarantee that the order of the returned vector of mutations corresponds to the order of elements in the parameters to this method; otherwise, no such guarantee exists.

Beginning in SLiM 2.1, this is a class method, not an instance method.  This means that it does not get multiplexed out to all of the elements of the receiver (which would add a different new mutation to each element); instead, it is performed as a single operation, adding the same new mutation objects to all of the elements of the receiver.  Before SLiM 2.1, to add the same mutations to multiple haplosomes, it was necessary to call addNewDrawnMutation() on one of the haplosomes, and then add the returned Mutation object to all of the other haplosomes using addMutations().  That is not necessary in SLiM 2.1 and later, because of this change (although doing it the old way does no harm and produces identical behavior).  Pre-2.1 code that actually relied upon the old multiplexing behavior will no longer work correctly (but this is expected to be an extremely rare pattern of usage).

Before SLiM 4, this method also took a originGeneration parameter.  This was deprecated (the origin generation was then required to be equal to the current generation, for internal consistency), and was removed in SLiM 4.

Calling this will normally affect the fitness values calculated at the end of the current tick (but not sooner); if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

Note that in nonWF models that use tree-sequence recording, mutations cannot be added to an individual after the tick in which the individual is created (i.e., when the age of the individual is greater than 0), to prevent the possibility of inconsistencies in the recorded tree sequence.

+ (object<Mutation>)addNewMutation(io<MutationType> mutationType, numeric selectionCoeff, integer position, [Nio<Subpopulation> originSubpop = NULL], [Nis nucleotide = NULL])

Add new mutations to the target haplosomes with the specified mutationType (specified by the MutationType object or by integer identifier), selectionCoeff, position, originTick (which may be NULL, the default, to specify the current tick; otherwise, beginning in SLiM 3.5, it must be equal to the current tick anyway, as other uses of this property have been deprecated), and originSubpop (specified by the Subpopulation object or by integer identifier, or by NULL, the default, to specify the subpopulation to which the first target haplosome belongs).  If originSubpop is supplied as an integer, it is intentionally not checked for validity; you may use arbitrary values of originSubpop to “tag” the mutations that you create.  The addNewDrawnMutation() method may be used instead if you wish selection coefficients to be drawn from the mutation types of the mutations.  All of the target haplosomes must be associated with the same Chromosome object, since each new mutation is added to all of the target haplosomes.

In non-nucleotide-based models, mutationType will always be a non-nucleotide-based mutation type, and so nucleotide must be NULL (the default).  In a nucleotide-based model, mutationType might still be non-nucleotide-based (in which case nucleotide must still be NULL), or mutationType might be nucleotide-based, in which case a non-NULL value must be supplied for nucleotide, specifying the nucleotide(s) to be associated with the new mutation(s).  Nucleotides may be specified with string values ("A", "C", "G", or "T"), or with integer values (A=0, C=1, G=2, T=3).  If a nucleotide mutation already exists at the mutating position, it is replaced automatically in accordance with the stacking policy for nucleotide-based mutation types.  No check is performed that a new mutation’s nucleotide differs from the ancestral sequence, or that its selection coefficient is consistent with other mutations that may already exist at the given position with the same nucleotide; model consistency is the responsibility of the model.

The new mutations created by this method are returned, even if their actual addition is prevented by the mutation stacking policy (see the mutationStackPolicy property of MutationType).  However, the order of the mutations in the returned vector is not guaranteed to be the same as the order in which the values are specified in parameter vectors, unless the position parameter is specified in ascending order.  In other words, pre-sorting the parameters to this method into ascending order by position, using order() and subsetting, will guarantee that the order of the returned vector of mutations corresponds to the order of elements in the parameters to this method; otherwise, no such guarantee exists.

Beginning in SLiM 2.1, this is a class method, not an instance method.  This means that it does not get multiplexed out to all of the elements of the receiver (which would add a different new mutation to each element); instead, it is performed as a single operation, adding the same new mutation object to all of the elements of the receiver.  Before SLiM 2.1, to add the same mutation to multiple haplosomes, it was necessary to call addNewMutation() on one of the haplosomes, and then add the returned Mutation object to all of the other haplosomes using addMutations().  That is not necessary in SLiM 2.1 and later, because of this change (although doing it the old way does no harm and produces identical behavior).  Pre-2.1 code that actually relied upon the old multiplexing behavior will no longer work correctly (but this is expected to be an extremely rare pattern of usage).

Before SLiM 4, this method also took a originGeneration parameter.  This was deprecated (the origin generation was then required to be equal to the current generation, for internal consistency), and was removed in SLiM 4.

Calling this will normally affect the fitness values calculated at the end of the current tick (but not sooner); if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

Note that in nonWF models that use tree-sequence recording, mutations cannot be added to an individual after the tick in which the individual is created (i.e., when the age of the individual is greater than 0), to prevent the possibility of inconsistencies in the recorded tree sequence.

 (Nlo<Mutation>$)containsMarkerMutation(io<MutationType>$ mutType, integer$ position, [logical$ returnMutation = F])

Returns T if the haplosome contains a mutation of type mutType at position, F otherwise (if returnMutation has its default value of F; see below).  This method is, as its name suggests, intended for checking for “marker mutations”: mutations of a special mutation type that are not literally mutations in the usual sense, but instead are added in to particular haplosomes to mark them as possessing some property.  Marker mutations are not typically added by SLiM’s mutation-generating machinery; instead they are added explicitly with addNewMutation() or addNewDrawnMutation() at a known, constant position in the haplosome.  This method provides a check for whether a marker mutation of a given type exists in a particular haplosome; because the position to check is known in advance, that check can be done much faster than the equivalent check with containsMutations() or countOfMutationsOfType(), using a binary search of the haplosome.

If returnMutation is T (an option added in SLiM 3), this method returns the actual mutation found, rather than just T or F.  More specifically, the first mutation found of mutType at position will be returned; if more than one such mutation exists in the target haplosome, which one is returned is not defined.  If returnMutation is T and no mutation of mutType is found at position, NULL will be returned.

– (logical)containsMutations(object<Mutation> mutations)

Returns a logical vector indicating whether each of the mutations in mutations is present in the target haplosome; each element in the returned vector indicates whether the corresponding mutation is present (T) or absent (F).  This method is provided for speed; it is much faster than the corresponding Eidos code.

Note that the mutations must be associated with the same chromosome as the target haplosome, otherwise an error is raised.  The containsMutations() method of Individual does not have this restriction, since it checks for mutations across all of the haplosomes of the target individual.  This restriction is intended to find logic errors, since it seems to make little sense to check for a mutation in a haplosome for the wrong chromosome; but if this restriction proves inconvenient in common situations, it could be relaxed.

 (integer$)countOfMutationsOfType(io<MutationType>$ mutType)

Returns the number of mutations that are of the type specified by mutType, out of all of the mutations in the haplosome.  If you need a vector of the matching Mutation objects, rather than just a count, use -mutationsOfType().  This method is provided for speed; it is much faster than the corresponding Eidos code.

+ (integer)mutationCountsInHaplosomes([No<Mutation> mutations = NULL])

Return an integer vector with the frequency counts of all of the Mutation objects passed in mutations, within the target Haplosome vector.  If the optional mutations argument is NULL (the default), frequency counts will be returned for all of the active Mutation objects in the species – the same Mutation objects, and in the same order, as would be returned by the mutations property of sim, in other words.

In multi-chromosome models, you might often wish to obtain counts only for mutations associated with one particular chromosome.  In that case, you would probably want to pass a vector of the mutations associated with that specific chromosome, as obtained from the subsetMutations() method of Species, rather than passing NULL.  (Passing NULL in that scenario would give you counts of 0 for all of the mutations associated with other chromosomes in the model.)

See the +mutationFrequenciesInHaplosomes() method to obtain float frequencies instead of integer counts.  See also the Species methods mutationCounts() and mutationFrequencies(), which might be more efficient for getting counts/frequencies for whole subpopulations or for the whole species.

+ (float)mutationFrequenciesInHaplosomes([No<Mutation> mutations = NULL])

Return a float vector with the frequencies of all of the Mutation objects passed in mutations, within the target Haplosome vector.  If the optional mutations argument is NULL (the default), frequencies will be returned for all of the active Mutation objects in the species – the same Mutation objects, and in the same order, as would be returned by the mutations property of sim, in other words.

In multi-chromosome models, the frequency of each mutation is assessed within the subset of target haplosomes that are associated with the same chromosome.  In other words, if a mutation is associated with chromosome 1, and the target haplosomes are associated with both chromosomes 1 and 2, the frequency of the mutation will be calculated only within the haplosomes for chromosome 1 (as you would expect).  However, you might often wish to obtain frequencies only for mutations associated with one particular chromosome.  In that case, you would probably want to pass a vector of the mutations associated with that specific chromosome, as obtained from the subsetMutations() method of Species, rather than passing NULL.  (Passing NULL in that scenario would give you frequencies of 0 for all of the mutations associated with other chromosomes in the model.)

See the +mutationCountsInHaplosomes() method to obtain integer counts instead of float frequencies.  See also the Species methods mutationCounts() and mutationFrequencies(), which might be more efficient for getting counts/frequencies for whole subpopulations or for the whole species.

 (object<Mutation>)mutationsOfType(io<MutationType>$ mutType)

Returns an object vector of all the mutations that are of the type specified by mutType, out of all of the mutations in the haplosome.  If you just need a count of the matching Mutation objects, rather than a vector of the matches, use -countOfMutationsOfType(); if you need just the positions of matching Mutation objects, use -positionsOfMutationsOfType(); and if you are aiming for a sum of the selection coefficients of matching Mutation objects, use -sumOfMutationsOfType().  This method is provided for speed; it is much faster than the corresponding Eidos code.  See also substitutionsOfType().

– (is)nucleotides([Ni$ start = NULL], [Ni$ end = NULL], [string$ format = "string"])

Returns the nucleotide sequence for the haplosome.  This is the current ancestral sequence, as would be returned by the Chromosome method ancestralNucleotides(), with the nucleotides for any nucleotide-based mutations in the haplosome overlaid.  The range of the returned sequence may be constrained by a start position given in start and/or an end position given in end; nucleotides will be returned from start to end, inclusive.  The default value of NULL for start and end represent the first and last base positions of the chromosome, respectively.

The format of the returned sequence is controlled by the format parameter.  A format of "string" will return the sequence as a singleton string (e.g., "TATA").  A format of "char" will return a string vector with one element per nucleotide (e.g., "T", "A", "T", "A").  A format of "integer" will return an integer vector with values A=0, C=1, G=2, T=3 (e.g., 3, 0, 3, 0).  A format of "codon" will return an integer vector with values from 0 to 63, based upon successive nucleotide triplets in the sequence (which, for this format, must have a length that is a multiple of three); see the ancestralNucleotides() documentation for details.  If the sequence returned is likely to be long, the "string" format will be the most memory-efficient, and may also be the fastest (but may be harder to work with).

Several helper functions are provided for working with sequences, such as nucleotideCounts() to get the counts of A/C/G/T nucleotides in a sequence, nucleotideFrequencies() to get the same information as frequencies, and codonsToAminoAcids() to convert a codon sequence (such as provided by the codon format described above) to an amino acid sequence.

+ (void)outputHaplosomes([Ns$ filePath = NULL], [logical$ append = F], [logical$ objectTags = F])

Output the target haplosomes in SLiM’s native format.  This low-level output method may be used to output any sample of Haplosome objects associated with a single chromosome.  The Eidos function sample() may be useful for constructing custom samples, as may the SLiM class Individual.  For output of a sample from a single Subpopulation, the outputSample() method of Subpopulation may be more straightforward to use.  If the optional parameter filePath is NULL (the default), output is directed to SLiM’s standard output.  Otherwise, the output is sent to the file specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.

The objectTags parameter may be used to request that tag values for objects be written out.  This option is turned off (F) by default, for brevity; if it turned on (T), the tag property values of all haplosomes and mutations in the output will be written.  If there is other state that you wish you persist, such as tags on objects of other classes, values attached to objects with setValue(), and so forth, you should persist that state in separate files using calls such as writeFile().

See outputHaplosomesToMS() and outputHaplosomesToVCF() for other output formats.  Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

+ (void)outputHaplosomesToMS([Ns$ filePath = NULL], [logical$ append = F], [logical$ filterMonomorphic = F])

Output the target haplosomes in MS format.  This low-level output method may be used to output any sample of Haplosome objects associated with a single chromosome.  The Eidos function sample() may be useful for constructing custom samples, as may the SLiM class Individual.  For output of a sample from a single Subpopulation, the outputMSSample() of Subpopulation may be more straightforward to use.  If the optional parameter filePath is NULL (the default), output is directed to SLiM’s standard output.  Otherwise, the output is sent to the file specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.  Positions in the output will span the interval [0,1].

If filterMonomorphic is F (the default), all mutations that are present in the sample will be included in the output.  This means that some mutations may be included that are actually monomorphic within the sample (i.e., that exist in every sampled haplosome, and are thus apparently fixed).  These may be filtered out with filterMonomorphic = T if desired; note that this option means that some mutations that do exist in the sampled haplosomes might not be included in the output, simply because they exist in every sampled haplosome.

See outputHaplosomes() and outputHaplosomesToVCF() for other output formats.  Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

+ (void)outputHaplosomesToVCF([Ns$ filePath = NULL], [logical$ outputMultiallelics = T], [logical$ append = F], [logical$ simplifyNucleotides = F], [logical$ outputNonnucleotides = T], [logical$ groupAsIndividuals = T])

Output the target haplosomes in VCF format.  This low-level output method may be used to output any sample of Haplosome objects associated with a single chromosome.  The Eidos function sample() may be useful for constructing custom samples, as may the SLiM class Individual.  For output of a sample from a single Subpopulation, the outputVCFSample() method of Subpopulation may be more straightforward to use.  If the optional parameter filePath is NULL (the default), output is directed to SLiM’s standard output.  Otherwise, the output is sent to the file specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.

The parameters outputMultiallelics, simplifyNucleotides, and outputNonnucleotides affect the format of the output produced.

With groupAsIndividuals being T (the default), the target haplosome vector should be structured as if it represents all of the haplosomes for some set of individuals, for a single focal chromosome.  All haplosomes for the focal chromosome should be present, including null haplosomes.  It should provide all of the haplosomes for the first individual (for the chosen chromosome); then for the second individual; and so forth.  The haplosomes in the target haplosome vector do not, in fact, need to belong to individuals in SLiM following this pattern; they just need to specify well-formed individuals in the VCF output.  For an intrinsically haploid chromosome, the target haplosome for a given output individual is used to generate a haploid call (0 or 1) for that individual; if the haplosome is a null haplosome, the call will be ~ (an ASCII tilde). For example, calls for (non-null) Y haplosomes in males will be emitted as 0 or 1, whereas calls for the (null) Y haplosomes in females will be emitted as ~.  For an intrinsically diploid chromosome, the pair of target haplosomes for a given individual is used to generate a call for that individual, but null haplosomes are allowed (in the patterns expected by SLiM given the chromosome type).  For example, a pair of non-null haplosomes for an X chromosome will be emitted as a diploid call (such as 1|0) for a female (XX), but if the second haplosome of the pair is a null haplosome, the pair will be emitted as a haploid call (0 or 1) for a male (X).  If the first haplosome of the pair were a null haplosome for an X chromosome, an error would be raised, since that is not an allowed pattern in SLiM (as discussed in the documentation for the Chromosome class).  For a diploid autosome of type "A", however, any pattern is legal, but the VCF format cannot distinguish between a non-null haplosome first and a null haplosome second, versus a null haplosome first and a non-null haplosome second; both will be emitted as a haploid call (0 or 1).  For a diploid autosome of type "A", if both haplosomes are null the call will be ~.  The VCF specification does not actually seem to discuss sex chromosomes, but this design is intended to follow standard usage.

With groupAsIndividuals being F, the focal chromosome is treated as being intrinsically haploid whether it is or not; each haplosome will be called as a haploid sample whether the chromosome type is diploid or haploid.  This provides more detailed and accurate information; the exact state of each haplosome will be represented with either 0, 1, or (for null haplosomes) ~, rather than the state of a pair of haplosomes being represented as a single call in a way that can sometimes be ambiguous, as discussed above.  However, the resulting output might confuse some VCF parsers that expect diploid calls for individuals, and it will not be as obvious which calls in the output belong to a given diploid individual.

See outputHaplosomesToMS() and outputHaplosomes() for other output formats.  Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

– (integer)positionsOfMutationsOfType(io<MutationType>$ mutType)

Returns the positions of mutations that are of the type specified by mutType, out of all of the mutations in the haplosome.  If you need a vector of the matching Mutation objects, rather than just positions, use -mutationsOfType().  This method is provided for speed; it is much faster than the corresponding Eidos code.

+ (object<Mutation>)readHaplosomesFromMS(string$ filePath, io<MutationType>$ mutationType)

Read new mutations from the MS format file at filePath and add them to the target haplosomes.  The number of target haplosomes must match the number of haplosomes represented in the MS file, and all target haplosomes must be associated with the same chromosome, and must not be null haplosomes.  The target haplosomes correspond, in order, to the call lines in the MS file.  To read into all of the non-null haplosomes in a given subpopulation pN in a single-chromosome model, simply call pN.haplosomesNonNull.readHaplosomesFromMS(), assuming the subpopulation’s size matches that of the MS file.  A vector containing all of the mutations created by readHaplosomesFromMS() is returned.

Each mutation is created at the position specified in the file, using the mutation type given by mutationType.  Positions are expected to be in [0,1], and are scaled to the length of the chromosome by multiplying by the last valid base position of the chromosome (i.e., one less than the chromosome length).  Selection coefficients are drawn from the mutation type.  The population of origin for each mutation is set to -1, and the tick of origin is set to the current tick.  In a nucleotide-based model, if mutationType is nucleotide-based, a random nucleotide different from the ancestral nucleotide at the position will be chosen with equal probability.

+ (object<Mutation>)readHaplosomesFromVCF(string$ filePath, [Nio<MutationType>$ mutationType = NULL])

Read new mutations from the VCF format file at filePath and add them to the target haplosomes.  The number of target haplosomes must match the number of haplosomes represented in the VCF file (i.e., two times the number of diploid samples plus one times the number of haploid samples).  To read into all of the haplosomes in a given subpopulation pN in a single-chromosome model, simply call pN.haplosomes.readHaplosomesFromVCF(), assuming the subpopulation’s size matches that of the VCF file taking ploidy into account.  A vector containing all of the mutations created by readHaplosomesFromVCF() is returned.

This method and the readIndividualsFromVCF() method of Individual provide two alternative ways of reading VCF data, focused in the perspective of either haplosomes (this method) or individuals (the Individual method).  See the documentation of readIndividualsFromVCF() for discussion of the pros and cons of each approach; that discussion will not be duplicated here.

SLiM’s VCF parsing is quite primitive.  The header is parsed only inasmuch as SLiM looks to see whether SLiM-specific VCF fields are defined or not; the rest of the header information is ignored.  Call lines are assumed to follow the format:

#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT i0...iN

The CHROM field is largely ignored, but readHaplosomesFromVCF() does check that its value is identical across all call lines, to prevent the genetic data for more than one chromosome from being glommed together nonsensically; the input VCF file must contain data for just a single chromosome.  In single-chromosome models the CHROM field is not otherwise checked or validated.  In multi-chromosome models, readHaplosomesFromVCF() imposes some additional restrictions.  First, all haplosomes in the target vector must be associated with the same single focal chromosome.  Second, the CHROM field for every call line in the VCF file must match the symbol property of that focal chromosome; the VCF file must indicate that it specifically matches the focal chromosome associated with the target haplosomes.  These restrictions boil down to the fact that readHaplosomesFromVCF() only reads data for a single chromosome.  If you wish to read multi-chromosome VCF data into a multi-chromosome SLiM model, the readIndividualsFromVCF() method provided by the Individual class supports that functionality (because it can work at the level of individuals, rather than haplosomes, making it possible to match calls to the corresponding haplosomes in a reasonable way).  Alternatively, you can call readHaplosomesFromVCF() multiple times to read data for different chromosomes one by one.

The ID, QUAL, FILTER, and FORMAT fields are ignored, and information in the genotype fields beyond the GT genotype subfield are also ignored.  SLiM’s own VCF annotations are honored; in particular, mutations will be created using the given values of MID, S, PO, TO, and MT if those subfields are present, and DOM, if it is present, must match the dominance coefficient of the mutation type.  The parameter mutationType (a MutationType object or id) will be used for any mutations that have no supplied mutation type id in the MT subfield; if mutationType would be used but is NULL an error will result.  Mutation IDs supplied in MID will be used if no mutation IDs have been used in the simulation so far; if any have been used, it is difficult for SLiM to guarantee that there are no conflicts, so a warning will be emitted and the MID values will be ignored.  If selection coefficients are not supplied with the S subfield, they will be drawn from the mutation type used for the mutation.  If a population of origin is not supplied with the PO subfield, -1 will be used.  If a tick of origin is not supplied with the TO subfield (or a generation of origin GO field, which was the SLiM convention before SLiM 4), the current tick will be used.

REF and ALT must always be comprised of simple nucleotides (A/C/G/T) rather than values representing indels or other complex states.  Beyond this, the handling of the REF and ALT fields depends upon several factors.  In non-nucleotide-based models, we have the first case: (1) These fields are ignored, although they are still checked for conformance.  In nucleotide-based models, when a header definition for SLiM’s NONNUC tag is present (as when nucleotide-based output is generated by SLiM) there are two further possibilities, given as (2) and (3) here: (2) If a NONNUC field is present in the INFO field the call line is taken to represent a non-nucleotide-based mutation, and REF and ALT are again ignored; in this case the mutation type used must be non-nucleotide-based.  (3) If a NONNUC field is not present the call line is taken to represent a nucleotide-based mutation; in this case, the mutation type used must be nucleotide-based, and the specified reference nucleotide must match the existing ancestral nucleotide at the given position.  Finally, in nucleotide-based models, when a header definition for SLiM’s NONNUC tag is not present (as when loading a non-SLiM-generated VCF file), there is a remaining possibility: (4) The mutation type used will govern the way nucleotides are handled.  In this case, if the mutation type used for a mutation is nucleotide-based, the nucleotide provided in the VCF file for that allele will be used, whereas if the mutation type is non-nucleotide-based, the nucleotide provided will be ignored.

If multiple alleles using the same nucleotide at the same position are specified in the VCF file, a separate mutation will be created for each, mirroring SLiM’s behavior with independent mutational lineages when writing VCF.  The MULTIALLELIC flag is ignored by readHaplosomesFromVCF(); call lines for mutations at the same base position in the same haplosome will result in stacked mutations whether or not MULTIALLELIC is present.

The target haplosomes correspond, in order, to the haploid or diploid calls provided for i0iN (the sample IDs) in the VCF file.  Null haplosomes in the target vector will be skipped, and will not be used to correspond to any of the calls for i0iN; however, care should be taken in this case that the haplosomes in the VCF file correspond to the target haplosomes in the manner desired.

A call of ~ (an ASCII tilde character) for an individual i0iN is taken to indicate that that individual possesses no genetic information for the chromosome; it lacks that chromosome entirely.  For example, if the VCF file represents Y-chromosome data, female individuals should have calls of ~.  This is treated differently than a call of 0  or 0|0; a call of 0 matches that call to a non-null target haplosome (but does not add the called mutation to that haplosome), and a call of 0|0 matches two non-null target haplosomes (but does not add the called mutation to either), whereas a call of ~ is simply skipped, without matching to any haplosome in the target vector, mirroring the fact that readHaplosomesFromVCF() skips over null haplosomes in the target haplosome vector.  (When reading Y-chromosome data, a female’s null Y haplosome could be omitted from the target haplosome vector, or it could be present since it would be skipped anyway – as stated above, all null haplosomes are skipped.)  Note that these semantics using ~ are non-standard; the VCF standard does not seem to say anything about how sex chromosomes should be represented (or anything about other types of chromosomes that might be absent from some individuals), so the usage of ~ was invented for SLiM.  This is an area where standardization is very much needed.

+ (void)removeMutations([No<Mutation> mutations = NULL], [logical$ substitute = F])

Remove the mutations in mutations from the target haplosomes, if they are present (if they are not present, they will be ignored).  If NULL is passed for mutations (which is the default), then all mutations will be removed from the target haplosomes; in this case, substitute must be F (a specific vector of mutations to be substituted is required).  Note that the Mutation objects removed remain valid, and will still be in the simulation’s mutation registry (i.e., will be returned by the Species property mutations), until the next tick.  All target haplosomes and all mutations in mutations must be associated with the same Chromosome object; attempting to remove a mutation from a haplosome associated with a different chromosome will raise an error.

Removing mutations will normally affect the fitness values calculated at the end of the current tick; if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

The optional parameter substitute was added in SLiM 2.2, with a default of F for backward compatibility.  If substitute is T, Substitution objects will be created for all of the removed mutations so that they are recorded in the simulation as having fixed, just as if they had reached fixation and been removed by SLiM’s own internal machinery.  This will occur regardless of whether the mutations have in fact fixed, regardless of the convertToSubstitution property of the relevant mutation types, and regardless of whether all copies of the mutations have even been removed from the simulation (making it possible to create Substitution objects for mutations that are still segregating).  It is up to the caller to perform whatever checks are necessary to preserve the integrity of the simulation’s records.  Typically substitute will only be set to T in the context of calls like sim.subpopulations.haplosomes.removeMutations(muts, T), such that the substituted mutations are guaranteed to be entirely removed from circulation.  As mentioned above, substitute may not be T if mutations is NULL.

 (float$)sumOfMutationsOfType(io<MutationType>$ mutType)

Returns the sum of the selection coefficients of all mutations that are of the type specified by mutType, out of all of the mutations in the haplosome.  This is often useful in models that use a particular mutation type to represent QTLs with additive effects; in that context, sumOfMutationsOfType() will provide the sum of the additive effects of the QTLs for the given mutation type.  This method is provided for speed; it is much faster than the corresponding Eidos code.  Note that this method also exists on Individual, for cases in which the sum across both haplosomes of an individual is desired.

5.5  Class GenomicElement

5.5.1  GenomicElement properties

endPosition => (integer$)

The last position in the chromosome contained by this genomic element.

genomicElementType => (object<GenomicElementType>$)

The GenomicElementType object that defines the behavior of this genomic element.

startPosition => (integer$)

The first position in the chromosome contained by this genomic element.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.

5.5.2  GenomicElement methods

– (void)setGenomicElementType(io<GenomicElementType>$ genomicElementType)

Set the genomic element type used for a genomic element.  The genomicElementType parameter should supply the new genomic element type for the element, either as a GenomicElementType object or as an integer identifier.  The genomic element type for a genomic element is normally a constant in simulations, so be sure you know what you are doing.

5.6  Class GenomicElementType

5.6.1  GenomicElementType properties

color <–> (string$)

The color used to display genomic elements of this type in SLiMgui.  Outside of SLiMgui, this property still exists, but is not used by SLiM.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB".  If color is the empty string, "", SLiMgui’s default color scheme is used; this is the default for new GenomicElementType objects.

id => (integer$)

The identifier for this genomic element type; for genomic element type g3, for example, this is 3.

mutationFractions => (float)

For each MutationType represented in this genomic element type, this property has the corresponding fraction of all mutations that will be drawn from that MutationType.

mutationMatrix => (float)

The nucleotide mutation matrix used for this genomic element type, set up by initializeGenomicElementType() and setMutationMatrix().  This property is only defined in nucleotide-based models; it is unavailable otherwise.

mutationTypes => (object<MutationType>)

The MutationType instances used by this genomic element type.

species => (object<Species>$)

The species to which the target object belongs.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to genomic element types.

5.6.2  GenomicElementType methods

– (void)setMutationFractions(io<MutationType> mutationTypes, numeric proportions)

Set the mutation type fractions contributing to a genomic element type.  The mutationTypes vector should supply the mutation types used by the genomic element (either as MutationType objects or as integer identifiers), and the proportions vector should be of equal length, specifying the relative proportion of mutations that will be drawn from each corresponding type.  This is normally a constant in simulations, so be sure you know what you are doing.

– (void)setMutationMatrix(float mutationMatrix)

Sets a new nucleotide mutation matrix for the genomic element type.  This replaces the mutation matrix originally set by initializeGenomicElementType().  This method may only be called in nucleotide-based models.

5.7  Class Individual

5.7.1  Individual properties

age <–> (integer$)

The age of the individual, measured in cycles.  A newly generated offspring individual will have an age of 0 in the same tick in which it was created.  The age of every individual is incremented by one at the same point that its species cycle counter is incremented, at the end of the tick cycle, if and only if its species was active in that tick.  The age of individuals may be changed; usually this only makes sense when setting up the initial state of a model, however.

color <–> (string$)

The color used to display the individual in SLiMgui.  Outside of SLiMgui, this property still exists, but is not used by SLiM.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB" (see the Eidos manual).  If color is the empty string, "", SLiMgui’s default (fitness-based) color scheme is used; this is the default for new Individual objects.  Note that named colors will be converted to RGB internally, so the value of this property will always be a hexadecimal RGB color string (or "").

fitnessScaling <–> (float$)

A float scaling factor applied to the individual’s fitness (i.e., the fitness value computed for the individual will be multiplied by this value).  This provides a simple, fast way to modify the fitness of an individual; conceptually it is similar to returning a fitness effect for the individual from a fitnessEffect() callback, but without the complexity and performance overhead of implementing such a callback.  To scale the fitness of all individuals in a subpopulation by the same factor, see the fitnessScaling property of Subpopulation.

The value of fitnessScaling is reset to 1.0 every tick, so that any scaling factor set lasts for only a single tick.  This reset occurs immediately after fitness values are calculated, in both WF and nonWF models.

haploidGenome1 => (object<Haplosome>)

A vector of all Haplosome objects associated with this individual that are attributed to its first parent (the female parent, in sexual models).  This method assumes the individual was generated by the typical method for each chromosome type, as explained below; it does not trace back the true ancestry of each haplosome.  The semantics of this are more obvious for some chromosome types than others, depending on the inheritance pattern of the chromosome as described in initializeChromosome().  For chromosomes with two associated haplosomes (types "A", "X", "Z", "H-", and "-Y"), the first haplosome is assumed to be from the first parent, and is thus included, whereas the second haplosome is assumed to be from the second parent and is thus not included.  For chromosomes with one associated haplosome that is inherited from the female/first parent in one way or another (types "W", "HF", and "FL"), that haplosome is always included.  For type "H", the single haplosome is assumed to have come from the first parent (since clonal inheritance is the common case), and so is included.  Other chromosome types ("Y", "HM", "ML") are never included.  See also the haploidGenome1NonNull property and the haplosomesForChromosomes() method.

haploidGenome1NonNull => (object<Haplosome>)

This provides the same vector of haplosomes as the haploidGenome1 property, except that null haplosomes are not included in this property.  This is a convenience shorthand, sometimes useful in models that involve null haplosomes.  See also the haplosomesForChromosomes() method.

haploidGenome2 => (object<Haplosome>)

A vector of all Haplosome objects associated with this individual that are attributed to its second parent (the male parent, in sexual models).  This method assumes the individual was generated by the typical method for each chromosome type, as explained below; it does not trace back the true ancestry of each haplosome.  The semantics of this are more obvious for some chromosome types than others, depending on the inheritance pattern of the chromosome as described in initializeChromosome().  For chromosomes with two associated haplosomes (types "A", "X", "Z", "H-", and"-Y"), the second haplosome is assumed to be from the second parent, and is thus included, whereas the first haplosome is assumed to be from the first parent and is thus not included.  For chromosomes with one associated haplosome that is inherited from the male/second parent in one way or another (types "Y", "HM", and "ML"), that haplosome is always included.  For type "H", the single haplosome is assumed to have come from the first parent (since clonal inheritance is the common case), and so is not included.  Other chromosome types ("W", "HF", "FL") are never included.  See also the haploidGenome2NonNull property and the haplosomesForChromosomes() method.

haploidGenome2NonNull => (object<Haplosome>)

This provides the same vector of haplosomes as the haploidGenome2 property, except that null haplosomes are not included in this property.  This is a convenience shorthand, sometimes useful in models that involve null haplosomes.  See also the haplosomesForChromosomes() method.

haplosomes => (object<Haplosome>)

A vector of all Haplosome objects associated with this individual, in the order in which the chromosomes were defined for the species.  See also the haplosomesNonNull, haploidGenome1, haploidGenome1NonNull, haploidGenome2, and haploidGenome2NonNull properties and the haplosomesForChromosomes() method.

haplosomesNonNull => (object<Haplosome>)

A vector of all Haplosome objects associated with this individual, in the order in which the chromosomes were defined for the species (as with the haplosomes property), but excluding any null haplosomes from the returned vector.  This is a convenience shorthand, sometimes useful in models that involve null haplosomes.

index => (integer$)

The index of the individual in the individuals vector of its Subpopulation.

meanParentAge => (float$)

The average age of the parents of this individual, measured in cycles.  Parentless individuals will have a meanParentAge of 0.0.  The mean parent age is determined when a new offspring is generated, from the age property of the parent or parents involved in generating the offspring.  For addRecombinant() and addMultiRecombinant() that is somewhat complex; see those methods for details.

migrant => (logical$)

Set to T if the individual is a recent migrant, F otherwise.  The definition of “recent” depends upon the model type (WF or nonWF).

In WF models, this flag is set at the point when a new child is generated if it is a migrant (i.e., if its source subpopulation is not the same as its subpopulation), and remains valid, with the same value, for the rest of the individual’s lifetime.

In nonWF models, this flag is F for all new individuals, is set to F in all individuals at the end of the reproduction tick cycle stage, and is set to T on all individuals moved to a new subpopulation by takeMigrants() or a survival() callback; the T value set by takeMigrants() or survival() will remain until it is reset at the end of the next reproduction tick cycle stage.

pedigreeID => (integer$)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T) or tree-sequence recording is turned on with initializeTreeSeq(), pedigreeID is a unique non-negative identifier for each individual in a simulation, never re-used throughout the duration of the simulation run.  If neither pedigree tracking nor tree-sequence recording is enabled, this property is unavailable.

pedigreeParentIDs => (integer)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), pedigreeParentIDs contains the values of pedigreeID that were possessed by the parents of an individual; it is thus a vector of two values.  If pedigree tracking is not enabled, this property is unavailable.  Parental values may be -1 if insufficient ticks have elapsed for that information to be available (because the simulation just started, or because a subpopulation is new).

pedigreeGrandparentIDs => (integer)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), pedigreeGrandparentIDs contains the values of pedigreeID that were possessed by the grandparents of an individual; it is thus a vector of four values.  If pedigree tracking is not enabled, this property is unavailable.  Grandparental values may be -1 if insufficient ticks have elapsed for that information to be available (because the simulation just started, or because a subpopulation is new).

reproductiveOutput => (integer$)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), reproductiveOutput contains the number of offspring for which this individual has been a parent.  If pedigree tracking is not enabled, this property is unavailable.  If an individual is a parent by cloning or selfing, or as both parents for a biparental mating, this value is incremented by two.  Involvement of an individual as a parent for an addRecombinant() or addMultiRecombinant() call does not change this property’s value, since the reproductive contribution in that case is unclear; one must conduct separate bookkeeping for that case if necessary, or use tree-sequence recording to infer it from the inheritance record.

See also the Subpopulation property lifetimeReproductiveOutput.

sex => (string$)

The sex of the individual.  This will be "H" if sex is not enabled in the simulation (i.e., for hermaphrodites), otherwise "F" or "M" as appropriate.

spatialPosition => (float)

The spatial position of the individual.  The length of the spatialPosition property (the number of coordinates in the spatial position of an individual) depends upon the spatial dimensionality declared with initializeSLiMOptions().  If the spatial dimensionality is zero (as it is by default), it is an error to access this property.  The elements of this property are identical to the values of the x, y, and z properties (if those properties are encompassed by the spatial dimensionality of the simulation).  In other words, if the declared dimensionality is "xy", the individual.spatialPosition property is equivalent to c(individual.x, individual.y); individual.z is not used since it is not encompassed by the simulation’s dimensionality.

subpopulation => (object<Subpopulation>$)

The Subpopulation object to which the individual belongs.

tag <–> (integer$)

A user-defined integer value (as opposed to tagF, which is of type float).  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to individuals.  Note that the Individual objects used by SLiM are new for every new offspring, so the tag value of each new offspring generated in each tick will be initially undefined.

tagF <–> (float$)

A user-defined float value (as opposed to tag, which is of type integer).  The value of tagF is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tagF is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to individuals.

Note that at present, although many classes in SLiM have an integer-type tag property, only Individual has a float-type tagF property, because attaching model state to individuals seems to be particularly common and useful.  If a tagF property would be helpful on another class, it would be easy to add.

See the description of the tag property above for additional comments.

tagL0 <–> (logical$)

A user-defined logical value (see also tag and tagF).  The value of tagL0 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tagL0 is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to individuals.

tagL1 <–> (logical$)

A user-defined logical value (see also tag and tagF).  The value of tagL1 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tagL1 is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to individuals.

tagL2 <–> (logical$)

A user-defined logical value (see also tag and tagF).  The value of tagL2 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tagL2 is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to individuals.

tagL3 <–> (logical$)

A user-defined logical value (see also tag and tagF).  The value of tagL3 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tagL3 is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to individuals.

tagL4 <–> (logical$)

A user-defined logical value (see also tag and tagF).  The value of tagL4 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tagL4 is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to individuals.

uniqueMutations => (object<Mutation>)

All of the Mutation objects present in this individual.  Mutations present in homologous haplosomes will occur only once in this property, and the mutations for a given chromosome will be given in sorted order by position, so in single-chromosome simulations this property is similar to sortBy(unique(individual.haplosomes.mutations), "position").  (Even with a single chromosome it is not identical to that call, since if multiple mutations exist at the exact same position, they might be sorted differently by this method than they would be by sortBy().)  This method is provided primarily for speed; it executes much faster than the Eidos equivalent above.  Indeed, it is faster than just individual.haplosomes.mutations, and gives uniquing and sorting on top of that, so it is advantageous unless duplicate entries for homozygous mutations are actually needed.  For more flexibility, see the method mutationsFromHaplosomes().

x <–> (float$)

A user-defined float value.  The value of x is initially undefined (i.e., has an effectively random value that could be different every time you run your model); if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of x is not used by SLiM unless the optional “continuous space” facility is enabled with the dimensionality parameter to initializeSLiMOptions(), in which case x will be understood to represent the x coordinate of the individual in space.  If continuous space is not enabled, you may use x as an additional tag value of type float.

xy => (float)

This property provides joint read-only access to the x and y properties; they are returned as a two-element float vector.  This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model.  See the documentation for the separate properties x and y for further comments.

xyz => (float)

This property provides joint read-only access to the x, y, and z properties; they are returned as a three-element float vector.  This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model.  See the documentation for the separate properties x, y, and z for further comments.

xz => (float)

This property provides joint read-only access to the x and z properties; they are returned as a two-element float vector.  This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model.  See the documentation for the separate properties x and z for further comments.

y <–> (float$)

A user-defined float value.  The value of y is initially undefined (i.e., has an effectively random value that could be different every time you run your model); if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of y is not used by SLiM unless the optional “continuous space” facility is enabled with the dimensionality parameter to initializeSLiMOptions(), in which case y will be understood to represent the y coordinate of the individual in space (if the dimensionality is "xy" or "xyz").  If continuous space is not enabled, or the dimensionality is not "xy" or "xyz", you may use y as an additional tag value of type float.

yz => (float)

This property provides joint read-only access to the y and z properties; they are returned as a two-element float vector.  This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model.  See the documentation for the separate properties y and z for further comments.

z <–> (float$)

A user-defined float value.  The value of z is initially undefined (i.e., has an effectively random value that could be different every time you run your model); if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of z is not used by SLiM unless the optional “continuous space” facility is enabled with the dimensionality parameter to initializeSLiMOptions(), in which case z will be understood to represent the z coordinate of the individual in space (if the dimensionality is "xyz").  If continuous space is not enabled, or the dimensionality is not "xyz", you may use z as an additional tag value of type float.

5.7.2  Individual methods

– (logical)containsMutations(object<Mutation> mutations)

Returns a logical vector indicating whether each of the mutations in mutations is present in the individual (in any of its haplosomes); each element in the returned vector indicates whether the corresponding mutation is present (T) or absent (F).  This method is provided for speed; it is much faster than the corresponding Eidos code.

 (integer$)countOfMutationsOfType(io<MutationType>$ mutType)

Returns the number of mutations that are of the type specified by mutType, out of all of the mutations in the individual (in all of its haplosomes; a mutation that is present in both homologous haplosomes counts twice).  If you need a vector of the matching Mutation objects, rather than just a count, you should probably use mutationsFromHaplosomes().  This method is provided for speed; it is much faster than the corresponding Eidos code.

– (object<Haplosome>)haplosomesForChromosomes([Niso<Chromosome> chromosomes = NULL], [Ni$ index = NULL], [logical$ includeNulls = T])

Returns a vector containing the haplosomes of the target individual that correspond to the chromosomes passed in chromosomes (following the order of the chromosomes property of Individual).  Chromosomes can be specified by id (integer), by symbol (string) or by the Chromosome objects themselves; if NULL is passed (the default), all chromosomes defined for the species are used, in the order in which they were defined.

For chromosomes that are intrinsically diploid (types "A", "X", and "Z", as well as the "H-" and "-Y" backward-compatibility chromosome types), index can be 0 or 1, requesting only the first or second haplosome, respectively, for that chromosome; for other chromosome types, index is ignored.  If includeNulls is T (the default), any null haplosomes corresponding to the specified chromosomes are included in the result; if it is F, null haplosomes are excluded.  See also the properties haplosomes, haplosomesNonNull, haploidGenome1, haploidGenome1NonNull, haploidGenome2, and haploidGenome2NonNull.

– (object<Mutation>)mutationsFromHaplosomes(string$ category, [Nio<MutationType>$ mutType = NULL], [Niso<Chromosome> chromosomes = NULL])

Returns a vector of mutations from the haplosomes of the target individual.  Several options are provided that filter which mutations are returned.

The category parameter must be one of five supported values: "unique", "homozygous", "heterozygous", "hemizygous", or "all".  If category is "unique", a given mutation will be returned only once, whether it is present homozygously or heterozygously (or hemizygously, for that matter); this mode of operation is similar to the uniqueMutations property, but provides more control due to the other options provided by this method.  If category is "homozygous", a given mutation will be returned only if it is present homozygously (in both of the homologous haplosomes for a given chromosome).  If category is "heterozygous", a given mutation will be returned only if it is present heterozygously (in only one of the two homologous non-null haplosomes for a given chromosome).  If category is "hemizygous", a given mutation will be returned only if it is present hemizygously (in one haplosome for an intrinsically diploid chromosome, when the other haplosome is a null haplosome).  If category is "all", a given mutation will be returned each time that it occurs in the haplosomes of the individual; in other words, it will be present in the returned vector twice if it is homozygous, once if it is heterozygous or hemizygous.  Mutations in the single haplosome of an intrinsically haploid chromosome will be returned for category values of "unique", "homozygous", and "all".

The mutType parameter may be NULL, or may specify a mutation type by its integer id or with the MutationType object itself.  If mutType is NULL (the default), mutations of every mutation type are returned; no filtering by mutation type is done.  Otherwise, only mutations of the specified mutation type will be returned.

The chromosomes parameter may be NULL, or may provide a vector of chromosomes specified by their integer id, string symbol, or with the Chromosome object itself.  If chromosomes is NULL (the default), mutations associated with every chromosome are returned; no filtering by chromosome is done.  Otherwise, only mutations associated with the specified chromosomes will be returned.

The returned vector will contain tranches of mutations, one tranche per chromosome, in the order that the chromosomes were specified (if chromosomes is non-NULL) or the order the chromosomes were defined in the model (if chromosomes is NULL).  Within a given tranche, the mutations for that chromosome will be returned in sorted order by position.  (If more than one mutation associated with a given chromosome exists at the same position, the order in which those mutations are returned is undefined.)

This method replaces the deprecated method uniqueMutationsOfType(), while providing additional useful options.  It is particularly useful for efficient, vectorized assessment of the homozygous versus heterozygous state of the mutations contained by an individual, which is otherwise difficult to assess efficiently.

+ (void)outputIndividuals([Ns$ filePath = NULL], [logical$ append = F], [Niso<Chromosome>$ chromosome = NULL], [logical$ spatialPositions = T], [logical$ ages = T], [logical$ ancestralNucleotides = F], [logical$ pedigreeIDs = F], [logical$ objectTags = F])

Output the state of the target vector of individuals in SLiM's own format.  If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.  This method is quite similar to the Species method outputFull(), but (1) it can produce output for any vector of individuals, not always for the entire population; (2) it does not support output in a binary format; (3) it can produce output regarding the genetics for all chromosomes or for just one focal chromosome; and (4) there is no corresponding read method, as readFromPopulationFile() can read the data saved by outputFull().

The chromosome parameter specifies a focal chromosome for which the genetics of the target individuals will be output.  If chromosome is NULL, all chromosomes will be output; otherwise, chromosome may specify the focal chromosome with an integer chromosome id, a string chromosome symbol, or a Chromosome object.

The spatialPositions parameter may be used to control the output of the spatial positions of individuals in species for which continuous space has been enabled using the dimensionality option of initializeSLiMOptions().  If spatialPositions is F, the output will not contain spatial positions.  If spatialPositions is T, spatial position information will be output if it is available.  If the species does not have continuous space enabled, the spatialPositions parameter will be ignored.

The ages parameter may be used to control the output of the ages of individuals in nonWF simulations.  If ages is F, the output will not contain ages.  If ages is T, ages will be output for nonWF models.  In WF simulations, the ages parameter will be ignored.

The ancestralNucleotides parameter may be used to control the output of the ancestral nucleotide sequence in nucleotide-based models.  If ancestralNucleotides is F, the output will not contain ancestral nucleotide information.  This option is provided because the ancestral sequence may be quite large, for models with a long chromosome.  If the model is not nucleotide-based (as enabled with the nucleotideBased parameter to initializeSLiMOptions()), the ancestralNucleotides parameter will be ignored.  Note that in nucleotide-based models the output format will always include the nucleotides associated with any nucleotide-based mutations; the ancestralNucleotides flag governs only the ancestral sequence.

The pedigreeIDs parameter may be used to request that pedigree IDs be written out.  This option is turned off (F) by default, for brevity.  This option may only be used if SLiM’s optional pedigree tracking has been enabled with initializeSLiMOptions(keepPedigrees=T).

Finally, the objectTags parameter may be used to request that tag values for objects be written out.  This option is turned off (F) by default, for brevity; if it turned on (T), the values of all tags for all objects of supported classes (Chromosome, Individual, Haplosome, Mutation) will be written.  For individuals, the tag, tagF, tagL0, tagL1, tagL2, tagL3, and tagL4 properties will be written; for chromosomes, haplosomes, and mutations, the tag property will be written.  If there is other state that you wish you persist, such as tags on objects of other classes, values attached to objects with setValue(), and so forth, you should persist that state in separate files using calls such as writeFile().

Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

+ (void)outputIndividualsToVCF([Ns$ filePath = NULL], [logical$ append = F], [Niso<Chromosome>$ chromosome = NULL], [logical$ outputMultiallelics = T], [logical$ simplifyNucleotides = F], [logical$ outputNonnucleotides = T])

Output the state of the target vector of individuals in VCF format.  If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.  This method is quite similar to the Subpopulation method outputVCFSample(), but (1) it can produce output for any vector of individuals, rather than sampling from a single population; (2) it can produce output regarding the genetics for all chromosomes or for just one focal chromosome, whereas outputVCFSample() can only output data for a single chromosome; and (3) because it can output genetic information for more than one chromosome, the groupAsIndividuals option provided by outputVCFSample() is not available for outputIndividualsToVCF(); each VCF sample has to correspond directly to one individual.

The chromosome parameter specifies a focal chromosome for which the genetics of the target individuals will be output.  If chromosome is NULL, all chromosomes will be output (distinguished in the VCF output by the chromosome symbol output in the CHROM column); otherwise, chromosome may specify the focal chromosome with an integer chromosome id, a string chromosome symbol, or a Chromosome object.

The parameters outputMultiallelics, simplifyNucleotides, and outputNonnucleotides affect the format of the output produced.

Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

+ (object<Mutation>)readIndividualsFromVCF(string$ filePath, [Nio<MutationType>$ mutationType = NULL])

Read new mutations from the VCF format file at filePath and add them to the target individuals.  The number of target individuals must match the number of samples represented in the VCF file; each sample will be associated with a corresponding target individual, in the order that the samples and the target individuals are provided.  To read into all of the individuals in a given subpopulation pN, simply call pN.individuals.readIndividualsFromVCF(), assuming the subpopulation’s size matches the number of samples in the VCF file.  A vector containing all of the mutations created by readIndividualsFromVCF() is returned (not necessarily in the order of the corresponding VCF call lines).

This method and the readHaplosomesFromVCF() method of Haplosome provide two alternative ways of reading VCF data, focused on the perspective of either individuals (this method) or haplosomes (the Haplosome method).  As described above, this method draws a correspondence between VCF samples and individuals, whereas the Haplosome method draws a correspondence between VCF calls and haplosomes.  For example, if a VCF call line contained a series of calls like “1|1 0|1 1 0 1|0” this method would see that as calls for five individuals, three of which are diploid for the chromosome being called, and two of which are haploid.  That would make sense if, for example, the chromosome being called is an X chromosome; the diploid individuals would be females, the haploid individuals would be males.  The vector of target individuals would need to contain two females, then two males, and then a female, so that the structure of the calls matched the haplosome structure of the individuals, or an error would result.  The readHaplosomesFromVCF() method of Haplosome, on the other hand, would see that same series of calls as corresponding to eight haplosomes, and would assign each call to the corresponding non-null target haplosome, without regard to whether the VCF’s grouping into diploid and haploid calls corresponded to any coherent structure of individuals in the SLiM model.  Each approach has advantages and disadvantages.  This method provides much more error-checking and safety when your intention is to read individual-level data from VCF; it can check that the ploidy in the VCF data matches the ploidy of each target individual, and that diploid calls like 1|1 get assigned to the two haplosomes of a single individual correctly.  The readHaplosomesFromVCF() method of Haplosome does not perform such checks, and can push VCF data into any arbitrary set of haplosomes, so it provides more power and flexibility, but less error-checking and less intelligence.

Because this method works at the level of individuals, it can read VCF data associated with multiple chromosomes into a multi-chromosome SLiM model and place mutations into the correct haplosomes of each individual based upon the CHROM column of the VCF file (which readHaplosomesFromVCF() cannot do).  For this to work, the values in the CHROM column of the call lines must correspond exactly to chromosome symbols in the SLiM model, as provided to initializeChromosome().  The call lines in the input file may be in any order (they do not have to be sorted by CHROM value, or any other such requirement).  The vector of mutations returned will contain all of the mutations created; when reading multi-chromosome data, that returned vector will therefore contain a mix of mutations with different associated chromosomes.  Alternatively, it would work equally well to make a separate call to readIndividualsFromVCF() for each chromosome, providing each call with a separate VCF file that contains only the mutations associated with one chromosome; in that case, each call would return only the mutations added to the chromosome associated with that call, which might be more convenient if post-processing of the returned mutations is necessary.

As in readHaplosomesFromVCF(), a call of ~ represents the fact that an individual has no genetic information for the chromosome being called; for example, a call line for a mutation on a Y chromosome should have haploid calls for male individuals (they have or do not have the called mutation), and calls of ~ for female individuals (they have no Y haplosome at all).  This convention was invented for SLiM, since the VCF standard does not seem to say anything about how sex chromosomes should be represented (or anything about other types of chromosomes that might be absent from some individuals).

The readHaplosomesFromVCF() method’s documentation provides many important details on how SLiM treats various VCF fields during input; those details will not be repeated here, for brevity.

– (float)relatedness(object<Individual> individuals, [Niso<Chromosome>$ chromosome = NULL])

Returns a vector containing the degrees of relatedness between the receiver and each of the individuals in individuals.  The relatedness between A and B is always 1.0 if A and B are actually the same individual; this facility works even if SLiM’s optional pedigree tracking is not enabled (in which case all other relatedness values will be 0.0).  Otherwise, if pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), this method will use the pedigree information to construct a relatedness estimate.  The relatedness is calculated based upon the type of the chromosome specified by chromosome (as an integer id, a string symbol, or a Chromosome object); if chromosome is NULL, it is assumed to be the single chromosome present in the model, or if more than one chromosome is present, an error results and the chromosome must be explicitly given.

More specifically, this method uses all available pedigree information from the grandparental and parental pedigree records of A and B to compute an estimate of the degree of consanguinity between A and B.  When considering a diploid autosome, siblings have a relatedness of 0.5, as do parents to their children and vice versa; cousins have a relatedness of 0.125; and so forth.  If, according to the pedigree information available, A and B have no blood relationship, the value returned is 0.0.  Note that the value returned by relatedness() is what is called the “coefficient of relationship” between the two individuals (Wright, 1922; https://doi.org/10.1086/279872), and ranges from 0.0 to 1.0.

There is another commonly used metric of relatedness, called the “kinship coefficient”, that reflects the probability of identity by descent between two individuals A and B.  In general, it is approximately equal to one-half of the coefficient of relationship; if an approximate estimate of the kinship coefficient is acceptable, especially in models in which individuals are expected to be outbred, you can simply divide relatedness() by two.  However, it should be noted that Wright’s coefficient of relationship is not a measure of the probability of identity by descent, and so it is not exactly double the kinship coefficient; they actually measure different things.  More precisely, the relationship between them is r = 2φ/sqrt((1+fA)(1+fB)), where r is Wright’s coefficient of relatedness, φ is the kinship coefficient, and fA and fB are the inbreeding coefficients of A and B respectively.

Note that this relatedness is simply pedigree-based relatedness, and does not necessarily correspond to genetic relatedness, because of the effects of factors like assortment and recombination.  If a metric of actual genetic relatedness is desired, tree-sequence recording can be used after simulation is complete, to compute the exact genetic relatedness between individuals based upon the complete ancestry tree (a topic which is beyond the scope of this manual).  Actual genetic relatedness cannot presently be calculated during a simulation run; the information is implicitly contained in the recorded tree-sequence tables, but calculating it is too computationally expensive to be reasonable.

This method assumes that the grandparents (or the parents, if grandparental information is not available) are themselves unrelated and that they are not inbred; this assumption is necessary because we have no information about their parentage, since SLiM’s pedigree tracking information only goes back two generations.  Be aware that in a model where inbreeding or selfing occurs at all (including “incidental selfing”, where a hermaphroditic individual happens to choose itself as a mate), some level of “background relatedness” will be present and this assumption will be violated.  In such circumstances, relatedness() will therefore tend to underestimate the degree of relatedness between individuals, and the greater the degree of inbreeding, the greater the underestimation will be.  If inbreeding is allowed in a model – and particularly if it is common – the results of relatedness() should therefore not be taken as an estimate of absolute relatedness, but can still be useful as an estimate of relative relatedness (indicating that, say, A appears from the information available to be more closely related to B than it is to C).

See also sharedParentCount() for a different metric of relatedness.

+ (void)setSpatialPosition(float position)

Sets the spatial position of the individual (as accessed through the spatialPosition property).  The length of position (the number of coordinates in the spatial position of an individual) depends upon the spatial dimensionality declared with initializeSLiMOptions().  If the spatial dimensionality is zero (as it is by default), it is an error to call this method.  The elements of position are set into the values of the x, y, and z properties (if those properties are encompassed by the spatial dimensionality of the simulation).  In other words, if the declared dimensionality is "xy", calling individual.setSpatialPosition(c(1.0, 0.5)) property is equivalent to individual.x = 1.0; individual.y = 0.5; individual.z is not set (even if a third value is supplied in position) since it is not encompassed by the simulation’s dimensionality in this example.

Note that this is an Eidos class method, somewhat unusually, which allows it to work in a special way when called on a vector of individuals.  When the target vector of individuals is non-singleton, this method can do one of two things.  If position contains just a single point (i.e., is equal in length to the spatial dimensionality of the model), the spatial position of all of the target individuals will be set to the given point.  Alternatively, if position contains one point per target individual (i.e., is equal in length to the number of individuals multiplied by the spatial dimensionality of the model), the spatial position of each target individual will be set to the corresponding point from position (where the point data is concatenated, not interleaved, just as it would be returned by accessing the spatialPosition property on the vector of target individuals).  Calling this method with a position vector of any other length is an error.

– (integer)sharedParentCount(object<Individual> individuals)

Returns a vector containing the number of parents shared between the receiver and each of the individuals in individuals.  The number of shared parents between A and B is always 2 if A and B are actually the same individual; this facility works even if SLiM’s optional pedigree tracking is not enabled (in which case all other relatedness values will be 0).  Otherwise, if pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), this method will use the pedigree information to construct a relatedness estimate.

More specifically, this method uses the parental pedigree IDs from the pedigree records of a pair of individuals to count the number of shared parents between them, such that full siblings (with all of the same parents) have a count of 2, and half siblings (with half of the same parents) have a count of 1.  If possible parents of the two individuals are A, B, C, and D, then the shared parent count is as follows, for some illustrative examples.  The first column showing the two parents of the first individual, the second column showing the two parents of the second individual; note that the two parents of an individual can be the same due to cloning or selfing:

AB CD 0 (no shared parents)

AB CC 0 (no shared parents)

AB AC 1 (half siblings)

AB AA 1 (half siblings)

AA AB 1 (half siblings)

AB AB 2 (full siblings)

AB BA 2 (full siblings)

AA AA 2 (full siblings)

This method does not estimate consanguinity.  For example, if one individual is itself a parent of the other individual, that is irrelevant for this method.  Similarly, in simulations of sex chromosomes, the sexes of the parents are irrelevant, even if no genetic material would have been inherited from a given parent.  See relatedness() for an assessment of pedigree-based relatedness that does estimate the consanguinity of individuals.  The sharedParentCount() method is preferable if your exact question is simply whether individuals are full siblings, half siblings, or non-siblings; in other cases, relatedness() is probably more useful.

 (float$)sumOfMutationsOfType(io<MutationType>$ mutType)

Returns the sum of the selection coefficients of all mutations that are of the type specified by mutType, out of all of the mutations in the haplosomes of the individual.  This is often useful in models that use a particular mutation type to represent QTLs with additive effects; in that context, sumOfMutationsOfType() will provide the sum of the additive effects of the QTLs for the given mutation type.  This method is provided for speed; it is much faster than the corresponding Eidos code.  Note that this method also exists on Haplosome, for cases in which the sum for just one haplosome is desired.

 (object<Mutation>)uniqueMutationsOfType(io<MutationType>$ mutType)

This method has been deprecated, and may be removed in a future release of SLiM.  Its functionality was replaced by mutationsFromHaplosomes() in SLiM 5.0.

Returns an object vector of all the mutations that are of the type specified by mutType, out of all of the mutations in the individual.  Mutations present in both homologous haplosomes will occur only once in the result of this method, and the mutations for a given chromosomes will be given in sorted order by position, so in single-chromosome simulations this method is similar to sortBy(unique(individual.haplosomes.mutationsOfType(mutType)), "position").  (Even with a single chromosome it is not identical to that call, since if multiple mutations exist at the exact same position, they may be sorted differently by this method than they would be by sortBy().)  If you just need a count of the matching Mutation objects, rather than a vector of the matches, use -countOfMutationsOfType().  This method is provided for speed; it is much faster than the corresponding Eidos code.  Indeed, it is faster than just individual.haplosomes.mutationsOfType(mutType), and gives uniquing and sorting on top of that, so it is advantageous unless duplicate entries for homozygous mutations are actually needed.

+ (integer)zygosityOfMutations([No<Mutation> mutations = NULL], [integer$ hemizygousValue = 1], [integer$ haploidValue = 1])

Returns an integer matrix with the target individuals’ zygosity for all of the Mutation objects passed in mutations.  If the optional mutations argument is NULL (the default), zygosity values will be returned for all of the active Mutation objects in the species – the same Mutation objects, and in the same order, as would be returned by the mutations property of sim, in other words.  The returned matrix has one column for each individual and one row for each mutation.

For a mutation on a diploid chromosome, the zygosity will be either 0 (absent), 1 (heterozygous), or 2 (homozygous).  This is the straightforward “base case” that is usually meant by the term “zygosity”.

If one of the two haplosomes of an intrinsically diploid chromosome is a null haplosome (as would be the case for an X chromosome in a male individual, for example), mutations present in the non-null haplosome are called “hemizygous”, and the zygosity returned for them is configurable using the hemizygousValue parameter.  By default, the zygosity returned for hemizygous mutations is 1.

Finally, although the term “zygosity” is not usually used in the context of haploidy, this method nevertheless supports intrinsically haploid chromosomes; the zygosity returned for mutations present on an intrinsically haploid chromosome is configurable using the haploidValue parameter.  By default, the zygosity returned for haploid mutations is 1.

For a large number of mutations – and especially for a mutations value of NULL, representing all mutations in the species – this method should be much more efficient than using methods such as containsMutations() to assess zygosity.  Nevertheless, calculating fitness effects in script based upon zygosity will generally be slower than SLiM’s internal fitness calculations.  Note that for just one or a few mutations – especially if mutations contains just a small fraction of all of the mutations in the species – this method will probably be much slower than alternative approaches; this method is optimized for the bulk case.

See also the method mutationsFromHaplosomes(), which provides an alternative approach for assessing the zygosity of mutations in an individual.

5.8  Class InteractionType

5.8.1  InteractionType properties

id => (integer$)

The identifier for this interaction type; for interaction type i3, for example, this is 3.

maxDistance <–> (float$)

The maximum distance over which this interaction will be evaluated.  For inter-individual distances greater than maxDistance, the interaction strength will be zero.

reciprocal => (logical$)

The reciprocality of the interaction, as specified in initializeInteractionType().  This will be T for reciprocal interactions (those for which the interaction strength of B upon A is equal to the interaction strength of A upon B), and F otherwise.

sexSegregation => (string$)

The sex-segregation of the interaction, as specified in initializeInteractionType() or with setConstraints().  For non-sexual simulations, this will be "**".  For sexual simulations, this string value indicates the sex of individuals feeling the interaction, and the sex of individuals exerting the interaction; see initializeInteractionType() for details.

spatiality => (string$)

The spatial dimensions used by the interaction, as specified in initializeInteractionType().  This will be "" (the empty string) for non-spatial interactions, or "x", "y", "z", "xy", "xz", "yz", or "xyz", for interactions using those spatial dimensions respectively.  The specified dimensions are used to calculate the distances between individuals for this interaction.  The value of this property is always the same as the value given to initializeInteractionType().

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to interaction types.

5.8.2  InteractionType methods

– (float)clippedIntegral(No<Individual> receivers)

Returns a vector containing the integral of the interaction function as experienced by each of the individuals in receivers.  For each given individual, the interaction function is clipped to the edges of the spatial bounds of the subpopulation that individual inhabits; the individual’s spatial position must be within bounds or an error is raised.  A periodic boundary will, correctly, not clip the interaction function.  The interaction function is also clipped to the interaction’s maximum distance; that distance must be less than half of the extent of the spatial bounds in each dimension (so that, for a given dimension, the interaction function is clipped by the spatial bounds on only one side), otherwise an error is raised.  Note that receiver constraints are not applied; an individual might not actually receive any interactions because of those constraints, but it is still considered to have the same interaction function integral.  If receivers is NULL, the maximal integral is returned, as would be experienced by an individual farther than the maximum distance from any edge.  The evaluate() method must have been previously called for the receiver subpopulation, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

The computed value of the integral is not exact; it is calculated by an approximate numerical method designed to be fast, but the error should be fairly small (typically less than 1% from the true value).  A large amount of computation will occur the first time this method is called (perhaps taking more than a second, depending upon hardware), but subsequent calls should be very fast.  This method does not invoke interaction() callbacks; the calculated integrals are only for the interaction function itself, and so will not be accurate if interaction() callbacks modify the relationship between distance and interaction strength.  For this reason, the overhead of the first call will not reoccur when individuals move or when the interaction is re-evaluated; for typical models, the initial overhead will be incurred only once.  The initial overhead will reoccur, however, if the interaction function itself, or the maximum interaction distance, are changed; frequent change of those parameters may render the performance of this method unacceptable.

The integral values returned by clippedIntegral() can be useful for computing interaction metrics that are scaled by the amount of “interaction field” (to coin a term) that is present for a given individual, producing metrics of interaction density.  Notably, the localPopulationDensity() method automatically incorporates the mechanics of clippedIntegral() into the calculations it performs; see that method’s documentation for further discussion of this concept.  This approach can also be useful with the interactingNeighborCount() method, provided that the interaction function is of type "f" (since the neighbor count does not depend upon interaction strength).

– (float)distance(object<Individual>$ receiver, [No<Individual> exerters = NULL])

Returns a vector containing distances between receiver and the individuals in exerters.  If exerters is NULL (the default), then a vector of the distances from receiver to all individuals in its subpopulation (including itself) is returned; this case may be handled differently internally, for greater speed, so supplying NULL is preferable to supplying the vector of all individuals in the subpopulation explicitly.  Otherwise, all individuals in exerters must belong to a single subpopulation (but not necessarily the same subpopulation as receiver).  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

Importantly, distances are calculated according to the spatiality of the InteractionType (as declared in initializeInteractionType()), not the dimensionality of the model as a whole (as declared in initializeSLiMOptions()).  The distances returned are therefore the distances that would be used to calculate interaction strengths.  However, distance() will return finite distances for all pairs of individuals, even if the individuals are non-interacting due to the maximum interaction distance or the interaction constraints; the distance() between an individual and itself will thus be 0.  See interactionDistance() for an alternative distance definition.

– (float)distanceFromPoint(float point, object<Individual> exerters)

Returns a vector containing distances between the point given by the spatial coordinates in point, which may be thought of as the “receiver”, and individuals in exerters.  The point vector is interpreted as providing coordinates precisely as specified by the spatiality of the interaction type; if the interaction type’s spatiality is "xz", for example, then point[0] is assumed to be an x value, and point[1] is assumed to be a z value, and point must be exactly two elements in length.  Be careful; this means that in general it is not safe to pass an individual’s spatialPosition property for point, for example (although it is safe if the spatiality of the interaction matches the dimensionality of the simulation); other properties on Individual exist for getting the individual’s coordinates in a particular spatiality, such as the xz property for this example.  A coordinate for a periodic spatial dimension must be within the spatial bounds for that dimension, since coordinates outside of periodic bounds are meaningless (pointPeriodic() may be used to ensure this); coordinates for non-periodic spatial dimensions are not restricted.

All individuals in exerters must belong to a single subpopulation; the evaluate() method must have been previously called for that subpopulation, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

Importantly, distances are calculated according to the spatiality of the InteractionType (as declared in initializeInteractionType()) not the dimensionality of the model as a whole (as declared in initializeSLiMOptions()).  The distances are therefore interaction distances: the distances that are used to calculate interaction strengths.  However, the maximum interaction distance and interaction constraints are not used.

This method replaces the distanceToPoint() method that existed prior to SLiM 4.

– (object)drawByStrength(object<Individual> receiver, [integer$ count = 1], [No<Subpopulation>$ exerterSubpop = NULL], [logical$ returnDict = F])

Returns an object<Individual> vector containing up to count individuals drawn from exerterSubpop, or if that is NULL (the default), then from the subpopulation of receiver, which must be singleton in the default mode of operation (but see below).  The probability of drawing particular individuals is proportional to the strength of interaction they exert upon receiver (which is zero for receiver itself).  All exerters must belong to a single subpopulation (but not necessarily the same subpopulation as receiver).  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.

This method may be used with either spatial or non-spatial interactions, but will be more efficient with spatial interactions that set a short maximum interaction distance.  Draws are done with replacement, so the same individual may be drawn more than once; sometimes using unique() on the result of this call is therefore desirable.  If more than one draw will be needed, it is much more efficient to use a single call to drawByStrength(), rather than drawing individuals one at a time.  Note that if no individuals exert a non-zero interaction strength upon receiver, the vector returned will be zero-length; it is important to consider this possibility.

Beginning in SLiM 4.1, this method has a vectorized mode of operation in which the receiver parameter may be non-singleton.  To switch the method to this mode, pass T for returnDict, rather than the default of F (the operation of which is described above).  In this mode, the return value is a Dictionary object instead of a vector of Individual objects.  This dictionary uses integer keys that range from 0 to N-1, where N is the number of individuals passed in receiver; these keys thus correspond directly to the indices of the individuals in receiver, and there is one entry in the dictionary for each receiver.  The value in the dictionary, for a given integer key, is an object<Individual> vector with the individuals drawn for the corresponding receiver, exactly as described above for the non-vectorized case.  The results for each receiver can therefore be obtained from the returned dictionary with getValue(), passing the index of the receiver.  The speed of this mode of operation will probably be similar to the speed of making N separate non-vectorized calls to drawByStrength(), when running single-threaded.  When running multi-threaded, however, a substantial performance improvement may be realized by using the vectorized version of this method, since the queries can then be executed in parallel.  In this mode of operation, all receivers must belong to the same subpopulation.

– (void)evaluate(io<Subpopulation> subpops)

Snapshots model state in preparation for the use of the interaction, for the receiver and exerter subpopulations specified by subpops.  The subpopulations may be supplied either as integer IDs, or as Subpopulation objects.  This method will discard all previously cached data for the subpopulation(s), and will cache the current spatial positions of all individuals they contain (so that the spatial positions of those individuals may then change without disturbing the state of the interaction at the moment of evaluation).  It will also cache which individuals in the subpopulation are eligible to act as exerters, according to the configured exerter constraints, but it will not cache such eligibility information for receiver constraints (which are applied at the time a spatial query is made).  Particular interaction distances and strengths are not computed by evaluate(), and interaction() callbacks will not be called in response to this method; that work is deferred until required to satisfy a query (at which point the tick and cycle counters may have advanced, so be careful with the tick ranges used in defining interaction() callbacks).

You must explicitly call evaluate() at an appropriate time in the tick cycle before the interaction is used, but after any relevant changes have been made to the population.  SLiM will invalidate any existing interactions after any portion of the tick cycle in which new individuals have been born or existing individuals have died.  In a WF model, this occurs just before late() events execute (see the WF tick cycle diagram), so late() events are often the appropriate place to put evaluate() calls, but first() or early() events can work too if the interaction is not needed until that point in the tick cycle anyway. In nonWF models, on the other hand, new offspring are produced just before early() events and then individuals die just before late() events (see the nonWF tick cycle diagram), so interactions will be invalidated twice during each tick cycle.  This means that in a nonWF model, an interaction that influences reproduction should usually be evaluated in a first() event, while an interaction that influences fitness or mortality should usually be evaluated in an early() event (and an interaction that affects both may need to be evaluated at both times).

If an interaction is never evaluated for a given subpopulation, it is guaranteed that there will be essentially no memory or computational overhead associated with the interaction for that subpopulation.  Furthermore, attempting to query an interaction for a receiver or exerter in a subpopulation that has not been evaluated is guaranteed to raise an error.

– (integer)interactingNeighborCount(object<Individual> receivers, [No<Subpopulation>$ exerterSubpop = NULL])

Returns the number of interacting individuals for each individual in receivers, within the maximum interaction distance according to the distance metric of the InteractionType, from among the exerters in exerterSubpop (or, if that is NULL, then from among all individuals in the receiver’s subpopulation).  More specifically, this method counts the number of individuals which can exert an interaction upon each receiver (which does not include the receiver itself).  All of the receivers must belong to a single subpopulation, and all of the exerters must belong to a single subpopulation, but those two subpopulations do not need to be the same.  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.

This method is similar to nearestInteractingNeighbors() (when passed a large count so as to guarantee that all interacting individuals are returned), but this method returns only a count of the interacting individuals, not a vector containing the individuals.

Note that this method uses interaction eligibility as a criterion; it will not count neighbors that do not exert an interaction upon a given receiver (due to the configured receiver or exerter constraints).  (It also does not count a receiver as a neighbor of itself.)  If a count of all neighbors is desired, rather than just interacting neighbors, use neighborCount().  If the InteractionType is non-spatial, this method may not be called.

– (float)interactionDistance(object<Individual>$ receiver, [No<Individual> exerters = NULL])

Returns a vector containing interaction-dependent distances between receiver and individuals in exerters.  If exerters is NULL (the default), then a vector of the interaction-dependent distances from receiver to all individuals in its subpopulation (including receiver itself) is returned; this case may be handled much more efficiently than if a vector of all individuals in the subpopulation is explicitly provided.  Otherwise, all individuals in exerters must belong to a single subpopulation (but not necessarily the same subpopulation as receiver).  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

Importantly, distances are calculated according to the spatiality of the InteractionType (as declared in initializeInteractionType()), not the dimensionality of the model as a whole (as declared in initializeSLiMOptions()).  The distances returned are therefore the distances that would be used to calculate interaction strengths.  In addition, interactionDistance() will return INF as the distance between receiver and any individual which does not exert an interaction upon receiver; the interactionDistance() between an individual and itself will thus be INF, and likewise for pairs excluded from interacting by receiver constraints, exerter constraints, or the maximum interaction distance of the interaction type.  See distance() for an alternative distance definition.

– (float)localPopulationDensity(object<Individual> receivers, [No<Subpopulation>$ exerterSubpop = NULL])

Returns a vector of the local population density present at the location of each individual in receivers, which does not need to be a singleton; indeed, it can be a vector of all of the individuals in a given subpopulation.  However, all receivers must be in the same subpopulation.  The local population density is computed from exerters in exerterSubpop, or if that is NULL (the default), then from the receiver’s subpopulation.  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.

Population density is estimated using interaction strengths, effectively doing a kernel density estimate using the interaction function as the kernel.  What is returned is computed as the total interaction strength present at a given point, divided by the integral of the interaction function around that point after clipping by the spatial bounds of the exerter subpopulation (what one might think of as the amount of “interaction field” around the point).  This provides an estimate of local population density, in units of individuals per unit area, as a weighted average over the area covered by the interaction function, where the weight of each exerter in the average is the value of the interaction function at that exerter’s position.  This can also be thought of as a measure of the amount of interaction happening per unit of interaction field in the space surrounding the point.

To calculate the clipped integral of the interaction function, this method uses the same numerical estimator used by the clippedIntegral() method of InteractionType, and all of the caveats described for that method apply here also; notably, all individuals must be within spatial bounds, the maximum interaction distance must be less than half the spatial extent of the subpopulation, and interaction() callbacks are not used (and so, for this method, are not allowed to be active).  See the documentation for clippedIntegral() for further discussion of the details of these calculations.

To calculate the total interaction strength around the position of a receiver, this method uses the same machinery as the totalOfNeighborStrengths() method of InteractionType, except that – in contrast to other InteractionType methods – the interaction strength exerted by the receiver itself is included in the total (if the exerter subpopulation is the receiver’s own subpopulation).  This is because population density at the location of an individual includes the individual itself.  If this is not desirable, the totalOfNeighborStrengths() method should probably be used.

To see the point of this method, consider a receiver located near the edge of the spatial bounds of its subpopulation.  Some portion of the interaction function that surrounds that receiver falls outside the spatial bounds of its subpopulation, and will therefore never contain an interacting exerter.  If, for example, interaction strengths are used as a measure of competition, this receiver will therefore have an advantage, because it will never feel any competition from the portion of its range that falls outside spatial bounds.  However, that portion of its range is presumably also not available to the receiver itself, for foraging or hunting, in which case this advantage is not biologically realistic, but is instead just an undesirable “edge effect” artifact.  Dividing by the integral of the interaction function, clipped to the spatial bounds, provides a way to compensate for this edge effect.  A nice side effect of using local population densities instead of total interaction strengths is that the maximum interaction strength passed to setInteractionFunction() no longer matters; it cancels out when the total interaction strength is divided by the receiver’s clipped integral.  However, the shape of the interaction function does still matter; it determines the relative weights used for exerters at different distances from the position of the receiver.

– (object)nearestInteractingNeighbors(object<Individual> receiver, [integer$ count = 1], [No<Subpopulation>$ exerterSubpop = NULL], [logical$ returnDict = F])

Returns an object<Individual> vector containing up to count interacting individuals that are spatially closest to receiver, according to the distance metric of the InteractionType, from among the exerters in exerterSubpop (or, if that is NULL, then from among all individuals in the receiver’s subpopulation).  More specifically, this method returns only individuals which can exert an interaction upon receiver, which must be singleton in the default mode of operation (but see below).  To obtain all of the interacting individuals within the maximum interaction distance of receiver, simply pass a value for count that is greater than or equal to the size of the exerter subpopulation.  Note that if fewer than count interacting individuals are within the maximum interaction distance, the vector returned may be shorter than count, or even zero-length; it is important to check for this possibility even when requesting a single neighbor.  If only the number of interacting individuals is needed, use interactingNeighborCount() instead.  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

Note that this method uses interaction eligibility as a criterion; it will not return neighbors that cannot exert an interaction upon the receiver (due to the configured receiver or exerter constraints).  (It will also never return the receiver as a neighbor of itself.)  To find all neighbors of a receiver, whether they can interact with it or not, use nearestNeighbors().

Beginning in SLiM 4.1, this method has a vectorized mode of operation in which the receiver parameter may be non-singleton.  To switch the method to this mode, pass T for returnDict, rather than the default of F (the operation of which is described above).  In this mode, the return value is a Dictionary object instead of a vector of Individual objects.  This dictionary uses integer keys that range from 0 to N-1, where N is the number of individuals passed in receiver; these keys thus correspond directly to the indices of the individuals in receiver, and there is one entry in the dictionary for each receiver.  The value in the dictionary, for a given integer key, is an object<Individual> vector with the interacting neighbors found for the corresponding receiver, exactly as described above for the non-vectorized case.  The results for each receiver can therefore be obtained from the returned dictionary with getValue(), passing the index of the receiver.  The speed of this mode of operation will probably be similar to the speed of making N separate non-vectorized calls to nearestInteractingNeighbors(), when running single-threaded.  When running multi-threaded, however, a substantial performance improvement may be realized by using the vectorized version of this method, since the queries can then be executed in parallel.  In this mode of operation, all receivers must belong to the same subpopulation.

– (object)nearestNeighbors(object<Individual> receiver, [integer$ count = 1], [No<Subpopulation>$ exerterSubpop = NULL], [logical$ returnDict = F])

Returns an object<Individual> vector containing up to count individuals that are spatially closest to receiver, according to the distance metric of the InteractionType, from among the exerters in exerterSubpop (or, if that is NULL, then from among all individuals in the receiver’s subpopulation).  In the default mode of operation, receiver must be singleton (but see below).  To obtain all of the individuals within the maximum interaction distance of receiver, simply pass a value for count that is greater than or equal to the size of individual’s subpopulation.  Note that if fewer than count individuals are within the maximum interaction distance, the vector returned may be shorter than count, or even zero-length; it is important to check for this possibility even when requesting a single neighbor.  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

Note that this method does not use interaction eligibility as a criterion; it will return neighbors that could not interact with the receiver due to the configured receiver or exerter constraints.  (It will never return the receiver as a neighbor of itself, however.)  To find only neighbors that are eligible to exert an interaction upon the receiver, use nearestInteractingNeighbors().

Beginning in SLiM 4.1, this method has a vectorized mode of operation in which the receiver parameter may be non-singleton.  To switch the method to this mode, pass T for returnDict, rather than the default of F (the operation of which is described above).  In this mode, the return value is a Dictionary object instead of a vector of Individual objects.  This dictionary uses integer keys that range from 0 to N-1, where N is the number of individuals passed in receiver; these keys thus correspond directly to the indices of the individuals in receiver, and there is one entry in the dictionary for each receiver.  The value in the dictionary, for a given integer key, is an object<Individual> vector with the neighbors found for the corresponding receiver, exactly as described above for the non-vectorized case.  The results for each receiver can therefore be obtained from the returned dictionary with getValue(), passing the index of the receiver.  The speed of this mode of operation will probably be similar to the speed of making N separate non-vectorized calls to nearestNeighbors(), when running single-threaded.  When running multi-threaded, however, a substantial performance improvement may be realized by using the vectorized version of this method, since the queries can then be executed in parallel.  In this mode of operation, all receivers must belong to the same subpopulation.

– (object<Individual>)nearestNeighborsOfPoint(float point, io<Subpopulation>$ exerterSubpop, [integer$ count = 1])

Returns up to count individuals in exerterSubpop that are spatially closest to the point given by the spatial coordinates in point, which may be thought of as the “receiver”, according to the distance metric of the InteractionType.  The point vector is interpreted as providing coordinates precisely as specified by the spatiality of the interaction type; if the interaction type’s spatiality is "xz", for example, then point[0] is assumed to be an x value, and point[1] is assumed to be a z value, and point must be exactly two elements in length.  Be careful; this means that in general it is not safe to pass an individual’s spatialPosition property for point, for example (although it is safe if the spatiality of the interaction matches the dimensionality of the simulation); other properties on Individual exist for getting the individual’s coordinates in a particular spatiality, such as the xz property for this example.  A coordinate for a periodic spatial dimension must be within the spatial bounds for that dimension, since coordinates outside of periodic bounds are meaningless (pointPeriodic() may be used to ensure this); coordinates for non-periodic spatial dimensions are not restricted.

The subpopulation may be supplied either as an integer ID, or as a Subpopulation object.  To obtain all of the individuals within the maximum interaction distance of point, simply pass a value for count that is greater than or equal to the size of exerterSubpop.  Note that if fewer than count individuals are within the maximum interaction distance, the vector returned may be shorter than count, or even zero-length; it is important to check for this possibility even when requesting a single neighbor.  The evaluate() method must have been previously called for exerterSubpop, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

– (integer)neighborCount(object<Individual> receivers, [No<Subpopulation>$ exerterSubpop = NULL])

Returns the number of neighbors for each individual in receivers, within the maximum interaction distance according to the distance metric of the InteractionType, from among the individuals in exerterSubpop (or, if that is NULL, then from among all individuals in the receiver’s subpopulation).  All of the receivers must belong to a single subpopulation, and all of the exerters must belong to a single subpopulation, but those two subpopulations do not need to be the same.  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.

This method is similar to nearestNeighbors() (when passed a large count so as to guarantee that all neighbors are returned), but this method returns only a count of the individuals, not a vector containing the individuals.

Note that this method does not use interaction eligibility as a criterion; it will count neighbors that cannot exert an interaction upon a receiver (due to the configured receiver or exerter constraints).  (It still does not count a receiver as a neighbor of itself, however.)  If a count of only interacting neighbors is desired, use interactingNeighborCount().  If the InteractionType is non-spatial, this method may not be called.

– (integer$)neighborCountOfPoint(float point, io<Subpopulation>$ exerterSubpop)

Returns the number of individuals in exerterSubpop that are within the maximum interaction distance of the point given by the spatial coordinates in point, which may be thought of as the “receiver”, according to the distance metric of the InteractionType.  The point vector is interpreted as providing coordinates precisely as specified by the spatiality of the interaction type; if the interaction type’s spatiality is "xz", for example, then point[0] is assumed to be an x value, and point[1] is assumed to be a z value, and point must be exactly two elements in length.  Be careful; this means that in general it is not safe to pass an individual’s spatialPosition property for point, for example (although it is safe if the spatiality of the interaction matches the dimensionality of the simulation); other properties on Individual exist for getting the individual’s coordinates in a particular spatiality, such as the xz property for this example.  A coordinate for a periodic spatial dimension must be within the spatial bounds for that dimension, since coordinates outside of periodic bounds are meaningless (pointPeriodic() may be used to ensure this); coordinates for non-periodic spatial dimensions are not restricted.

The subpopulation may be supplied either as an integer ID, or as a Subpopulation object.  The evaluate() method must have been previously called for exerterSubpop, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

This method is similar to nearestNeighborsOfPoint() (when passed a large count so as to guarantee that all neighbors are returned), but this method returns only a count of the individuals, not a vector containing the individuals.

– (void)setConstraints(string$ who, [Ns$ sex = NULL], [Ni$ tag = NULL], [Ni$ minAge = NULL], [Ni$ maxAge = NULL], [Nl$ migrant = NULL], [Nl$ tagL0 = NULL], [Nl$ tagL1 = NULL], [Nl$ tagL2 = NULL], [Nl$ tagL3 = NULL], [Nl$ tagL4 = NULL])

Sets constraints upon which individuals can be receivers and/or exerters, making the target InteractionType measure interactions between only subsets of the population.  The parameter who specifies upon whom the specified constraints apply; it may be "exerter" to set constraints upon exerters, "receiver" to set constraints upon receivers, or "both" to set constraints upon both.  If "both" is used, the same constraints are set for both exerters and receivers; different constraints can be set for exerters versus receivers by making a separate call to setConstraints() for each.  Constraints only affect queries that involve the concept of interaction; for example, they will affect the result of nearestInteractingNeighbors(), but not the result of nearestNeighbors().  The constraints specified by a given call to setConstraints() override all previously set constraints for the category specified (receivers, exerters, or both).

There is a general policy for the remaining arguments: they are NULL by default, and if NULL is used, it specifies “no constraint” for that property (removing any currently existing constraint for that property).  The sex parameter constrains the sex of individuals; it may be "M" or “F" (or "*" as another way of specifying no constraint, for historical reasons).  If sex is "M" or "F", the individuals to which the constraint is applied (potential receivers/exerters) must belong to a sexual species.  The tag parameter constrains the tag property of individuals; if this set, the individuals to which the constraint is applied must have defined tag values.  The minAge and maxAge properties constrain the age property of individuals to the given minimum and/or maximum values; these constraints can only be used in nonWF models.  The migrant property constraints the migrant property of individuals (T constrains to only migrants, F to only non-migrants).  Finally, the tagL0, tagL1, tagL2, tagL3, and tagL4 properties constrain the corresponding logical properties of individuals, requiring them to be either T or F as specified; the individuals to which these constraints are applied must have defined values for the constrained property or properties.  Again, NULL should be supplied (as it is by default) for any property which you do not wish to constrain.

These constraints may be used in any combination, as desired.  For example, calling setConstraints("receivers", sex="M", minAge=5, tagL0=T) constrains the interaction type’s operation so that receivers must be males, with an age of at least 5, with a tagL0 property value of T.  For that configuration the potential receivers used with the interaction type must be sexual (since sex is specified), must be in a nonWF model (since minAge is specified), and must have a defined value for their tagL0 property (since that property is constrained).  Note that the sexSegregation parameter to initializeInteractionType() is a shortcut which does the same thing as the corresponding calls to setConstraints().

Exerter constraints are applied at evaluate() time, whereas receiver constraints are applied at query time; see the InteractionType class documentation for further discussion.  The interaction constraints for an interaction type are normally a constant in simulations; in any case, they cannot be changed when an interaction has already been evaluated, so either they should be set prior to evaluation, or unevaluate() should be called first.

– (void)setInteractionFunction(string$ functionType, ...)

Set the function used to translate spatial distances into interaction strengths for an interaction type.  The functionType may be "f", in which case the ellipsis ... should supply a numeric$ fixed interaction strength; "l", in which case the ellipsis should supply a numeric$ maximum strength for a linear function; "e", in which case the ellipsis should supply a numeric$ maximum strength and a numeric$ lambda (rate) parameter for a negative exponential function; "n", in which case the ellipsis should supply a numeric$ maximum strength and a numeric$ sigma (standard deviation) parameter for a Gaussian function; "c", in which case the ellipsis should supply a numeric$ maximum strength and a numeric$ scale parameter for a Cauchy distribution function; or "t", in which case the ellipsis should supply a numeric$ maximum strength, a numeric$ degrees of freedom, and a numeric$ scale parameter for a t-distribution function.  See the InteractionType class documentation for discussions of these interaction functions.  Non-spatial interactions must use function type "f", since no distance values are available in that case.

The interaction function for an interaction type is normally a constant in simulations; in any case, it cannot be changed when an interaction has already been evaluated, so either it should be set prior to evaluation, or unevaluate() should be called first.

– (float)strength(object<Individual>$ receiver, [No<Individual> exerters = NULL])

Returns a vector containing the interaction strengths exerted upon receiver by the individuals in exerters.  If exerters is NULL (the default), then a vector of the interaction strengths exerted by all individuals in the subpopulation of receiver (including receiver itself, with a strength of 0.0) is returned; this case may be handled much more efficiently than if a vector of all individuals in the subpopulation is explicitly provided.  Otherwise, all individuals in exerters must belong to a single subpopulation (but not necessarily the same subpopulation as receiver).  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.

If the strengths of interactions exerted by a single individual upon multiple individuals are needed instead (the inverse of what this method provides), multiple calls to this method will be necessary, one per pairwise interaction queried; the interaction engine is not optimized for the inverse case, and so it will likely be quite slow to compute.  If the interaction is reciprocal and has the same receiver and exerter constraints, the opposite query should provide identical results in a single efficient call (because then the interactions exerted are equal to the interactions received); otherwise, the best approach might be to define a second interaction type representing the inverse interaction that you wish to be able to query efficiently.

– (lo<Individual>)testConstraints(object<Individual> individuals, string$ constraints, [logical$ returnIndividuals = F])

Tests the individuals in the parameter individuals against the interaction constraints specified by constraints.  The value of constraints may be "receiver" to use the receiver constraints, or "exerter" to use the exerter constraints.  If returnIndividuals is F (the default), a logical vector will be returned, with T values indicating that the corresponding individual satisfied the constraints, F values indicating that it did not.  If returnIndividuals is T, an object vector of class Individual will be returned containing only those elements of individuals that satisfied the constraints (in the same order as individuals).  Note that unlike most queries, the InteractionType does not need to have been evaluated before calling this method, and the individuals passed in need not belong to a single population or even a single species.

This method can be useful for narrowing a vector of individuals down to just those that satisfy constraints.  Outside the context of InteractionType, similar functionality is provided by the Subpopulation method subsetIndividuals().  Note that the use of testConstraints() is somewhat rare; usually, queries are evaluated across a vector of individuals, each of which might or might not satisfy the defined constraints.  Individuals that do not satisfy constraints do not participate in interactions, so their interaction strength with other individuals will simply be zero.

See the setConstraints() method to set up constraints, as well as the sexSegregation parameter to initializeInteractionType().  Note that if the constraints tested involve tag values (including tagL0 / tagL1 / tagL2 / tagL3 / tagL4), the corresponding property or properties of the tested individuals must be defined (i.e., must have been set to a value), or an error will result because the constraints cannot be applied.

– (float)totalOfNeighborStrengths(object<Individual> receivers, [No<Subpopulation>$ exerterSubpop = NULL])

Returns a vector of the total interaction strength felt by each individual in receivers by the exerters in exerterSubpop (or, if that is NULL, then by all individuals in the receiver’s subpopulation).  The receivers parameter does not need to be a singleton; indeed, it can be a vector of all of the individuals in a given subpopulation.  All of the receivers must belong to a single subpopulation, and all of the exerters must belong to a single subpopulation, but those two subpopulations do not need to be the same.  The evaluate() method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.  If the InteractionType is non-spatial, this method may not be called.

For one individual, this is essentially the same as calling nearestInteractingNeighbors() with a large count so as to obtain the complete vector of all interacting neighbors, calling strength() for each of those interactions to get each interaction strength, and adding those interaction strengths together with sum().  This method is much faster than that implementation, however, since all of that work is done as a single operation.  Also, totalOfNeighborStrengths() can total up interactions for more than one receiver in a single vectorized call.

Similarly, for one individual this is essentially the same as calling strength() to get the interaction strengths between a receiver and all individuals in the exerter subpopulation, and then calling sum().  Again, this method should be much faster, since this algorithm looks only at neighbors, whereas calling strength() directly assesses interaction strengths with all other individuals.  This will make a particularly large difference when the subpopulation size is large and the maximum distance of the InteractionType is small.

See localPopulationDensity() for a related method that calculates the total interaction strength divided by the amount of “interaction field” present for an individual (i.e., the integral of the interaction function clipped to the spatial bounds of the subpopulation) to provide an estimate of the “interaction density” felt by an individual.

– (void)unevaluate(void)

Discards all evaluation of this interaction, for all subpopulations.  The state of the InteractionType is reset to a state prior to evaluation.  This can be useful if the model state has changed in such a way that the evaluation already conducted is no longer valid.  For example, if the maximum distance, the interaction function, or the receiver or exerter constraints of the InteractionType need to be changed with immediate effect, or if the data used by an interaction() callback has changed in such a way that previously calculated interaction strengths are no longer correct, unevaluate() allows the interaction to begin again from scratch.

In WF models, all interactions are automatically reset to an unevaluated state at the moment when the new offspring generation becomes the parental generation (at step 4 in the tick cycle).

In nonWF models, all interactions are automatically reset to an unevaluated state twice per tick: immediately after reproduction() callbacks have completed (after step 1 in the tick cycle), and immediately before viability/survival selection (before step 4 in the tick cycle).

Given this automatic invalidation, most simulations have no reason to call unevaluate().

5.9  Class LogFile

5.9.1  LogFile properties

filePath => (string$)

The path of the log file being written to.  This may be changed with setFilePath().

logInterval => (integer$)

The interval for automatic logging; a new row of data will be logged every logInterval ticks.  This may be set with the logInterval parameter to createLogFile() and changed with setLogInterval().  If automatic logging has been disabled, this property will be 0.

precision <–> (integer$)

The precision of float output.  To be exact, precision specifies the preferred number of significant digits that will be output for float values.  The default is 6; values in [1,22] are legal, but 17 is probably the largest value that makes sense given the limits of double-precision floating point.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.

5.9.2  LogFile methods

– (void)addCustomColumn(string$ columnName, string$ source, [* context = NULL])

Adds a new data column with its name provided by columnName.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  The value for the column, when a given row is generated, will be produced by the code supplied in source, which is expected to return either NULL (which will write out NA), or a singleton value of any non-object type.

The context parameter will be set up as a pseudo-parameter, named context, when source is called, allowing the same source code to be used to generate values for multiple data columns; you might, for example, pass the id of the particular Subpopulation object that you wish source to use for its calculations, and source could then use the Community method subpopulationsWithIDs() to look up the subpopulation from the id value provided in context.

Note that the Subpopulation object itself cannot be passed in context, because class Subpopulation is not under retain-release memory management in SLiM, meaning essentially that subpopulation objects can cease to exist unpredictably (because they go extinct, for example).  A reference to such an object cannot be kept long-term by your script (including by LogFile), because if the object ceases to exist, the reference would become invalid and a crash would result.  Passing such an object – one not under retain-release – to addCustomColumn() will therefore raise an error, to safeguard against that possible crash.  The workaround for this limitation is to find a way to look up the desired object, such as the suggestion above of using the subpopulation’s id to look it up.

The use of context is optional; if the default value of NULL is used, then context will be NULL when source is called.

See addMeanSDColumns() for a useful variant.

– (void)addCycle([No<Species>$ species = NULL])

Adds a new data column that provides the cycle counter for species (the same as the value of the cycle property of that species).  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  In single-species models, species may be NULL to indicate that single species.  The column will simply be named cycle in single-species models; an underscore and the name of the species will be appended in multispecies models.

– (void)addCycleStage(void)

Adds a new data column that provides the cycle stage, named cycle_stage.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  The stage is provided as a string, and will typically be "first", "early", "late", or "end" (the latter used for the point in time at which end-of-tick automatic logging occurs).  Other possible values are discussed in the documentation for the cycleStage property of Community, which this column reflects.

– (void)addKeysAndValuesFrom(object<Dictionary>$ source)

This Dictionary method has an override in LogFile to make it illegal to call, since LogFile manages its Dictionary entries.

– (void)addMeanSDColumns(string$ columnName, string$ source, [* context = NULL])

Adds two new data columns with names of columnName_mean and columnName_sd.  The new columns will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  When a given row is generated, the code supplied in source is expected to return either a zero-length vector of any type including NULL (which will write out NA to both columns), or a non-zero-length vector of integer or float values.  In the latter case, the result vector will be summarized in the two columns by its mean and standard deviation respectively.  If the result vector has exactly one value, the standard deviation will be written as NA.

The context parameter is set up as a pseudo-parameter when source is called, as described in addCustomColumn().  See the documentation for that method for further discussion of context, including limitations on its use.

– (void)addPopulationSexRatio([No<Species>$ species = NULL])

Adds a new data column that provides the population sex ratio M:(M+F) for species.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  In single-species models, species may be NULL to indicate that single species.  The column will simply be named sex_ratio in single-species models; an underscore and the name of the species will be appended in multispecies models.  If the species is hermaphroditic, NA will be written.

– (void)addPopulationSize([No<Species>$ species = NULL])

Adds a new data column that provides the total population size for species.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  In single-species models, species may be NULL to indicate that single species.  The column will simply be named num_individuals in single-species models; an underscore and the name of the species will be appended in multispecies models.

– (void)addSubpopulationSexRatio(io<Subpopulation>$ subpop)

Adds a new data column that provides the sex ratio M:(M+F) of the subpopulation subpop, named pX_sex_ratio.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  If the subpopulation exists but has a size of zero, NA will be written.

– (void)addSubpopulationSize(io<Subpopulation>$ subpop)

Adds a new data column that provides the size of the subpopulation subpop, named pX_num_individuals.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  If the subpopulation exists but has a size of zero, 0 will be written.

– (void)addSuppliedColumn(string$ columnName)

Adds a new data column with its name provided by columnName.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().  The value for the column is initially undefined, and will be written as NA.  A different value may (optionally) be provided by calling setSuppliedValue() with a value for columnName.  That value will be used for the column the next time a row is generated (whether automatically or by a call to logRow()), and the column’s value will subsequently be undefined again.  In other words, for any given logged row the default of NA may be kept, or a different value may be supplied.  This allows the value for the column to be set at any point during the tick cycle, which can be convenient if the column’s value depends upon transient state that is no longer available at the time the row is logged.

– (void)addTick(void)

Adds a new data column, named tick, that provides the tick number for the simulation.  The new column will be logged each time that a row is generated, either by automatic logging or by a call to logRow().

– (void)clearKeysAndValues(void)

This Dictionary method has an override in LogFile to make it illegal to call, since LogFile manages its Dictionary entries.

– (void)flush(void)

Flushes all buffered data to the output file, synchronously.  This will make the contents of the file on disk be up-to-date with the running simulation.  Flushing frequently may entail a small performance penalty.  More importantly, if .gz compression has been requested with compress=T the size of the resulting file will be larger – potentially much larger – if flush() is called frequently.  Note that automatic periodic flushing can be requested with the flushInterval parameter to createLogFile().

– (void)logRow(void)

This logs a new row of data, by evaluating all of the generators added to the LogFile with add...() calls.  Note that the new row may be buffered, and thus may not be written out to disk immediately; see flush().  This method may be used instead of, or in conjunction with, automatic logging.

You can get the LogFile instance, in order to call logRow() on it, from community.logFiles, or you can remember it in a global constant with defineConstant().

– (void)setLogInterval([Ni$ logInterval = NULL])

Sets the automatic logging interval.  A logInterval of NULL stops automatic logging immediately.  Other values request that a new row should be logged (as if logRow() were called) at the end of every logInterval ticks (just before the tick count increment, in both WF and nonWF models), starting at the end of the tick in which setLogInterval() was called.

– (void)setFilePath(string$ filePath, [Ns initialContents = NULL], [logical$ append = F], [Nl$ compress = NULL], [Ns$ sep = NULL], [Nl$ header = NULL])

Redirects the LogFile to write new rows to a new filePath.  Any rows that have been buffered but not flushed will be written to the previous file first, as if flush() had been called.  With this call, new initialContents may be supplied, which will either replace any existing file or will be appended to it, depending upon the value of append.  New values may be supplied for compress, sep, and header; the meaning of these parameters is identical to their meaning in createLogFile(), except that a value of NULL for these means “do not change this setting from its previous value”.  In effect, then, this method lets you start a completely new log file at a new path, without having to create and configure a new LogFile object.  The new file will be created (or appended) synchronously, with the specified initial contents.

– (void)setSuppliedValue(string$ columnName, +$ value)

Registers a value, passed in value, to be used for the supplied column named columnName when a row is next logged.  This column must have been added with addSuppliedColumn().  A value of NULL may be passed to log NA, but logging NA is the default behavior for supplied columns in any case.  Otherwise, the value must be a singleton, and its type should match the values previously supplied for the column (otherwise the log file may be difficult to parse, since the values within the column will not be of one consistent type).  See addSuppliedColumn() for further details.

– (void)setValue(is$ key, * value)

This Dictionary method has an override in LogFile to make it illegal to call, since LogFile manages its Dictionary entries.

– (logical$)willAutolog(void)

Returns T if the log file is configured to log a new row automatically at the end of the current tick; otherwise, returns F.  This is useful for calculating a value that will be logged only in ticks when the value is needed.

5.10  Class Mutation

5.10.1  Mutation properties

chromosome => (object<Chromosome>$)

The Chromosome object with which the mutation is associated.

id => (integer$)

The identifier for this mutation.  Each mutation created during a run receives an immutable identifier that will be unique across the duration of the run.  These identifiers are not re-used during a run, except that if a population file is loaded from disk, the loaded mutations will receive their original identifier values as saved in the population file.

isFixed => (logical$)

T if the mutation has fixed (in the SLiM sense of having been converted to a Substitution object), F otherwise.  Since fixed/substituted mutations are removed from the simulation, you will only see this flag be T if you have held onto a mutation beyond its usual lifetime.

isSegregating => (logical$)

T if the mutation is segregating (in the SLiM sense of not having been either lost or converted to a Substitution object), F otherwise.  Since both lost and fixed/substituted mutations are removed from the simulation, you will only see this flag be F if you have held onto a mutation beyond its usual lifetime.  Note that if isSegregating is F, isFixed will let you determine whether the mutation is no longer segregating because it was lost, or because it fixed.

mutationType => (object<MutationType>$)

The MutationType from which this mutation was drawn.

nucleotide <–> (string$)

A string representing the nucleotide associated with this mutation; this will be "A", "C", "G", or "T".  If the mutation is not nucleotide-based, this property is unavailable.

nucleotideValue <–> (integer$)

An integer representing the nucleotide associated with this mutation; this will be 0 (A), 1 (C), 2 (G), or 3 (T).  If the mutation is not nucleotide-based, this property is unavailable.

originTick => (integer$)

The tick in which this mutation arose.

position => (integer$)

The position in the chromosome of this mutation.

selectionCoeff => (float$)

The selection coefficient of the mutation, drawn from the distribution of fitness effects of its MutationType.  If a mutation has a selectionCoeff of s, the multiplicative fitness effect of the mutation in a homozygote is 1+s; in a heterozygote it is 1+hs, where h is the dominance coefficient kept by the mutation type.

Note that this property has a quirk: it is stored internally in SLiM using a single-precision float, not the double-precision float type normally used by Eidos.  This means that if you set a mutation mut’s selection coefficient to some number x, mut.selectionCoeff==x may be F due to floating-point rounding error.  Comparisons of floating-point numbers for exact equality is often a bad idea, but this is one case where it may fail unexpectedly.  Instead, it is recommended to use the id or tag properties to identify particular mutations.

subpopID <–> (integer$)

The identifier of the subpopulation in which this mutation arose.  This property can be used to track the ancestry of mutations through their subpopulation of origin.

If you don’t care which subpopulation a mutation originated in, the subpopID may be used as an arbitrary integer “tag” value for any purpose you wish; SLiM does not do anything with the value of subpopID except propagate it to Substitution objects and report it in output.  (It must still be >= 0, however, since SLiM object identifiers are limited to nonnegative integers).

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.

5.10.2  Mutation methods

– (void)setMutationType(io<MutationType>$ mutType)

Set the mutation type of the mutation to mutType (which may be specified as either an integer identifier or a MutationType object).  This implicitly changes the dominance coefficient of the mutation to that of the new mutation type, since the dominance coefficient is a property of the mutation type.  On the other hand, the selection coefficient of the mutation is not changed, since it is a property of the mutation object itself; it can be changed explicitly using the setSelectionCoeff() method if so desired.

The mutation type of a mutation is normally a constant in simulations, so be sure you know what you are doing.  Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

In nucleotide-based models, a restriction applies: nucleotide-based mutations may not be changed to a non-nucleotide-based mutation type, and non-nucleotide-based mutations may not be changed to a nucleotide-based mutation type.

– (void)setSelectionCoeff(float$ selectionCoeff)

Set the selection coefficient of the mutation to selectionCoeff.  The selection coefficient will be changed for all individuals that possess the mutation, since they all share a single Mutation object (note that the dominance coefficient will remain unchanged, as it is determined by the mutation type).

This is normally a constant in simulations, so be sure you know what you are doing; often setting up a mutationEffect() callback is preferable, in order to modify the selection coefficient in a more limited and controlled fashion.  Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

5.11  Class MutationType

5.11.1  MutationType properties

color <–> (string$)

The color used to display mutations of this type in SLiMgui.  Outside of SLiMgui, this property still exists, but is not used by SLiM.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB".  If color is the empty string, "", SLiMgui’s default (selection-coefficient–based) color scheme is used; this is the default for new MutationType objects.

colorSubstitution <–> (string$)

The color used to display substitutions of this type in SLiMgui (see the discussion for the colorSubstitution property of the Chromosome class for details).  Outside of SLiMgui, this property still exists, but is not used by SLiM.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB".  If colorSubstitution is the empty string, "", SLiMgui’s default (selection-coefficient–based) color scheme is used; this is the default for new MutationType objects.

convertToSubstitution <–> (logical$)

This property governs whether mutations of this mutation type will be converted to Substitution objects when they reach fixation.

In WF models this property is T by default, since conversion to Substitution objects provides large speed benefits; it should be set to F only if necessary, and only on the mutation types for which it is necessary.  This might be needed, for example, if you are using a mutationEffect() callback to implement an epistatic relationship between mutations; a mutation epistatically influencing the fitness of other mutations through a mutationEffect() callback would need to continue having that influence even after reaching fixation, but if the simulation were to replace the fixed mutation with a Substitution object the mutation would no longer be considered in fitness calculations (unless the callback explicitly consulted the list of Substitution objects kept by the simulation).  Other script-defined behaviors in mutationEffect(), interaction(), mateChoice(), modifyChild(), and recombination() callbacks might also necessitate the disabling of substitution for a given mutation type; this is an important consideration to keep in mind.

In contrast, for nonWF models this property is F by default, because even mutations with no epistatis or other indirect fitness effects will continue to influence the survival probabilities of individuals.  For nonWF models, only neutral mutation types with no epistasis or other side effects can safely be converted to substitutions upon fixation.  When such a pure-neutral mutation type is defined in a nonWF model, this property should be set to T to tell SLiM that substitution is allowed; this may have very large positive effects on performance, so it is important to remember when modeling background neutral mutations.

SLiM consults this flag at the end of each tick when deciding whether to substitute each fixed mutation.  If this flag is T, all eligible fixed mutations will be converted at the end of the current tick, even if they were previously left unconverted because of the previous value of the flag.  Setting this flag to F will prevent future substitutions, but will not cause any existing Substitution objects to be converted back into Mutation objects.

distributionParams => (fs)

The parameters that configure the chosen distribution of fitness effects.  This will be of type string for DFE type "s", and type float for all other DFE types.

distributionType => (string$)

The type of distribution of fitness effects; one of "f", "g", "e", "n", "w", or "s":

"f" – A fixed fitness effect.  This DFE type has a single parameter, the selection coefficient s to be used by all mutations of the mutation type.

"g" – A gamma-distributed fitness effect.  This DFE type is specified by two parameters, a shape parameter and a mean value.  The gamma distribution from which mutations are drawn is given by the probability density function P(s | α,β= [Γ(α)βα]−1exp(−s/β), where α is the shape parameter, and the specified mean for the distribution is equal to αβ.  Note that this parameterization is the same as for the Eidos function rgamma().  A gamma distribution is often used to model deleterious mutations at functional sites.

"e" – An exponentially-distributed fitness effect.  This DFE type is specified by a single parameter, the mean of the distribution.  The exponential distribution from which mutations are drawn is given by the probability density function P(s | β) = β−1exp(−s/β), where β is the specified mean for the distribution.  This parameterization is the same as for the Eidos function rexp().  An exponential distribution is often used to model beneficial mutations.

"n" – A normally-distributed fitness effect.  This DFE type is specified by two parameters, a mean and a standard deviation.  The normal distribution from which mutations are drawn is given by the probability density function P(s | μ,σ) = (2πσ2)−1/2exp(−(sμ)2/2σ2), where μ is the mean and σ is the standard deviation.  This parameterization is the same as for the Eidos function rnorm().  A normal distribution is often used to model mutations that can be either beneficial or deleterious, since both tails of the distribution are unbounded.

"p" – A Laplace-distributed fitness effect.  This DFE type is specified by two parameters, a mean and a scale.  The Laplace distribution from which mutations are drawn is given by the probability density function P(s | μ,b) = exp(−|sμ|/b)/2b, where μ is the mean and b is the scale parameter.  A Laplace distribution is sometimes used to model a mix of both deleterious and beneficial mutations.

"w" – A Weibull-distributed fitness effect.  This DFE type is specified by a scale parameter and a shape parameter.  The Weibull distribution from which mutations are drawn is given by the probability density function P(s | λ,k) = (k/λk)sk−1exp(−(s/λ)k), where λ is the scale parameter and k is the shape parameter.  This parameterization is the same as for the Eidos function rweibull().  A Weibull distribution is often used to model mutations following extreme-value theory.

"s" – A script-based fitness effect.  This DFE type is specified by a script parameter of type string, specifying an Eidos script to be executed to produce each new selection coefficient.  For example, the script "return rbinom(1);" could be used to generate selection coefficients drawn from a binomial distribution, using the Eidos function rbinom(), even though that mutational distribution is not supported by SLiM directly.  The script must return a singleton float or integer.

Note that these distributions can in principle produce selection coefficients smaller than -1.0. In that case, the mutations will be evaluated as “lethal” by SLiM, and the relative fitness of the individual will be set to 0.0.

dominanceCoeff <–> (float$)

The dominance coefficient used for mutations of this type when heterozygous.  Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

Note that the dominance coefficient is not bounded.  A dominance coefficient greater than 1.0 may be used to achieve an overdominance effect.  By making the selection coefficient very small and the dominance coefficient very large, an overdominance scenario in which both homozygotes have the same fitness may be approximated, to a nearly arbitrary degree of precision.

Note that this property has a quirk: it is stored internally in SLiM using a single-precision float, not the double-precision float type normally used by Eidos.  This means that if you set a mutation type muttype’s dominance coefficient to some number x, muttype.dominanceCoeff==x may be F due to floating-point rounding error.  Comparisons of floating-point numbers for exact equality is often a bad idea, but this is one case where it may fail unexpectedly.  Instead, it is recommended to use the id or tag properties to identify particular mutation types.

hemizygousDominanceCoeff <–> (float$)

The dominance coefficient used for mutations of this type when they occur opposite a null haplosome (as can occur in sex-chromosome models and models involving a mix of haploids and diploids).  This defaults to 1.0, and is used only in models where null haplosomes are present; the dominanceCoeff property is the dominance coefficient used in most circumstances.  Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the Species method recalculateFitness() – but see the documentation of that method for caveats.

As with the dominanceCoeff property, this is stored internally using a single-precision float; see the documentation for dominanceCoeff for discussion.

id => (integer$)

The identifier for this mutation type; for mutation type m3, for example, this is 3.

mutationStackGroup <–> (integer$)

The group into which this mutation type belongs for purposes of mutation stacking policy.  This is equal to the mutation type’s id by default.  See mutationStackPolicy, below, for discussion.

In nucleotide-based models, the stacking group for nucleotide-based mutation types is always -1, and cannot be changed.  Non-nucleotide-based mutation types may also be set to share the -1 stacking group, if they should participate in the same stacking policy as nucleotide-based mutations, but that would be quite unusual.

mutationStackPolicy <–> (string$)

This property and the mutationStackGroup property together govern whether mutations of this mutation type’s stacking group can “stack” – can occupy the same position in a single individual.  A set of mutation types with the same value for mutationStackGroup is called a “stacking group”, and all mutation types in a given stacking group must have the same mutationStackPolicy value, which defines the stacking behavior of all mutations of the mutation types in the stacking group.  In other words, one stacking group might allow its mutations to stack, while another stacking group might not, but the policy within each stacking group must be unambiguous.

This property is "s" by default, indicating that mutations in this stacking group should be allowed to stack without restriction.  If the policy is set to "f", the first mutation of stacking group at a given site is retained; further mutations of this stacking group at the same site are discarded with no effect.  This can be useful for modeling one-way changes; once a gene is disabled by a premature stop codon, for example, you might wish to assume, for simplicity, that further mutations cannot alter that fact.  If the policy is set to "l", the last mutation of this stacking group at a given site is retained; earlier mutation of this stacking group at the same site are discarded.  This can be useful for modeling an “infinite-alleles” scenario in which every new mutation at a site generates a completely new allele, rather than retaining the previous mutations at the site.

The mutation stacking policy applies only within the given mutation type’s stacking group; mutations of different stacking groups are always allowed to stack in SLiM.  The policy applies to all mutations added to the model after the policy is set, whether those mutations are introduced by calls such as addMutation(), addNewMutation(), or addNewDrawnMutation(), or are added by SLiM’s own mutation-generation machinery.  However, no attempt is made to enforce the policy for mutations already existing at the time the policy is set; typically, therefore, the policy is set in an initialize() callback so that it applies throughout the simulation.  The policy is also not enforced upon the mutations loaded from a file with readFromPopulationFile(); such mutations were governed by whatever stacking policy was in effect when the population file was generated.

In nucleotide-based models, the stacking policy for nucleotide-based mutation types is always "l", and cannot be changed.  This ensures that new nucleotide mutations always replace the previous nucleotide at a site, and that more than one nucleotide mutation is never present at the same position in a single haplosome.

nucleotideBased => (logical$)

If the mutation type was created with initializeMutationType(), it is not nucleotide-based, and this property is F.  If it was created with initializeMutationTypeNuc(), it is nucleotide-based, and this property is T.  See those methods for further discussion.

species => (object<Species>$)

The species to which the target object belongs.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to mutation types.

5.11.2  MutationType methods

– (float)drawSelectionCoefficient([integer$ n = 1])

Draws and returns a vector of n selection coefficients using the currently defined distribution of fitness effects (DFE) for the target mutation type.  If the DFE is type "s", this method will result in synchronous execution of the DFE’s script.

– (void)setDistribution(string$ distributionType, ...)

Set the distribution of fitness effects for a mutation type.  The distributionType may be "f", in which case the ellipsis ... should supply a numeric$ fixed selection coefficient; "e", in which case the ellipsis should supply a numeric$ mean selection coefficient for the exponential distribution; "g", in which case the ellipsis should supply a numeric$ mean selection coefficient and a numeric$ alpha shape parameter for a gamma distribution; "n", in which case the ellipsis should supply a numeric$ mean selection coefficient and a numeric$ sigma (standard deviation) parameter for a normal distribution; "p", in which case the ellipsis should supply a numeric$ mean selection coefficient and a numeric$ scale parameter for a Laplace distribution; "w", in which case the ellipsis should supply a numeric$ λ scale parameter and a numeric$ k shape parameter for a Weibull distribution; or "s", in which case the ellipsis should supply a string$ Eidos script parameter.  The DFE for a mutation type is normally a constant in simulations, so be sure you know what you are doing.

5.12  Class Plot

5.12.1  Plot properties

title => (string$)

The title of the plot, as originally passed to createPlot().  See also the plotWithTitle() method of SLiMgui.

5.12.2  Plot methods

– (void)abline([Nif a = NULL], [Nif b = NULL], [Nif h = NULL], [Nif v = NULL], [string color = "red"], [numeric lwd = 1.0], [float alpha = 1.0])

Adds one or more straight lines to the plot.  There are three supported modes of operation for this method.  In the first mode, the lines are specified by a and b, representing the intercepts and slopes of the lines, respectively; in this case, a and b may be the same length, or one of them may be a singleton to provide a single value used for all of the lines specified by the other.  In the second mode, the lines are specified by h, representing the y-values of horizontal lines.  In the third mode, the lines are specified by v, representing the x-values of vertical lines.  These modes are mutually exclusive and cannot be mixed within one call to abline().  The new lines will be plotted on top of any previously added data.

The lines will be drawn in colors, line widths, and alpha (opacity) values specified by color, lwd, and alpha, each of which may be either a singleton (to provide one value used for all lines) or a vector equal in length to the number of lines plotted (to provide one value per line).  Alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.

See also lines() and segments() for more common approaches to line plotting.  The abline() method is different, and more specialized; the lines plotted by it span the full extent of the plot area, and their coordinates are not considered when dynamically resizing the axes of the plot (i.e., when createPlot() is not passed explicit, non-NULL values for xrange or yrange).  This is typically useful for plotting things such as expected values and fit lines.

– (void)addLegend([Ns$ position = NULL], [Ni$ inset = NULL], [Nif$ labelSize = NULL], [Nif$ lineHeight = NULL], [Nif$ graphicsWidth = NULL], [Nif$ exteriorMargin = NULL], [Nif$ interiorMargin = NULL])

Adds a legend to the plot.  The legend will be displayed within the plot at the location specified by position, which must be "topRight", "topLeft", "bottomRight", or "bottomLeft", or NULL (the default) requesting that SLiMgui choose the position.  The position of the legend will be inset from the chosen corner by a margin inset, measured in pixels; the default of NULL allows SLiMgui to choose the inset.

The internal layout of the legend is a bit complex, and can be controlled by five parameters; in all cases, the default value of NULL requests that SLiMgui provide a reasonable default.  The labelSize parameter specifies the font size used for the text labels for each legend entry (measured in “points”, the standard metric of font sizes).  The lineHeight parameter specifies the vertical size, in pixels, of one entry line.  The graphicsWidth parameter species the width, in pixels, of the column used to display the “graphics” (whether a line segment, a point symbol, a swatch, or a combination of those) associated with each entry.  The exteriorMargin parameter specifies the width/height of margins, in pixels, outside of the entries (between the entries and the legend’s frame).  Finally, the interiorMargin parameter specifies the width/height of margins, in pixels, vertically between entries, and also between the “graphics” column and the label.  It is easy to produce a legend that looks terrible, using these layout metrics; SLiMgui does only minimal sanity-checking of their values, to provide maximal flexibility.

The legend is initially empty; entries for it can be added with legendLineEntry(), legendPointEntry(), and legendSwatchEntry().

– (void)axis(integer$ side, [Nif at = NULL], [ls labels = T])

Configures an axis of the plot.  The side parameter controls which axis is being configured; at present, it may be 1 for the x-axis (at the bottom of the plot), or 2 for the y-axis (at the left of the plot).

The positions of tick marks (and of any associated labels) are controlled by at; if at is NULL (the default) these positions will be computed automatically based upon the range of the data in the plot, otherwise at must be a vector of numeric positions.  Note that the coordinate system of the plot is controlled not by this method, but by the xrange and yrange parameters of createPlot(); at controls only the positions of axis ticks within that coordinate system.

The labels parameter controls the text labels displayed for ticks; it may be T (the default) to label ticks with their numeric positions, F to suppress all tick labels, or a vector of type string, equal in length to at, providing the label for each position in at.  Label values may be the empty string, "", if a label at a given position is not desired, and if labels is T, some ticks may not receive a label for readability.

– (void)image(object$ image, numeric$ x1, numeric$ y1, numeric$ x2, numeric$ y2, [logical$ flipped = F], [float$ alpha = 1.0])

Adds image data given by image to the plot.  The data will be plotted as a raster (bitmap, pixel) image in the rectangle specified by x1, y1, x2, and y2, which must be sorted such that x1 <= x2 and y1 <= y2.  When flipped is F (the default), the plotted image is shown in a default orientation that is typically best given the source of the image; if flipped is T the image’s orientation is flipped vertically relative to that default.  The plotted image will use opacity alpha; alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.  The new image data will be plotted on top of any previously added data.

The image data may be specified in one of two ways.  First, image may be a singleton Image object.  In this case, the image is simply plotted inside the rectangle specified by x1/y1/x2/y2, fully displaying all of its pixels (in contrast to how the grid values of a spatial map area displayed, as described below).  The image may be either RGB or grayscale.

Second, image may be a singleton SpatialMap object.  In this case, the grid values of the spatial map are plotted aligned to the corners of the rectangle specified by x1/y1/x2/y2, as the map would be used by SLiM; the edge and corner grid points of the map are therefore only partially displayed.  (If desired, the gridValues() or mapImage() methods of SpatialMap could be used to convert the map to a matrix or Image representation that could be plotted differently.)  The spatial map’s display uses the color scheme that was specified for the map.  The map’s values are not interpolated, regardless of the map’s interpolate property; only the raw grid data of the spatial map is displayed.  (If interpolated display is desired, the interpolate() method of SpatialMap can be used to increase the grid resolution of the map prior to display.)

This method caches the image data being plotted so that plotting the same SpatialMap or Image object multiple times is more efficient.  See the matrix() method of Plot for an alternative way of plotting raster data that may be more suitable in some situations, but that is less efficient because it does not provide such caching.

– (void)legendLineEntry(string$ label, [string$ color = "red"], [numeric$ lwd = 1.0])

Adds a legend entry with text label to the plot.  The entry will be displayed as a line segment drawn in color color, using line width lwd, mirroring the appearance produced by lines() for the same parameters.  If one or more other legend entries already exist with the same label, the new entry will be drawn on top of the previously set entries for that label (allowing legend entries that display both a line and a point, for example).

– (void)legendPointEntry(string$ label, [integer$ symbol = 0], [string$ color = "red"], [string$ border = "black"], [numeric$ lwd = 1.0], [numeric$ size = 1.0])

Adds a legend entry with text label to the plot.  The entry will be displayed as a point symbol specified by symbol, color, border, lwd, and size, mirroring the appearance produced by points() for the same parameters; see points() for further details.  If one or more other legend entries already exist with the same label, the new entry will be drawn on top of the previously set entries for that label (allowing legend entries that display both a line and a point, for example).

– (void)legendSwatchEntry(string$ label, [string$ color = "red"])

Adds a legend entry with text label to the plot.  The entry will be displayed as a swatch drawn in color color.  If one or more other legend entries already exist with the same label, the new entry will be drawn on top of the previously set entries for that label (allowing legend entries that display both a line and a point, for example).

– (void)legendTitleEntry(string$ label)

Adds a legend entry with text label to the plot.  The entry will be displayed as a title, left-aligned with no graphical representation (no line, point, or swatch).  A label of "", the empty string, is allowed and will produce a blank line in the legend (for spacing).  Note that title entries always produce their own separate line in the legend; they do not participate in the overdrawing scheme used for other types of legend entries.

– (void)lines(numeric x, numeric y, [string$ color = "red"], [numeric$ lwd = 1.0], [float$ alpha = 1.0])

Adds line data given by x and y to the plot.  The data will be plotted as a series of connected line segments, following the (x, y) positions given; note that the x and y vectors must be the same length.  The new line data will be plotted on top of any previously added data.

The lines will be drawn in color color, using line width lwd, with opacity alpha.  Alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.  Unlike points() and text(), the parameters color, lwd, and alpha must be singletons, since each point (except the two ends) is shared by two line segments; if you wish to vary the color/width/opacity for each line segment, separate calls to lines() are necessary, or the segments() method may be useful.   See also abline() and segments().

– (void)matrix(numeric matrix, numeric$ x1, numeric$ y1, numeric$ x2, numeric$ y2, [logical$ flipped = F], [Nif valueRange = NULL], [Ns$ colors = NULL], [float$ alpha = 1.0])

Adds image data given by matrix to the plot.  The image data must be specified as a numeric (i.e., integer or float) matrix, as for example produced by the matrix() function in Eidos.  The data will be plotted as a raster (bitmap, pixel) image in the rectangle specified by x1, y1, x2, and y2, which must be sorted such that x1 <= x2 and y1 <= y2.  When flipped is F (the default), the plotted image is shown in a default orientation that matches the orientation of the matrix (with row 0 topmost); if flipped is T the image’s orientation is flipped vertically relative to that default (with row 0 bottommost).

Prior to display, the values in the matrix will be rescaled according to the parameter valueRange.  If valueRange is NULL (the default), the values will be left unscaled; this is equivalent to passing c(0,1) for valueRange.  Otherwise, valueRange should be a numeric vector containing two elements that define the range of values that will be rescaled to span the interval [0, 1] – the range to which the color scheme will then be applied.  After rescaling the given range to [0, 1], the resulting values are clamped to the range [0, 1].  It is legal for valueRange[0] to be greater than valueRange[1]; in this case, the rescaling operation will, in effect, reverse the direction of the color scheme by reversing the ranks of the reordered values.

The rescaled and clamped values are then colored according to the color scheme named by colors, which should be one of the named schemes supported by the Eidos function colors(): "cm", "heat", "terrain", "parula", "hot", "jet", "turbo", "gray", "magma", "inferno", "plasma", "viridis", or "cividis".  If colors is NULL (the default), the color scheme used is the reverse of the "gray" color scheme, shading from black for 0 up to white for 1.  In all cases, the color scheme is applied across the range [0, 1] for the rescaled and clamped values.

The plotted image will use opacity alpha; alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.  The new image data will be plotted on top of any previously added data.

See the image() method of Plot for an alternative way of plotting raster data from SpatialMap and Image objects that may be more suitable in some situations.

– (void)mtext(numeric x, numeric y, string labels, [string color = "black"], [numeric size = 10.0], [Nif adj = NULL], [float alpha = 1.0], [numeric angle = 0.0])

Adds marginal text data given by x, y, and labels to the plot.  The string values in labels will be plotted at the (x, y) positions given; note that x, y, and labels must all be the same length.  This method differs from the text() method in two respects.  The first – the reason that this method draws “marginal” text – is that the text drawn is not clipped to the plot area, allowing text to be drawn in the axis and border areas outside the plot itself.  The second is that the x and y coordinates are interpreted differently than other Plot methods; rather than being in the coordinate system of the plot, they are in a coordinate system in which the plot area spans [0,1] on both axes.  This is intended to make positioning text outside of the plot area not depend upon the axis ranges of the plot; those axis ranges might vary, but the positions at which mtext() draws, relative to the plot area, will remain fixed.  The new marginal text data will be plotted on top of any previously added data.

The text will be drawn in color color, at the font size given by size (measured in “points”, the standard metric of font sizes); the font family and style cannot be controlled at this time.  Opacity values may be supplied with alpha; alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.  The angle at which the text is drawn can be specified with angle; angles are measured in degrees, clockwise.  All of these parameters (color, size, alpha, and angle) may either be a singleton value applied to all points, or a vector with one value per corresponding point.

The exact position of the text, relative to each point (x, y), is adjusted by the optional parameter adj.  The value of adj, if specified, must be a vector of length 2, where adj[0] adjusts the x position and adj[1] adjusts the y position of the text.  Relative to a given x position, a value of 0.0 aligns the left edge of the text to it; a value of 0.5 aligns the center of the text to it; and a value of 1.0 aligns the right edge of the text to it.  Similarly, relative to a given y position, a value of 0.0 aligns the bottom edge of the text to it; a value of 0.5 aligns the center of the text to it; and a value of 1.0 aligns the top edge of the text to it.  Intermediate values will produce intermediate alignments, and values of adj outside of [0.0, 1.0] are also allowed.  The default value of adj, NULL, is equivalent to c(0.5, 0.5), aligning the center of the text to (x, y) both horizontally and vertically.

– (void)points(numeric x, numeric y, [integer symbol = 0], [string color = "red"], [string border = "black"], [numeric lwd = 1.0], [numeric size = 1.0], [float alpha = 1.0])

Adds point data given by x and y to the plot.  The data will be plotted as a set of point symbols, centered at the (x, y) positions given; note that the x and y vectors must be the same length.  The new point data will be plotted on top of any previously added data.

The symbol plotted for each point depends upon the value of symbol.  In general, symbols will be drawn in color color, with a line width lwd used for lines (if any) in the symbol, and an overall size scaled by size.  Symbols 2125 involve both a filled shape and a border line around that shape; for those symbols, the border line will use the color provided by border, while the filled shape will use color color.  Opacity values may be supplied with alpha; alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.  All of these parameters (symbol, color, border, lwd, size, and alpha) may either be a singleton value applied to all points, or a vector with one value per corresponding point.

– (void)rects(numeric x1, numeric y1, numeric x2, numeric y2, [string color = "red"], [string border = "black"], [numeric lwd = 1.0], [float alpha = 1.0])

Adds rectangle data given by x1, y1, x2, and y2 to the plot; note that these four vectors must all be the same length.  The data will be plotted as a series of rectangles, defined by each (x1, y1, x2, y2) quadret providing the left, right, top and bottom of each rectangle.  The coordinates of each rectangle do not need to be sorted; in other words, it is not required that x1[i] <= x2[i], or that y1[i] <= y2[i], and different sorting orders for the coordinates of a given rectangle will draw identically.  The fill of a each rectangle is contained within its coordinates, but the frame of each rectangle is composed of lines of some width, drawn between the vertices of the rectangle, and will therefore extend beyond the coordinates of the rectangle by one-half of the line width; if this is not desired, inset the rectangle coordinates by one-half of the line width to compensate.  The new rectangle data will be plotted on top of any previously added data.

The rectangles will be drawn in fill colors color, with border colors border, using line widths lwd, with opacities alpha applied to both the fill and the border.  The color, border, lwd, and alpha parameters may each be either a singleton value (applying to all rectangles) or a vector with one value per rectangle.  Alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.  If a filled rectangle with no border is desired, a border value of "none" can be used; similarly, if a framed rectangle with no fill is desired, a color value of "none" can be used.  Note that that "none" a special color value supported only by rects(); it is not part of the standard set of named colors in Eidos.

– (void)segments(numeric x1, numeric y1, numeric x2, numeric y2, [string color = "red"], [numeric lwd = 1.0], [float alpha = 1.0])

Adds line segment data given by x1, y1, x2, and y2 to the plot; note that these four vectors must all be the same length.  The data will be plotted as a series of unconnected line segments, from each (x1, y1) position to the corresponding (x2, y2) position.  The new line segment data will be plotted on top of any previously added data.

The line segments will be drawn in colors color, using line widths lwd, with opacities alpha.  The color, lwd, and alpha parameters may each be either a singleton value (applying to all line segments) or a vector with one value per line segment.  Alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.   See also lines() and abline().

– (void)setBorderless([numeric$ marginLeft = 0.0], [numeric$ marginTop = 0.0], [numeric$ marginRight = 0.0], [numeric$ marginBottom = 0.0])

Reconfigures the plot to be borderless – to not display axes, ticks, or the labels for axes and ticks.  Since those elements are not present, the plot area in borderless plots essentially fills the window’s available area, except for being inset by the margins given in marginLeft, marginTop, marginRight, and marginBottom, which are specified in pixels.  The axis ranges are not extended in the usual manner; if the range of the x axis is specified as c(0,1), for example, then that is the range actually used for the x axis.  (This is similar to R’s "xaxs" and "yaxs" being specified as "i", rather than the usual behavior specified by the default of "r".)  If you want the plotted data to have “breathing room” around it, you should therefore specify non-zero margins.

Note that the fullBox parameter of createPlot() is still honored; for borderless plots, the box is drawn at the outer edge of the margin area.  If you don’t want the box to overdraw any of the data area of the plot (within the extents of the axes), you should specify each margin value as 1 (or greater), to provide a pixel of margin space within which the box will be drawn without impinging upon the plotted data.

– (void)text(numeric x, numeric y, string labels, [string color = "black"], [numeric size = 10.0], [Nif adj = NULL], [float alpha = 1.0], [numeric angle = 0.0])

Adds text data given by x, y, and labels to the plot.  The string values in labels will be plotted at the (x, y) positions given; note that x, y, and labels must all be the same length.  The new text data will be plotted on top of any previously added data.

The text will be drawn in color color, at the font size given by size (measured in “points”, the standard metric of font sizes); the font family and style cannot be controlled at this time.  Opacity values may be supplied with alpha; alpha values must be in [0.0, 1.0], where 0.0 is fully transparent and 1.0 is fully opaque.  The angle at which the text is drawn can be specified with angle; angles are measured in degrees, clockwise.  All of these parameters (color, size, alpha, and angle) may either be a singleton value applied to all points, or a vector with one value per corresponding point.

The exact position of the text, relative to each point (x, y), is adjusted by the optional parameter adj.  The value of adj, if specified, must be a vector of length 2, where adj[0] adjusts the x position and adj[1] adjusts the y position of the text.  Relative to a given x position, a value of 0.0 aligns the left edge of the text to it; a value of 0.5 aligns the center of the text to it; and a value of 1.0 aligns the right edge of the text to it.  Similarly, relative to a given y position, a value of 0.0 aligns the bottom edge of the text to it; a value of 0.5 aligns the center of the text to it; and a value of 1.0 aligns the top edge of the text to it.  Intermediate values will produce intermediate alignments, and values of adj outside of [0.0, 1.0] are also allowed.  The default value of adj, NULL, is equivalent to c(0.5, 0.5), aligning the center of the text to (x, y) both horizontally and vertically.

See also the mtext() method, for drawing text outside of the plot area.

– (void)write(string$ filePath)

Writes the plot to the given filesystem path filePath as a PDF file.  It is suggested, but not required, that filePath should end in a .pdf or .PDF filename extension.  If the file cannot be written, an error will result.

5.13  Class SLiMEidosBlock

5.13.1  SLiMEidosBlock properties

active <–> (integer$)

If this evaluates to logical F (i.e., is equal to 0), the script block is inactive and will not be called.  The value of active for all registered script blocks is reset to -1 at the beginning of each tick, prior to script events being called, thus activating all blocks (except callbacks associated with a species that is not active in that tick, which are deactivated as part of the deactivation of the species).  Any integer value other than -1 may be used instead of -1 to represent that a block is active; for example, active may be used as a counter to make a block execute a fixed number of times in each tick.  This value is not cached by SLiM; if it is changed, the new value takes effect immediately.  For example, a callback might be activated and inactivated repeatedly during a single tick.

end => (integer$)

The last tick in which the script block is active.

id => (integer$)

The identifier for this script block; for script s3, for example, this is 3.  A script block for which no id was given will have an id of -1.

source => (string$)

The source code string of the script block.

speciesSpec => (object<Species>)

The species specifier for the script block.  The species specifier for a callback block indicates the callback’s associated species; the callback is called to modify the default behavior for that species.  If the script block has no species specifier, this property’s value is a zero-length object vector of class Species.  This property is read-only; normally it is set by preceding the definition of a callback with a species specifier, of the form species <species-name>.

start => (integer$)

The first tick in which the script block is active.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.

ticksSpec => (object<Species>)

The ticks specifier for the script block.  The ticks specifier for an event block indicates the event’s associated species; the event executes only in ticks when that species is active.  If the script block has no ticks specifier, this property’s value is a zero-length object vector of class Species.  This property is read-only; normally it is set by preceding the definition of an event with a ticks specifier, of the form ticks <species-name>.

type => (string$)

The type of the script block; this will be "first", "early", or "late" for the three types of Eidos events, or "initialize", "fitnessEffect", "interaction", "mateChoice", "modifyChild", "mutation", "mutationEffect", "recombination", "reproduction", or "survival" for the respective types of Eidos callbacks.

5.13.2  SLiMEidosBlock methods


5.14  Class SLiMgui

5.14.1  SLiMgui properties

pid => (integer$)

The Un*x process identifier (commonly called the “pid”) of the running SLiMgui application.  This can be useful for scripts that wish to use system calls to influence the SLiMgui application.

5.14.2  SLiMgui methods

– (No<Plot>$)createPlot(string$ title, [Nif xrange = NULL], [Nif yrange = NULL], [string$ xlab = "x"], [string$ ylab = "y"], [Nif$ width = NULL], [Nif$ height = NULL], [logical$ horizontalGrid = F], [logical$ verticalGrid = F], [logical$ fullBox = T], [numeric$ axisLabelSize = 15], [numeric$ tickLabelSize = 10])

Creates and returns a new custom plot referred to by title, or restarts and returns the existing plot with that title; if the plot cannot be created (notably, when running under SLiMguiLegacy rather than SLiMgui), NULL is returned.  The range for the x and y axes of the plot can optionally be provided in xrange and yrange, as vectors of length 2 containing the minimum and maximum values for the corresponding axis; the default of NULL for these parameters requests that the axis ranges be determined heuristically based upon the data subsequently added to the plot.  Labels for the x and y axes can be provided in xlab and ylab; if no axis label is desired, the empty string "" may be passed.  The width and height of the window itself can optionally be set with width and height, in units of pixels (perhaps 70–100 pixels per inch, depending on your screen’s pixel density); the default of NULL for these parameters requests SLiMgui’s default plot window size.  The display of horizontal grid lines, vertical grid lines, and a full box around the plot area can be controlled with horizontalGrid, verticalGrid, and fullBox respectively, and the size (in points) of axis and tick labels can be controlled with axisLabelSize and tickLabelSize respectively.

Once the plot has been created, data can be added to it using Plot methods such as lines(), points(), and text().  As with other plot windows in SLiMgui, the “action button” can be used to access plot configuration options, and to copy or save the final plot as a raster image or a PDF file.

– (Nfs)logFileData(object<LogFile>$ logFile, is$ column)

Returns a vector containing data from the LogFile object logFile, taken from a specified column (identified in column either by the column’s name or by its zero-based index).  If the data are all numeric, they will be returned as a float vector.  Otherwise – if the data are non-numeric – they will be returned as a string vector.  If the specified column does not exist in the log file, NULL will be returned.

This functionality is provided as a method on the SLiMgui class, rather than on LogFile, because in SLiMgui logged data is kept in memory anyway, for display in the debugging output viewer window.  When running at the command line logged data is not kept in memory, and thus is not available.

– (void)openDocument(string$ filePath)

Open the document at filePath in SLiMgui, if possible.  Supported document types include SLiM model files (typically with a .slim path extension), text files (typically with a .txt path extension, and opened as untitled model files), and PNG, JPG/JPEG, BMP, and GIF image file formats (typically .png / .jpg / .jpeg / .bmp / .gif, respectively).  (Note that in SLiMguiLegacy, PDF files (.pdf) are supported but these other image file formats are not.)  This method can be particularly useful for opening images created by the simulation itself, often by sublaunching a plotting process in R or another environment.

– (void)pauseExecution(void)

Pauses a model that is playing in SLiMgui.  This is essentially equivalent to clicking the “Play” button to stop the execution of the model.  Execution can be resumed by the user, by clicking the “Play” button again; unlike calling stop() or simulationFinished(), the simulation is not terminated.  This method can be useful for debugging or exploratory purposes, to pause the model at a point of interest.  Execution is paused at the end of the currently executing tick, not mid-tick.

If the model is being profiled, or is executing forward to a tick number entered in the tick field, pauseExecution() will do nothing; by design, pauseExecution() only pauses execution when SLiMgui is doing a simple “Play” of the model.

– (No<Plot>$)plotWithTitle(string$ title)

Returns an existing plot that was created by createPlot() with title; if such a plot does not exist, NULL is returned.  Note that other SLiMgui plots cannot be accessed through this method; only plots created by createPlot() are available in Eidos.

5.15  Class SpatialMap

(object<SpatialMap>$)SpatialMap(string$ name, object<SpatialMap>$ map)

Creates a new SpatialMap object that is a copy of map, named name.

5.15.1  SpatialMap properties

gridDimensions => (integer)

The dimensions of the spatial map’s grid of values, in the order of the components of the map’s spatiality.  For example, a map with spatiality "xz" and a grid of values that is 500 in the "x" dimension by 300 in the "z" dimension would return c(500, 300) for this property.

interpolate <–> (logical$)

Whether interpolation between grid values is enabled (T) or disabled (F).  The initial value of this property is set by defineSpatialMap(), but it can be changed.  The interpolation performed is linear; for cubic interpolation, use the interpolate() method.

name => (string$)

The name of the spatial map, usually as provided to defineSpatialMap().  The names of spatial maps must be unique within any given subpopulation, but the same name may be reused for different spatial maps in different subpopulations.  The name is used to identify a map for methods such as spatialMapValue(), and is also used for display in SLiMgui.

spatialBounds => (float)

The spatial bounds to which the spatial map is aligned.  These bounds come from the subpopulation that originally created the map, with the defineSpatialMap() method, and cannot be subsequently changed.  All subpopulations that use a given spatial map must match that map’s spatial bounds, so that the map does not stretch or shrink relative to its initial configuration.  The components of the spatial bounds of a map correspond to the components of the map’s spatiality; for example, a map with spatiality "xz" will have bounds (x0, z0, x1, z1); bounds for "y" are not included, since that dimension is not used by the spatial map.

spatiality => (string$)

The spatiality of the map: the subset of the model’s dimensions that are used by the spatial map.  The spatiality of a map is configured by defineSpatialMap() and cannot subsequently be changed.  For example, a 3D model (with dimensionality "xyz") might define a 2D spatial map with spatiality "xz", providing spatial values that do not depend upon the "y" dimension.  Often, however, the spatiality of a map will match the dimensionality of the model.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to spatial maps.

5.15.2  SpatialMap methods

– (object<SpatialMap>$)add(ifo<SpatialMap> x)

Adds x to the spatial map.  One possibility is that x is a singleton integer or float value; in this case, x is added to each grid value of the target spatial map.  Another possibility is that x is an integer or float vector/matrix/array of the same dimensions as the target spatial map’s grid; in this case, each value of x is added to the corresponding grid value of the target spatial map.  The third possibility is that x is itself a (singleton) spatial map; in this case, each grid value of x is added to the corresponding grid value of the target spatial map (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions).  The target spatial map is returned, to allow easy chaining of operations.

– (object<SpatialMap>$)blend(ifo<SpatialMap> x, float$ xFraction)

Blends x into the spatial map, giving x a weight of xFraction and the existing values in the target spatial map a weight of 1 - xFraction, such that the resulting values in the target spatial map are then given by x * xFraction + target * (1 - xFraction).  The value of xFraction must be in [0.0, 1.0].

One possibility is that x is a singleton integer or float value; in this case, x is blended with each grid value of the target spatial map.  Another possibility is that x is an integer or float vector/matrix/array of the same dimensions as the target spatial map’s grid; in this case, each value of x is blended with the corresponding grid value of the target spatial map.  The third possibility is that x is itself a (singleton) spatial map; in this case, each grid value of x is blended with the corresponding grid value of the target spatial map (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions).  The target spatial map is returned, to allow easy chaining of operations.

– (void)changeColors([Nif valueRange = NULL], [Ns colors = NULL])

Changes the color scheme for the target spatial map.  The meaning of valueRange and colors are identical to their meaning in defineSpatialMap(), but are also described here.

The valueRange and colors parameters travel together; either both are NULL, or both are specified.  They control how map values will be transformed into colors, by SLiMgui and by the mapColor() method.  The valueRange parameter establishes the color-mapped range of spatial map values, as a vector of length two specifying a minimum and maximum; this does not need to match the actual range of values in the map.  The colors parameter then establishes the corresponding colors for values within the interval defined by valueRange: values less than or equal to valueRange[0] will map to colors[0], values greater than or equal to valueRange[1] will map to the last colors value, and intermediate values will shade continuously through the specified vector of colors, with interpolation between adjacent colors to produce a continuous spectrum.  This is much simpler than it sounds in this description; see the recipes for an illustration of its use.

If valueRange and colors are both NULL, a default grayscale color scheme will be used in SLiMgui, but an error will result if mapColor() is called.

– (void)changeValues(ifo<SpatialMap> x)

Changes the grid values used for the target spatial map.  The parameter x should be either a SpatialMap object from which values are taken directly, or a vector, matrix, or array of numeric values as described in the documentation for defineSpatialMap().  Other characteristics of the spatial map, such as its color mapping (if defined), its spatial bounds, and its spatiality, will remain unchanged.  The grid resolution of the spatial map is allowed to change with this method.  This method is useful for changing the values of a spatial map over time, such as to implement changes to the landscape’s characteristics due to seasonality, climate change, processes such as fire or urbanization, and so forth.  As with the original map values provided to defineSpatialMap(), it is often useful to read map values from a PNG image file using the Eidos class Image.

– (object<SpatialMap>$)divide(ifo<SpatialMap> x)

Divides the spatial map by x.  One possibility is that x is a singleton integer or float value; in this case, each grid value of the target spatial map is divided by x.  Another possibility is that x is an integer or float vector/matrix/array of the same dimensions as the target spatial map’s grid; in this case, each grid value of the target spatial map is divided by the corresponding value of x.  The third possibility is that x is itself a (singleton) spatial map; in this case, each grid value of the target spatial map is divided by the corresponding grid value of x (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions).  The target spatial map is returned, to allow easy chaining of operations.

– (object<SpatialMap>$)exp(void)

Exponentiates the values of the spatial map.  More precisely, each grid value x of the target spatial map is exponentiated – replaced by the value ex.  The target spatial map is returned, to allow easy chaining of operations.

– (float)gridValues(void)

Returns the values for the spatial map’s grid as a vector (for a 1D map), a matrix (for a 2D map), or an array (for a 3D map).  The form and orientation of the returned values is such that it could be used to create a new spatial map, with defineSpatialMap(), which would be identical to the original.

– (object<SpatialMap>$)interpolate(integer$ factor, [string$ method = "linear"])

Increases the resolution of the spatial map by factor, changing the dimensions of the spatial map’s grid of values (while leaving its spatial bounds unchanged), by interpolating new values between the existing values.  The parameter factor must be an integer in [2, 10001], somewhat arbitrarily.  The target spatial map is returned, to allow easy chaining of operations.

For a 1D spatial map, factor-1 new values will be inserted between every pair of values in the original value grid.  A factor of 2 would therefore insert one new value between each pair of existing values, thereby increasing the map’s resolution by a factor of two.  Note that if the spatial map’s original grid dimension was N, the new grid dimension with a factor of k would be k(N−1)+1, not kN, because new values are inserted only between existing values.  For 2D and 3D spatial maps, essentially the same process is conducted along each axis of the map’s spatiality, increasing the resolution of the map by factor in every dimension.

If method is "linear" (the default), linear (or bilinear or trilinear, for 2D/3D maps) interpolation will be used to interpolate the values for the new grid points.  Alternatively, if method is "nearest", the nearest value in the old grid will be used for new grid points; with this method, it is recommended that factor be odd, not even, to avoid artifacts due to rounding of coordinates midway between the original grid positions.  If method is "cubic", cubic (or bicubic, for 2D maps) will be used; this generally produces smoother interpolation with fewer artifacts than "linear", but it is not supported for 3D maps.  The choice of interpolation method used here is independent of the map’s interpolate property.  Note that while the "nearest" and "linear" interpolation methods will leave the range of values in the map unchanged, "cubic" interpolation may produce interpolated values that are outside the original range of values (by design).  Periodic boundaries are currently supported only for "nearest", "linear", and 1D "cubic" interpolation.

– (string)mapColor(numeric value)

Uses the spatial map’s color-translation machinery (as defined by the valueRange and colors parameters to defineSpatialMap()) to translate each element of value into a corresponding color string.  If the spatial map does not have color-translation capabilities, an error will result.  See the documentation for defineSpatialMap() for information regarding the details of color translation.  See the Eidos manual for further information on color strings.

– (object<Image>$)mapImage([Ni$ width = NULL], [Ni$ height = NULL], [logical$ centers = F], [logical$ color = T])

Returns an Image object sampled from the spatial map.  The image will be width pixels wide and height pixels tall; the intrinsic size of the spatial map itself will be used if one of these parameters is NULL.  The image will be oriented in the same way as it is displayed in SLiMgui (which conceptually entails a transformation from matrix coordinates, which store values by column, to standard image coordinates, which store values by row; see the Eidos manual’s documentation of Image for details).  This method may only be called for 2D spatial maps at present.

The sampling of the spatial map can be done in one of two ways, as controlled by the centers parameter.  If centers is T, a (width+1) × (height+1) grid of lines that delineates width × height rectangular pixels will be overlaid on top of the spatial map, and values will be sampled from the spatial map at the center of each of these pixels.  If centers is F (the default), a width × height grid of lines will be overlaid on top of the spatial map, and values will be sampled from the spatial map at the vertices of the grid.  If interpolation is not enabled for the spatial map, these two options will both recover the original matrix of values used to define the spatial map (assuming, here and below, that width and height are NULL).  If interpolation is enabled for the spatial map, however, centers == F will recover the original values, but will not capture the “typical” value of each pixel in the image; centers == T, on the other hand, will not recover the original values, but will capture the “typical” value of each pixel in the image (i.e., the value at the center of each pixel, as produced by interpolation).

If color is T (the default), the valueRange and colors parameters supplied to defineSpatialMap() will be used to translate map values to RGB color values as described in the documentation of that method, providing the same appearance as in SLiMgui; of course those parameters must have been supplied, otherwise an error will result.  If color is F, on the other hand, a grayscale image will be produced that directly reflects the map values without color translation.  In this case, this method needs to translate map values, which can have any float value, into grayscale pixel values that are integers in [0, 255].  To do so, the map values are multiplied by 255.0, clamped to [0.0, 255.0], and then rounded to the nearest integer.  This translation scheme essentially assumes that map values are in [0, 1]; for spatial maps that were defined using the floatK channel of a grayscale PNG image, this should recover the original image’s pixel values.  (If a different translation scheme is desired, color=T with the desired valueRange and colors should be used.)

– (float)mapValue(float point)

Uses the spatial map’s mapping machinery (as defined by the gridSize, values, and interpolate parameters to defineSpatialMap()) to translate the coordinates of point into a corresponding map value.  The length of point must be equal to the spatiality of the spatial map; in other words, for a spatial map with spatiality "xz", point must be of length 2, specifying the x and z coordinates of the point to be evaluated.  Interpolation will automatically be used if it was enabled for the spatial map.  Point coordinates are clamped into the range defined by the spatial boundaries, even if the spatial boundaries are periodic; use pointPeriodic() to wrap the point coordinates first if desired.  See the documentation for defineSpatialMap() for information regarding the details of value mapping.

The point parameter may also contain more than one point to be looked up.  In this case, the length of point must be an exact multiple of the spatiality of the spatial map; for a spatial map with spatiality "xz", for example, the length of point must be an exact multiple of 2, and successive pairs of elements from point (elements 0 and 1, then elements 2 and 3, etc.) will be taken as the x and z coordinates of the points to be evaluated.  This allows mapValue() to be used in a vectorized fashion.

The spatialMapValue() method of Subpopulation provides essentially the same functionality as this method; it may be more convenient to use, for some usage cases, and it checks that the spatial map is actually added to the subpopulation in question, providing an additional consistency check.  However, either method may be used.

– (object<SpatialMap>$)multiply(ifo<SpatialMap> x)

Multiplies the spatial map by x.  One possibility is that x is a singleton integer or float value; in this case, each grid value of the target spatial map is multiplied by x.  Another possibility is that x is an integer or float vector/matrix/array of the same dimensions as the target spatial map’s grid; in this case, each grid value of the target spatial map is multiplied by the corresponding value of x.  The third possibility is that x is itself a (singleton) spatial map; in this case, each grid value of the target spatial map is multiplied by the corresponding grid value of x (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions).  The target spatial map is returned, to allow easy chaining of operations.

– (object<SpatialMap>$)power(ifo<SpatialMap> x)

Raises the spatial map to the power x.  One possibility is that x is a singleton integer or float value; in this case, each grid value of the target spatial map is raised to the power x.  Another possibility is that x is an integer or float vector/matrix/array of the same dimensions as the target spatial map’s grid; in this case, each grid value of the target spatial map is raised to the power of the corresponding value of x.  The third possibility is that x is itself a (singleton) spatial map; in this case, each grid value of the target spatial map is raised to power of the corresponding grid value of x (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions).  The target spatial map is returned, to allow easy chaining of operations.

– (float)range(void)

Returns the range of values contained in the spatial map.  The result is a float vector of length 2; the first element is the minimum map value, and the second element is the maximum map value.

– (object<SpatialMap>$)rescale([numeric$ min = 0.0], [numeric$ max = 1.0])

Rescales the values of the spatial map to the range [min, max].  By default, the rescaling is to the range [0.0, 1.0].  It is required that min be less than max, and that both be finite.  Note that the final range may not be exactly [min, max] due to numerical error.  The target spatial map is returned, to allow easy chaining of operations.

– (float)sampleImprovedNearbyPoint(float point, float$ maxDistance, string$ functionType, ...)

This variant of sampleNearbyPoint() samples a Metropolis–Hastings move on the spatial map.  See sampleNearbyPoint() for discussion of the basic idea.  This method proposes a nearby point drawn from the given kernel.  If the drawn point has a larger map value than the original point, the new point is returned.  If the drawn point has a smaller map value than the original point, it is returned with a probability equal to the ratio between its map value and the original map value, otherwise the original point is returned.  The distribution of points that move (or not) to new locations governed by this method will converge upon the map itself, in a similar manner to how MCMC converges upon the posterior distribution (assuming no other forces, such as birth or death, influence the distribution of individuals).  Movement governed by this method is “improved” in the sense that points will tend to remain where they are unless the new sampled point is an improvement for them – a higher map value.  Note that unlike sampleNearbyPoint(), this method requires that all map values are non-negative.

The parameter point may contain any number of points; the returned vector will contain corresponding points sampled as described above.  Each supplied point must provide coordinates precisely as specified by the spatiality of the target map; for example, if the target map’s spatiality is "xz" (in an "xyz" species), each point must contain two elements, providing the x and z coordinate.  Be careful; this means that in general it is not safe to pass an individual’s spatialPosition property for point, for example (although it is safe if the spatiality of the map matches the dimensionality of the simulation); other properties on Individual exist for getting the individual’s coordinates in a particular spatiality, such as the xz property for this example.  Supplied points are not required to be within bounds, but since nearby points are sampled from the given kernel and must be within bounds, an infinite loop might result if a supplied point is substantially outside bounds.

The kernel is specified with a kernel type, functionType, followed by zero or more ellipsis arguments; see smooth() for further information.  For this method, at present only kernel types "f", "l", "e", "n", and "t" are supported, and type "t" is not presently supported for 3D kernels.  The parameters that define the kernel’s shape – the ellipsis arguments that follow functionType – may each, independently, be either a singleton or a vector with length equal to the number of points, providing a separate value for each point being processed.  In this way, all of the nearby points can be drawn from the same kernel, or each from a separately defined kernel.  Since maxDistance and functionType are required to be singletons, however, their values cannot vary from point to point in the present design.

See also the Subpopulation method deviatePositionsWithMap(), which is conceptually similar to this method.

– (float)sampleNearbyPoint(float point, float$ maxDistance, string$ functionType, ...)

For a spatial point supplied in point, returns a nearby point sampled from a kernel weighted by the spatial map’s values.  Only points within the maximum distance of the kernel, maxDistance, will be chosen, and the probability that a given point is chosen will be proportional to the density of the kernel at that point multiplied by the value of the map at that point (interpolated, if interpolation is enabled for the map).  Negative values of the map will be treated as zero.  The point returned will be within spatial bounds, respecting periodic boundaries if in effect (so there is no need to call pointPeriodic() on the result).

The parameter point may contain any number of points; the returned vector will contain corresponding points sampled as described above.  Each supplied point must provide coordinates precisely as specified by the spatiality of the target map; for example, if the target map’s spatiality is "xz" (in an "xyz" species), each point must contain two elements, providing the x and z coordinate.  Be careful; this means that in general it is not safe to pass an individual’s spatialPosition property for point, for example (although it is safe if the spatiality of the map matches the dimensionality of the simulation); other properties on Individual exist for getting the individual’s coordinates in a particular spatiality, such as the xz property for this example.  Supplied points are not required to be within bounds, but since nearby points are sampled from the given kernel and must be within bounds, an infinite loop might result if a supplied point is substantially outside bounds.

The kernel is specified with a kernel type, functionType, followed by zero or more ellipsis arguments; see smooth() for further information.  For this method, at present only kernel types "f", "l", "e", "n", and "t" are supported, and type "t" is not presently supported for 3D kernels.  The parameters that define the kernel’s shape – the ellipsis arguments that follow functionType – may each, independently, be either a singleton or a vector with length equal to the number of points, providing a separate value for each point being processed.  In this way, all of the nearby points can be drawn from the same kernel, or each from a separately defined kernel.  Since maxDistance and functionType are required to be singletons, however, their values cannot vary from point to point in the present design.

This method can be used to find points in the vicinity of individuals that are favorable – possessing more resources, or better environmental conditions, etc.  It can also be used to guide the dispersal or foraging behavior of individuals.  See sampleImprovedNearbyPoint() for a variant that may be useful for directed movement across a landscape.  Note that the algorithm for sampleNearbyPoint() works by rejection sampling, and so will be very inefficient if the maximum value of the map (anywhere, across the entire map) is much larger than the typical value of the map where individuals are.  The algorithm for sampleImprovedNearbyPoint() is different, and does not exhibit this performance issue.

See also the Subpopulation method deviatePositionsWithMap(), which is conceptually similar to this method.

– (object<SpatialMap>$)smooth(float$ maxDistance, string$ functionType, ...)

Smooths (or blurs, one could say) the values of the spatial map by convolution with a kernel.  The kernel is specified with a maximum distance maxDistance (beyond which the kernel cuts off to a value of zero), a kernel type functionType that should be "f", "l", "e", "n", "c", or "t", and additional parameters in the ellipsis ... that depend upon the kernel type and further specify its shape.  The target spatial map is returned, to allow easy chaining of operations.

The kernel specification is similar to that for the setInteractionType() method of InteractionType, but omits the maximum value of the kernel.  Specifically, functionType may be "f", in which case no ellipsis arguments should be supplied; "l", similarly with no ellipsis arguments; "e", in which case the ellipsis should supply a numeric$ lambda (rate) parameter for a negative exponential function; "n", in which case the ellipsis should supply a numeric$ sigma (standard deviation) parameter for a Gaussian function; "c", in which case the ellipsis should supply a numeric$ scale parameter for a Cauchy distribution function; or "t", in which case the ellipsis should supply a numeric$ degrees of freedom and a numeric$ scale parameter for a t-distribution function.  See the InteractionType class documentation for discussions of these kernel types.

Distance metrics specified to this method, such as maxDistance and the additional kernel shape parameters, are measured in the distance scale of the spatial map – the same distance scale in which the spatial bounds of the map are specified.  The operation is performed upon the grid values of the spatial map; distances are internally translated into the scale of the value grid.  For non-periodic boundaries, clipping at the edge of the spatial map is done; in a 2D map with no periodic boundaries, for example, the weights of edge and corner grid values are adjusted for their partial (one-half and one-quarter) coverage.  For periodic boundaries, the smoothing operation will automatically wrap around based upon the assumption that the grid values at the two connected edges of the periodic boundary have identical values (which they should, since by definition they represent the same position in space).

The density scale of the kernel has no effect and will be normalized; this is the reason that smooth(), unlike InteractionType, does not require specification of the maximum value of the kernel.  This normalization prevents the kernel from increasing or decreasing the average spatial map value (apart from possible edge effects).

– (object<SpatialMap>$)subtract(ifo<SpatialMap> x)

Subtracts x from the spatial map.  One possibility is that x is a singleton integer or float value; in this case, x is subtracted from each grid value of the target spatial map.  Another possibility is that x is an integer or float vector/matrix/array of the same dimensions as the target spatial map’s grid; in this case, each value of x is subtracted from the corresponding grid value of the target spatial map.  The third possibility is that x is itself a (singleton) spatial map; in this case, each grid value of x is subtracted from the corresponding grid value of the target spatial map (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions).  The target spatial map is returned, to allow easy chaining of operations.

5.16  Class Species

5.16.1  Species properties

avatar => (string$)

The avatar string used to represent this species in SLiMgui.  Outside of SLiMgui, this property still exists, but is not used by SLiM.  Avatars are typically one-character strings, often using an emoji that symbolizes the species.  This property is read-only; its value should be set with the avatar parameter of initializeSpecies().

chromosome => (object<Chromosome>$)

The Chromosome object used by the species.  This property may only be accessed in a single-chromosome model; if there are multiple chromosomes (or none), the chromosomes property must be used instead.

chromosomes => (object<Chromosome>)

The Chromosome objects used by the species, in the order in which they were defined.  See also the sexChromosomes property.

color => (string$)

The color used to display information about this species in SLiMgui.  Outside of SLiMgui, this property still exists, but is not used by SLiM.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB" (see the Eidos manual).  This property is read-only; its value should be set with the color parameter of initializeSpecies().

cycle <–> (integer$)

The current cycle count for this species.  This counter begins at 1, and increments at the end of every tick in which the species is active.  In models with non-overlapping generations, particularly WF models, this can be thought of as a generation counter.

description <–> (string$)

A human-readable string description for the species.  By default, this is the empty string, ""; however, it may be set to whatever you wish.

dimensionality => (string$)

The spatial dimensionality of the simulation for this species, as specified in initializeSLiMOptions().  This will be "" (the empty string) for non-spatial simulations (the default), or "x", "xy", or "xyz", for simulations using those spatial dimensions respectively.

genomicElementTypes => (object<GenomicElementType>)

The GenomicElementType objects being used in the species.  These are guaranteed to be in sorted order, by their id property.

id => (integer$)

The identifier for this species.  Species identifiers are determined by their declaration order in the script; the first declared species is given an id of 0, the second is given an id of 1, and so forth.

mutationTypes => (object<MutationType>)

The MutationType objects being used in the species.  These are guaranteed to be in sorted order, by their id property.

mutations => (object<Mutation>)

The Mutation objects that are currently active in the species.

name => (string$)

A human-readable string name for the subpopulation.  This is always the declared name of the species, as given in the explicit species declaration in script, and cannot be changed.  The name of a species may appear as a label in SLiMgui, and it can be useful in generating output, debugging, and other purposes.  See also the description property, which can be changed by the user and used for any purpose.

nucleotideBased => (logical$)

If T, the model for this species is nucleotide-based; if F, it is not.  See the discussion of the nucleotideBased parameter to initializeSLiMOptions() for discussion.

periodicity => (string$)

The spatial periodicity of the simulation for this species, as specified in initializeSLiMOptions().  This will be "" (the empty string) for non-spatial simulations and simulations with no periodic spatial dimensions (the default).  Otherwise, it will be a string representing the subset of spatial dimensions that have been declared to be periodic, as specified to initializeSLiMOptions().

scriptBlocks => (object<SLiMEidosBlock>)

All registered SLiMEidosBlock objects in the simulation that have been declared with this species as their species specifier (not ticks specifier).  These will always be callback blocks; callbacks are species-specific, while other types of blocks are not.

sexChromosomes => (object<Chromosome>)

The Chromosome objects used by the species that represent sex chromosomes, in the order in which they were defined.  Sex chromosomes are specifically those of type "X", "Y", "W", "Z", and "-Y".  See also the chromosomes property, and the isSexChromosome property of Chromosome.

sexEnabled => (logical$)

If T, sex is enabled for this species; if F, individuals are hermaphroditic.

subpopulations => (object<Subpopulation>)

The Subpopulation instances currently defined in the species.  These are guaranteed to be in sorted order, by their id property.

substitutions => (object<Substitution>)

A vector of Substitution objects, representing all mutations that have been fixed in this species.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to the simulation.

5.16.2  Species methods

– (object<Dictionary>$)addPatternForClone(iso<Chromosome>$ chromosome, No<Dictionary>$ pattern, object<Individual>$ parent, [Ns$ sex = NULL])

Adds an inheritance dictionary for the specified chromosome to the pattern dictionary pattern, representing producing a clone of parent, with sex optionally specified by sex.  The parameter chromosome can provide a chromosome id (an integer), a chromosome symbol (a string), or a Chromosome object.  The resulting pattern dictionary is intended for use with the Subpopulation method addMultiRecombinant(); see that method for background on the use of pattern dictionaries.

The parameter pattern must be a Dictionary (or a subclass of Dictionary), or NULL.  If pattern is NULL, a new singleton object of class Dictionary will be created, set up, and returned; otherwise, the returned object is the same object passed in as pattern.  The inheritance dictionary generated by addPatternForClone() will be added to pattern as the value for a particular key.  If pattern is already configured to use string keys, the key used will be the symbol property of the chromosome; otherwise, including if pattern is NULL, the key used will be the id property of the chromosome.  If the key in question already exists in pattern, its value will be replaced.

The precise inheritance pattern generated by this method depends upon the chromosome’s type; see initializeChromosome() for a description of the different chromosome types and the ways in which they are inherited.  The pattern will be the same as would be used by the addCloned() method for the chromosome.  If sex is NULL, the sex of the offspring is assumed to be the same as the parent; in non-sexual models, NULL must be passed.  If the sex of the offspring will be different from the parent, "M" or "F" should be passed; if changing the sex from parent to offspring presents any genetic problems (if the chromosome is a sex chromosome, for example), an error will be raised, but if the chromosome does not depend upon sex, the change of sex will be allowed.  Note that the generated inheritance dictionary does not encode the offspring sex; the sex parameter is simply used to determine and validate the inheritance pattern for the specified chromosome.  The final pattern dictionary passed to addMultiRecombinant() will be validated against the sex parameter given to that method.

It is typically not necessary to call addPatternForClone(), since addMultiRecombinant() will usually automatically infer the correct inheritance pattern from its parental individual parent1 if an inheritance dictionary for the chromosome is not supplied in pattern.  This method is needed primarily for edge cases, such as if addMultiRecombinant() is being used to generate a biparental cross, but a particular chromosome should be cloned from just one of the parents in a manner that the biparental cross would not do automatically.

– (object<Dictionary>$)addPatternForCross(iso<Chromosome>$ chromosome, No<Dictionary>$ pattern, object<Individual>$ parent1, object<Individual>$ parent2, [Ns$ sex = NULL])

Adds an inheritance dictionary for the specified chromosome to the pattern dictionary pattern, representing a biparental cross between parent1 and parent2 to generate an offspring, with sex optionally specified by sex.  The parameter chromosome can provide a chromosome id (an integer), a chromosome symbol (a string), or a Chromosome object.  The resulting pattern dictionary is intended for use with the Subpopulation method addMultiRecombinant(); see that method for background on the use of pattern dictionaries.

The parameter pattern must be a Dictionary (or a subclass of Dictionary), or NULL.  If pattern is NULL, a new singleton object of class Dictionary will be created, set up, and returned; otherwise, the returned object is the same object passed in as pattern.  The inheritance dictionary generated by addPatternForCross() will be added to pattern as the value for a particular key.  If pattern is already configured to use string keys, the key used will be the symbol property of the chromosome; otherwise, including if pattern is NULL, the key used will be the id property of the chromosome.  If the key in question already exists in pattern, its value will be replaced.

The precise inheritance pattern generated by this method depends upon the chromosome’s type; see initializeChromosome() for a description of the different chromosome types and the ways in which they are inherited.  The pattern will be the same as would be used by the addCrossed() method for the chromosome.  In some cases, the value of sex is unimportant and may be left as NULL; a NULL value for sex essentially asserts that the sex of the offspring is unimportant to the inheritance pattern for the chromosome with the given parents.  If that assertion is untrue – if the sex needs to be known, for example to know how a sex chromosome should be inherited – an error will be raised.  When the sex needs to be known, "M" or "F" must be passed so that the correct inheritance pattern can be generated.  In non-sexual models, NULL must be passed.  Note that the generated inheritance dictionary does not encode the offspring sex; the sex parameter is simply used to determine and validate the inheritance pattern for the specified chromosome.  The final pattern dictionary passed to addMultiRecombinant() will be validated against the sex parameter given to that method.

It is typically not necessary to call addPatternForCross(), since addMultiRecombinant() will usually automatically infer the correct inheritance pattern from its parental individuals parent1 and parent2 if an inheritance dictionary for the chromosome is not supplied in pattern.  This method is needed primarily for edge cases, such as if addMultiRecombinant() is being used to generate a clonal offspring, but a particular chromosome should be produced with recombination.

– (object<Dictionary>$)addPatternForNull(iso<Chromosome>$ chromosome, No<Dictionary>$ pattern, [Ns$ sex = NULL])

Adds an inheritance dictionary for the specified chromosome to the pattern dictionary pattern, representing a non-inheritance event producing null haplosomes, with sex optionally specified by sex.  The parameter chromosome can provide a chromosome id (an integer), a chromosome symbol (a string), or a Chromosome object.  The resulting pattern dictionary is intended for use with the Subpopulation method addMultiRecombinant(); see that method for background on the use of pattern dictionaries.

The parameter pattern must be a Dictionary (or a subclass of Dictionary), or NULL.  If pattern is NULL, a new singleton object of class Dictionary will be created, set up, and returned; otherwise, the returned object is the same object passed in as pattern.  The inheritance dictionary generated by addPatternForNull() will be added to pattern as the value for a particular key.  If pattern is already configured to use string keys, the key used will be the symbol property of the chromosome; otherwise, including if pattern is NULL, the key used will be the id property of the chromosome.  If the key in question already exists in pattern, its value will be replaced.

For all chromosome types, this method will simply produce a null haplosome or haplosomes for the specified chromosome.  If the chromosome does not allow a null haplosome, an error will be raised.  In some cases, the value of sex is unimportant and may be left as NULL; a NULL value for sex essentially asserts that the chromosome allows null haplosomes for any sex.  If that assertion is untrue – if the sex needs to be known, for example to know whether a null haplosome is allowed for a sex chromosome – an error will be raised.  When the sex needs to be known, "M" or "F" must be passed so that addPatternForNull() is satisfied that a legal pattern for the chromosome will be generated.  In non-sexual models, NULL must be passed.  Note that the generated inheritance dictionary does not encode the offspring sex; the sex parameter is simply used to determine and validate the inheritance pattern for the specified chromosome.  The final pattern dictionary passed to addMultiRecombinant() will be validated against the sex parameter given to that method.

If only one of the two haplosomes for a diploid chromosome should be a null haplosome, and addPatternForCrossed() and addPatternForCloned() would not produce the desired pattern, use addPatternForRecombinant(), which provides complete control.

– (object<Dictionary>$)addPatternForRecombinant(iso<Chromosome>$ chromosome, No<Dictionary>$ pattern, No<Haplosome>$ strand1, No<Haplosome>$ strand2, Ni breaks1, No<Haplosome>$ strand3, No<Haplosome>$ strand4, Ni breaks2, [Ns$ sex = NULL], [logical$ randomizeStrands = T])

Adds an inheritance dictionary for the specified chromosome to the pattern dictionary pattern, representing inheritance by cloning, recombination, or both, to generate an offspring from up to four parental haplosomes, with sex optionally specified by sex.  The parameter chromosome can provide a chromosome id (an integer), a chromosome symbol (a string), or a Chromosome object.  The resulting pattern dictionary is intended for use with the Subpopulation method addMultiRecombinant(); see that method for background on the use of pattern dictionaries.

The parameter pattern must be a Dictionary (or a subclass of Dictionary), or NULL.  If pattern is NULL, a new singleton object of class Dictionary will be created, set up, and returned; otherwise, the returned object is the same object passed in as pattern.  The inheritance dictionary generated by addPatternForRecombinant() will be added to pattern as the value for a particular key.  If pattern is already configured to use string keys, the key used will be the symbol property of the chromosome; otherwise, including if pattern is NULL, the key used will be the id property of the chromosome.  If the key in question already exists in pattern, its value will be replaced.

When passed the resulting pattern dictionary, the addMultiRecombinant() method will produce the first offspring haplosome using the Haplosome objects strand1 and strand2 with the vector of recombination breakpoints breaks1, and likewise will produce the second offspring haplosome using strand3, strand4, and breaks2.  If both parental strands for an offspring haplosome are NULL, the breaks vector must be NULL or empty, and a null haplosome will be produced.  If the first parental strand is non-NULL and the second is NULL for an offspring haplosome, the breaks vector must again be NULL or empty, and the first strand will be cloned with mutation.  If both parental strands for an offspring haplosome are non-NULL, recombination between the strands will be done using the supplied breaks vector; in this case, if the breaks vector is NULL then addMultiRecombinant() will automatically generate breakpoints for the recombination.  All of these semantics are discussed further in the documentation for addMultiRecombinant(); this method is just a helper for that method.  The documentation for addRecombinant() may also be helpful for understanding the concepts here, since it is the conceptual foundation upon which the very complex architecture of the addMultiRecombinant() method is built.

Unlike addPatternForClone() and addPatternForCrossed(), which must infer the inheritance pattern given the chromosome type and the offspring sex, addPatternForRecombinant() is given the inheritance pattern, and must simply confirm that it is valid.  If sex is NULL (the default), this validation only needs to check that the inheritance pattern is possible, for some sex.  For example, if the chromosome type is "X" then the inheritance pattern must produce a non-null first haplosome, and the second haplosome can be null (for a male, X–) or non-null (for a female, XX); other inheritance patterns would fail validation.  When the sex is known, "M" or "F" may optionally be passed to validate against that sex; X– would then fail validation if a female is specified, for example.  In non-sexual models, NULL must be passed.  Note that the generated inheritance dictionary does not encode the offspring sex; the sex parameter is simply used for validation.  The final pattern dictionary passed to addMultiRecombinant() will be validated against the sex parameter given to that method.

The randomizeStrands parameter indicates whether or not the order of recombining parental strands should be randomized in the inheritance dictionary.  If randomizeStrands is T, then if strand1 and strand2 are both non-NULL, their order will be randomized; and similarly for strand3 and strand4.  If randomizeStrands is F, no randomization will be done in the inheritance dictionary – but strand order randomization may still be done by addMultiRecombinant() if T is passed for its own randomizeStrands parameter.  Randomizing the strand order is usually desirable, to avoid an inheritance bias due to a lack of randomization in the initial copy strand.  Whether you wish to randomize strand order in addPatternForRecombinant() or in addMultiRecombinant() is up to you; it is harmless to do it in both places, apart from a small performance penalty, but there is no benefit.

Of the family of addPatternFor...() methods, addPatternForRecombinant() is the most commonly used.  Typically, addMultiRecombinant() can automatically infer the correct inheritance pattern for crossing or cloning (as described in its documentation), but for more complex inheritance patterns, using addPatternForRecombinant() is necessary (unless you want to build the pattern dictionary yourself).

– (object<Subpopulation>$)addSubpop(is$ subpopID, integer$ size, [float$ sexRatio = 0.5], [logical$ haploid = F])

Add a new subpopulation with id subpopID and size individuals.  The subpopID parameter may be either an integer giving the ID of the new subpopulation, or a string giving the name of the new subpopulation (such as "p5" to specify an ID of 5).  Only if sex is enabled for the species, the initial sex ratio may optionally be specified as sexRatio (as the male fraction, M:M+F); if it is not specified, a default of 0.5 is used.  The new subpopulation will be defined as a global variable immediately by this method, and will also be returned by this method.  Subpopulations added by this method will initially consist of individuals with empty haplosomes.  In order to model subpopulations that split from an already existing subpopulation, use addSubpopSplit().

The haploid parameter defaults to F, indicating that the generated individuals should adopt their natural ploidy; in particular, type "A" chromosomes should be represented in each individual by two empty haplosomes.  The haploid parameter may instead be T; in this case, for all chromosomes of type "A" (and only that type), the second haplosome of each new individual will be a null haplosome, rather than an empty haplosome.  This could be useful in a model of haplodiploidy, for example, to generate initial individuals that are haploid for the autosomal chromosomes of the species.  For even greater control in nonWF models, you can call addSubpop() with an initial size of 0 and then stock the population with new individuals created however you wish in the next tick’s reproduction() callback, such as with the addEmpty() method, providing separate control over the configuration of each individual.

– (object<Subpopulation>$)addSubpopSplit(is$ subpopID, integer$ size, io<Subpopulation>$ sourceSubpop, [float$ sexRatio = 0.5])

Split off a new subpopulation with id subpopID and size individuals derived from subpopulation sourceSubpop.  The subpopID parameter may be either an integer giving the ID of the new subpopulation, or a string giving the name of the new subpopulation (such as "p5" to specify an ID of 5).  The sourceSubpop parameter may specify the source subpopulation either as a Subpopulation object or by integer identifier.  Only if sex is enabled for the species, the initial sex ratio may optionally be specified as sexRatio (as the male fraction, M:M+F); if it is not specified, a default of 0.5 is used.  The new subpopulation will be defined as a global variable immediately by this method, and will also be returned by this method.

Subpopulations added by this method will consist of individuals that are clonal copies of individuals from the source subpopulation, randomly chosen with probabilities proportional to fitness.  The fitness of all of these initial individuals is considered to be 1.0, to avoid a doubled round of selection in the initial tick, given that fitness values were already used to choose the individuals to clone.  Once this initial set of individuals has mated to produce offspring, the model is effectively of parental individuals in the source subpopulation mating randomly according to fitness, as usual in SLiM, with juveniles migrating to the newly added subpopulation.  Effectively, then, then new subpopulation is created empty, and is filled by migrating juveniles from the source subpopulation, in accordance with SLiM’s usual model of juvenile migration.

– (object<Chromosome>)chromosomesOfType(string$ type)

Returns a vector of Chromosome objects of the chromosome type supplied in type, in the order if which they were defined.  If type does not correspond to a chromosome type accepted by initializeChromosome(), an error will be raised.  See also chromosomesWithIDs() and chromosomesWithSymbols().

– (object<Chromosome>)chromosomesWithIDs(integer ids)

Returns a vector of Chromosome objects corresponding to the chromosome ids supplied in ids, in the same order.  If any chromosome id in ids does not correspond to a chromosome in the target species, an error will be raised.  See also chromosomesOfType() and chromosomesWithSymbols().

– (object<Chromosome>)chromosomesWithSymbols(string symbols)

Returns a vector of Chromosome objects corresponding to the chromosome symbols supplied in symbols, in the same order.  If any chromosome symbol in symbols does not correspond to a chromosome in the target species, an error will be raised.  See also chromosomesOfType() and chromosomesWithIDs().

– (integer$)countOfMutationsOfType(io<MutationType>$ mutType)

Returns the number of mutations that are of the type specified by mutType, out of all of the mutations that are currently active in the species.  If you need a vector of the matching Mutation objects, rather than just a count, use -mutationsOfType().  This method is often used to determine whether an introduced mutation is still active (as opposed to being either lost or fixed).  This method is provided for speed; it is much faster than the corresponding Eidos code.

– (object<Individual>)individualsWithPedigreeIDs(integer pedigreeIDs, [Nio<Subpopulation> subpops = NULL])

Looks up individuals by pedigree ID, optionally within specific subpopulations.  Pedigree tracking must be turned on with initializeSLiMOptions(keepPedigrees=T) to use this method, otherwise an error will result.  This method is vectorized; more than one pedigree id may be passed in pedigreeID, in which case the returned vector will contain all of the individuals for which a match was found (in the same order in which they were supplied).  If a given id is not found, the returned vector will contain no entry for that id (so the length of the returned vector may not match the length of pedigreeIDs).  If none of the given ids were found, the returned vector will be object<Individual>(0), an empty object vector of class Individual.  If you have more than one pedigree ID to look up, calling this method just once, in vectorized fashion, may be much faster than calling it once for each ID, due to internal optimizations.

To find individuals within all subpopulations, pass the default of NULL for subpops.  If you are interested only in matches within a specific subpopulation, pass that subpopulation for subpops; that will make the search faster.  Similarly, if you know that a particular subpopulation is the most likely to contain matches, you should supply that subpopulation first in the subpops vector so that it will be searched first; the supplied subpopulations are searched in order.  Subpopulations may be supplied either as integer IDs, or as Subpopulation objects.

– (void)killIndividuals(object<Individual> individuals)

Immediately kills the individuals in individuals.  This removes them from their subpopulation and gives them an index value of -1.  The Individual objects are not freed immediately, since references to them could still exist in local Eidos variables; instead, the individuals are kept in a temporary “graveyard” until they can be freed safely.  It therefore continues to be safe to use them and their haplosomes, except that accessing their subpopulation property will raise an error since they no longer have a subpopulation.

Note that the indices and order of individuals and haplosomes in all source subpopulations will change unpredictably as a side effect of this method.  All evaluated interactions are invalidated as a side effect of calling this method.

Note that this method is only for use in nonWF models, in which mortality is managed manually by the model script.  In WF models, mortality is managed automatically by the SLiM core when the new offspring generation becomes the parental generation and the previous parental generation dies; mortality does not otherwise occur in WF models.  In nonWF models, mortality normally occurs during the survival stage of the tick cycle, based upon the fitness values calculated by SLiM, and survival() callbacks can influence the outcome of that survival stage.  Calls to killIndividuals(), on the other hand, can be made at any time during first(), early(), or late() events, and the result cannot be modified by survival() callbacks; the given individuals are simply immediately killed.  This method therefore provides an alternative, and relatively rarely used, mortality mechanism that is disconnected from fitness.

– (integer)mutationCounts(Nio<Subpopulation> subpops, [No<Mutation> mutations = NULL])

Return an integer vector with the frequency counts of all of the Mutation objects passed in mutations, within the Subpopulation objects in subpops.  The subpops argument is required, but you may pass NULL to get population-wide frequency counts.  Subpopulations may be supplied either as integer IDs, or as Subpopulation objects.  If the optional mutations argument is NULL (the default), frequency counts will be returned for all of the active Mutation objects in the species – the same Mutation objects, and in the same order, as would be returned by the mutations property of sim, in other words.

See the -mutationFrequencies() method to obtain float frequencies instead of integer counts.  See also the Haplosome methods mutationCountsInHaplosomes() and mutationFrequenciesInHaplosomes().

– (float)mutationFrequencies(Nio<Subpopulation> subpops, [No<Mutation> mutations = NULL])

Return a float vector with the frequencies of all of the Mutation objects passed in mutations, within the Subpopulation objects in subpops.  The subpops argument is required, but you may pass NULL to get population-wide frequencies.  Subpopulations may be supplied either as integer IDs, or as Subpopulation objects.  If the optional mutations argument is NULL (the default), frequencies will be returned for all of the active Mutation objects in the species – the same Mutation objects, and in the same order, as would be returned by the mutations property of sim, in other words.

See the -mutationCounts() method to obtain integer counts instead of float frequencies.  See also the Haplosome methods mutationCountsInHaplosomes() and mutationFrequenciesInHaplosomes().

 (object<Mutation>)mutationsOfType(io<MutationType>$ mutType)

Returns an object vector of all the mutations that are of the type specified by mutType, out of all of the mutations that are currently active in the species.  If you just need a count of the matching Mutation objects, rather than a vector of the matches, use -countOfMutationsOfType().  This method is often used to look up an introduced mutation at a later point in the simulation, since there is no way to keep persistent references to objects in SLiM.  This method is provided for speed; it is much faster than the corresponding Eidos code.

– (void)outputFixedMutations([Ns$ filePath = NULL], [logical$ append = F], [logical$ objectTags = F])

Output all fixed mutations – all Substitution objects, in other words – in a SLiM native format.  If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.  Mutations which have fixed but have not been turned into Substitution objects – typically because convertToSubstitution has been set to F for their mutation type – are not output; they are still considered to be segregating mutations by SLiM.

In SLiM 3.3 and later, the output format includes the nucleotides associated with any nucleotide-based mutations.

In SLiM 5.0 and later, in models with multiple chromosome the output includes the symbol of the chromosome associated with each mutation.

Beginning with SLiM 5.0, the objectTags parameter may be used to request that tag values for substitutions be written out.

Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

– (void)outputFull([Ns$ filePath = NULL], [logical$ binary = F], [logical$ append = F], [logical$ spatialPositions = T], [logical$ ages = T], [logical$ ancestralNucleotides = T], [logical$ pedigreeIDs = F], [logical$ objectTags = F], [logical$ substitutions = F])

Output the state of the entire population.  If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.  When writing to a file, a logical flag, binary, may be supplied as well.  If binary is T, the population state will be written as a binary file instead of a text file (binary data cannot be written to the standard output stream).  The binary file is usually smaller, and in any case will be read much faster than the corresponding text file would be read.  Binary files are not guaranteed to be portable between platforms; in other words, a binary file written on one machine may not be readable on a different machine (but in practice it usually will be, unless the platforms being used are fairly unusual).  If binary is F (the default), a text file will be written.

Beginning with SLiM 2.3, the spatialPositions parameter may be used to control the output of the spatial positions of individuals in species for which continuous space has been enabled using the dimensionality option of initializeSLiMOptions().  If spatialPositions is F, the output will not contain spatial positions, and will be identical to the output generated by SLiM 2.1 and later.  If spatialPositions is T, spatial position information will be output if it is available.  If the species does not have continuous space enabled, the spatialPositions parameter will be ignored.  Positional information may be output for all output destinations – the Eidos output stream, a text file, or a binary file.

Beginning with SLiM 3.0, the ages parameter may be used to control the output of the ages of individuals in nonWF simulations.  If ages is F, the output will not contain ages, preserving backward compatibility with the output format of SLiM 2.1 and later.  If ages is T, ages will be output for nonWF models.  In WF simulations, the ages parameter will be ignored.

Beginning with SLiM 3.3, the ancestralNucleotides parameter may be used to control the output of the ancestral nucleotide sequence in nucleotide-based models.  If ancestralNucleotides is F, the output will not contain ancestral nucleotide information, and so the ancestral sequence will not be restored correctly if the saved file is loaded with readPopulationFile().  This option is provided because the ancestral sequence may be quite large, for models with a long chromosome (e.g., 1 GB if the chromosome is 109 bases long, when saved in text format, or 0.25 GB when saved in binary format).  If the model is not nucleotide-based (as enabled with the nucleotideBased parameter to initializeSLiMOptions()), the ancestralNucleotides parameter will be ignored.  Note that in nucleotide-based models the output format will always include the nucleotides associated with any nucleotide-based mutations; the ancestralNucleotides flag governs only the ancestral sequence.

Beginning with SLiM 3.5, the pedigreeIDs parameter may be used to request that pedigree IDs be written out (and read in by readFromPopulationFile(), subsequently).  This option is turned off (F) by default, for brevity.  This option may only be used if SLiM’s optional pedigree tracking has been enabled with initializeSLiMOptions(keepPedigrees=T).

Beginning with SLiM 5.0, the objectTags parameter may be used to request that tag values for objects be written out.  This option is turned off (F) by default, for brevity; if it turned on (T), the values of all tags for all objects of supported classes (Chromosome, Subpopulation, Individual, Haplosome, Mutation, Substitution) will be written.  For individuals, the tag, tagF, tagL0, tagL1, tagL2, tagL3, and tagL4 properties will be written; for chromosomes, subpopulations, haplosomes, and mutations, the tag property will be written.  The saved tag information can be read in by readFromPopulationFile(), but only if the output is in binary format (binary=T).  Note that if there is other state that you wish you persist, such as tags on objects of other classes, values attached to objects with setValue(), and so forth, you should persist that state in separate files using calls such as writeFile().

Beginning with SLiM 5.0, the substitutions parameter may be used to request that information about Substitution objects in the simulation be written out.  This option is turned off (F) by default, for brevity.  The saved substitution information can be read in by readFromPopulationFile(), but only if the output is in binary format (binary=T).

Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

– (void)outputMutations(object<Mutation> mutations, [Ns$ filePath = NULL], [logical$ append = F], [logical$ objectTags = F])

Output all of the given mutations.  This can be used to output all mutations of a given mutation type, for example.  If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.

In SLiM 3.3 and later, the output format includes the nucleotides associated with any nucleotide-based mutations.

In SLiM 5 and later, in models with multiple chromosome the output includes the symbol of the chromosome associated with each mutation.

Beginning with SLiM 5.0, the objectTags parameter may be used to request that tag values for mutations be written out.

Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

– (integer$)readFromPopulationFile(string$ filePath, [No<Dictionary>$ subpopMap = NULL])

Read from a population file, whether in text or binary format as previously specified to outputFull(), and return the tick counter value represented by the file’s contents (i.e., the tick at which the file was generated).  Although this is most commonly used to set up initial populations (often in an Eidos event set to run in tick 1, immediately after simulation initialization), it may be called in any early() or late() event; the current state of all populations in the target species will be wiped and replaced by the state in the file at filePath.  All Eidos variables that are of type object and have element class Subpopulation, Haplosome, Mutation, Individual, or Substitution will be removed as a side effect of this method if they contain any element that belongs to the target species, because those objects will no longer exist in the SLiM simulation; if you want to preserve any of that state, you should output it or save it to a file prior to this call.  New symbols (p1, p2, etc.) will be defined to refer to the new Subpopulation objects loaded from the file.  Note that fitness values are not calculated as a side effect of this call (because the simulation will often need to evaluate interactions or modify other state prior to doing so).

In SLiM 2.3 and later when using the WF model, calling readFromPopulationFile() from any context other than a late() event causes a warning; calling from a late() event is almost always correct in WF models, so that fitness values can be automatically recalculated by SLiM at the usual time in the tick cycle without the need to force their recalculation (see comments on recalculateFitness()).

In SLiM 3.0 when using the nonWF model, calling readFromPopulationFile() from any context other than an early() event causes a warning; calling from an early() event is almost always correct in nonWF models, so that fitness values can be automatically recalculated by SLiM at the usual time in the tick cycle without the need to force their recalculation (see comments on recalculateFitness()).

This method changes the tick and cycle counters to the tick and cycle read from the file.  If you do not want these counters to be changed, you can change them back after reading, by setting community.tick and sim.cycle to whatever values you wish.  Note that restoring a saved past state and running forward again will not yield the same simulation results, because the random number generator’s state will not be the same; if you wish to ensure reproducibility from a given time point, setSeed() can be used to establish a new seed value.

Any changes made to the structure of the species (mutation types, genomic element types, etc.) will not be wiped and re-established by readFromPopulationFile(); this method loads only the population’s state, not the species configuration, so care should be taken to ensure that the species structure meshes coherently with the loaded data.  Indeed, state such as the selfing and cloning rates of subpopulations, and values set onto objects with setValue(), will also be lost, since it is not saved out by outputFull().  Only information saved by outputFull() will be restored; all other state associated with the mutations, haplosomes, individuals, and subpopulations in the simulation will be lost, and should be re-established by the model if it is still needed.  Note that some state is saved by outputFull() only optionally, such as the tag values of individuals; if a given option is enabled and the corresponding information is saved, then that information will be restored, otherwise it will not be.

As of SLiM 2.3, this method will read and restore the spatial positions of individuals if that information is present in the output file and the species has enabled continuous space.  If spatial positions are present in the output file but the species has not enabled continuous space (or the number of spatial dimensions does not match), an error will result.  If the species has enabled continuous space but spatial positions are not present in the output file, the spatial positions of the individuals read will be undefined, but an error is not raised.

As of SLiM 3.0, this method will read and restore the ages of individuals if that information is present in the output file and the simulation is based upon the nonWF model.  If ages are present but the simulation uses a WF model, an error will result; the WF model does not use age information.  If ages are not present but the simulation uses a nonWF model, an error will also result; the nonWF model requires age information.

As of SLiM 3.3, this method will restore the nucleotides of nucleotide-based mutations, and will restore the ancestral nucleotide sequence, if that information is present in the output file.  Loading an output file that contains nucleotide information in a non-nucleotide-based model, and vice versa, will produce an error.

As of SLiM 3.5, this method will read and restore the pedigree IDs of individuals and haplosomes if that information is present in the output file (as requested with outputFull(pedigreeIDs=T)) and if SLiM’s optional pedigree tracking has been enabled with initializeSLiMOptions(keepPedigrees=T).

As of SLiM 5.0, this method will read and restore tag values for objects of supported classes (Chromosome, Subpopulation, Individual, Haplosome, Mutation, Substitution) if they were saved by outputFull() with its objectTags=T option.  This facility is only available when reading binary output from outputFull(), as chosen by its binary=T option; otherwise, an error will result.

As of SLiM 5.0, this method will read and restore substitutions if they were saved by outputFull() with its substitutions=T option.  This facility is only available when reading binary output from outputFull(), as chosen by its binary=T option; otherwise, an error will result.

This method can also be used to read tree-sequence information, in the form of single-chromosome .trees files and multi-chromosome trees archives, as saved by treeSeqOutput() or generated by the Python pyslim package.  Note that the user metadata for a tree-sequence file can be read separately with the treeSeqMetadata() function.  Beginning with SLiM 4, the subpopMap parameter may be supplied to re-order the populations of the input tree sequence when it is loaded in to SLiM.  This parameter must have a value that is a Dictionary; the keys of this dictionary should be SLiM population identifiers as string values (e.g., "p2"), and the values should be indexes of populations in the input tree sequence; a key/value pair of "p2", 4 would mean that the fifth population in the input (the one at zero-based index 4) should become p2 on loading into SLiM.  If subpopMap is non-NULL, all populations in the tree sequence must be explicitly mapped, even if their index will not change and even if they will not be used by SLiM; the only exception is for unused slots in the population table, which can be explicitly remapped but do not have to be.  For instance, suppose we have a tree sequence in which population 0 is unused, population 1 is not a SLiM population (for example, an ancestral population produced by msprime), and population 2 is a SLiM population, and we want to load this in with population 2 as p0 in SLiM.  To do this, we could supply a value of Dictionary("p0", 2, "p1", 1, "p2", 0) for subpopMap, or we could leave out slot 0 since it is unused, with Dictionary("p0", 2, "p1", 1).  Although this facility cannot be used to remove populations in the tree sequence, note that it may add populations that will be visible when treeSeqOutput() is called (although these will not be SLiM populations); if, in this example, we had used Dictionary("p0", 0, "p1", 1, "p5", 2) and then we wrote the result out with treeSeqOutput(), the resulting tree sequence would have six populations, although three of them would be empty and would not be used by SLiM.  The use of subpopMap makes it easier to load simulation data that was generated in Python, since that typically uses an id of 0.  The subpopMap parameter may not be used with file formats other than tree-sequence files, at the present time; setting up the correct subpopulation ids is typically easier when working with those other formats.  Note the tskit command-line interface can be used, like python3 -m tskit populations file.trees, to find out the number of subpopulations in a tree-sequence file and their IDs.

When loading a tree sequence, a crosscheck of the loaded data will be performed to ensure that the tree sequence was well-formed and was loaded correctly.  When running a Release build of SLiM, however, this crosscheck will only occur the first time that readFromPopulationFile() is called to load a tree sequence; subsequent calls will not perform this crosscheck, for greater speed when running models that load saved population state many times (such as models that are conditional on fixation).  If you suspect that a tree sequence file might be corrupted, or might be read incorrectly, running a Debug build of SLiM enables crosschecks after every load.

– (void)recalculateFitness([Ni$ tick = NULL])

Force an immediate recalculation of fitness values for all individuals in all subpopulations.  Normally fitness values are calculated at a fixed point in each tick, and those values are cached and used until the next recalculation.  If simulation parameters are changed in script in a way that affects fitness calculations, and if you wish those changes to take effect immediately rather than taking effect at the next automatic recalculation, you may call recalculateFitness() to force an immediate recalculation and recache.

The optional parameter tick provides the tick for which mutationEffect() and fitnessEffect() callbacks should be selected; if it is NULL (the default), the current tick value for the simulation, community.tick, is used.  If you call recalculateFitness() in an early() event in a WF model, you may wish this to be community.tick - 1 in order to utilize the mutationEffect() and fitnessEffect() callbacks for the previous tick, as if the changes that you have made to fitness-influencing parameters were already in effect at the end of the previous tick when the new generation was first created and evaluated (usually it is simpler to just make such changes in a late() event instead, however, in which case calling recalculateFitness() is probably not necessary at all since fitness values will be recalculated immediately afterwards).  Regardless of the value supplied for tick here, community.tick inside callbacks will report the true tick number, so if your callbacks consult that parameter in order to create tick-specific fitness effects you will need to handle the discrepancy somehow.  (Similar considerations apply for nonWF models that call recalculateFitness() in a late() event, which is also not advisable in general.)

After this call, the fitness values used for all purposes in SLiM will be the newly calculated values.  Calling this method will trigger the calling of any enabled and applicable mutationEffect() and fitnessEffect() callbacks, so this is quite a heavyweight operation; you should think carefully about what side effects might result (which is why fitness recalculation does not just occur automatically after changes that might affect fitness values).

– (object<SLiMEidosBlock>$)registerFitnessEffectCallback(Nis$ id, string$ source, [Nio<Subpopulation>$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos fitnessEffect() callback in the current simulation (specific to the target species), with an optional subpopulation subpop (which may be an integer identifier, or NULL, the default, to indicate all subpopulations), and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerMateChoiceCallback(Nis$ id, string$ source, [Nio<Subpopulation>$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos mateChoice() callback in the current simulation (specific to the target species), with optional subpopulation subpop (which may be an integer identifier, or NULL, the default, to indicate all subpopulations) and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerModifyChildCallback(Nis$ id, string$ source, [Nio<Subpopulation>$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos modifyChild() callback in the current simulation (specific to the target species), with optional subpopulation subpop (which may be an integer identifier, or NULL, the default, to indicate all subpopulations) and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerMutationCallback(Nis$ id, string$ source, [Nio<MutationType>$ mutType = NULL], [Nio<Subpopulation>$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos mutation() callback in the current simulation (specific to the target species), with an optional mutation type mutType (which may be an integer mutation type identifier, or NULL, the default, to indicate all mutation types), optional subpopulation subpop (which may also be an integer identifier, or NULL, the default, to indicate all subpopulations), and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerMutationEffectCallback(Nis$ id, string$ source, io<MutationType>$ mutType, [Nio<Subpopulation>$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos mutationEffect() callback in the current simulation (specific to the target species), with a required mutation type mutType (which may be an integer mutation type identifier), optional subpopulation subpop (which may also be an integer identifier, or NULL, the default, to indicate all subpopulations), and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerRecombinationCallback(Nis$ id, string$ source, [Nio<Subpopulation>$ subpop = NULL], [Niso<Chromosome>$ chromosome = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos recombination() callback in the current simulation (specific to the target species), with optional subpopulation subpop (which may be an integer identifier, or NULL, the default, to indicate all subpopulations) and optional start and end ticks all limiting its applicability.  In multi-chromosome models, parameter chromosome, if non-NULL, may specify a chromosome to which the callback will apply (as either an integer id, a string symbol, or a Chromosome object); otherwise, NULL indicates that the callback applies to all chromosomes.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerReproductionCallback(Nis$ id, string$ source, [Nio<Subpopulation>$ subpop = NULL], [Ns$ sex = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos reproduction() callback in the current simulation (specific to the target species), with optional subpopulation subpop (which may be an integer identifier, or NULL, the default, to indicate all subpopulations), optional sex-specificity sex (which may be "M" or "F" in sexual species to make the callback specific to males or females respectively, or NULL for no sex-specificity), and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (object<SLiMEidosBlock>$)registerSurvivalCallback(Nis$ id, string$ source, [Nio<Subpopulation>$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Register a block of Eidos source code, represented as the string singleton source, as an Eidos survival() callback in the current simulation (specific to the target species), with optional subpopulation subpop (which may be an integer identifier, or NULL, the default, to indicate all subpopulations) and optional start and end ticks all limiting its applicability.  The script block will be given identifier id (specified as an integer, or as a string symbolic name such as "s5"); this may be NULL if there is no need to be able to refer to the block later.  The registered callback is added to the end of the list of registered SLiMEidosBlock objects, and is active immediately; it may be eligible to execute in the current tick.  The new SLiMEidosBlock will be defined as a global variable immediately by this method, and will also be returned by this method.

– (void)simulationFinished(void)

Declare the current simulation finished.  This method is equivalent to the Community method simulationFinished(), except that this method is only legal to call in single-species models (to provide backward compatibility).  It is recommended that new code should call the Community method; this method may be deprecated in the future.

– (void)skipTick(void)

Deactivate the target species for the current tick.  This sets the active property of the species to F; it also set the active property of all callbacks that belong to the species (with the species as their species specifier) to F, and sets the active property of all events that are synchronized with the species (with the species as their ticks specifier) to F.  The cycle counter for the species will not be incremented at the end of the tick.  This method may only be called in first() events, to ensure that species are either active or inactive throughout a given tick.

– (object<Mutation>)subsetMutations([No<Mutation>$ exclude = NULL], [Nio<MutationType>$ mutType = NULL], [Ni$ position = NULL], [Nis$ nucleotide = NULL], [Ni$ tag = NULL], [Ni$ id = NULL], [Niso<Chromosome> chromosome = NULL])

Returns a vector of mutations subset from the list of all active mutations in the species (as would be provided by the mutations property).  The parameters specify constraints upon the subset of mutations that will be returned.  Parameter exclude, if non-NULL, may specify a specific mutation that should not be included (typically the focal mutation in some operation).  Parameter mutType, if non-NULL, may specify a mutation type for the mutations to be returned (as either a MutationType object or an integer identifier).  Parameter position, if non-NULL, may specify a base position for the mutations to be returned.  Parameter nucleotide, if non-NULL, may specify a nucleotide for the mutations to be returned (either as a string, "A" / "C" / "G" / "T", or as an integer, 0 / 1 / 2 / 3 respectively).  Parameter tag, if non-NULL, may specify a tag value for the mutations to be returned.  Parameter id, if non-NULL, may specify a required value for the id property of the mutations to be returned.  Parameter chromosome, if non-NULL, may specify a chromosome or chromosomes with which the mutations returned must be associated (as either integer ids, string symbols, or Chromosome objects).

This method is shorthand for getting the mutations property of the subpopulation, and then using operator [] to select only mutations with the desired properties; besides being much simpler than the equivalent Eidos code, it is also much faster.  Note that if you only need to select on mutation type, the mutationsOfType() method will be even faster.

– (object<Substitution>)substitutionsOfType(io<MutationType>$ mutType)

Returns an object vector of all the substitutions that are of the type specified by mutType, out of all of the substitutions that are currently present in the species.  This method is provided for speed; it is much faster than the corresponding Eidos code.  See also mutationsOfType().

– (logical$)treeSeqCoalesced(void)

Returns the coalescence state for the recorded tree sequence at the last simplification.  The returned value is a logical singleton flag, T to indicate that full coalescence was observed at the last tree-sequence simplification (meaning that there is a single ancestral individual that roots all ancestry trees at all sites along the chromosome – although not necessarily the same ancestor at all sites), or F if full coalescence was not observed.  For simple models, reaching coalescence may indicate that the model has reached an equilibrium state, but this may not be true in models that modify the dynamics of the model during execution by changing migration rates, introducing new mutations programmatically, dictating non-random mating, etc., so be careful not to attach more meaning to coalescence than it is due; some models may require burn-in beyond coalescence to reach equilibrium, or may not have an equilibrium state at all.  Also note that some actions by a model, such as adding a new subpopulation, may cause the coalescence state to revert from T back to F (at the next simplification), so a return value of T may not necessarily mean that the model is coalesced at the present moment – only that it was coalesced at the last simplification.

This method may only be called if tree sequence recording has been turned on with initializeTreeSeq(); in addition, checkCoalescence=T must have been supplied to initializeTreeSeq(), so that the necessary work is done during each tree-sequence simplification.  Since this method does not perform coalescence checking itself, but instead simply returns the coalescence state observed at the last simplification, it may be desirable to call treeSeqSimplify() immediately before treeSeqCoalesced() to obtain up-to-date information.  However, the speed penalty of doing this in every tick would be large, and most models do not need this level of precision; usually it is sufficient to know that the model has coalesced, without knowing whether that happened in the current tick or in a recent preceding tick.

– (void)treeSeqOutput(string$ path, [logical$ simplify = T], [logical$ includeModel = T], [No<Dictionary>$ metadata = NULL], [logical$ overwriteDirectory = F])

Outputs the current tree sequence recording tables to the path specified by path.  This method may only be called if tree sequence recording has been turned on with initializeTreeSeq().  If simplify is T (the default), simplification will be done immediately prior to output; this is almost always desirable, unless a model wishes to avoid simplification entirely.  (Note that if simplification is not done, then all haplosomes since the last simplification will be marked as samples in the resulting tree sequence.)

In a model of a single chromosome, a binary tree sequence file will be written to the specified path; a filename extension of .trees is suggested for this type of file, and such a file is often referred to as a “.trees file”.  In a multi-chromosome model, a directory will instead be created at the specified path, and a separate .trees file will be created within that directory for each chromosome in the model, mirroring the fact that SLiM keeps a separate tree sequence for each chromosome in a multi-chromosome model.  These .trees files will be given filenames based upon the symbol property of each chromosome, as provided to initializeChromosome(); for example, the tree sequence for a chromosome with symbol "X" will be saved as chromosome_X.trees within the specified directory.  For the name of the directory itself, a suffix of _trees is suggested, rather than .trees, since the use of dot-extensions in directory names is not common; for example, "model_Q_seed_17_trees" would be a path you might pass to treeSeqOutput() as a directory name in a multi-chromosome model.  Such a directory, containing separate .trees files for each chromosome, is called a “trees archive”.  Both .trees files and trees archives can be read by readFromPopulationFile(), as discussed in its documentation.

Normally, the full SLiM script used to generate the tree sequence is written out to the provenance entry of the tree sequence file, to the model subkey of the parameters top-level key.  Supplying F for includeModel suppresses output of the full script.

A Dictionary object containing user-generated metadata may be supplied with the metadata parameter.  If present, this dictionary will be serialized as JSON and attached to the saved tree sequence under a key named user_metadata, within the SLiM key.  If tskit is used to read the tree sequence in Python, this metadata will automatically be deserialized and made available at ts.metadata["SLiM"]["user_metadata"].  This metadata dictionary is not used by SLiM, or by pyslim, tskit, or msprime; you may use it for any purpose you wish.  Note that metadata may actually be any subclass of Dictionary, such as a DataFrame.  It can even be a Species object such as sim, or a LogFile instance; however, only the keys and values contained by the object’s Dictionary superclass state will be serialized into the metadata (properties of the subclass will be ignored).  This metadata dictionary can be recovered from the saved file using the treeSeqMetadata() function.

When saving a single .trees file, the standard behavior is to overwrite an existing file of the same name; the convenience of this generally outweighs the danger.  When saving a trees archive, however, that balance shifts; overwriting an entire directory is potentially quite dangerous.  For this reason, overwriteDirectory=F (the default) specifies that treeSeqOutput() should not overwrite an existing directory; it will instead raise an error.  If overwriteDirectory is T, treeSeqOutput() will overwrite an existing directory of the same name (if the existing directory can be deleted without permissions errors and so forth), but only if the existing directory is empty or contains only files with a .trees suffix, for safety; if other files are present, an error will still be raised.

– (void)treeSeqRememberIndividuals(object<Individual> individuals, [logical$ permanent = T])

Mark the individuals specified by individuals to be kept across tree sequence table simplification.  This method may only be called if tree sequence recording has been turned on with initializeTreeSeq().  All currently living individuals are always kept across simplification; this method does not need to be called, and indeed should not be called, for that purpose.  Instead, treeSeqRememberIndividuals() allows any individual, including dead individuals, to be kept in the final tree sequence.  Typically this would be used, for example, to keep particular individuals that you wanted to be able to trace ancestry back to in later analysis.  However, this is not the typical usage pattern for tree sequence recording; most models will not need to call this method.

There are two ways to keep individuals across simplification.  If permanent is T (the default), then the specified individuals will be permanently remembered: their haplosomes will be added to the current sample, and they will always be present in the tree sequence.  Permanently remembering a large number of individuals will, of course, markedly increase memory usage and runtime.

Supplying F for permanent will instead mark the individuals only for (temporary) retention: their haplosomes will not be added to the sample, and they will appear in the final tree sequence only if one of their haplosomes is retained across simplification.  In other words, the rule of thumb for retained individuals is simple: if a haplosome is kept by simplification, the haplosome’s corresponding individual is kept also, if it is retained.  Note that permanent remembering takes priority; calling this function with permanent=F on an individual that has previously been permanently remembered will not remove it from the sample.

The behavior of simplification for individuals retained with permanent=F depends upon the value of the retainCoalescentOnly flag passed to initializeTreeSeq(); here we will discuss the behavior of that flag in detail.  First of all, haplosomes are always removed by simplification unless they are (a) part of the final generation (i.e., in a living individual when simplification occurs), (b) ancestral to the final generation, (c) a haplosome of a permanently remembered individual, or (d) ancestral to a permanently remembered individual.  If retainCoalescentOnly is T (the default), they are also always removed if they are not a branch point (i.e., a coalescent node or most recent common ancestor) in the tree sequence.  In some cases it may be useful to retain a haplosome and its associated individual when it is simply an intermediate node in the ancestry (i.e., in the middle of a branch).  This can be enabled by setting retainCoalescentOnly to F in your call to initializeTreeSeq().  In this case, ancestral haplosomes that are intermediate (“unary nodes”, in tskit parlance) and are within an individual that has been retained using the permanent=F flag here are kept, along with the retained individual itself.  Since setting retainCoalescentOnly to F will prevent the unary nodes for retained individuals from being pruned, simplification may often be unable to prune very much at all from the tree sequence, and memory usage and runtime may increase rapidly.  If you are retaining many individuals, this setting should therefore be used only with caution; it is not necessary if you are purely interested in the most recent common ancestors.  See the pyslim documentation for further discussion of retaining and remembering individuals and the effects of the retainCoalescentOnly flag.

The metadata (age, location, etc) that are stored in the resulting tree sequence are those values present at either (a) the final generation, if the individual is alive when the tree sequence is output, or (b) the last time that the individual was remembered, if not.  Calling treeSeqRememberIndividuals() on an individual that is already remembered will cause the archived information about the remembered individual to be updated to reflect the individual’s current state.  A case where this is particularly important is for the spatial location of individuals in continuous-space models.  SLiM automatically remembers the individuals that comprise the first generation of any new subpopulation created with addSubpop(), for easy recapitation and other analysis.  However, since these first-generation individuals are remembered at the moment they are created, their spatial locations have not yet been set up, and will contain garbage – and those garbage values will be archived in their remembered state.  If you need correct spatial locations of first-generation individuals for your post-simulation analysis, you should call treeSeqRememberIndividuals() explicitly on the first generation, after setting spatial locations, to update the archived information with the correct spatial positions.

– (void)treeSeqSimplify(void)

Triggers an immediate simplification of the tree sequence recording tables.  This method may only be called if tree sequence recording has been turned on with initializeTreeSeq().  A call to this method will free up memory being used by entries that are no longer in the ancestral path of any individual within the current sample (currently living individuals, in other words, plus those explicitly added to the sample with treeSeqRememberIndividuals()), but it can also take a significant amount of time.  Typically calling this method is not necessary; the automatic simplification performed occasionally by SLiM should be sufficient for most models.

5.17  Class Subpopulation

5.17.1  Subpopulation properties

cloningRate => (float)

The fraction of children in the next generation that will be produced by cloning (as opposed to biparental mating).  In non-sexual (i.e. hermaphroditic) simulations, this property is a singleton float representing the overall subpopulation cloning rate.  In sexual simulations, this property is a float vector with two values: the cloning rate for females (at index 0) and for males (at index 1).

description <–> (string$)

A human-readable string description for the subpopulation.  By default, this is the empty string, ""; however, it may be set to whatever you wish.  When tree-sequence recording is enabled, description is persisted in the subpopulation’s metadata in tree-sequence output.

firstMaleIndex => (integer$)

The index of the first male individual in the subpopulation.  The individuals vector of the subpopulation is sorted into females first and males second; firstMaleIndex gives the position of the boundary between those sections.  The firstMaleIndex property is also the number of females in the subpopulation, given this design.  For non-sexual (i.e. hermaphroditic) simulations, the value of this property is undefined and should not be used.

fitnessScaling <–> (float$)

A float scaling factor applied to the fitness of all individuals in this subpopulation (i.e., the fitness value computed for each individual will be multiplied by this value).  This is primarily of use in nonWF models, where fitness is absolute, rather than in WF models, where fitness is relative (and thus a constant factor multiplied into the fitness of every individual will make no difference); however, it may be used in either type of model.  This provides a simple, fast way to modify the fitness of all individuals in a subpopulation; conceptually it is similar to returning the same fitness effect for all individuals in the subpopulation from a fitnessEffect() callback, but without the complexity and performance overhead of implementing such a callback.  To scale the fitness of individuals by different (individual-specific) factors, see the fitnessScaling property of Individual.

The value of fitnessScaling is reset to 1.0 every tick, so that any scaling factor set lasts for only a single tick.  This reset occurs immediately after fitness values are calculated, in both WF and nonWF models.

haplosomes => (object<Haplosome>)

All of the haplosomes contained by the subpopulation.  All of the haplosomes for the first individual in the individuals property are provided, followed by all the haplosomes for the second individual, etc., in the same order as individuals.

haplosomesNonNull => (object<Haplosome>)

All of the haplosomes contained by the subpopulation, as with the haplosomes property, if all of them are not null haplosomes; any null haplosomes present are excluded from the returned vector.  This is a convenience shorthand, sometimes useful in models that involve null haplosomes.

id => (integer$)

The identifier for this subpopulation; for subpopulation p3, for example, this is 3.

immigrantSubpopFractions => (float)

The expected value of the fraction of children in the next generation that are immigrants arriving from particular subpopulations.

immigrantSubpopIDs => (integer)

The identifiers of the particular subpopulations from which immigrants will arrive in the next generation.

individualCount => (integer$)

The number of individuals in the subpopulation.

individuals => (object<Individual>)

All of the individuals contained by the subpopulation.  See the sampleIndividuals() and subsetIndividuals() for fast ways to get a subset of the individuals in a subpopulation.

lifetimeReproductiveOutput => (integer)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), lifetimeReproductiveOutput contains the value of the Individual property reproductiveOutput for all individuals in the subpopulation that died in the last viability/survival tick cycle stage (or, for WF models, immediately after reproduction).  This allows access to the lifetime reproductive output of individuals in the subpopulation at the end of their lives.  If pedigree tracking is not on, this property is unavailable.

lifetimeReproductiveOutputF => (integer)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), lifetimeReproductiveOutputF contains the value of the Individual property reproductiveOutput for all female individuals in the subpopulation that died in the last viability/survival tick cycle stage (or, for WF models, immediately after reproduction).  This property is undefined if separate sexes have not been enabled, or if pedigree tracking is not on.

lifetimeReproductiveOutputM => (integer)

If pedigree tracking is turned on with initializeSLiMOptions(keepPedigrees=T), lifetimeReproductiveOutputM contains the value of the Individual property reproductiveOutput for all male individuals in the subpopulation that died in the last viability/survival tick cycle stage (or, for WF models, immediately after reproduction).  This property is undefined if separate sexes have not been enabled, or if pedigree tracking is not on.

name <–> (string$)

A human-readable string name for the subpopulation.  By default, this is the subpopulation’s symbol as a string; for subpopulation p3, for example, name defaults to "p3".  However, it may be set to whatever you wish except that subpopulation names must be unique across time (two different subpopulations may not both have the name "foo", even if they never exist at the same time).  A subpopulation’s name may appear as a label in SLiMgui, and it can be useful in generating output, debugging, and other purposes.  When tree-sequence recording is enabled, name is persisted in the subpopulation’s metadata in tree-sequence output, and can then be used in Python to identify the subpopulation; if you plan to take advantage of that feature, name should follow the syntax of Python identifiers: starting with a letter or underscore [a-zA-Z_], followed by letters, digits, or underscores [a-zA-Z0-9_], without spaces, hyphens, or other characters.

selfingRate => (float$)

The expected value of the fraction of children in the next generation that will be produced by selfing (as opposed to biparental mating).  Selfing is only possible in non-sexual (i.e. hermaphroditic) simulations; for sexual simulations this property always has a value of 0.0.

sexRatio => (float$)

For sexual simulations, the sex ratio for the subpopulation.  This is defined, in SLiM, as the fraction of the subpopulation that is male; in other words, it is actually the M:(M+F) ratio.  For non-sexual (i.e. hermaphroditic) simulations, this property has an undefined value and should not be used.

spatialMaps => (object<SpatialMap>)

The spatial maps that are currently added to the subpopulation.

spatialBounds => (float)

The spatial boundaries of the subpopulation.  The length of the spatialBounds property depends upon the spatial dimensionality declared with initializeSLiMOptions().  If the spatial dimensionality is zero (as it is by default), the value of this property is float(0) (a zero-length float vector).  Otherwise, minimums are supplied for each coordinate used by the dimensionality of the simulation, followed by maximums for each.  In other words, if the declared dimensionality is "xy", the spatialBounds property will contain values (x0, y0, x1, y1); bounds for the z coordinate will not be included in that case, since that coordinate is not used in the simulation’s dimensionality.  This property cannot be set, but the setSpatialBounds() method may be used to achieve the same thing.

species => (object<Species>$)

The species to which the target object belongs.

tag <–> (integer$)

A user-defined integer value.  The value of tag is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code.  The value of tag is not used by SLiM; it is free for you to use.  See also the getValue() and setValue() methods (provided by the Dictionary class; see the Eidos manual), for another way of attaching state to subpopulations.

5.17.2  Subpopulation methods

– (object<Individual>)addCloned(object<Individual>$ parent, [integer$ count = 1], [logical$ defer = F])

Generates a new offspring individual from the given parent by clonal reproduction, queues it for addition to the target subpopulation, and returns it.  The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage.  The subpopulation of parent will be used to locate applicable mutation() and modifyChild() callbacks governing the generation of the offspring individual.

Beginning in SLiM 4.1, the count parameter dictates how many offspring will be generated (previously, exactly one offspring was generated).  Each offspring is generated independently, based upon the given parameters.  The returned vector contains all generated offspring, except those that were rejected by a modifyChild() callback.  If all offspring are rejected, object<Individual>(0) is returned, which is a zero-length object vector of class Individual; note that this is a change in behavior from earlier versions, which would return NULL.

Beginning in SLiM 4.1, passing T for defer requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase.  SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if defer were F.  Haplosome generation can only be deferred if there are no active mutation() callbacks; otherwise, an error will result.  Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a modifyChild() callback or otherwise).  There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of F for defer is generally preferable since it has fewer restrictions.  When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).

Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from parent; more specifically, the x property will be inherited in all spatial models (1D/2D/3D), the y property in 2D/3D models, and the z property in 3D models.  Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1.  The parent’s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to deviatePositions() / pointDeviated().

Note that this method is only for use in nonWF models.  See addCrossed() for further general notes on the addition of new offspring individuals.

– (object<Individual>)addCrossed(object<Individual>$ parent1, object<Individual>$ parent2, [Nfs$ sex = NULL], [integer$ count = 1], [logical$ defer = F])

Generates a new offspring individual from the given parents by biparental sexual reproduction, queues it for addition to the target subpopulation, and returns it.  The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage.  Attempting to use a newly generated offspring individual as a mate, or to reference it as a member of the target subpopulation in any other way, will result in an error.  In most models the returned individual is not used, but it is provided for maximal generality and flexibility.

The new offspring individual is generated from parent1 and parent2 by crossing them.  In sexual models parent1 must be female and parent2 must be male; in hermaphroditic models, parent1 and parent2 are unrestricted.  If parent1 and parent2 are the same individual in a hermaphroditic model, that parent self-fertilizes, or “selfs”, to generate the offspring sexually (note this is not the same as clonal reproduction).  Such selfing is considered “incidental” by addCrossed(), however; if the preventIncidentalSelfing flag of initializeSLiMOptions() is T, supplying the same individual for parent1 and parent2 is an error (you must check for and prevent incidental selfing if you set that flag in a nonWF model).  If non-incidental selfing is desired, addSelfed() should be used instead.

The sex parameter specifies the sex of the offspring.  A value of NULL means “make the default choice”; in non-sexual models it is the only legal value for sex, and does nothing, whereas in sexual models it causes male or female to be chosen with equal probability.  A value of "M" or "F" for sex specifies that the offspring should be male or female, respectively.  Finally, a float value from 0.0 to 1.0 for sex provides the probability that the offspring will be male; a value of 0.0 will produce a female, a value of 1.0 will produce a male, and for intermediate values SLiM will draw the sex of the offspring randomly according to the specified probability.  Unless you wish the bias the sex ratio of offspring, the default value of NULL should generally be used.

Note that any defined, active, and applicable recombination(), mutation(), and modifyChild() callbacks will be called as a side effect of calling this method, before this method even returns.  For recombination() and mutation() callbacks, the subpopulation of the parent that is generating a given gamete is used; for modifyChild() callbacks the situation is more complex.  In most biparental mating events, parent1 and parent2 will belong to the same subpopulation, and modifyChild() callbacks for that subpopulation will be used, just as in WF models.  In certain models (such as models of pollen flow and broadcast spawning), however, biparental mating may occur between parents that are not from the same subpopulation; that is legal in nonWF models, and in that case, modifyChild() callbacks for the subpopulation of parent1 are used (since that is the maternal parent).

If the modifyChild() callback process results in rejection of the proposed child, a new offspring individual is not generated.  To force the generation of an offspring individual from a given pair of parents, you could loop until addCrossed() succeeds, but note that if your modifyChild() callback rejects all proposed children from those particular parents, your model will then hang, so care must be taken with this approach.  Usually, nonWF models do not force generation of offspring in this manner; rejection of a proposed offspring by a modifyChild() callback typically represents a phenomenon such as post-mating reproductive isolation or lethal genetic incompatibilities that would reduce the expected litter size, so the default behavior is typically desirable.

Beginning in SLiM 4.1, the count parameter dictates how many offspring will be generated (previously, exactly one offspring was generated).  Each offspring is generated independently, based upon the given parameters.  The returned vector contains all generated offspring, except those that were rejected by a modifyChild() callback.  If all offspring are rejected, object<Individual>(0) is returned, which is a zero-length object vector of class Individual; note that this is a change in behavior from earlier versions, which would return NULL.

Beginning in SLiM 4.1, passing T for defer requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase.  SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if defer were F.  Haplosome generation can only be deferred if there are no active mutation() or recombination() callbacks; otherwise, an error will result.  Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a modifyChild() callback or otherwise).  There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of F for defer is generally preferable since it has fewer restrictions.  When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).

Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from parent1; more specifically, the x property will be inherited in all spatial models (1D/2D/3D), the y property in 2D/3D models, and the z property in 3D models.  Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1.  The parent’s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to deviatePositions() / pointDeviated().

Note that this method is only for use in nonWF models, in which offspring generation is managed manually by the model script; in such models, addCrossed() must be called only from reproduction() callbacks, and may not be called at any other time.  In WF models, offspring generation is managed automatically by the SLiM core.

– (object<Individual>)addEmpty([Nfs$ sex = NULL], [Nl$ haplosome1Null = NULL], [Nl$ haplosome2Null = NULL], [integer$ count = 1])

Generates a new offspring individual with empty haplosomes (i.e., containing no mutations), queues it for addition to the target subpopulation, and returns it.  The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage.  No recombination() or mutation() callbacks will be called.  The target subpopulation will be used to locate applicable modifyChild() callbacks governing the generation of the offspring individual (unlike the other addX() methods, because there is no parental individual to reference).  The offspring is considered to have no parents for the purposes of pedigree tracking.  The sex parameter is treated as in addCrossed().

For all chromosome types except "A", null haplosomes will be generated as dictated by the sex of the individual and type of the chromosome.  For example, for chromosome type "X" a female would be generated with two empty haplosomes for that chromosome (XX), whereas a male would be generated with one empty haplosome and one null haplosome (X–, in SLiM parlance).  For chromosome type "H" an empty haplosome is always generated, not a null haplosome.  But for chromosome type "A", in particular, more control is afforded.  Passing NULL (the default) or F for haplosome1Null will make the first haplosome for every chromosome of type "A" be a non-null (empty) haplosome, the standard behavior.  More interestingly, passing T for haplosome1Null would make the first haplosome for every chromosome of type "A" be a null haplosome.  Similarly, passing T for haplosome2Null would make the second haplosome for every chromosome of type "A" be a null haplosome.  This option could be useful for situations such as adding new haploids into a haplodiploid model.  (Separate control over the haploid or diploid configuration of each chromosome of type "A" is not presently supported, but would be a simple extension to the design, by allowing haplosome1Null and haplosome2Null to provide a whole vector of logical flags rather than just a singleton value; please request this feature if you require it.)

Beginning in SLiM 4.1, the count parameter dictates how many offspring will be generated (previously, exactly one offspring was generated).  Each offspring is generated independently, based upon the given parameters.  The returned vector contains all generated offspring, except those that were rejected by a modifyChild() callback.  If all offspring are rejected, object<Individual>(0) is returned, which is a zero-length object vector of class Individual; note that this is a change in behavior from earlier versions, which would return NULL.

Note that this method is only for use in nonWF models.  See addCrossed() for further general notes on the addition of new offspring individuals.

– (object<Individual>)addMultiRecombinant(object<Dictionary>$ pattern, [Nfs$ sex = NULL], [No<Individual>$ parent1 = NULL], [No<Individual>$ parent2 = NULL], [Nl$ randomizeStrands = NULL], [integer$ count = 1], [logical$ defer = F])

Generates a new offspring individual based upon the inheritance pattern specified by pattern, queues it for addition to the target subpopulation, and returns it.  The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage.  The “pattern dictionary” supplied in pattern must be of class Dictionary (or a subclass of Dictionary), and more particularly, must be a dictionary of dictionaries structured in a specific way as described below.  This method is a multi-chromosome version of the addRecombinant() method.  For single-chromosome models, using addRecombinant() will be simpler; and it will be easier to understand this extremely complex method if you understand addRecombinant() first.

The top-level “pattern dictionary” given by pattern specifies the way in which each chromosome should be handled.  It can use integer keys, in which case each key is the id of a chromosome, or string keys, in which case each key is the symbol of a chromosome.  In either case, a chromosome’s inheritance pattern is specified by an “inheritance dictionary” in pattern attached to such a chromosome id or symbol key.  That inheritance dictionary should itself contain up to six keys, with the standard names "strand1", "strand2", "breaks1", "strand3", "strand4", and "breaks2".  Any key which is missing in an inheritance dictionary is assumed to have a value of NULL, and missing keys will be referred to having a value of NULL here for simplicity.  These key-value pairs are used in precisely the same way as the parameters of the same names for addRecombinant(), to produce the offspring haplosome(s) for each specified chromosome.  There is some complication regarding how these six values can be used to produce results like crossing, cloning, and selfing, involving as many as four different “parents” for each chromosome; rather than repeating all of that documentation here, please see the addRecombinant() documentation for more information.  When an inheritance dictionary is supplied for a particular chromosome, this method uses the six values that dictionary contains (strand1, strand2, breaks1, strand3, strand4, breaks2) in exactly the same way as addRecombinant() does; addMultiRecombinant() simply supports multiple chromosomes.  In addition to that, however, addMultiRecombinant() also allows the pattern dictionary to omit the inheritance dictionaries for particular chromosomes; the behavior of addMultiRecombinant() in that special case will be described below, after discussing all other aspects of the method’s implementation.

The sex parameter optionally specifies the sex of the offspring.  The default value of NULL for sex specifies “default behavior”; in a non-sexual model this is the only legal value, and produces a hermaphroditic offspring.  In a sexual model, the “default behavior” of NULL is that the offspring’s sex is dictated by the haplosome structure it inherits.  For example, if pattern specifies that the offspring will have two non-null haplosomes for a chromosome of type "X", the sex of the offspring must therefore be female, whereas if it will have one non-null "X" haplosome and one null "X" haplosome, it must therefore be male.  SLiM will scan through pattern to determine such constraints and enforce them.  If the constraints implied by pattern are not self-consistent (if the offspring would have two non-null "X" haplosomes but also a non-null "Y" haplosome, for example), an error will be raised.  The constraints defined by each chromosome type, as described in initializeChromosome(), must always be followed, even when using addMultiRecombinant().  If sex is NULL and pattern imposes no constraints upon the sex of the offspring, the offspring will be chosen as male or female with equal probability.  A value of "M" or "F" for sex specifies that the offspring should be male or female, respectively.  A float value from 0.0 to 1.0 for sex provides the probability that the offspring will be male; a value of 0.0 will produce a female, a value of 1.0 will produce a male, and for intermediate values SLiM will draw the sex of the offspring randomly according to the specified probability.  In these cases where sex is not NULL, SLiM will first determine the sex of the individual as just described, and will then scan through pattern to confirm that it is compatible with the sex that was determined.  Again, if there is a conflict an error will be raised; you cannot specify the sex of an individual to be incompatible with the haplosomes that it inherits, and if you specify a sex with a string or float value it is up to you to ensure that that is compatible with the specifications in pattern.  (If you need more flexibility, you should probably not use a sexual model at all, but simply use chromosome types "A" and "H" in a non-sexual model, track the sex of individuals yourself with a tag value such as tagL0, and manipulate haplosomes during reproduction however you wish; SLiM then imposes no constraints.)

By default, the offspring is considered to have no parents, since there may be more than two “parents” in the general case.  If specifying parentage is desired, parent1 and/or parent2 may be passed explicitly; this will establish those individuals as the parents of the offspring for purposes of pedigree tracking, and for several other purposes described below.  If only one of parent1 and parent2 is non-NULL, that individual will be set as both of the parents of the offspring, mirroring the way that parentage is tracked for other cases such as addCloned() and addSelfed().  It is not required for parent1 or parent2 to actually be a genetic parent of the offspring at all, although typically they would be.  To benefit from the full functionality of addMultiRecombinant() as described below, it is best to supply parent1 and parent2 when possible.

The randomizeStrands parameter is used to control the recombination behavior of addMultiRecombinant().  An inheritance dictionary can specify two parental strands with crossover breakpoints between them to generate one offspring strand with recombination – for example, with the "strand1", "strand2", and "breaks1" keys, as described above, but the same is true of the "strand3", "strand4", and "breaks2" keys, and the discussion that follows applies to both cases.  If randomizeStrands is F, the supplied strands are used as given; for example, "strand1" will be the initial copy strand when generating the first gamete to form the offspring.  This mode should be used if you want explicit control over the initial copy strand; one example would be if your script is explicitly generating all four of the products of a meiosis event.  If randomizeStrands is T, then if "strand1" and "strand2" are both non-NULL, 50% of the time they will be swapped, making "strand2" the initial copy strand for the first gamete instead.  This mode (randomizeStrands=T) is usually the desired behavior, to avoid an inheritance bias due to a lack of randomization in the initial copy strand, so passing T for randomizeStrands is recommended unless you specifically desire otherwise.  The default value of randomizeStrands is NULL in order to force either T or F to be explicitly chosen whenever it would make a difference; if it is left as NULL, an error will be raised if generation of the specified offspring involves recombination, since then SLiM needs to know whether the value is T or F.  (This unconventional approach has been adopted because the default value was F prior to SLiM 5, but T is almost always the correct behavior, as explained above.  To try to prevent accidental bugs, this new policy was adopted to force the user to explicitly choose T or F whenever it matters.)

The value of the meanParentAge property of the generated offspring is calculated from the mean parent age of each of its two haplosomes (whether they turn out to be null haplosomes or not).  The addRecombinant() documentation provides a simple example; for addMultiRecombinant() the logic is the same, but potentially extended to more than two offspring haplosomes.

Callbacks can be involved in offspring generation with addMultiRecombinant(), but because there are up to four strands with up to four different parents, things are a bit complicated and different from other add...() methods; the policy described here seems like the best compromise.  The target subpopulation for the addMultiRecombinant() call will be used to locate applicable mutation() and modifyChild() callbacks governing the generation of the offspring individual.  On the other hand, recombination() callbacks will be found based upon the subpopulations to which parent1 and parent2 belong (for reasons discussed further in drawBreakpoints()); if a parent individual is not supplied, recombination() callbacks will not be called at all when generating the corresponding offspring haplosome.

When breakpoints are explicitly supplied to addMultiRecombinant() with breaks1 or breaks2, gene conversion tracts are not well-supported by this method; the breaks1 and breaks2 vectors provide simple crossover breakpoints, which may be used to implement crossovers or simple gene conversion tracts, but complex gene conversion tracts with heteroduplex mismatch repair are not supported in this mode of operation since there is no way to supply the relevant information.  If, on the other hand, breaks1 or breaks2 is NULL when generating a haplosome with recombination, then as described above, addRecombinant() will generate breakpoints internally for that cross, and in this case, complex gene conversion tracts with heteroduplex mismatch repair are supported, since all of the necessary information is available.  Similarly, if the inheritance dictionary for a given chromosome is omitted from pattern entirely and a cross between parent1 and parent2 is inferred (as discussed below), the recombination algorithm used will support gene conversion including heteroduplex mismatch repair.

Finally, count is the number of offspring to generate using the given pattern and parameters, and defer is used for deferral of offspring generation, as described for addRecombinant().  Any other details omitted from this documentation are all as described for addRecombinant().

Constructing a well-formed pattern dictionary with inheritance dictionaries for every chromosome can be a bit complex and require many lines of code.  To ease the process, see the Species methods addPatternForCross(), addPatternForClone(), addPatternForNull(), and addPatternForRecombinant(), which help you to build a pattern dictionary one inheritance dictionary at a time.  However, several of these methods will probably be used infrequently, because of the final aspect of addMultiRecombinant() that we have not yet properly discussed.

As mentioned earlier, not all chromosomes need to be specified with an inheritance dictionary in pattern; whenever SLiM’s default inheritance behavior is well-defined and is desired for a given chromosome, the inheritance dictionary for that chromosome may be omitted, and will be inferred by addMultiRecombinant() automatically.  This behavior makes it easy to specify a reproduction event that is, for example, like a regular biparental cross involving many chromosomes, but that uses a different reproductive pattern just for one particular chromosome that behaves in a special way.  The inferred inheritance dictionary for a given chromosome is based upon the chromosome type, the values of parent1 and parent2, and the sex of the offspring (which has, by this point, been determined in all cases).  The rules for this inference are actually quite simple.  If both parents are specified (that is, are both non-NULL), the inferred inheritance dictionary is the same as would be produced by a call to addPatternForCross() with those two parents, given in that order, for that chromosome, for the determined offspring sex.  If only one parent is specified (non-NULL), the inferred inheritance dictionary is the same as would be produced by a call to addPatternForClone() with that one parent, for that chromosome, for the determined offspring sex.  If selfing is desired for the inferred inheritance dictionary, pass the same individual for both parent1 and parent2; the behavior of addPatternForCross() in that case is essentially to self the individual, as discussed in that method.  If the inferred inheritance dictionary for a given chromosome is not well-defined, as discussed in the documentation for addPatternForCross() and addPatternForClone(), or if both parent1 and parent2 are NULL, an error will be raised.  In such cases, the inheritance dictionary cannot be inferred, and will need to be given explicitly in pattern.

– (object<Individual>)addRecombinant(No<Haplosome>$ strand1, No<Haplosome>$ strand2, Ni breaks1, No<Haplosome>$ strand3, No<Haplosome>$ strand4, Ni breaks2, [Nfs$ sex = NULL], [No<Individual>$ parent1 = NULL], [No<Individual>$ parent2 = NULL], [Nl$ randomizeStrands = NULL], [integer$ count = 1], [logical$ defer = F])

Generates a new offspring individual from the given parental haplosomes with the specified crossover breakpoints, queues it for addition to the target subpopulation, and returns it.  The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage.  This method is an advanced feature; most models will use addCrossed(), addSelfed(), or addCloned() instead.  This method may only be used in single-chromosome models; in multi-chromosome models, use addMultiRecombinant(), a more general version of addRecombinant().

This method supports several possible configurations for strand1, strand2, and breaks1 (and the same applies for strand3, strand4, and breaks2).  If strand1 and strand2 are both NULL, the corresponding haplosome in the generated offspring will be a null haplosome; in this case, breaks1 must be NULL or zero-length.  If strand1 is non-NULL but strand2 is NULL, the corresponding haplosome in the generated offspring will be a clonal copy of strand1 with mutations added, as from addCloned(); in this case, breaks1 must again be NULL or zero-length.  If strand1 and strand2 are both non-NULL, the corresponding haplosome in the generated offspring will result from recombination between strand1 and strand2 with mutations added, as from addCrossed(), with strand1 being the initial copy strand by default (but see below).  Copying will switch between strands at each crossover breakpoint.  Breakpoints may be supplied in breaks1, which need not be sorted or uniqued (SLiM will sort and unique the supplied breakpoints internally).  Alternatively, breaks1 may be NULL, which requests that addRecombinant() draw breakpoints automatically to recombine strand1 and strand2, following SLiM’s usual breakpoint-drawing algorithm.  (If you do not want any breakpoints, pass integer(0), a zero-length integer vector, for breaks1.)  Finally, it is not currently legal for strand1 to be NULL and strand2 non-NULL; that variant may be assigned some meaning in future.  Again, this discussion applies equally to strand3, strand4, and breaks2, mutatis mutandis.  Null haplosomes may never be passed as any of the four parental strands; pass NULL, not a null haplosome, if that strand is not inherited from.  When modeling a chromosome that is intrinsically haploid, such as the Y, NULL must be passed for strand3, strand4, and breaks2; you cannot supply genetic information for an offspring haplosome that will not exist.  Note that when new mutations are generated by addRecombinant(), their subpopID property will be the id of the offspring’s subpopulation, since the parental subpopulation is ambiguous in the general case; this behavior differs from the other add...() methods.

These semantics allow several uses for addRecombinant().  When all strands are non-NULL, it is similar to addCrossed() except that the recombination breakpoints can be specified explicitly, allowing very precise offspring generation without having to override SLiM’s breakpoint generation with a recombination() callback.  When only strand1 and strand3 are supplied, it is very similar to addCloned(), creating a clonal offspring, except that the two parental haplosomes need not belong to the same individual (whatever that might mean biologically).  Supplying only strand1 is useful for modeling clonally reproducing haploids, or any chromosome type that is intrinsically haploid, such as the Y chromosome.  For a model of clonally reproducing haploids that undergo horizontal gene transfer (HGT), supplying only strand1 and strand2 will allow HGT from strand2 to replace segments of an otherwise clonal copy of strand1, while the second haplosome of the generated offspring will be a null haplosome; this could be useful for modeling bacterial conjugation, for example.  Other variations are also possible.

The sex parameter optionally specifies the sex of the offspring.  The default value of NULL for sex specifies “default behavior”; in a non-sexual model this is the only legal value, and produces a hermaphroditic offspring.  In a sexual model, the “default behavior” of NULL is that the offspring’s sex is dictated by the haplosome structure it inherits.  For example, if the supplied strands indicate that the offspring will have two non-null haplosomes for a chromosome of type "X", the sex of the offspring must therefore be female, whereas if it will have one non-null "X" haplosome and one null "X" haplosome, it must therefore be male.  SLiM will examine the supplied strands to determine such constraints and enforce them.  The constraints defined by each chromosome type, as described in initializeChromosome(), must always be followed, even when using addRecombinant().  If sex is NULL and the sex of the offspring is unconstrained, the offspring will be chosen as male or female with equal probability.  A value of "M" or "F" for sex specifies that the offspring should be male or female, respectively.  A float value from 0.0 to 1.0 for sex provides the probability that the offspring will be male; a value of 0.0 will produce a female, a value of 1.0 will produce a male, and for intermediate values SLiM will draw the sex of the offspring randomly according to the specified probability.  In these cases where sex is not NULL, SLiM will first determine the sex of the individual as just described, and will then examine the supplied strands to confirm that it is compatible with the sex that was determined.  Again, if there is a conflict an error will be raised; you cannot specify the sex of an individual to be incompatible with the haplosomes that it inherits, and if you specify a sex with a string or float value it is up to you to ensure that that is compatible with the supplied strands.  (If you need more flexibility, you should probably not use a sexual model at all, but simply use chromosome type "A" or "H" in a non-sexual model, track the sex of individuals yourself with a tag value such as tagL0, and manipulate haplosomes during reproduction however you wish; SLiM then imposes no constraints.)

By default, the offspring is considered to have no parents, since there may be more than two “parents” in the general case.  If specifying parentage is desired, parent1 and/or parent2 may be passed to explicitly; this will establish those individuals as the parents of the offspring for purposes of pedigree tracking, and for several other purposes described below.  If only one of parent1 and parent2 is non-NULL, that individual will be set as both of the parents of the offspring, mirroring the way that parentage is tracked for other cases such as addCloned() and addSelfed().  It is not required for parent1 or parent2 to actually be a genetic parent of the offspring at all, although typically they would be.  To benefit from the full functionality of addRecombinant() as described below, it is best to supply parent1 and parent2 when possible.

The randomizeStrands parameter is used to control the recombination behavior of addRecombinant().  As described above, two parental strands with crossover breakpoints between them can be specified to generate one offspring strand with recombination – for example, with the strand1, strand2, and breaks1 parameters, but the same is true of the strand3, strand4, and breaks2 parameters, and the discussion that follows applies to both cases.  If randomizeStrands is F, the supplied strands are used as given; for example, strand1 will be the initial copy strand when generating the first gamete to form the offspring.  This mode should be used if you want explicit control over the initial copy strand; one example would be if your script is explicitly generating all four of the products of a meiosis event.  If randomizeStrands is T, then if strand1 and strand2 are both non-NULL, 50% of the time they will be swapped, making strand2 the initial copy strand for the first gamete instead.  This mode (randomizeStrands=T) is usually the desired behavior, to avoid an inheritance bias due to a lack of randomization in the initial copy strand, so passing T for randomizeStrands is recommended unless you specifically desire otherwise.  The default value of randomizeStrands is NULL in order to force either T or F to be explicitly chosen whenever it would make a difference; if it is left as NULL, an error will be raised if generation of the specified offspring involves recombination, since then SLiM needs to know whether the value is T or F.  (This unconventional approach has been adopted because the default value was F prior to SLiM 5, but T is almost always the correct behavior, as explained above.  To try to prevent accidental bugs, this new policy was adopted to force the user to explicitly choose T or F whenever it matters.)

The value of the meanParentAge property of the generated offspring is calculated from the mean parent age of each of its two haplosomes (whether they turn out to be null haplosomes or not); that may be an average of two values (if both offspring haplosomes have at least one parent), a single value (if one offspring haplosome has no parent), or no values (if both offspring haplosomes have no parent, in which case 0.0 results).  The mean parent age of a given offspring haplosome is the mean of the ages of the parents of the two strands used to generate that offspring haplosome; if one strand is NULL then the mean parent age for that offspring haplosome is the age of the parent of the non-NULL strand, while if both strands are NULL then that offspring haplosome is parentless and is not used in the final calculation.  In other words, if one offspring haplosome has two parents with ages A and B, and the other offspring haplosome has one parent with age C, the meanParentAge of the offspring will be (A+B+C+C) / 4, or equivalently, ((A+B)/2 + C) / 2, not (A+B+C) / 3.

Callbacks can be involved in offspring generation with addRecombinant(), but because there are up to four strands with up to four different parents, things are a bit complicated and different from other add...() methods; the policy described here seems like the best compromise.  The target subpopulation for the addRecombinant() call will be used to locate applicable mutation() and modifyChild() callbacks governing the generation of the offspring individual.  On the other hand, recombination() callbacks will be found based upon the subpopulations to which parent1 and parent2 belong (for reasons discussed further in drawBreakpoints()); if a parent individual is not supplied, recombination() callbacks will not be called at all when generating the corresponding offspring haplosome.

When breakpoints are explicitly supplied to addRecombinant() with breaks1 or breaks2, gene conversion tracts are not well-supported by this method; the breaks1 and breaks2 vectors provide simple crossover breakpoints, which may be used to implement crossovers or simple gene conversion tracts, but complex gene conversion tracts with heteroduplex mismatch repair are not supported in this mode of operation since there is no way to supply the relevant information.  If, on the other hand, breaks1 or breaks2 is NULL when generating a haplosome with recombination, then as described above, addRecombinant() will generate breakpoints internally for that cross, and in this case, complex gene conversion tracts with heteroduplex mismatch repair are supported, since all of the necessary information is available.

Beginning in SLiM 4.1, the count parameter dictates how many offspring will be generated (previously, exactly one offspring was generated).  Each offspring is generated independently, based upon the given parameters.  The returned vector contains all generated offspring, except those that were rejected by a modifyChild() callback.  If all offspring are rejected, object<Individual>(0) is returned, which is a zero-length object vector of class Individual; note that this is a change in behavior from earlier versions, which would return NULL.

Beginning in SLiM 4.1, passing T for defer requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase.  SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if defer were F.  Haplosome generation can only be deferred if there are no active mutation() callbacks; otherwise, an error will result.  Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a modifyChild() callback or otherwise).  There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of F for defer is generally preferable since it has fewer restrictions.  When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).

Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from parent1; more specifically, the x property will be inherited in all spatial models (1D/2D/3D), the y property in 2D/3D models, and the z property in 3D models.  Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1.  The parent’s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to deviatePositions() / pointDeviated().  If parent1 is NULL, parent2 will be used; if it is also NULL, no spatial position will be inherited.

Note that this method is only for use in nonWF models.  See addCrossed() for further general notes on the addition of new offspring individuals.

– (object<Individual>)addSelfed(object<Individual>$ parent, [integer$ count = 1], [logical$ defer = F])

Generates a new offspring individual from the given parent by selfing, queues it for addition to the target subpopulation, and returns it.  The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage.  The subpopulation of parent will be used to locate applicable mutation(), recombination(), and modifyChild() callbacks governing the generation of the offspring individual.

Since selfing requires that parent act as a source of both a male and a female gamete, this method may be called only in hermaphroditic models; calling it in sexual models will result in an error.  This method represents a non-incidental selfing event, so the preventIncidentalSelfing flag of initializeSLiMOptions() has no effect on this method (in contrast to the behavior of addCrossed(), where selfing is assumed to be incidental).

Beginning in SLiM 4.1, the count parameter dictates how many offspring will be generated (previously, exactly one offspring was generated).  Each offspring is generated independently, based upon the given parameters.  The returned vector contains all generated offspring, except those that were rejected by a modifyChild() callback.  If all offspring are rejected, object<Individual>(0) is returned, which is a zero-length object vector of class Individual; note that this is a change in behavior from earlier versions, which would return NULL.

Beginning in SLiM 4.1, passing T for defer requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase.  SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if defer were F.  Haplosome generation can only be deferred if there are no active mutation() or recombination() callbacks; otherwise, an error will result.  Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a modifyChild() callback or otherwise).  There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of F for defer is generally preferable since it has fewer restrictions.  When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).

Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from parent; more specifically, the x property will be inherited in all spatial models (1D/2D/3D), the y property in 2D/3D models, and the z property in 3D models.  Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1.  The parent’s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to deviatePositions() / pointDeviated().

Note that this method is only for use in nonWF models.  See addCrossed() for further general notes on the addition of new offspring individuals.

– (void)addSpatialMap(object<SpatialMap>$ map)

Adds the given SpatialMap object, map, to the subpopulation.  (The spatial map would have been previously created with a call to defineSpatialMap() on a different subpopulation; addSpatialMap() can then be used to add that existing spatial map with other subpopulations, sharing the map between subpopulations.)  If the map is already added to the target subpopulation, this method does nothing; if a different map with the same name is already added to the subpopulation, an error results (because map names must be unique within each subpopulation).  The map being added must be compatible with the target subpopulation; in particular, the spatial bounds utilized by the map must exactly match the corresponding spatial bounds for the subpopulation, and the dimensionality of the subpopulation must encompass the spatiality of the map.  For example, if the map has a spatiality of "xz" then the subpopulation must have a dimensionality of "xyz" so that it encompasses both "x" and "z", and the subpopulation’s spatial bounds for "x" and "z" must match those for the map (but the spatial bounds for "y" are unimportant, since the map does not use that dimension).

Adding a map to a subpopulation is not strictly necessary, at present; one may query a SpatialMap object directly using mapValue(), regarding points in a subpopulation, without the map actually having been added to that subpopulation.  However, it is a good idea to use addSpatialMap(), both for its compatibility check that prevents unnoticed scripting errors, and because it ensures correct display of the model in SLiMgui.

– (float)cachedFitness(Ni indices)

The fitness values calculated for the individuals at the indices given are returned.  If NULL is passed, fitness values for all individuals in the subpopulation are returned.  The fitness values returned are cached values; mutationEffect() and fitnessEffect() callbacks are therefore not called as a side effect of this method.  It is always an error to call cachedFitness() from inside a mutationEffect() or fitnessEffect() callback, since fitness values are in the middle of being set up.  In WF models, it is also an error to call cachedFitness() from a late() event, because fitness values for the new offspring generation have not yet been calculated and are undefined.  In nonWF models, the population may be a mixture of new and old individuals, so instead, NAN will be returned as the fitness of any new individuals whose fitness has not yet been calculated.  When new subpopulations are first created with addSubpop() or addSubpopSplit(), the fitness of all of the newly created individuals is considered to be 1.0 until fitness values are recalculated.

– (void)configureDisplay([Nf center = NULL], [Nf$ scale = NULL], [Ns$ color = NULL])

This method customizes the display of the subpopulation in SLiMgui’s Population Visualization graph.  When this method is called by a model running outside SLiMgui, it will do nothing except type-checking and bounds-checking its arguments.  When called by a model running in SLiMgui, the position, size, and color of the subpopulation’s displayed circle can be controlled as specified below.

The center parameter sets the coordinates of the center of the subpopulation’s displayed circle; it must be a float vector of length two, such that center[0] provides the x-coordinate and center[1] provides the y-coordinate.  The square central area of the Population Visualization occupies scaled coordinates in [0,1] for both x and y, so the values in center must be within those bounds.  If a value of NULL is provided, SLiMgui’s default center will be used (which currently arranges subpopulations in a circle).

The scale parameter sets a scaling factor to be applied to the radius of the subpopulation’s displayed circle.  The default radius used by SLiMgui is a function of the subpopulation’s number of individuals; this default radius is then multiplied by scale.  If a value of NULL is provided, the default radius will be used; this is equivalent to supplying a scale of 1.0.  Typically the same scale value should be used by all subpopulations, to scale all of their circles up or down uniformly, but that is not required.

The color parameter sets the color to be used for the displayed subpopulation’s circle.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB" (see the Eidos manual).  If color is NULL or the empty string, "", SLiMgui’s default (fitness-based) color will be used.

– (object<SpatialMap>$)defineSpatialMap(string$ name, string$ spatiality, numeric values, [logical$ interpolate = F], [Nif valueRange = NULL], [Ns colors = NULL])

Defines a spatial map for the subpopulation; see the SpatialMap documentation regarding this class.  The new map is automatically added to the subpopulation; addSpatialMap() does not need to be called.  (That method is for sharing the map with additional subpopulations, beyond the one for which the map was originally defined.)  The new SpatialMap object is returned, and may be retained permanently using defineConstant() or defineGlobal() for convenience.

The name of the map is given by name, and can be used to identify it.  The map uses the spatial dimensions referenced by spatiality, which must be a subset of the dimensions defined for the simulation in initializeSLiMOptions().  Spatiality "x" is permitted for dimensionality "x"; spatiality "x", "y", or "xy" for dimensionality "xy"; and spatiality "x", "y", "z", "xy", "yz", "xz", or "xyz" for dimensionality "xyz".  The spatial map is defined by a grid of values supplied in parameter values.  That grid of values is aligned with the spatial bounds of the subpopulation, as described in more detail below; the spatial map is therefore coupled to those spatial bounds, and can only be used in subpopulations that match those particular spatial bounds (to avoid stretching or shrinking the map).  The remaining optional parameters are described below.

Note that the semantics of this method changed in SLiM 3.5; in particular, the gridSize parameter was removed, and the interpretation of the values parameter changed as described below.  Existing code written prior to SLiM 3.5 will produce an error, due to the removed gridSize parameter, and must be revised carefully to obtain the same result, even if NULL had been passed for gridSize previously.

Beginning in SLiM 3.5, the values parameter must be a vector/matrix/array with the number of dimensions appropriate for the declared spatiality of the map; for example, a map with spatiality "x" would require a (one-dimensional) vector, spatiality "xy" would require a (two-dimensional) matrix, and a map with spatiality of "xyz" would require a three-dimensional array.  (See the Eidos manual for discussion of vectors, matrices, and arrays.)  The data in values is interpreted in such a way that a two-dimensional matrix of values, with (0, 0) at upper left and values by column, is transformed into the format expected by SLiM, with (0, 0) at lower left and values by row; in other words, the two-dimensional matrix as it prints in the Eidos console will match the appearance of the two-dimensional spatial map as seen in SLiMgui.  This is a change in behavior from versions prior to SLiM 3.5; it ensures that images loaded from disk with the Eidos class Image can be used directly as spatial maps, achieving the expected orientation, with no need for transposition or flipping.  If the spatial map is a three-dimensional array, it is read as successive z-axis “planes”, each of which is a two-dimensional matrix that is treated as described above.

Moving on to the other parameters of defineSpatialMap(): if interpolate is F, values across the spatial map are not interpolated; the value at a given point is equal to the nearest value defined by the grid of values specified.  If interpolate is T, values across the spatial map will be interpolated (using linear, bilinear, or trilinear interpolation as appropriate) to produce spatially continuous variation in values.  In either case, the corners of the value grid are exactly aligned with the corners of the spatial boundaries of the subpopulation as specified by setSpatialBounds(), and the value grid is then stretched across the spatial extent of the subpopulation in such a manner as to produce equal spacing between the values along each dimension.  The setting of interpolation only affects how values between these grid points are calculated: by nearest-neighbor, or by linear interpolation.  Interpolation of spatial maps with periodic boundaries is not handled specially; to ensure that the edges of a periodic spatial map join smoothly, simply ensure that the grid values at the edges of the map are identical, since they will be coincident after periodic wrapping.  Note that cubic/bicubic interpolation is generally smoother than linear/bilinear interpolation, with fewer artifacts, but it is substantially slower to calculate; use the interpolate() method of SpatialMap to precalculate an interpolated map using cubic/bucubic interpolation.

The valueRange and colors parameters travel together; either both are unspecified, or both are specified.  They control how map values will be transformed into colors, by SLiMgui and by the mapColor() method.  The valueRange parameter establishes the color-mapped range of spatial map values, as a vector of length two specifying a minimum and maximum; this does not need to match the actual range of values in the map.  The colors parameter then establishes the corresponding colors for values within the interval defined by valueRange: values less than or equal to valueRange[0] will map to colors[0], values greater than or equal to valueRange[1] will map to the last colors value, and intermediate values will shade continuously through the specified vector of colors, with interpolation between adjacent colors to produce a continuous spectrum.  This is much simpler than it sounds in this description; see the recipes for an illustration of its use.

Note that at present, SLiMgui will only display spatial maps of spatiality "x", "y", or "xy"; the color-mapping parameters will simply be ignored by SLiMgui for other spatiality values (even if the spatiality is a superset of these values; SLiMgui will not attempt to display an "xyz" spatial map, for example, since it has no way to choose which 2D slice through the xyz space it ought to display).  The mapColor() method will return translated color strings for any spatial map, however, even if SLiMgui is unable to display the spatial map.  If there are multiple spatial maps that SLiMgui is capable of displaying, it choose one for display by default, but other maps may be selected from the action menu on the individuals view (by clicking on the button with the gear icon).

– (object<Individual>)deviatePositions(No<Individual> individuals, string$ boundary, numeric$ maxDistance, string$ functionType, ...)

Deviates the spatial positions of the individuals supplied in individuals, using the provided boundary condition and dispersal kernel.  If individuals is NULL, the positions of all individuals in the target subpopulation are deviated.  This method is essentially a more efficient shorthand for getting the spatial positions of individuals from the spatialPosition property, deviating those positions with pointDeviated(), and setting the deviated positions back into individuals with the setSpatialPosition() method.

The boundary condition boundary must be one of "none", "periodic", "reflecting", "stopping", "reprising", or "absorbing", and the spatial kernel type functionType must be one of "f", "l", "e", "n", or "t", with the ellipsis parameters ... supplying kernel configuration parameters appropriate for that kernel type; see pointDeviated() for further details.  As with pointDeviated(), the ellipsis parameters that follow functionType may each, independently, be either a singleton or a vector of length equal to n.  This allows each individual’s position to be deviated with a different kernel, representing, for example, the movements of individuals with differing dispersal capabilities/propensities.  (However, other parameters such as boundary, maxDistance, and functionType must be the same for all of the points, in the present design.)

The returned vector contains individuals that did not survive the dispersal process.  For "absorbing" boundaries, this will contain the individuals that attempted to disperse beyond the spatial bounds, and in most cases the caller will then kill those individuals – probably by passing them to killIndividuals(), but perhaps by setting their fitnessScaling to zero.  (The positions of the individuals in the returned vector will be the out-of-bounds positions that were drawn for them; rather than killing those individuals, the caller could conceivably handle them in some other way.)  For all other boundary conditions, the returned vector of individuals will be empty and may be ignored by the caller.

– (object<Individual>)deviatePositionsWithMap(No<Individual> individuals, string$ boundary, so<SpatialMap>$ map, numeric$ maxDistance, string$ functionType, ...)

Deviates the spatial positions of the individuals supplied in individuals, using the provided boundary condition and dispersal kernel.  The supplied SpatialMap object (or a string specifying a map by name), map, is used (in addition to the spatial bounds of the subpopulation) to define which positions are considered to be “out of bounds”, as described below.  If individuals is NULL, the positions of all individuals in the target subpopulation are deviated.  This method is essentially an extension of the deviatePositions() method, adding bounds-checking using map; however, there are some differences as described below.

The boundary condition boundary must be either "reprising" or "absorbing".  In the simple case where map values are either 0 (bad habitat) or 1 (good habitat), "reprising" means a new location is drawn conditional on falling within the good habitat; "absorbing" means individuals falling outside the good habitat are “absorbed” (killed, probably).  The details are discussed below, when map is discussed.  Note that the boundary condition "none" is not supported because points must be within the boundaries of the spatial map to be checked.  Reflecting and stopping boundaries are not supported because, with the spatial map, if a drawn point is considered out-of-bounds it is not clear where the “edge” is, and therefore reflecting off of the edge, or stopping at the edge, are not well-defined.  Finally, "periodic" is not supported because it does not specify any action to be taken when a drawn point is considered to be “out of bounds” according to the spatial map; instead, this method automatically applies any periodic boundaries that have been defined and then, using the resulting point, checks the subpopulation’s spatial bounds and the spatial map, and applies reprising or absorbing boundaries as requested if the point is “out of bounds”.

The spatial map defined by map must be configured in a specific way.  First of all, it must be defined in, or added to, the target subpopulation (and thus, by implication, it must match the spatial bounds of the subpopulation.  Second, its spatiality must be equal to the dimensionality of the species; in an "xy" species, for example, the map must also be "xy".  Third, the values in the spatial map must represent “habitability”, in the following sense.  The value of map at a given drawn point is obtained, symbolized here by x.  Next, x is clamped to the range [0, 1]; values less than 0 become 0, values greater than 1 become 1.  The resulting x value is then interpreted as the probability that the point is considered “within bounds” (as far as the spatial map is concerned; points that are outside the subpopulation’s spatial bounds are always considered “out of bounds”).  If boundary is "reprising", 1-x is thus the probability that the point will be redrawn; if boundary is "absorbing", 1-x is thus the probability that the individual will be considered “absorbed”, as discussed below.  In this manner, the concept of “out of bounds” is treated as a probability by this method, rather than a binary state.

The spatial kernel type functionType must be one of "f", "l", "e", "n", or "t", with the ellipsis parameters ... supplying kernel configuration parameters appropriate for that kernel type; see pointDeviated() for further details.  As with pointDeviated(), the ellipsis parameters that follow functionType may each, independently, be either a singleton or a vector of length equal to n.  This allows each individual’s position to be deviated with a different kernel, representing, for example, the movements of individuals with differing dispersal capabilities/propensities.  (However, other parameters such as boundary, maxDistance, and functionType must be the same for all of the points, in the present design.)

The returned vector contains individuals that did not survive the dispersal process.  For "absorbing" boundaries, this will contain the individuals that attempted to disperse to a point considered “out of bounds” as described above, and in most cases the caller will then kill those individuals – probably by passing them to killIndividuals(), but perhaps by setting their fitnessScaling to zero.  (The positions of the individuals in the returned vector will be the out-of-bounds positions that were drawn for them; rather than killing those individuals, the caller could conceivably handle them in some other way.)  For all other boundary conditions, the returned vector of individuals will be empty and may be ignored by the caller.

See also the SpatialMap methods sampleNearbyPoint() and sampleImprovedNearbyPoint(), which are in some ways conceptually similar to this method.

– (object<Haplosome>)haplosomesForChromosomes([Niso<Chromosome> chromosomes = NULL], [Ni$ index = NULL], [logical$ includeNulls = T])

Returns a vector containing the subpopulation’s haplosomes that correspond to the chromosomes passed in chromosomes (following the order of the chromosomes property of Individual).  Chromosomes can be specified by id (integer), by symbol (string) or by the Chromosome objects themselves; if NULL is passed (the default), all chromosomes defined for the species are used, in the order in which they were defined.

This method is equivalent to calling haplosomesForChromosomes(chromosomes, index, includeNulls) on subpop.individuals, where subpop is the target subpopulation.  It therefore appends together the specified haplosomes from each individual in the subpopulation to form a single vector.  See the documentation for the Individual method haplosomesForChromosomes() for further details, such as on the meaning of the index and includeNulls parameters.

– (void)outputMSSample(integer$ sampleSize, [logical$ replace = T], [string$ requestedSex = "*"], [Ns$ filePath = NULL], [logical$ append = F], [logical$ filterMonomorphic = F], [Niso<Chromosome>$ chromosome = NULL])

Output a random sample from the subpopulation in MS format.  Positions in the output will span the interval [0,1].  A sample of non-null haplosomes (not entire individuals, note) of size sampleSize from the subpopulation will be output.  The sample may be done either with or without replacement, as specified by replace; the default is to sample with replacement.  A particular sex of individuals may be requested for the sample, for simulations in which sex is enabled, by passing "M" or "F" for requestedSex; passing "*", the default, indicates that haplosomes from individuals should be selected randomly, without respect to sex.  If the sampling options provided by this method are not adequate, see the outputHaplosomesToMS() method of Haplosome for a more flexible low-level option.

If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.

If filterMonomorphic is F (the default), all mutations that are present in the sample will be included in the output.  This means that some mutations may be included that are actually monomorphic within the sample (i.e., that exist in every sampled haplosome, and are thus apparently fixed).  These may be filtered out with filterMonomorphic = T if desired; note that this option means that some mutations that do exist in the sampled haplosomes might not be included in the output, simply because they exist in every sampled haplosome.

The chromosome parameter identifies the chromosome for which the sample of haplosomes should be taken.  The default of NULL may be used only in single-chromosome models where the choice of chromosome is unambiguous.  In multi-chromosome models, chromosome must be non-NULL; it must specify the chromosome by id (integer), by symbol (string) or by the Chromosome object itself.

See outputSample() and outputVCFSample() for other output formats.  Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

– (void)outputSample(integer$ sampleSize, [logical$ replace = T], [string$ requestedSex = "*"], [Ns$ filePath = NULL], [logical$ append = F], [Niso<Chromosome>$ chromosome = NULL])

Output a random sample from the subpopulation in SLiM’s native format.  A sample of non-null haplosomes (not entire individuals, note) of size sampleSize from the subpopulation will be output.  The sample may be done either with or without replacement, as specified by replace; the default is to sample with replacement.  A particular sex of individuals may be requested for the sample, for simulations in which sex is enabled, by passing "M" or "F" for requestedSex; passing "*", the default, indicates that haplosomes from individuals should be selected randomly, without respect to sex.  If the sampling options provided by this method are not adequate, see the outputHaplosomes() method of Haplosome for a more flexible low-level option.

If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.

The chromosome parameter identifies the chromosome for which the sample of haplosomes should be taken.  The default of NULL may be used only in single-chromosome models where the choice of chromosome is unambiguous.  In multi-chromosome models, chromosome must be non-NULL; it must specify the chromosome by id (integer), by symbol (string) or by the Chromosome object itself.

See outputMSSample() and outputVCFSample() for other output formats.  Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

– (void)outputVCFSample(integer$ sampleSize, [logical$ replace = T], [string$ requestedSex = "*"], [logical$ outputMultiallelics = T], [Ns$ filePath = NULL], [logical$ append = F], [logical$ simplifyNucleotides = F], [logical$ outputNonnucleotides = T], [logical$ groupAsIndividuals = T], [Niso<Chromosome>$ chromosome = NULL])

Output a random sample from the subpopulation in VCF format.  A sample of individuals (not haplosomes, note – unlike the outputSample() and outputMSSample() methods) of size sampleSize from the subpopulation will be output.  The sample may be done either with or without replacement, as specified by replace; the default is to sample with replacement.  A particular sex of individuals may be requested for the sample, for simulations in which sex is enabled, by passing "M" or "F" for requestedSex; passing "*", the default, indicates that individuals should be selected randomly, without respect to sex.  If the sampling options provided by this method are not adequate, see the outputHaplosomesToVCF() method of Haplosome for a more flexible low-level option.

If the optional parameter filePath is NULL (the default), output will be sent to Eidos’s output stream.  Otherwise, output will be sent to the filesystem path specified by filePath, overwriting that file if append if F, or appending to the end of it if append is T.

The parameters outputMultiallelics, simplifyNucleotides, outputNonnucleotides, and groupAsIndividuals affect the format of the output produced.

The chromosome parameter identifies the chromosome for which haplosomes of the sampled individuals should be output.  The default of NULL may be used only in single-chromosome models where the choice of chromosome is unambiguous.  In multi-chromosome models, chromosome must be non-NULL; it must specify the chromosome by id (integer), by symbol (string) or by the Chromosome object itself.  The symbol property of the chromosome will be output in the CHROM field of call lines in the VCF output.

See outputMSSample() and outputSample() for other output formats.  Output is generally done in a late() event, so that the output reflects the state of the simulation at the end of a tick.

– (float)pointDeviated(integer$ n, float point, string$ boundary, numeric$ maxDistance, string$ functionType, ...)

Returns a vector containing n points that are derived from point by adding a deviation drawn from a dispersal kernel (specified by maxDistance, functionType, and the ellipsis parameters ..., as detailed below) and then applying a boundary condition specified by boundary.  This method therefore performs the steps of a simple dispersal algorithm in a single vectorized call.  See deviatePositions() for an even more efficient approach.

The parameter point may contain a single point which is deviated and bounded n independent times, or may contain n points each of which is deviated and bounded.  In any case, each point in point should match the dimensionality of the model – one element in a 1D model, two elements in a 2D model, or three elements in a 3D model.  This method should not be called in a non-spatial model.

The dispersal kernel is specified similarly to other kernel-based methods, such as setInteractionFunction() and smooth().  For pointDeviated(), functionType may be "f" with no ellipsis arguments ... to use a flat kernel out to maxDistance; "l" with no ellipsis arguments for a kernel that decreases linearly from the center to zero at maxDistance; "e", in which case the ellipsis should supply a numeric$ lambda (rate) parameter for a negative exponential function; "n", in which case the ellipsis should supply a numeric$ sigma (standard deviation) parameter for a Gaussian function; or "t", in which case the ellipsis should supply a numeric$ degrees of freedom and a numeric$ scale parameter for a t-distribution function.  The Cauchy ("c") kernel is not supported by pointDeviated() since it is not well-behaved for this purpose, and the Student’s t ("t") kernel is not allowed in 3D models at present simply because it hasn’t been implemented.  See the InteractionType class documentation for more detailed discussion of the available kernel types and their parameters and probability distribution functions.  For pointDeviated(), the ellipsis parameters that follow functionType may each, independently, be either a singleton or a vector of length equal to n.  This allows each point to be deviated with a different kernel, representing, for example, the movements of individuals with differing dispersal capabilities/propensities.  (However, other parameters such as boundary, maxDistance, and functionType must be the same for all of the points, in the present design.)

The random points returned from this method are drawn from the probability distribution that is radially symmetric and has density proportional to the kernel – in other words, at distance r the density is proportional to the kernel type referred to by functionType.  (Said another way, the shape of the cross-section through the probability density function is given by the kernel.)  For instance, the value of the type "e" (exponential) kernel with rate a at r is proportional to exp(−ar), and so in 2D, the probability density that this method with kernel type "e" draws from has density proportional to p(xy) = exp(−a sqrt(x2 + y2)), since r = sqrt(x2 + y2) is the distance.  Note that the distribution of the distance is not given by the kernel except in 1D: in the type "e" example, the distribution of the distance in 1D is exponential, while in 2D it has density proportional to r exp(−ar) (i.e., Gamma with shape parameter 1).  For another example, the value of the type "n" (Normal) kernel at r with standard deviation 1 is proportional to exp(−r2 / 2), and so the density is proportional to p(xy) = exp(−(x2 + y2) / 2).  This is the standard bivariate Normal, and equivalent to drawing independent Normals for the x and y directions; however, the Normal is the only distribution for which independent draws along each axis will result in a radially symmetric distribution.  The distribution of the distance in 2D with type "n" is proportional to r exp(−r2 / 2), i.e., Rayleigh.

The boundary condition must be one of "none", "periodic", "reflecting", "stopping", or "reprising".  For "none", no boundary condition is enforced; the deviated points are simply returned as is.  For "periodic", "reflecting", and "stopping", the boundary condition is enforced just as it is by the pointPeriodic(), pointReflected(), and pointStopped() methods; see their documentation for further details.  For "reprising", if the deviated point is out of bounds a new deviated point will be chosen, based upon the same original point, until a point inside bounds is obtained.  Note that absorbing boundaries (for which being out-of-bounds is lethal) would need to be implemented in script; this method cannot enforce them.  (Note, however, that the deviatePositions() method of Subpopulation can enforce absorbing boundaries.)

Note that for the typical usage case, in which point comes from the spatialPosition property for a vector of individuals, and the result is then set back onto the same vector of individuals using the setSpatialPosition() method, the deviatePositions() method provides an even more efficient alternative.

– (logical)pointInBounds(float point)

Returns T if point is inside the spatial boundaries of the subpopulation, F otherwise.  For example, for a simulation with "xy" dimensionality, if point contains exactly two values constituting an (x,y) point, the result will be T if and only if ((point[0]>=x0) & (point[0]<=x1) & (point[1]>=y0) & (point[1]<=y1)) given spatial bounds (x0, y0, x1, y1).  This method is useful for implementing absorbing or reprising boundary conditions.  This may only be called in simulations for which continuous space has been enabled with initializeSLiMOptions().

The length of point must be an exact multiple of the dimensionality of the simulation; in other words, point may contain values comprising more than one point.  In this case, a logical vector will be returned in which each element is T if the corresponding point in point is inside the spatial boundaries of the subpopulation, F otherwise.

– (float)pointPeriodic(float point)

Returns a revised version of point that has been brought inside the periodic spatial boundaries of the subpopulation (as specified by the periodicity parameter of initializeSLiMOptions()) by wrapping around periodic spatial boundaries.  In brief, if a coordinate of point lies beyond a periodic spatial boundary, that coordinate is wrapped around the boundary, so that it lies inside the spatial extent by the same magnitude that it previously lay outside, but on the opposite side of the space; in effect, the two edges of the periodic spatial boundary are seamlessly joined.  This is done iteratively until all coordinates lie inside the subpopulation’s periodic boundaries.  Note that non-periodic spatial boundaries are not enforced by this method; they should be enforced using pointReflected(), pointStopped(), or some other means of enforcing boundary constraints (which can be used after pointPeriodic() to bring the remaining coordinates into bounds; coordinates already brought into bounds by pointPeriodic() will be unaffected by those calls).  This method is useful for implementing periodic boundary conditions.  This may only be called in simulations for which continuous space  and at least one periodic spatial dimension have been enabled with initializeSLiMOptions().

The length of point must be an exact multiple of the dimensionality of the simulation; in other words, point may contain values comprising more than one point.  In this case, each point will be processed as described above and a new vector containing all of the processed points will be returned.

– (float)pointReflected(float point)

Returns a revised version of point that has been brought inside the spatial boundaries of the subpopulation by reflection.  In brief, if a coordinate of point lies beyond a spatial boundary, that coordinate is reflected across the boundary, so that it lies inside the boundary by the same magnitude that it previously lay outside the boundary.  This is done iteratively until all coordinates lie inside the subpopulation’s boundaries.  This method is useful for implementing reflecting boundary conditions.  This may only be called in simulations for which continuous space has been enabled with initializeSLiMOptions().

The length of point must be an exact multiple of the dimensionality of the simulation; in other words, point may contain values comprising more than one point.  In this case, each point will be processed as described above and a new vector containing all of the processed points will be returned.

– (float)pointStopped(float point)

Returns a revised version of point that has been brought inside the spatial boundaries of the subpopulation by clamping.  In brief, if a coordinate of point lies beyond a spatial boundary, that coordinate is set to exactly the position of the boundary, so that it lies on the edge of the spatial boundary.  This method is useful for implementing stopping boundary conditions.  This may only be called in simulations for which continuous space has been enabled with initializeSLiMOptions().

The length of point must be an exact multiple of the dimensionality of the simulation; in other words, point may contain values comprising more than one point.  In this case, each point will be processed as described above and a new vector containing all of the processed points will be returned.

– (float)pointUniform([integer$ n = 1])

Returns a new point (or points, for n > 1) generated from uniform draws for each coordinate, within the spatial boundaries of the subpopulation.  The returned vector will contain n points, each comprised of a number of coordinates equal to the dimensionality of the simulation, so it will be of total length n*dimensionality.  This may only be called in simulations for which continuous space has been enabled with initializeSLiMOptions().  See pointUniformWithMap() for an extension to this method which uses a spatial map to govern the probability of a particular point being chosen.

– (float)pointUniformWithMap(integer$ n, so<SpatialMap>$ map)

Returns a new point (or points, for n > 1) generated from uniform draws for each coordinate, within the spatial boundaries of the subpopulation, and rejection sampled using the spatial map map as described below.  The returned vector will contain n points, each comprised of a number of coordinates equal to the dimensionality of the simulation, so it will be of total length n*dimensionality.  This may only be called in simulations for which continuous space has been enabled with initializeSLiMOptions().

The spatial map defined by map must be configured in a specific way.  First of all, it must be defined in, or added to, the target subpopulation (and thus, by implication, it must match the spatial bounds of the subpopulation, and its spatiality must be compatible with the subpopulation’s dimensionality, as discussed in defineSpatialMap() and/or addSpatialMap()).  Second, the values in the spatial map must represent “habitability”, in the following sense.  The value of map at a given drawn point is obtained, symbolized here by x.  Next, x is clamped to the range [0, 1]; values less than 0 become 0, values greater than 1 become 1.  The resulting x value is then interpreted as the probability that the point is considered “within bounds” (as far as the spatial map is concerned; points that are outside the subpopulation’s spatial bounds are always considered “out of bounds”).  Given this, 1-x is thus the probability that the point will be redrawn because it fell out of bounds.  Each point will be redrawn repeatedly until a point considered “within bounds” is obtained.

– (void)removeSpatialMap(so<SpatialMap>$ map)

Removes the SpatialMap object specified by map from the subpopulation.  The parameter map may be either a SpatialMap object, or a string name for spatial map.  The map must have been added to the subpopulation with addSpatialMap(); if it has not been, an error results.  Removing spatial maps that are no longer in use is optional in most cases.  It is generally a good idea because it might decrease SLiM’s memory footprint; also, it avoids an error if the subpopulation’s spatial bounds are changed (see setSpatialBounds()).

– (void)removeSubpopulation(void)

Removes this subpopulation from the model.  The subpopulation is immediately removed from the list of active subpopulations, and the symbol representing the subpopulation is undefined.  The subpopulation object itself remains unchanged until children are next generated (at which point it is deallocated), but it is no longer part of the simulation and should not be used.

Note that this method is only for use in nonWF models, in which there is a distinction between a subpopulation being empty and a subpopulation being removed from the simulation; an empty subpopulation may be re-colonized by migrants, whereas as a removed subpopulation no longer exists at all.  WF models do not make this distinction; when a subpopulation is empty it is automatically removed.  WF models should therefore call setSubpopulationSize(0) instead of this method; setSubpopulationSize() is the standard way for WF models to change the subpopulation size, including to a size of 0.

– (object<Individual>)sampleIndividuals(integer$ size, [logical$ replace = F], [No<Individual>$ exclude = NULL], [Ns$ sex = NULL], [Ni$ tag = NULL], [Ni$ minAge = NULL], [Ni$ maxAge = NULL], [Nl$ migrant = NULL], [Nl$ tagL0 = NULL], [Nl$ tagL1 = NULL], [Nl$ tagL2 = NULL], [Nl$ tagL3 = NULL], [Nl$ tagL4 = NULL])

Returns a vector of individuals, of size less than or equal to parameter size, sampled from the individuals in the target subpopulation.  Sampling is done without replacement if replace is F (the default), or with replacement if replace is T.  The remaining parameters specify constraints upon the pool of individuals that will be considered candidates for the sampling.  Parameter exclude, if non-NULL, may specify a specific individual that should not be considered a candidate (typically the focal individual in some operation).  Parameter sex, if non-NULL, may specify a sex ("M" or "F") for the individuals to be drawn, in sexual models.  Parameter tag, if non-NULL, may specify a tag property value for the individuals to be drawn.  Parameters minAge and maxAge, if non-NULL, may specify a minimum or maximum age for the individuals to be drawn, in nonWF models.  Parameter migrant, if non-NULL, may specify a required value for the migrant property of the individuals to be drawn (so T will require that individuals be migrants, F will require that they not be).  Finally, parameters tagL0, tagL1, tagL2, tagL3, and tagL4, if non-NULL, may specify a required value (T or F) for the corresponding properties (tagL0, tagL1, tagL2, tagL3, and tagL4) of the individuals to be drawn.  Note that if any tag/tagL parameter is specified as non-NULL, that tag/tagL property must have a defined value for every individual in the subpopulation, otherwise an error may result (although this requirement will not necessarily be checked comprehensively by this method in every invocation).  If the candidate pool is smaller than the requested sample size, all eligible candidates will be returned (in randomized order); the result will be a zero-length vector if no eligible candidates exist (unlike sample()).

This method is similar to getting the individuals property of the subpopulation, using operator [] to select only individuals with the desired properties, and then using sample() to sample from that candidate pool.  However, besides being much simpler than the equivalent Eidos code, it is also much faster, and it does not fail if less than the full sample size is available.  See subsetIndividuals() for a similar method that returns a full subset, rather than a sample.

– (void)setCloningRate(numeric rate)

Set the cloning rate of this subpopulation.  The rate is changed to rate, which should be between 0.0 and 1.0, inclusive (see the SLiM manual for further details).  Clonal reproduction can be enabled in both non-sexual (i.e. hermaphroditic) and sexual simulations.  In non-sexual simulations, rate must be a singleton value representing the overall clonal reproduction rate for the subpopulation.  In sexual simulations, rate may be either a singleton (specifying the clonal reproduction rate for both sexes) or a vector containing two numeric values (the female and male cloning rates specified separately, at indices 0 and 1 respectively).  During mating and offspring generation, the probability that any given offspring individual will be generated by cloning – by asexual reproduction without gametes or meiosis – will be equal to the cloning rate (for its sex, in sexual simulations) set in the parental (not the offspring!) subpopulation.

– (void)setMigrationRates(io<Subpopulation> sourceSubpops, numeric rates)

Set the migration rates to this subpopulation from the subpopulations in sourceSubpops to the corresponding rates specified in rates; in other words, rates gives the expected fractions of the children in this subpopulation that will subsequently be generated from parents in the subpopulations sourceSubpops.  The rates parameter may be a singleton value, in which case that rate is used for all subpopulations in sourceSubpops.  This method will only set the migration fractions from the subpopulations given; migration rates from other subpopulations will be left unchanged (explicitly set a zero rate to turn off migration from a given subpopulation).  The type of sourceSubpops may be either integer, specifying subpopulations by identifier, or object, specifying subpopulations directly.

In general it is illegal to try to set the migration rate into a subpopulation from itself; that rate is, by definition, equal to the remainder after all migration from other subpopulations.  As a special case for convenience, it is legal to set a rate of 0.0 for all subpopulations in the species, including the target subpopulation.  For example, subpops.setMigrationRates(allSubpops, 0.0) will turn off all migration into the subpopulations in subpops.  The given rate of 0.0 from a subpop into itself is simply ignored, for this specific case only.

– (void)setSelfingRate(numeric$ rate)

Set the selfing rate of this subpopulation.  The rate is changed to rate, which should be between 0.0 and 1.0, inclusive (see the SLiM manual for further details).  Selfing can only be enabled in non-sexual (i.e. hermaphroditic) simulations.  During mating and offspring generation, the probability that any given offspring individual will be generated by selfing – by self-fertilization via gametes produced by meiosis by a single parent – will be equal to the selfing rate set in the parental (not the offspring!) subpopulation.

– (void)setSexRatio(float$ sexRatio)

Set the sex ratio of this subpopulation to sexRatio.  As defined in SLiM, this is actually the fraction of the subpopulation that is male; in other words, the M:(M+F) ratio.  This will take effect when children are next generated; it does not change the current subpopulation state.  Unlike the selfing rate, the cloning rate, and migration rates, the sex ratio is deterministic: SLiM will generate offspring that exactly satisfy the requested sex ratio (within integer roundoff limits).

– (void)setSpatialBounds(numeric bounds)

Set the spatial boundaries of the subpopulation to bounds.  This method may be called only for simulations in which continuous space has been enabled with initializeSLiMOptions().  The length of bounds must be double the spatial dimensionality, so that it supplies both minimum and maximum values for each coordinate.  More specifically, for a dimensionality of "x", bounds should supply (x0, x1) values; for dimensionality "xy" it should supply (x0, y0, x1, y1) values; and for dimensionality "xyz" it should supply (x0, y0, z0, x1, y1, z1) (in that order).  These boundaries will be used by SLiMgui to calibrate the display of the subpopulation, and will be used by methods such as pointInBounds(), pointReflected(), pointStopped(), and pointUniform().  The default spatial boundaries for all subpopulations span the interval [0,1] in each dimension.  Spatial dimensions that are periodic (as established with the periodicity parameter to initializeSLiMOptions()) must have a minimum coordinate value of 0.0 (a restriction that allows the handling of periodicity to be somewhat more efficient).  The current spatial bounds for the subpopulation may be obtained through the spatialBounds property.

The spatial bounds of a subpopulation are shared with any SpatialMap objects added to the subpopulation.  For this reason, once a spatial map has been added to a subpopulation, the spatial bounds of the subpopulation can no longer be changed (because it would stretch or shrink the associated spatial map, which does not seem to make physical sense).  The bounds for a subpopulation should therefore be configured before any spatial maps are added to it.  If those bounds do need to change subsequently, any associated spatial maps must first be removed with removeSpatialMap(), to ensure model consistency.

– (void)setSubpopulationSize(integer$ size)

Set the size of this subpopulation to size individuals (see the SLiM manual for further details).  This will take effect when children are next generated; it does not change the current subpopulation state.  Setting a subpopulation to a size of 0 does have some immediate effects that serve to disconnect it from the simulation: the subpopulation is removed from the list of active subpopulations, the subpopulation is removed as a source of migration for all other subpopulations, and the symbol representing the subpopulation is undefined.  In this case, the subpopulation itself remains unchanged until children are next generated (at which point it is deallocated), but it is no longer part of the simulation and should not be used.

 (string)spatialMapColor(string$ name, numeric value)

This method has been deprecated, and may be removed in a future release of SLiM.  In SLiM 4.1 and later, use the SpatialMap method mapColor() instead, and see that method’s documentation.  (This method differs only in taking a name parameter, which is used to look up the spatial map from those that have been added to the subpopulation.)

– (object<Image>$)spatialMapImage(string$ name, [Ni$ width = NULL], [Ni$ height = NULL], [logical$ centers = F], [logical$ color = T])

This method has been deprecated, and may be removed in a future release of SLiM.  In SLiM 4.1 and later, use the SpatialMap method mapImage() instead, and see that method’s documentation.  (This method differs only in taking a name parameter, which is used to look up the spatial map from those that have been added to the subpopulation.)

 (float)spatialMapValue(so<SpatialMap>$ map, float point)

Looks up the spatial map specified by map, and uses its mapping machinery (as defined by the gridSize, values, and interpolate parameters to defineSpatialMap()) to translate the coordinates of point into a corresponding map value.  The parameter map may specify the map either as a SpatialMap object, or by its string name; in either case, the map must have been added to the subpopulation.  The length of point must be equal to the spatiality of the spatial map; in other words, for a spatial map with spatiality "xz", point must be of length 2, specifying the x and z coordinates of the point to be evaluated.  Interpolation will automatically be used if it was enabled for the spatial map.  Point coordinates are clamped into the range defined by the spatial boundaries, even if the spatial boundaries are periodic; use pointPeriodic() to wrap the point coordinates first if desired.  See the documentation for defineSpatialMap() for information regarding the details of value mapping.

Beginning in SLiM 3.3, point may contain more than one point to be looked up.  In this case, the length of point must be an exact multiple of the spatiality of the spatial map; for a spatial map with spatiality "xz", for example, the length of point must be an exact multiple of 2, and successive pairs of elements from point (elements 0 and 1, then elements 2 and 3, etc.) will be taken as the x and z coordinates of the points to be evaluated.  This allows spatialMapValue() to be used in a vectorized fashion.

The mapValue() method of SpatialMap provides the same functionality directly on the SpatialMap class; spatialMapValue() is provided on Subpopulation partly for backward compatibility, but also for convenience in some usage cases.

– (object<Individual>)subsetIndividuals([No<Individual>$ exclude = NULL], [Ns$ sex = NULL], [Ni$ tag = NULL], [Ni$ minAge = NULL], [Ni$ maxAge = NULL], [Nl$ migrant = NULL], [Nl$ tagL0 = NULL], [Nl$ tagL1 = NULL], [Nl$ tagL2 = NULL], [Nl$ tagL3 = NULL], [Nl$ tagL4 = NULL])

Returns a vector of individuals subset from the individuals in the target subpopulation.  The parameters specify constraints upon the subset of individuals that will be returned.  Parameter exclude, if non-NULL, may specify a specific individual that should not be included (typically the focal individual in some operation).  Parameter sex, if non-NULL, may specify a sex ("M" or "F") for the individuals to be returned, in sexual models.  Parameter tag, if non-NULL, may specify a tag property value for the individuals to be returned.  Parameters minAge and maxAge, if non-NULL, may specify a minimum or maximum age for the individuals to be returned, in nonWF models.  Parameter migrant, if non-NULL, may specify a required value for the migrant property of the individuals to be returned (so T will require that individuals be migrants, F will require that they not be).  Finally, parameters tagL0, tagL1, tagL2, tagL3, and tagL4, if non-NULL, may specify a required value (T or F) for the corresponding properties (tagL0, tagL1, tagL2, tagL3, and tagL4) of the individuals to be returned.  Note that if any tag/tagL parameter is specified as non-NULL, that tag/tagL property must have a defined value for every individual in the subpopulation, otherwise an error may result (although this requirement will not necessarily be checked comprehensively by this method in every invocation).

This method is shorthand for getting the individuals property of the subpopulation, and then using operator [] to select only individuals with the desired properties; besides being much simpler than the equivalent Eidos code, it is also much faster.  See sampleIndividuals() for a similar method that returns a sample taken from a chosen subset of individuals.

– (void)takeMigrants(object<Individual> migrants)

Immediately moves the individuals in migrants to the target subpopulation (removing them from their previous subpopulation).  Individuals in migrants that are already in the target subpopulation are unaffected.  Note that the indices and order of individuals and haplosomes in both the target and source subpopulations will change unpredictably as a side effect of this method.

Note that this method is only for use in nonWF models, in which migration is managed manually by the model script.  In WF models, migration is managed automatically by the SLiM core based upon the migration rates set for each subpopulation with setMigrationRates().

5.18  Class Substitution

5.18.1  Substitution properties

chromosome => (object<Chromosome>$)

The Chromosome object with which the mutation is associated.

id => (integer$)

The identifier for this mutation.  Each mutation created during a run receives an immutable identifier that will be unique across the duration of the run, and that identifier is carried over to the Substitution object when the mutation fixes.

fixationTick => (integer$)

The tick in which this mutation fixed.

mutationType => (object<MutationType>$)

The MutationType from which this mutation was drawn.

nucleotide => (string$)

A string representing the nucleotide associated with this mutation; this will be "A", "C", "G", or "T".  If the mutation is not nucleotide-based, this property is unavailable.

nucleotideValue => (integer$)

An integer representing the nucleotide associated with this mutation; this will be 0 (A), 1 (C), 2 (G), or 3 (T).  If the mutation is not nucleotide-based, this property is unavailable.

originTick => (integer$)

The tick in which this mutation arose.

position => (integer$)

The position in the chromosome of this mutation.

selectionCoeff => (float$)

The selection coefficient of the mutation, drawn from the distribution of fitness effects of its MutationType.

subpopID <–> (integer$)

The identifier of the subpopulation in which this mutation arose.  This value is carried over from the Mutation object directly; if a “tag” value was used in the Mutation object, that value will carry over to the corresponding Substitution object.  The subpopID in Substitution is a read-write property to allow it to be used as a “tag” in the same way, if the origin subpopulation identifier is not needed.

tag <–> (integer$)

A user-defined integer value.  The value of tag is carried over automatically from the original Mutation object.  Apart from that, the value of tag is not used by SLiM; it is free for you to use.

5.18.2  Substitution methods


================================================ FILE: QtSLiM/help/SLiMHelpFunctions.html ================================================

3.1.  Initialization functions

(integer$)initializeAncestralNucleotides(is sequence)

This function, which may be called only in nucleotide-based models, supplies an ancestral nucleotide sequence for the model.  The sequence parameter may be an integer vector providing nucleotide values (A=0, C=1, G=2, T=3), or a string vector providing single-character nucleotides ("A", "C", "G", "T"), or a singleton string providing the sequence as one string ("ACGT..."), or a singleton string providing the filesystem path of a FASTA file which will be read in to provide the sequence (if the file contains than one sequence, the first sequence will be used).  Only A/C/G/T nucleotide values may be provided; other symbols, such as those for amino acids, gaps, or nucleotides of uncertain identity, are not allowed.  The two semantic meanings of sequence that involve a singleton string value are distinguished heuristically; a singleton string that contains only the letters ACGT will be assumed to be a nucleotide sequence rather than a filename.  The length of the ancestral sequence is returned.

A utility function, randomNucleotides(), is provided by SLiM to assist in generating simple random nucleotide sequences.

(object<Chromosome>$)initializeChromosome(integer$ id, [Ni$ length = NULL], [string$ type = "A"], [Ns$ symbol = NULL], [Ns$ name = NULL], [integer$ mutationRuns = 0])

Calling this function, added in SLiM 5, initiates the configuration of a chromosome in the species being initialized.  The new Chromosome object is returned, but it is still under construction and will error if used; see below for details.  That chromosome is then the “focal chromosome” for subsequent genetic initialization functions – specifically, for initializeAncestralNucleotides(), initializeGeneConversion(), initializeGenomicElement(), initializeHotspotMap(), initializeMutationRate(), and initializeRecombinationRate().  If you wish to call initializeChromosome() at all (which is not required), you must call it before calling any of those genetic initialization functions, so that the focal chromosome is created before being configured further; otherwise, SLiM will assume that you want a default single-chromosome model, and when initializeChromosome() is called later (contradicting that assumption), an error will result.

Furthermore, there are some other initialization functions must be called before initializeChromosome() if they are called at all – specifically, initializeSex(), initializeTreeSeq(), initializeSpecies(), and initializeSLiMOptions().  This is so that initializeChromosome() knows the context within which the new chromosome is to be created; if these methods have not been called when initializeChromosome() is called, the default context is assumed (non-sexual, no tree-sequence recording, single-species, non-nucleotide-based), and an error will result downstream if one of those functions is later called (indicating that those assumptions might be incorrect).

The parameters to initializeChromosome() configure the chromosome created.  They will be discussed out of order here, because that order of presentation will, I hope, be clearer.

There are three parameters that in some way identify the chromosome.  First, the required id parameter provides an integer identifier for the chromosome, which can be used to look up the chromosome later in the simulation; it can be any non-negative integer value, but must be unique within the species (two chromosomes in the same species cannot have the same id).  Often it is an empirical chromosome number, for convenience and clarity; if modeling human chromosome 7, for example, you might provide 7.  Second, the symbol parameter provides a string identifier for the chromosome, which can also be used to look up the chromosome later in the simulation.  If NULL (the default) is passed for symbol, the chromosome’s default symbol value will be the string version of its id ("7" for an id of 7, for example).  The chromosome’s symbol value will be used to identify the chromosome in output – in VCF output, for example, and in SLiMgui.  It must be non-empty (not ""), no more than five characters long, and unique within the species.  Third, the name parameter can be any string value; if NULL (the default) is passed, the name value will be "".  The name is not used by SLiM, and can be used in any way you wish.

The length parameter sets the length, in base positions, of the chromosome, and must either be NULL, or an integer greater than or equal to 1.  If length is NULL, the length of the chromosome will be calculated after all initialize() callbacks have been called, as the maximum position referenced by the chromosome’s genomic elements, recombination map, mutation rate map, and (in nucleotide-based models) hotspot map; in other words, the chromosome will be sized to encompass all of the things it contains (which is also the behavior of the implicitly defined chromosome if initializeChromosome() is not called).  Otherwise – if length is specified with an integer value – the chromosome’s length will be fixed at that value, and the last valid base position in the chromosome will be length-1.  Attempting to add a genomic element or a mutation after the last position will raise an error.  Similarly, the last position of the chromosome must match the last position specified for recombination, mutation, and hotspot maps for that chromosome, but not all positions on a chromosome have to actually be used in the model (i.e., not all positions must be covered by a genomic element).

The type parameter specifies the type of chromosome to be created.  There are numerous options, and they are somewhat complex.  They are discussed in more detail in the documentation for class Chromosome, particularly their specific patterns of inheritance; but they are briefly summarized here for quick reference.  Note that “–“ below indicates a null haplosome.  First of all, in hermaphroditic models type will generally be one of:

"A" (autosome), the default, specifying a diploid autosomal chromosome.

"H" (haploid), specifying a haploid autosomal chromosome that recombines in biparental crosses.

"HF" (haploid female-inherited), specifying a haploid autosomal chromosome that is inherited by both sexes from the first (female) parent in biparental crosses (also allowed in hermaphroditic models for inheritance that is always from the first parent).

"HM" (haploid male-inherited), specifying a haploid autosomal chromosome that is inherited by both sexes from the second (male) parent in biparental crosses (also allowed in hermaphroditic models for inheritance that is always from the second parent).

Some sex-chromosome types are supported only in sexual models:

"X" (X), specifying an X chromosome that is diploid (XX) in females, haploid (X–) in males.

"Y" (Y), specifying a Y chromosome that is haploid (Y) in males, absent (–) in females.

"Z" (Z), specifying a Z chromosome that is diploid (ZZ) in males, haploid (–Z) in females.

"W" (W), specifying a W chromosome that is haploid (W) in females, absent (–) in males.

And there are some haploid chromosome types that are also supported only in sexual models:

"FL" (female line), specifying a haploid autosomal chromosome that is inherited only by females, from the female parent, and is represented by a null haplosome in males.

"ML" (male line), specifying a haploid autosomal chromosome that is inherited only by males, from the male parent, and is represented by a null haplosome in females.

Finally, two additional values of type, "H-" and "-Y", are supported for backward compatibility (not intended for use in new models).  They are discussed in the Chromosome documentation.

The mutationRuns parameter specifies how many mutation runs the chromosome should use.  Internally, SLiM divides haplosomes into a sequence of consecutive mutation runs, allowing more efficient internal computations.  The optimal mutation run length is short enough that each mutation run is relatively unlikely to be modified by mutation/recombination events when inherited, but long enough that each mutation run is likely to contain a relatively large number of mutations; these priorities are in tension, so an intermediate balance between them is generally optimal.  The optimal number of mutation runs will depend on the model’s details, and may also depend upon the machine and even the compiler used to build SLiM.  If the mutationRuns parameter is not 0, SLiM will use the value given as the number of mutation runs inside Haplosome objects for the chromosome.  If mutationRuns is 0 (the default), then the behavior depends upon a parameter to the initializeSLiMOptions() function, doMutationRunExperiments.  If that flag is F, the behavior here is as if mutationRuns=1 had been passed: one mutation run will be used, and mutation run experiments will not be conducted.  If that flag is T (the default), then for mutationRuns=0 SLiM will conduct experiments at runtime, using different mutation run counts, to try to determine the number of mutation runs that produces the best performance.  The value that SLiM’s experiments determine may not be optimal, however, and in any case there is some overhead associated with conducting these experiments; for maximal performance it can thus be beneficial to determine the true optimal value for the simulation yourself, and set it explicitly using this parameter. Specifying the number of mutation runs is an advanced technique, but in some cases it can improve performance significantly.

The order in which initializeChromosome() calls are made is generally unimportant, since the chromosomes assort independently of each other anyway, but SLiM will preserve the order in which they were defined for you (for the chromosomes property of Species, for display in SLiMgui, for writing out to VCF, and so forth).  All of the above types of chromosomes can be defined any number of times; you can have any number of autosomal chromosomes, for example.  In a sexual model you could even have multiple defined sex chromosomes – not in the sense of a female being XX, but in the sense of a female being X1X1X2X2, where X1 and X2 are two different kinds of X chromosome.  Similarly, you could define both an X and a Z for a species, if you wish; each would segregate correctly according to the sex of the offspring.  In sexual models in SLiM the sex of an offspring is determined randomly or given by the user in script; it is not a function of the sex chromosomes present in the individual, although the sex chromosomes present in the individual will correlate with sex.  In other words, SLiM does not know and does not care what sex-determination system the species is using; the chromosomes follow the sex, rather than the sex following the chromosomes.  This should allow any sex-determination system to be modeled, even if it is unusual, non-genetic, etc.

As stated above, the new Chromosome object is returned by this call, but it is still under construction so most of its methods and properties will error.  It will remain in this state until initialize() callbacks have completed, and will then become active and usable.  Until that point, there are only a handful of uses that are guaranteed to be allowed: storing it in a variable; remembering it with defineConstant(); using the methods and properties of its superclasses, notably Dictionary; setting SLiMgui display-related properties such as colorSubstitution; getting and setting its tag property; and accessing those of its properties that were passed to the initializeChromosome() call, specifically id, symbol, name, type, length, and lastPosition.  Its other properties, and all Chromosome methods, will raise an error while in this state.  This safeguard protects the new Chromosome object from being used while still in an inconsistent state.

(void)initializeGeneConversion(numeric$ nonCrossoverFraction, numeric$ meanLength, numeric$ simpleConversionFraction, [numeric$ bias = 0], [logical$ redrawLengthsOnFailure = F])

Calling this function switches the recombination model from a “simple crossover” model to a “double-stranded break (DSB)” model, and configures the details of the gene conversion tracts that will therefore be modeled.  The fraction of DSBs that will be modeled as non-crossover events is given by nonCrossoverFraction.  The mean length of gene conversion tracts (whether associated with crossover or non-crossover events) is given by meanLength; the actual extent of a gene conversion tract will be the sum of two independent draws from a geometric distribution with mean meanLength/2.  The fraction of gene conversion tracts that are modeled as “simple” is given by simpleConversionFraction; the remainder will be modeled as “complex”, involving repair of heteroduplex mismatches.  Finally, the GC bias during heteroduplex mismatch repair is given by bias, with the default of 0.0 indicating no bias, 1.0 indicating an absolute preference for G/C mutations over A/T mutations, and -1.0 indicating an absolute preference for A/T mutations over G/C mutations.  A non-zero bias may only be set in nucleotide-based models.  This function, and the way that gene conversion is modeled, fundamentally changed in SLiM 3.3.

Beginning in SLiM 4.1, the redrawLengthsOnFailure parameter can be used to modify the internal mechanics of layout of gene conversion tracts.  If it is F (the default, and the only behavior supported before SLiM 4.1), then if an attempt to lay out gene conversion tracts fails (because the tracts overlap each other, or overlap the start or end of the chromosome), SLiM will try again by drawing new positions for the tracts – essentially shuffling the tracts around to try to find positions for them that don’t overlap.  If redrawLengthsOnFailure is T, then if an attempt to lay out gene conversion tracts fails, SLiM will try again by drawing new lengths for the tracts, as well as new positions.  This makes it more likely that layout will succeed, but risks biasing the realized mean tract length downward from the requested mean length (since layout of long tracts is more likely fail due to overlap).  In either case, if SLiM attempts to lay out gene conversion tracts 100 times without success, an error will result.  That error indicates that the specified constraints for gene conversion are difficult to satisfy – tracts may commonly be so long that it is difficult or impossible to find an acceptable layout for them within the specified chromosome length.  Setting redrawLengthsOnFailure to T may mitigate this problem, at the price of biasing the mean tract length downward as discussed.

(object<GenomicElement>)initializeGenomicElement(io<GenomicElementType> genomicElementType, [Ni start = NULL], [Ni end = NULL])

Add a genomic element to the chromosome at initialization time.  The start and end parameters give the first and last base positions to be spanned by the new genomic element.  The new element will be based upon the genomic element type identified by genomicElementType, which can be either an integer, representing the ID of the desired element type, or an object of type GenomicElementType specified directly.

Beginning in SLiM 3.3, this function is vectorized: the genomicElementType, start, and end parameters do not have to be singletons.  In particular, start and end may be of any length, but must be equal in length; each start/end element pair will generate one new genomic element spanning the given base positions.  In this case, genomicElementType may still be a singleton, providing the genomic element type to be used for all of the new genomic elements, or it may be equal in length to start and end, providing an independent genomic element type for each new element.  When adding a large number of genomic elements, it will be much faster to add them in order of ascending position with a vectorized call.

Beginning in SLiM 5, passing NULL for start and end is allowed by initializeGenomicElement(), but only in one specific case: if the focal chromosome being configured was explicitly defined with initializeChromosome(), and that focal chromosome was given an explicit length (rather than a length of NULL).  In that case, start and end may be NULL (both of them, not just one of them), indicating that the genomic element created should span the entire length of the focal chromosome.  Since NULL is now the default value for start and end, this makes this common configuration very simple to set up.

The return value provides the genomic element(s) created by the call, in the order in which they were specified in the parameters to initializeGenomicElement().

(object<GenomicElementType>$)initializeGenomicElementType(is$ id, io<MutationType> mutationTypes, numeric proportions, [Nf mutationMatrix = NULL])

Add a genomic element type at initialization time.  The id must not already be used for any genomic element type in the simulation.  The mutationTypes vector identifies the mutation types used by the genomic element, and the proportions vector should be of equal length, specifying the relative proportion of mutations that will be drawn from the corresponding mutation type (proportions do not need to add up to one; they are interpreted relatively).  The id parameter may be either an integer giving the ID of the new genomic element type, or a string giving the name of the new genomic element type (such as "g5" to specify an ID of 5).  The mutationTypes parameter may be either an integer vector representing the IDs of the desired mutation types, or an object vector of MutationType elements specified directly.  The global symbol for the new genomic element type is immediately available; the return value also provides the new object.

The mutationMatrix parameter is NULL by default, and in non-nucleotide-based models it must be NULL.  In nucleotide-based models, on the other hand, it must be non-NULL, and therefore must be supplied.  In that case, mutationMatrix should take one of two standard forms.  For sequence-based mutation rates that depend upon only the single nucleotide at a mutation site, mutationMatrix should be a 4×4 float matrix, specifying mutation rates for an existing nucleotide state (rows from 03 representing A/C/G/T) to each of the four possible derived nucleotide states (columns, with the same meaning).  The mutation rates in this matrix are absolute rates, per nucleotide per gamete; they will be used by SLiM directly unless they are multiplied by a factor from the hotspot map (see initializeHotspotMap()).  Rates in mutationMatrix that involve the mutation of a nucleotide to itself (A to A, C to C, etc.) are not used by SLiM and must be 0.0 by convention.

It is important to note that the order of the rows and columns used in SLiM, A/C/G/T, is not a universal convention; other sources will present substitution-rate/transition-rate matrices using different conventions, and so care must be taken when importing such matrices into SLiM.

For sequence-based mutation rates that depend upon the trinucleotide sequence centered upon a mutation site (the adjacent bases to the left and right, in other words, as well as the mutating nucleotide itself), mutationMatrix should be a 64×4 float matrix, specifying mutation rates for the central nucleotide of an existing trinucleotide sequence (rows from 063, representing codons as described in the documentation for the ancestralNucleotides() method of Chromosome) to each of the four possible derived nucleotide states (columns from 03 for A/C/G/T as before).  Note that in every case it is the central nucleotide of the trinucleotide sequence that is mutating, but rates can be specified independently based upon the nucleotides in the first and third positions as well, with this type of mutation matrix.

Several helper functions are defined to construct common types of mutation matrices, such as mmJukesCantor() to create a mutation matrix for a Jukes–Cantor model.

(void)initializeHotspotMap(numeric multipliers, [Ni ends = NULL], [string$ sex = "*"])

In nucleotide-based models, set the mutation rate multiplier along the chromosome.  Nucleotide-based models define sequence-based mutation rates that are set up with the mutationMatrix parameter to initializeGenomicElementType().  If no hotspot map is specified by calling initializeHotspotMap(), a hotspot map with a multiplier of 1.0 across the whole chromosome is assumed (and so the sequence-based rates are the absolute mutation rates used by SLiM).  A hotspot map modifies the sequence-based rates by scaling them up in some regions, with multipliers greater than 1.0 (representing mutational hot spots), and/or scaling them down in some regions, with multipliers less than 1.0 (representing mutational cold spots).

There are two ways to call this function.  If the optional ends parameter is NULL (the default), then multipliers must be a singleton value that specifies a single multiplier to be used along the entire chromosome (typically 1.0, but not required to be).  If, on the other hand, ends is supplied, then multipliers and ends must be the same length, and the values in ends must be specified in ascending order.  In that case, multipliers and ends taken together specify the multipliers to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in ends should extend to the end of the chromosome (i.e. at least to the end of the last genomic element, if not further).

For example, if the following call is made:

initializeHotspotMap(c(1.0, 1.2), c(5000, 9999));

then the result is that the mutation rate multiplier for bases 0...5000 (inclusive) will be 1.0 (and so the specified sequence-based mutation rates will be used verbatim), and the multiplier for bases 5001...9999 (inclusive) will be 1.2 (and so the sequence-based mutation rates will be multiplied by 1.2 within the region).

Note that mutations are generated by SLiM only within genomic elements, regardless of the hotspot map.  In effect, the hotspot map given is intersected with the coverage area of the genomic elements defined; areas outside of any genomic element are given a multiplier of zero.  There is no harm in supplying a hotspot map that specifies multipliers for areas outside of the genomic elements defined; the excess information is simply not used.

If the optional sex parameter is "*" (the default), then the supplied hotspot map will be used for both sexes (which is the only option for hermaphroditic simulations).  In sexual simulations sex may be "M" or "F" instead, in which case the supplied hotspot map is used only for that sex (i.e., when generating a gamete from a parent of that sex).  In this case, two calls must be made to initializeHotspotMap(), one for each sex, even if a multiplier of 1.0 is desired for the other sex; no default hotspot map is supplied.

(object<InteractionType>$)initializeInteractionType(is$ id, string$ spatiality, [logical$ reciprocal = F], [numeric$ maxDistance = INF], [string$ sexSegregation = "**"])

Add an interaction type at initialization time.  The id must not already be used for any interaction type in the simulation.  The id parameter may be either an integer giving the ID of the new interaction type, or a string giving the name of the new interaction type (such as "i5" to specify an ID of 5).

The spatiality may be "", for non-spatial interactions (i.e., interactions that do not depend upon the distance between individuals); "x", "y", or "z" for one-dimensional interactions; "xy", "xz", or "yz" for two-dimensional interactions; or "xyz" for three-dimensional interactions.  The dimensions referenced by spatiality must be defined as spatial dimensions with initializeSLiMOptions(); if the simulation has dimensionality "xy", for example, then interactions in the simulation may have spatiality "", "x", "y", or "xy", but may not reference spatial dimension z and thus may not have spatiality "xz", "yz", or "xyz".  If no spatial dimensions have been configured, only non-spatial interactions may be defined.

The reciprocal flag may be T, in which case the interaction is guaranteed by the user to be reciprocal: whatever the interaction strength is for exerter B upon receiver A, it will be equal (in magnitude and sign) for exerter A upon receiver B.  In principle, this allows the InteractionType to reduce the amount of computation necessary by up to a factor of two (although it may or may not be used).  If reciprocal is F, the interaction is not guaranteed to be reciprocal and each interaction will be computed independently.  The built-in interaction formulas are all reciprocal, but if you implement an interaction() callback, you must consider whether the callback you have implemented preserves reciprocality or not.  For this reason, the default is reciprocal=F, so that bugs are not inadvertently introduced by an invalid assumption of reciprocality.  See below for a note regarding reciprocality in sexual simulations when using the sexSegregation flag.

The maxDistance parameter supplies the maximum distance over which interactions of this type will be evaluated; at greater distances, the interaction strength is considered to be zero (for efficiency).  The default value of maxDistance, INF (positive infinity), indicates that there is no maximum interaction distance; note that this can make some interaction queries much less efficient, and is therefore not recommended.  In SLiM 3.1 and later, a warning will be issued if a spatial interaction type is defined with no maximum distance to encourage a maximum distance to be defined.

The sexSegregation parameter governs the applicability of the interaction to each sex, in sexual simulations.  It does not affect distance calculations in any way; it only modifies the way in which interaction strengths are calculated.  The default, "**", implies that the interaction is felt by both sexes (the first character of the string value) and is exerted by both sexes (the second character of the string value).  Either or both characters may be M or F instead; for example, "MM" would indicate a male-male interaction, such as male-male competition, whereas "FM" would indicate an interaction influencing only female receivers that is influenced only by male exerters, such as male mating displays that influence female attraction.  This parameter may be set only to "**" unless sex has been enabled with initializeSex().  Note that a value of sexSegregation other than "**" may imply some degree of non-reciprocality, but it is not necessary to specify reciprocal to be F for this reason; SLiM will take the sex-segregation of the interaction into account for you.  The value of reciprocal may therefore be interpreted as meaning: in those cases, if any, in which A interacts with B and B interacts with A, is the interaction strength guaranteed to be the same in both directions?  The sexSegregation parameter is shorthand for setting sex constraints on the interaction type using the setConstraints() method; see that method for a more extensive set of constraints that may be used.

By default, the interaction strength is 1.0 for all interactions within maxDistance.  Often it is desirable to change the interaction function using setInteractionFunction(); modifying interaction strengths can also be achieved with interaction() callbacks if necessary.  In any case, interactions beyond maxDistance always have a strength of 0.0, and the interaction strength of an individual with itself is always 0.0, regardless of the interaction function or callbacks.

The global symbol for the new interaction type is immediately available; the return value also provides the new object.  Note that in multispecies models, initializeInteractionType() must be called from a non-species-specific interaction() callback (declared as species all initialize()), since interactions are managed at the community level.

(void)initializeMutationRate(numeric rates, [Ni ends = NULL], [string$ sex = "*"])

Set the mutation rate per base position per gamete.  To be precise, this mutation rate is the expected mean number of mutations that will occur per base position per gamete; note that this is different from how the recombination rate is defined (see initializeRecombinationRate()).  The number of mutations that actually occurs at a given base position when generating an offspring haplosome is, in effect, drawn from a Poisson distribution with that expected mean (but under the hood SLiM uses a mathematically equivalent but much more efficient strategy).  It is possible for this Poisson draw to indicate that two or more new mutations have arisen at the same base position, particularly when the mutation rate is very high; in this case, the new mutations will be added to the site one at a time, and as always the mutation stacking policy will be followed.

There are two ways to call this function.  If the optional ends parameter is NULL (the default), then rates must be a singleton value that specifies a single mutation rate to be used along the entire chromosome.  If, on the other hand, ends is supplied, then rates and ends must be the same length, and the values in ends must be specified in ascending order.  In that case, rates and ends taken together specify the mutation rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in ends should extend to the end of the chromosome (i.e. at least to the end of the last genomic element, if not further).

For example, if the following call is made:

initializeMutationRate(c(1e-7, 2.5e-8), c(5000, 9999));

then the result is that the mutation rate for bases 0...5000 (inclusive) will be 1e-7, and the rate for bases 5001...9999 (inclusive) will be 2.5e-8.

Note that mutations are generated by SLiM only within genomic elements, regardless of the mutation rate map.  In effect, the mutation rate map given is intersected with the coverage area of the genomic elements defined; areas outside of any genomic element are given a mutation rate of zero.  There is no harm in supplying a mutation rate map that specifies rates for areas outside of the genomic elements defined; that rate information is simply not used.  The overallMutationRate family of properties on Chromosome provide the overall mutation rate after genomic element coverage has been taken into account, so it will reflect the rate at which new mutations will actually be generated in the simulation as configured.

If the optional sex parameter is "*" (the default), then the supplied mutation rate map will be used for both sexes (which is the only option for hermaphroditic simulations).  In sexual simulations sex may be "M" or "F" instead, in which case the supplied mutation rate map is used only for that sex (i.e., when generating a gamete from a parent of that sex).  In this case, two calls must be made to initializeMutationRate(), one for each sex, even if a rate of zero is desired for the other sex; no default mutation rate map is supplied.

In nucleotide-based models, initializeMutationRate() may not be called.  Instead, the desired sequence-based mutation rate(s) should be expressed in the mutationMatrix parameter to initializeGenomicElementType().  If variation in the mutation rate along the chromosome is desired, initializeHotspotMap() should be used.

The initializeMutationRateFromFile() function is a useful convenience function if you wish to read the mutation rate map from a file.

(void)initializeMutationRateFromFile(string$ path, integer$ lastPosition, [float$ scale = 1.0e-08], [string$ sep = "\t"], [string$ dec = "."], [string$ sex = "*"])

Set a mutation rate map from data read from the file at path.  This function is essentially a wrapper for initializeMutationRate() that uses readCSV() and passes the data through.  The file is expected to contain two columns of data.  The first column must be integer start positions for rate map regions; the first region should start at position 0 if the map’s positions are 0-based, or at position 1 if the map’s positions are 1-based; in the latter case, 1 will be subtracted from every position since SLiM uses 0-based positions.  The second column must be float rates, relative to the scaling factor specified in scale; for example, if a given rate is 1.2 and scale is 1e-8 (the default), the rate used will be 1.2e-8.  No column header line should be present; the file should start immediately with numerical data.  The expected separator between columns is a tab character by default, but may be passed in sep; the expected decimal separator is a period by default, but may be passed in dec.  Once read, the map is converted into a rate map specified with end positions, rather than start positions, and the position given by lastPosition is used as the end of the last rate region; it should be the last position of the chromosome.

See readCSV() for further details on sep and dec, which are passed through to it; and see initializeMutationRate() for details on how the rate map is validated and used, and how the sex parameter is used.

This function is written in Eidos, and its source code can be viewed with functionSource(), so you can copy and modify its code if you need to modify its functionality.

(object<MutationType>$)initializeMutationType(is$ id, numeric$ dominanceCoeff, string$ distributionType, ...)

Add a mutation type at initialization time.  The id must not already be used for any mutation type in the simulation.  The id parameter may be either an integer giving the ID of the new mutation type, or a string giving the name of the new mutation type (such as "m5" to specify an ID of 5).  The dominanceCoeff parameter supplies the dominance coefficient for the mutation type; 0.0 produces no dominance, 1.0 complete dominance, and values greater than 1.0, overdominance.  The distributionType may be "f", in which case the ellipsis ... should supply a numeric$ fixed selection coefficient; "e", in which case the ellipsis should supply a numeric$ mean selection coefficient for an exponential distribution; "g", in which case the ellipsis should supply a numeric$ mean selection coefficient and a numeric$ alpha shape parameter for a gamma distribution; "n", in which case the ellipsis should supply a numeric$ mean selection coefficient and a numeric$ sigma (standard deviation) parameter for a normal distribution; "p", in which case the ellipsis should supply a numeric$ mean selection coefficient and a numeric$ scale parameter for a Laplace distribution; "w", in which case the ellipsis should supply a numeric$ λ scale parameter and a numeric$ k shape parameter for a Weibull distribution; or "s", in which case the ellipsis should supply a string$ Eidos script parameter.  The global symbol for the new mutation type is immediately available; the return value also provides the new object.

Note that by default in WF models, all mutations of a given mutation type will be converted into Substitution objects when they reach fixation, for efficiency reasons.  If you need to disable this conversion, to keep mutations of a given type active in the simulation even after they have fixed, you can do so by setting the convertToSubstitution property of MutationType to F.  In contrast, by default in nonWF models mutations will not be converted into Substitution objects when they reach fixation; convertToSubstitution is F by default in nonWF models.  To enable conversion in nonWF models for neutral mutation types with no indirect fitness effects, you should therefore set convertToSubstitution to T.

(object<MutationType>$)initializeMutationTypeNuc(is$ id, numeric$ dominanceCoeff, string$ distributionType, ...)

Add a nucleotide-based mutation type at initialization time.  This function is identical to initializeMutationType() except that the new mutation type will be nucleotide-based – in other words, mutations belonging to the new mutation type will have an associated nucleotide.  This function may be called only in nucleotide-based models (as enabled by the nucleotideBased parameter to initializeSLiMOptions()).

Nucleotide-based mutations always use a mutationStackGroup of -1 and a mutationStackPolicy of "l".  This ensures that a new nucleotide mutation always replaces any previously existing nucleotide mutation at a given position, regardless of the mutation types of the nucleotide mutations.  These values are set automatically by initializeMutationTypeNuc(), and may not be changed.

See the documentation for initializeMutationType() for all other discussion.

(void)initializeRecombinationRate(numeric rates, [Ni ends = NULL], [string$ sex = "*"])

Set the recombination rate per base position per gamete.  To be precise, this recombination rate is the probability that a breakpoint will occur between one base and the next base; note that this is different from how the mutation rate is defined (see initializeMutationRate()).  A recombination rate of 1 centimorgan/Mbp corresponds to a recombination rate of 1e-8 in the units used by SLiM.  All rates must be in the interval [0.0, 0.5].  A rate of 0.5 implies complete independence between the adjacent bases, which might be used to implement unlinked loci.  Whether a breakpoint occurs between two bases is then, in effect, determined by a binomial draw with a single trial and the given rate as probability (but under the hood SLiM uses a mathematically equivalent but much more efficient strategy).  The recombinational process in SLiM will never generate more then one crossover between one base and the next (in one generation/haplosome), and a supplied rate of 0.5 will therefore result in an actual probability of 0.5 for a crossover at the relevant position.  (Note that this was not true in SLiM 2.x and earlier, however; their implementation of recombination resulted in a crossover probability of about 39.3% for a rate of 0.5, due to the use of an inaccurate approximation method.  Recombination rates lower than about 0.01 would have been essentially exact, since the approximation error became large only as the rate approached 0.5.)

There are two ways to call this function.  If the optional ends parameter is NULL (the default), then rates must be a singleton value that specifies a single recombination rate to be used along the entire chromosome.  If, on the other hand, ends is supplied, then rates and ends must be the same length, and the values in ends must be specified in ascending order.  In that case, rates and ends taken together specify the recombination rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in ends should extend to the end of the chromosome (i.e. at least to the end of the last genomic element, if not further).

If the optional sex parameter is "*" (the default), then the supplied recombination rate map will be used for both sexes (which is the only option for hermaphroditic simulations).  In sexual simulations sex may be "M" or "F" instead, in which case the supplied recombination map is used only for that sex.  In this case, two calls must be made to initializeRecombinationRate(), one for each sex, even if a rate of zero is desired for the other sex; no default recombination map is supplied.

The initializeRecombinationRateFromFile() function is a useful convenience function if you wish to read the recombination rate map from a file.

(void)initializeRecombinationRateFromFile(string$ path, integer$ lastPosition, [float$ scale = 1.0e-08], [string$ sep = "\t"], [string$ dec = "."], [string$ sex = "*"])

Set a recombination rate map from data read from the file at path.  This function is essentially a wrapper for initializeRecombinationRate() that uses readCSV() and passes the data through.  The file is expected to contain two columns of data.  The first column must be integer start positions for rate map regions; the first region should start at position 0 if the map’s positions are 0-based, or at position 1 if the map’s positions are 1-based; in the latter case, 1 will be subtracted from every position since SLiM uses 0-based positions.  The second column must be float rates, relative to the scaling factor specified in scale; for example, if a given rate is 1.2 and scale is 1e-8 (the default), the rate used will be 1.2e-8.  No column header line should be present; the file should start immediately with numerical data.  The expected separator between columns is a tab character by default, but may be passed in sep; the expected decimal separator is a period by default, but may be passed in dec.  Once read, the map is converted into a rate map specified with end positions, rather than start positions, and the position given by lastPosition is used as the end of the last rate region; it should be the last position of the chromosome.

See readCSV() for further details on sep and dec, which are passed through to it; and see initializeRecombinationRate() for details on how the rate map is validated and used, and how the sex parameter is used.

This function is written in Eidos, and its source code can be viewed with functionSource(), so you can copy and modify its code if you need to modify its functionality.

(void)initializeSex([Ns$ chromosomeType = NULL])

Enable sex in the simulation.  Beginning in SLiM 5, this method should generally be passed NULL, simply indicating that sex should be enabled: individuals will then be male and female (rather than hermaphroditic), biparental crosses will be required to be between a female first parent and a male second parent, and selfing will not be allowed.  In this new configuration style, if a sexual simulation involving sex chromosomes is desired, the new initializeChromosome() call should be used to configure the chromosome setup for the simulation.

For backward compatibility, the old style of configuring a sexual simulation is still supported, however.  This implicitly defines a single chromosome, without a call to initializeChromosome().  With this old configuration approach, the chromosomeType parameter to initializeSex() gives the type of chromosome that should be simulated; this should be "A", "X", or "Y", and this chromosomeType value will be used as the symbol ("A", "X", or "Y") for the implicit chromosome.  These legacy chromosome types correspond to the new chromosome types "A", "X", and "-Y" respectively (note that it is not "Y"), when using initializeChromosome().  The implicit chromosome’s id property is always 1.  This old style of chromosome configuration is much less flexible, however, allowing only these three chromosome types, and only allowing a single chromosome to be set up.  This backward compatibility mode may be removed for SLiM in the future, and should be considered deprecated; new models should call initializeChromosome() explicitly instead.

There is no way to disable sex once it has been enabled; if you don’t want to have sex, don’t call this function.  If you require more flexibility with mating types and reproductive strategies than SLiM’s built-in support for sex provides, do not call initializeSex(); instead, track the sex or mating type of individuals yourself in script (with the tag property of Individual, for example), and manage the consequences of that in your script yourself, in terms of which individuals can mate with which, and exactly how the offspring is produced.

The xDominanceCoeff parameter has been deprecated and removed.  In SLiM 5 and later, use the hemizygousDominanceCoeff property of MutationType instead.  If the chromosomeType is "X", the optional xDominanceCoeff parameter can supply the dominance coefficient used when a mutation is present in an XY male, and is thus “heterozygous” (but in a different sense than the heterozygosity of an XX female with one copy of the mutation).

(void)initializeSLiMModelType(string$ modelType)

Configure the type of SLiM model used for the simulation.  At present, one of two model types may be selected.  If modelType is "WF", SLiM will use a Wright-Fisher (WF) model; this is the model type that has always been supported by SLiM, and is the model type used if initializeSLiMModelType() is not called.  If modelType is "nonWF", SLiM will use a non-Wright-Fisher (nonWF) model instead; this is a new model type supported by SLiM 3.0 and above.

If initializeSLiMModelType() is called at all then it must be called before any other initialization function, so that SLiM knows from the outset which features are enabled and which are not.

(void)initializeSLiMOptions([logical$ keepPedigrees = F], [string$ dimensionality = ""], [string$ periodicity = ""], [logical$ doMutationRunExperiments = T], [logical$ preventIncidentalSelfing = F], [logical$ nucleotideBased = F], [logical$ randomizeCallbacks = T], [logical$ checkInfiniteLoops = T])

Configure options for the simulation.  If initializeSLiMOptions() is called at all then it must be called before any other initialization function (except initializeSLiMModelType()), so that SLiM knows from the outset which optional features are enabled and which are not.

If keepPedigrees is T, SLiM will keep pedigree information for every individual in the simulation, tracking the identity of its parents and grandparents.  This allows individuals to assess their degree of pedigree-based relatedness to other individuals (see Individual’s relatedness() and sharedParentCount() methods), as well as allowing a model to find “trios” (two parents and an offspring they generated) using the pedigree properties of Individual.  As a side effect of keepPedigrees being T, the pedigreeID, pedigreeParentIDs, and pedigreeGrandparentIDs properties of Individual will have defined values, as will the haplosomePedigreeID property of Haplosome.  Note that pedigree-based relatedness doesn’t necessarily correspond to genetic relatedness, due to effects such as assortment and recombination.  Beginning in SLiM 3.5, keepPedigrees=T also enables tracking of individual reproductive output, available through the reproductiveOutput property of Individual and the lifetimeReproductiveOutput property of Subpopulation.

If dimensionality is not "", SLiM will enable its optional “continuous space” facility.  Three values for dimensionality are presently supported: "x", "xy", and "xyz", specifying that continuous space should be enabled for one, two, or three dimensions, respectively, using (x), (x, y), and (x, y, z) coordinates respectively.  This has a number of side effects.  First of all, it means that the specified properties of Individual (x, y, and/or z) will be interpreted by SLiM as spatial positions; in particular, SLiMgui will use those properties to display subpopulations spatially.  Second, it allows spatial interactions to be defined, evaluated, and queried using initializeInteractionType() and interaction() callbacks.  And third, it enables the use of any other properties and methods related to continuous space, such as setting the spatial boundaries of subpopulations, which would otherwise raise an error.

If periodicity is not "", SLiM will designate the specified spatial dimensions as being periodic – wrapping around at the edges of the spatial boundaries of that dimension.  This option may only be used if the dimensionality parameter to initializeSLiMOptions() has been used to enable spatiality in the model, and only spatial dimensions that were specified in the dimensionality of the model may be declared to be periodic (but if desired, it is permissible to make just a subset of those dimensions periodic; it is not an all-or-none proposition).  For example, if the specified dimensionality is "xy", the model’s periodicity may be "x", "y", or "xy" (or "", the default, to specify that there are no periodic dimensions).  A one-dimensional periodic model would model a space like the perimeter of a circle.  A two-dimensional model periodic in one of those dimensions would model a space like a cylinder without its end caps; if periodic in both dimensions, the modeled space is a torus.  The shapes of three-dimensional periodic models are harder to visualize, but are essentially higher-dimensional analogues of these concepts.  Periodic boundary conditions are commonly used to model spatial scenarios without “edge effects”, since there are no edges in the periodic spatial dimensions.  The pointPeriodic() method of Subpopulation is typically used in conjunction with this option, to actually implement the periodic boundary condition for the specified dimensions.

The doMutationRunExperiments parameter specifies whether SLiM should attempt to conduct experiments at runtime to determine the optimal number of mutation runs used in the model.  This is a performance optimization.  If doMutationRunExperiments is T (the default), this optimization is enabled for all chromosomes that do not have an explicitly specified mutation run count; this is generally desirable and may significantly improve performance.  If doMutationRunExperiments is F, this optimization is disabled and chromosomes that do not have an explicitly specified mutation run count will simply use a single mutation run.  See the documentation for initializeChromosome() for further discussion.  Note that this parameter used to be [integer$ mutationRuns = 0], specifying the mutation run count directly.  That parameter has been moved to initializeChromosome(), allowing a different mutation run count to be specified for each chromosome in multi-chromosome models.

If preventIncidentalSelfing is T, incidental selfing in hermaphroditic models will be prevented by SLiM.  By default (i.e., if preventIncidentalSelfing is F), SLiM chooses the first and second parents in a biparental mating event independently.  It is therefore possible for the same individual to be chosen as both the first and second parent, resulting in selfing events even when the selfing rate is zero.  In many models this is unimportant, since it happens fairly infrequently and does not have large consequences.  This behavior is SLiM’s default because it is the simplest option, and produces results that most closely align with simple analytical population genetics models.  However, in some models this selfing can be undesirable and problematic.  In particular, models that involve very high variance in fitness or very small effective population sizes may see elevated rates of selfing that substantially influence model results.  If preventIncidentalSelfing is set to T, all such incidental selfing will be prevented (by choosing a new second parent if the first parent was chosen again).  Non-incidental selfing, as requested by the selfing rate, will still be permitted.  Note that if incidental selfing is prevented, SLiM will hang if it is unable to find a different second parent; there must always be at least two individuals in the population with non-zero fitness, and mateChoice() and modifyChild() callbacks must not absolutely prevent those two individuals from producing viable offspring.  Enforcement of the prohibition on incidental selfing will occur after mateChoice() callbacks have been called (and thus the default mating weights provided to mateChoice() callbacks will not exclude the first parent!), but will occur before modifyChild() callbacks are called (so those callbacks may assume that the first and second parents are distinct).

If nucleotideBased is T, the model will be nucleotide-based.  In this case, auto-generated mutations (i.e., mutation types used by genomic element types) must be nucleotide-based, and an ancestral nucleotide sequence must be supplied with initializeAncestralNucleotides().  Non-nucleotide-based mutations may still be used, but may not be referenced by genomic element types.  A mutation rate (or rate map) may not be supplied with initializeMutationRate(); instead, a hotspot map may (optionally) be supplied with initializeHotspotMap().  This choice has many consequences across SLiM. 

If randomizeCallbacks is T (the default), the order in which individuals are processed in callbacks will be randomized to make it easier to avoid order-dependency bugs.  This flag exists because the order of individuals in each subpopulation is non-random; most notably, females always come before males in the individuals vector, but non-random ordering may also occur with respect to things like migrant versus non-migrant status, origin by selfing versus cloning versus biparental mating, and other factors.  When this option is F, individuals in a subpopulation are processed in the order of the individuals vector in each tick cycle stage, which may lead to order-dependency issues if there is an enabled callback whose behavior is not fully independent between calls.  Setting this option to T will cause individuals within each subpopulation to be processed in a randomized order in each tick cycle stage; specifically, this randomizes the order of calls to mutationEffect() callbacks in both WF and nonWF models, and calls to reproduction() and survival() callbacks in nonWF models.  Each subpopulation is still processed separately, in sequential order, so order-dependency issues between subpopulations are still possible if callbacks have effects that are not fully independent.  This feature was added in SLiM 4, breaking backward compatibility; to recover the behavior of previous versions of SLiM, pass F for this option (but then be very careful about order-dependency issues in your script).  The default of T is the safe option, but a small speed penalty is incurred by the randomization of the processing order – for most models the difference will be less than 1%, but in the worst case it may approach 10%.  Models that do not have any order-dependency issue may therefore run somewhat faster if this is set to F.  Note that anywhere that your script uses the individuals property of Subpopulation, the order of individuals returned will be non-random (regardless of the setting of this option); you should use sample() to shuffle the order of the individuals vector if necessary to avoid order-dependency issues in your script.

If checkInfiniteLoops is T (the default), SLiM and Eidos will check for infinite loops in various circumstances, such as while and do–while loops.  This check is conducted only when running in SLiMgui; at the command line, checks for infinite loops are never conducted regardless of the value of this flag.  When checking is enabled, an error will be raised if any loop executes more than 10 million times, preventing SLiMgui’s user interface from freezing.  Normally this is desirable, but if you actually want to execute a loop more than 10 million times, this checking will prove inconvenient.  In that case, you can pass F for checkInfiniteLoops to disable these checks.  There is no way to turn these checks on or off for individual loops; it is a global setting.

This function will likely be extended with further options in the future, added on to the end of the argument list.  Using named arguments with this call is recommended for readability.  Note that turning on optional features may increase the runtime and memory footprint of SLiM.

(void)initializeSpecies([integer$ tickModulo = 1], [integer$ tickPhase = 1], [string$ avatar = ""], [string$ color = ""])

Configure options for the species being initialized.  This initialization function may only be called in multispecies models (i.e., models with explicit species declarations); in single-species models, the default values are assumed and cannot be changed.

The tickModulo and tickPhase parameters determine the activation schedule for the species.  The active property of the species will be set to T (thus activating the species) every tickModulo ticks, beginning in tick tickPhase.  (However, when the species is activated in a given tick, the skipTick() method may still be called in a first() event to deactivate it.)  See the active property of Species for more details.

The avatar parameter, if not "", sets a string value used to represent the species graphically, particularly in SLiMgui but perhaps in other contexts also.  The avatar should generally be a single character – usually an emoji corresponding to the species, such as "🦊" for foxes or "🐭" for mice.  If avatar is the empty string, "", SLiMgui will choose a default avatar.

The color parameter, if not "", sets a string color value used to represent the species in SLiMgui.  Colors may be specified by name, or with hexadecimal RGB values of the form "#RRGGBB" (see the Eidos manual for details).  If color is the empty string, "", SLiMgui will choose a default color.

(void)initializeTreeSeq([logical$ recordMutations = T], [Nif$ simplificationRatio = NULL], [Ni$ simplificationInterval = NULL], [logical$ checkCoalescence = F], [logical$ runCrosschecks = F], [logical$ retainCoalescentOnly = T], [Ns$ timeUnit = NULL])

Configure options for tree sequence recording.  Calling this function turns on tree sequence recording, as a side effect, for later reconstruction of the simulation’s evolutionary dynamics; if you do not want tree sequence recording to be enabled, do not call this function.  Note that tree-sequence recording internally uses SLiM’s “pedigree tracking” feature to uniquely identify individuals and haplosomes; however, if you want to use pedigree tracking in your script you must still enable it yourself with initializeSLiMOptions(keepPedigrees=T).  A separate tree sequence will be recorded for each chromosome in the simulation, as configured with initializeChromosome().

The recordMutations flag controls whether information about individual mutations is recorded or not.  Such recording takes time and memory, and so can be turned off if only the tree sequence itself is needed, but it is turned on by default since mutation recording is generally useful.

The simplificationRatio and simplificationInterval parameters control how often automatic simplification of the recorded tree sequence occurs.  This is a speed–memory tradeoff: more frequent simplification (lower simplificationRatio or smaller simplificationInterval) means the stored tree sequences will use less memory, but at a cost of somewhat longer run times.  Conversely, a larger simplificationRatio or simplificationInterval means that SLiM will wait longer between simplifications.  There are three ways these parameters can be used.  With the first option, with a non-NULL simplificationRatio and a NULL value for simplificationInterval, SLiM will try to find an optimal tick interval for simplification such that the ratio of the memory used by the tree sequence tables, (before:after) simplification, is close to the requested ratio. The default of 10 (used if both simplificationRatio and simplificationInterval are NULL) thus requests that SLiM try to find a tick interval such that the maximum size of the stored tree sequences is ten times the size after simplification. INF may be supplied to indicate that automatic simplification should never occur; 0 may be supplied to indicate that automatic simplification should be performed at the end of every tick.  Alternatively – the second option – simplificationRatio may be NULL and simplificationInterval may be set to the interval, in ticks, between simplifications.  This may provide more reliable performance, but the interval must be chosen carefully to avoid exceeding the available memory.  The simplificationInterval value may be a very large number to specify that simplification should never occur (not INF, though, since it is an integer value), or 1 to simplify every tick.  Finally – the third option – both parameters may be non-NULL, in which case simplificationRatio is used as described above, while simplificationInterval provides the initial interval first used by SLiM (and then subsequently increased or decreased to try to match the requested simplification ratio).  The default initial interval, used when simplificationInterval is NULL, is usually 20; this is chosen to be relatively frequent, and thus unlikely to lead to a memory overflow, but it can result in rather slow spool-up for models where the equilibrium simplification interval, as determined by the simplification ratio, is much longer.  It can therefore be helpful to set a larger initial interval so that the early part of the model run is not excessively bogged down in simplification.

The checkCoalescence parameter controls whether a check for full coalescence is conducted after each simplification.  If a model will call treeSeqCoalesced() to check for coalescence during its execution, checkCoalescence should be set to T.  Since the coalescence checks entail a performance penalty, the default of F is preferable otherwise.  See the documentation for treeSeqCoalesced() for further discussion.

The runCrosschecks parameter controls whether cross-checks between SLiM’s internal data structures and the tree-sequence recording data structures will be conducted.  These two sets of data structures record much the same thing (mutations in haplosomes), but using completely different representations, so such cross-checks can be useful to confirm that the two data structures do indeed represent the same conceptual state.  This slows down the model considerably, however, and would normally be turned on only for debugging purposes, so it is turned off by default.

The retainCoalescentOnly parameter controls how, exactly, simplification of the tree-sequence data is performed in SLiM (both for auto-simplification and for calls to treeSeqSimplify()).  More specifically, this parameter controls the behavior of simplification for individuals and haplosomes that have been “retained” by calling treeSeqRememberIndividuals() with the parameter permanent=F.  The default of retainCoalescentOnly=T helps to keep the number of retained individuals relatively small, which is helpful if your simulation regularly flags many individuals for retaining.  In this case, changing retainCoalescentOnly to F may dramatically increase memory usage and runtime, in a similar way to permanently remembering all the individuals.  See the documentation of treeSeqRememberIndividuals() for further discussion.

The timeUnit parameter controls the time unit stated in the tree sequence when it is saved (which can be accessed through tskit APIs); it has no effect on the running simulation whatsoever.  The default value, NULL, means that a time unit of "ticks" will be used for all model types.  (In SLiM 3.7 / 3.7.1, NULL implied a time unit of "generations" for WF models, but "ticks" for nonWF models; given the new multispecies timescale parameters in SLiM 4, a default of "ticks" makes sense in all cases since now even in WF models one tick might not equal one biological generation.)  It may be helpful to set timeUnit to "generations" explicitly when modeling non-overlapping generations in which one tick equals one generation, to tell tskit that the time unit does in fact represent biological generations; doing so may avoid warnings from tskit or msprime regarding the time unit, in cases such as recapitation where the simulation timescale is important.

3.2.  Nucleotide utilities

(is)codonsToAminoAcids(integer codons, [li$ long = F], [logical$ paste = T])

Returns the amino acid sequence corresponding to the codon sequence in codons.  Codons should be represented with values in [0, 63] where AAA is 0, AAC is 1, AAG is 2, and TTT is 63; see ancestralNucleotides() for discussion of this encoding.  If long is F (the default), the standard single-letter codes for amino acids will be used (where Serine is "S", etc.); if long is T, the standard three-letter codes will be used instead (where Serine is "Ser", etc.).  Beginning in SLiM 3.5, if long is 0, integer codes will be used as follows (and paste will be ignored):

stop (TAA, TAG, TGA) 0
Alanine 1
Arginine 2
Asparagine 3
Aspartic acid (Aspartate) 4
Cysteine 5
Glutamine 6
Glutamic acid (Glutamate) 7
Glycine 8
Histidine 9
Isoleucine 10
Leucine 11
Lysine 12
Methionine 13
Phenylalanine 14
Proline 15
Serine 16
Threonine 17
Tryptophan 18
Tyrosine 19
Valine 20

There does not seem to be a widely used standard for integer coding of amino acids, so SLiM just numbers them alphabetically, making stop codons 0.  If you want a different coding, you can make your own 64-element vector and use it to convert codons to whatever integer codes you need.  Other integer values of long are reserved for future use (to support other codings), and will currently produce an error.

When long is T or F and paste is T (the default), the amino acid sequence returned will be a singleton string, such as "LYATI" (when long is F) or "Leu-Tyr-Ala-Thr-Ile" (when long is T).  When long is T or F and paste is F, the amino acid sequence will instead be returned as a string vector, with one element per amino acid, such as "L" "Y" "A" "T" "I" (when long is F) or "Leu" "Tyr" "Ala" "Thr" "Ile" (when long is T).  Using the paste=T option is considerably faster than using paste() in script.

This function interprets the supplied codon sequence as the sense strand (i.e., the strand that is not transcribed, and which mirrors the mRNA’s sequence).  This uses the standard DNA codon table directly.  For example, if the nucleotide sequence is CAA TTC, that will correspond to a codon vector of 16 61, and will result in the amino acid sequence Gln-Phe ("QF").

(is)codonsToNucleotides(integer codons, [string$ format = "string"])

Returns the nucleotide sequence corresponding to the codon sequence supplied in codons.  Codons should be represented with values in [0, 63] where AAA is 0, AAC is 1, AAG is 2, and TTT is 63; see ancestralNucleotides() for discussion of this encoding.

The format parameter controls the format of the returned sequence.  It may be "string" to obtain the sequence as a singleton string (e.g., "TATACG"), "char" to obtain it as a string vector of single characters (e.g., "T", "A", "T", "A", "C", "G"), or "integer" to obtain it as an integer vector (e.g., 3, 0, 3, 0, 1, 2), using SLiM’s standard code of A=0, C=1, G=2, T=3.

(float)mm16To256(float mutationMatrix16)

Returns a 64×4 mutation matrix that is functionally identical to the supplied 4×4 mutation matrix in mutationMatrix16.  The mutation rate for each of the 64 trinucleotides will depend only upon the central nucleotide of the trinucleotide, and will be taken from the corresponding entry for the same nucleotide in mutationMatrix16.  This function can be used to easily construct a simple trinucleotide-based mutation matrix which can then be modified so that specific trinucleotides sustain a mutation rate that does not depend only upon their central nucleotide.

See the documentation for initializeGenomicElementType() for further discussion of how these 64×4 mutation matrices are interpreted and used.

(float)mmJukesCantor(float$ alpha)

Returns a mutation matrix representing a Jukes–Cantor (1969) model with mutation rate alpha to each possible alternative nucleotide at a site.  This 2×2 matrix is suitable for use with initializeGenomicElementType().  Note that the actual mutation rate produced by this matrix is 3*alpha.

(float)mmKimura(float$ alpha, float$ beta)

Returns a mutation matrix representing a Kimura (1980) model with transition rate alpha and transversion rate beta.  This 2×2 matrix is suitable for use with initializeGenomicElementType().  Note that the actual mutation rate produced by this model is alpha+2*beta.

(integer)nucleotideCounts(is sequence)

A convenience function that returns an integer vector of length four, providing the number of occurrences of A / C / G / T nucleotides, respectively, in the supplied nucleotide sequence.  The parameter sequence may be a singleton string (e.g., "TATA"), a string vector of single characters (e.g., "T", "A", "T", "A"), or an integer vector (e.g., 3, 0, 3, 0), using SLiM’s standard code of A=0, C=1, G=2, T=3.

(float)nucleotideFrequencies(is sequence)

A convenience function that returns a float vector of length four, providing the frequencies of occurrences of A / C / G / T nucleotides, respectively, in the supplied nucleotide sequence.  The parameter sequence may be a singleton string (e.g., "TATA"), a string vector of single characters (e.g., "T", "A", "T", "A"), or an integer vector (e.g., 3, 0, 3, 0), using SLiM’s standard code of A=0, C=1, G=2, T=3.

(integer)nucleotidesToCodons(is sequence)

Returns the codon sequence corresponding to the nucleotide sequence in sequence.  The codon sequence is an integer vector with values from 0 to 63, based upon successive nucleotide triplets in the nucleotide sequence.  The codon value for a given nucleotide triplet XYZ is 16X + 4Y + Z, where X, Y, and Z have the usual values A=0, C=1, G=2, T=3.  For example, the triplet AAA has a codon value of 0, AAC is 1, AAG is 2, AAT is 3, ACA is 4, and on upward to TTT which is 63.  If the nucleotide sequence AACACATTT is passed in, the codon vector 1 4 63 will therefore be returned.  These codon values can be useful in themselves; they can also be passed to codonsToAminoAcids() to translate them into the corresponding amino acid sequence if desired.

The nucleotide sequence in sequence may be supplied in any of three formats: a string vector with single-letter nucleotides (e.g., "T", "A", "T", "A"), a singleton string of nucleotide letters (e.g., "TATA"), or an integer vector of nucleotide values (e.g., 3, 0, 3, 0) using SLiM’s standard code of A=0, C=1, G=2, T=3.  If the choice of format is not driven by other considerations, such as ease of manipulation, then the singleton string format will certainly be the most memory-efficient for long sequences, and will probably also be the fastest.  The nucleotide sequence provided must be a multiple of three in length, so that it translates to an integral number of codons.

(is)randomNucleotides(integer$ length, [Nif basis = NULL], [string$ format = "string"])

Generates a new random nucleotide sequence with length bases.  The four nucleotides ACGT are equally probable if basis is NULL (the default); otherwise, basis may be a 4-element integer or float vector providing relative fractions for A, C, G, and T respectively (these need not sum to 1.0, as they will be normalized).  More complex generative models such as Markov processes are not supported intrinsically in SLiM at this time, but arbitrary generated sequences may always be loaded from files on disk.

The format parameter controls the format of the returned sequence.  It may be "string" to obtain the generated sequence as a singleton string (e.g., "TATA"), "char" to obtain it as a string vector of single characters (e.g., "T", "A", "T", "A"), or "integer" to obtain it as an integer vector (e.g., 3, 0, 3, 0), using SLiM’s standard code of A=0, C=1, G=2, T=3.  For passing directly to initializeAncestralNucleotides(), format "string" (a singleton string) will certainly be the most memory-efficient, and probably also the fastest.  Memory efficiency can be a significant consideration; the nucleotide sequence for a chromosome of length 109 will occupy approximately 1 GB of memory when stored as a singleton string (with one byte per nucleotide), and much more if stored in the other formats.  However, the other formats can be easier to work with in Eidos, and so may be preferable for relatively short chromosomes if you are manipulating the generated sequence.

3.3.  Population genetics utilities

(float$)calcDxy(object<Haplosome> haplosomes1, object<Haplosome> haplosomes2, [No<Mutation> muts = NULL], [Ni$ start = NULL], [Ni$ end = NULL], [logical$ normalize = F])

Calculates the estimated Dxy between two Haplosome vectors for the set of mutations given in muts.  Dxy is the expected number of differences between two sequences, typically drawn from two different subpopulations whose haplosomes are given in haplosomes1 and haplosomes2.  It is therefore a metric of genetic divergence, comparable in some respects to FST; see Cruickshank and Hahn (2014, Molecular Ecology) for a discussion of FST versus Dxy.  This method implements Dxy as defined by Nei (1987) in Molecular Evolutionary Genomics (eq. 10.20), with optimizations for computational efficiency based upon an assumption that that multiallelic loci are rare (this is compatible with the infinite-sites model).

The calculation can be narrowed to apply to only a window – a subrange of the full haplosomes – by passing the interval bounds [start, end] for the desired window.  In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window.  The default behavior, with start and end of NULL, provides the haplosome-wide Dxy.

If normalize is F (the default), the returned float value is simply the expected number of differences, following Nei.  Often, however, it will be desirable to normalize that value by dividing by the length of the sequence considered, yielding the expected number of differences per site, a metric that then does not depend upon the sequence length; passing normalize=T will return that normalized value, and that is probably what most users of this function will want.

The implementation of calcDxy(), viewable with functionSource(), treats every mutation in muts as independent in its calculations (similar to calcPi()); in other words, if mutations are stacked, the Dxy value calculated is by mutation, not by site.  Similarly, if multiple Mutation objects exist in different haplosomes at the same site (whether representing different genetic states, or multiple mutational lineages for the same genetic state), each Mutation object is treated separately for purposes of the calculation, just as if they were at different sites.  One could regard these choices as embodying an infinite-sites interpretation of the segregating mutations.  In most biologically realistic models, such genetic states will be quite rare, and so the impact of these choices will be negligible; however, in some models these distinctions may be important.  See calcPairHeterozygosity() for further discussion.

All haplosomes and mutations must be associated with the same chromosome.  If muts is NULL (the default), all mutations in the population associated with the same chromosome as the given haplosomes will be used.

This function was written by Vitor Sudbrack (currently affiliated with University of Lausanne).

(float$)calcFST(object<Haplosome> haplosomes1, object<Haplosome> haplosomes2, [No<Mutation> muts = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Calculates the FST between two Haplosome vectors – typically, but not necessarily, the haplosomes that constitute two different subpopulations (which we will assume for the purposes of this discussion).  In general, higher FST indicates greater genetic divergence between subpopulations.  The haplosomes may be associated with more than one chromosome, in a multi-chromosome model; if so, haplosomes1 and haplosomes2 must be associated with the same set of chromosomes, defining the focal set of chromosomes for the calculation.

The calculation is done using only the mutations in muts; if muts is NULL, all mutations associated with the focal chromosomes are used.  The muts parameter can be used to calculate the FST only for a particular mutation type (by passing only mutations of that type), for example; it can focus the calculation on particular mutations of interest.  The mutations in muts must always be associated with the focal chromosomes.

If there is a single focal chromosome, the calculation can be narrowed to apply to only a window – a subrange of the focal chromosome – by passing the interval bounds [start, end] for the desired window.  In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window.  The default behavior, with start and end of NULL, provides the chromosome-wide FST, which is often used to assess the overall level of genetic divergence between sister species or allopatric subpopulations.

The code for calcFST() is, roughly, an Eidos implementation of Wright’s definition of FST (but see below for further discussion and clarification):

FST = 1 - HS / HT

where HS is the average heterozygosity in the two subpopulations, and HT is the total heterozygosity when both subpopulations are combined.  In this implementation, the two haplosome vectors are weighted equally, not weighted by their size.  In SLiM 3, the implementation followed Wright’s definition closely, and returned the average of ratios: mean(1.0 - H_s/H_t), in the Eidos code.  In SLiM 4, it returns the ratio of averages instead: 1.0 - mean(H_s)/mean(H_t).  In other words, the FST value reported by SLiM 4 is an average across the specified mutations in the two sets of haplosomes, where H_s and H_t are first averaged across all specified mutations prior to taking the ratio of the two.  This ratio of averages is less biased than the average of ratios, and and is generally considered to be best practice (see, e.g., Bhatia et al., 2013).  This means that the behavior of calcFST() differs between SLiM 3 and SLiM 4.

As can be seen from its equation, the FST is undefined if HT is zero, which occurs if no mutations are present in the haplosomes provided (given the optionally specified window and set of mutations).  In that case, calcFST() will return NAN.  It is up to the caller to detect this with isNAN() and handle it as necessary.

The implementation of calcFST(), viewable with functionSource(), treats every mutation in muts as independent in the heterozygosity calculations; in other words, if mutations are stacked, the heterozygosity calculated is by mutation, not by site.  Similarly, if multiple Mutation objects exist in different haplosomes at the same site (whether representing different genetic states, or multiple mutational lineages for the same genetic state), each Mutation object is treated separately for purposes of the heterozygosity calculation, just as if they were at different sites.  One could regard these choices as embodying an infinite-sites interpretation of the segregating mutations.  In most biologically realistic models, such genetic states will be quite rare, and so the impact of these choices will be negligible; however, in some models these distinctions may be important.

(float$)calcHeterozygosity(object<Haplosome> haplosomes, [No<Mutation> muts = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Calculates the heterozygosity for a vector of haplosomes (containing at least one element), based upon the frequencies of mutations in the haplosomes.  The result is the expected heterozygosity, for the individuals to which the haplosomes belong, assuming that they are under Hardy-Weinberg equilibrium; this can be compared to the observed heterozygosity of an individual, as calculated by calcPairHeterozygosity().  Often haplosomes will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used.  By default, with muts=NULL, the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for muts.

In multi-chromosome models, all of the haplosomes and mutations passed in haplosomes and muts must all be associated with the same single chromosome.  If you wish to calculate heterozygosity across multiple chromosomes, you can simply write a for loop that calculates it for each chromosome and combines the results; but it is not entirely clear how to weight the chromosomes to produce a single number, especially when sex chromosomes and other chromosomes of variable ploidy might be represented in haplosomes, so it is not done automatically by this function.

The calculation can be narrowed to apply to only a window – a subrange of the full chromosome – by passing the interval bounds [start, end] for the desired window.  In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window.  The default behavior, with start and end of NULL, provides the haplosome-wide heterozygosity.

The implementation of calcHeterozygosity(), viewable with functionSource(), treats every mutation as independent in the heterozygosity calculations.  One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations.  In most biologically realistic models, such genetic states will be quite rare, and so the impact of this choice will be negligible; however, in some models this distinction may be important.  See calcPairHeterozygosity() for further discussion.

(float$)calcInbreedingLoad(object<Haplosome> haplosomes, [Nio<MutationType>$ mutType = NULL])

Calculates inbreeding load (the haploid number of lethal equivalents, or B) for a vector of haplosomes (containing at least one element) passed in haplosomes.  The calculation can be limited to a focal mutation type passed in mutType (which may be either an integer representing the ID of the desired mutation type, or a MutationType object specified directly); if mutType is NULL (the default), all of the mutations for the focal species will be considered.  In any case, only deleterious mutations (those with a negative selection coefficient) will be included in the final calculation.

The inbreeding load is a measure of the quantity of recessive deleterious variation that is heterozygous in a population and can contribute to fitness declines under inbreeding.  This function implements the following equation from Morton et al. (1956), which assumes no epistasis and random mating:

B = sum(qs) − sum(q2s) − 2sum(q(1−q)sh)

where q is the frequency of a given deleterious allele, s is the absolute value of the selection coefficient, and h is its dominance coefficient.  Note that the implementation, viewable with functionSource(), sets a maximum |s| of 1.0 (i.e., a lethal allele); |s| can sometimes be greater than 1.0 when s is drawn from a distribution, but in practice an allele with s < -1.0 has the same lethal effect as when s = -1.0.  Also note that this implementation will not work when the model changes the dominance coefficients of mutations using mutationEffect() callbacks, since it relies on the dominanceCoeff property of MutationType. Finally, note that, to estimate the diploid number of lethal equivalents (2B), the result from this function can simply be multiplied by two.

This function was contributed by Chris Kyriazis; thanks, Chris!

(float)calcLD_D(object<Mutation>$ mut1, [No<Mutation> mut2 = NULL], [No<Haplosome> haplosomes = NULL])

Calculates the linkage disequilibrium (LD) coefficient D between a focal mutation mut1 and one or more mutations in mut2, evaluated across a set of haplosomes given by haplosomes.  The result is a float vector that matches the size and order of mut2.  The implementation of this function, viewable with functionSource(), calculates D as defined by Hill and Robertson (1968, p. 226).  The coefficient D is within [−p(1−p), p(1−p)], where p is the frequency of the more common mutation (that is, p = max(f1, f2) where f1 and f2 are the frequencies of the two mutations for which D is being calculated); for the normalized LD metric r2, which is within [0, 1], see calcLD_Rsquared().  Departures of D from zero indicate LD; more specifically, D > 0 indicates that the mutations occur together more often than expected by chance (positive linkage), whereas D < 0 indicates they occur together less often than expected by chance (negative linkage).

All mutations in mut2 must be associated with the same chromosome as mut1; this function does not currently calculate LD between mutations associated with different chromosomes.  If mut2 is NULL (the default), all such mutations in the population (including mut1 itself) will be used.  Similarly, all haplosomes must be associated with the same chromosome as mut1.  If the haplosomes parameter is NULL (the default), all such haplosomes in the population will be used.

This function was written by Vitor Sudbrack (currently affiliated with University of Lausanne).

(float)calcLD_Rsquared(object<Mutation>$ mut1, [No<Mutation> mut2 = NULL], [No<Haplosome> haplosomes = NULL], [logical$ squared = T])

Calculates the linkage disequilibrium (LD) squared correlation coefficient r2 between a focal mutation mut1 and one or more mutations in mut2, evaluated across a set of haplosomes given by haplosomes.  The result is a float vector that matches the size and order of mut2.  The implementation of this function, viewable with functionSource(), calculates r2 as defined by Hill and Robertson (1968, p. 227).  The squared correlation coefficient r2 is a normalized measure of LD within [0, 1] (for the unnormalized LD coefficient D, see calcLD_D()).  When r2 = 0, there is no statistical association between the mutations; they co-occur as expected by chance.  A value of r2 = 1 indicates complete correlation: the mutations either always appear together or never appear together, depending on the sign of the underlying correlation coefficient r.  To obtain the raw (signed) r value instead of r2, you can pass squared=F instead of the default of T.

All mutations in mut2 must be associated with the same chromosome as mut1; this function does not currently calculate LD between mutations associated with different chromosomes.  If mut2 is NULL (the default), all such mutations in the population (including mut1 itself) will be used.  Similarly, all haplosomes must be associated with the same chromosome as mut1.  If the haplosomes parameter is NULL (the default), all such haplosomes in the population will be used.

This function was written by Vitor Sudbrack (currently affiliated with University of Lausanne).

(float$)calcMeanFroh(object<Individual> individuals, [integer$ minimumLength = 1000000], [Niso<Chromosome>$ chromosome = NULL])

Calculates the mean value of the Froh statistic across the individuals passed in individuals.  This statistic is a measure of individual autozygosity, likely resulting from inbreeding, and is calculated based upon “runs of homozygosity”, or ROH, in the genome of an individual.  Broadly speaking, Froh is the proportion of an individual’s genome that is spanned by ROH longer than a given threshold length.  However, it should be noted that there are many different ways of calculating Froh, producing different results.  For example, the threshold length might be a given constant, or might be determined statistically from the characteristics of the population.  Furthermore, some heterozygous sites might be discarded (to compensate for genotyping errors), a minimum SNP density might be required within a sliding window for an ROH to be diagnosed, and so forth – it can get quite complex, as seen in the software PLINK (Purcell et al., 2007) and GARLIC (Szpiech, Blant and Pemberton, 2017).  The method used by calcMeanFroh() is the simplest possible method, assessing ROH for each individual directly from the simulated mutations without filtering or modification, and applying a given constant threshold length.  If a more sophisticated Froh algorithm is desired, one could modify the implementation of calcMeanFroh(), which is viewable with functionSource(), or one could output VCF data from SLiM and analyze it with other tools, perhaps calling out from the running SLiM script with system().

The threshold ROH length used by calcMeanFroh() is supplied by the parameter minimumLength.  It defaults to 1e6, or 1 Mbp, since that is a length commonly used in the literature, but can be adjusted as desired.

The chromosome parameter can be supplied to focus the Froh calculation on a specific chromosome; otherwise, the calculation spans all chromosomes for which the individual is actually diploid (without a null haplosome).  If Froh cannot be calculated for an individual (due to the presence of null haplosomes for every intrinsically diploid chromosome being analyzed), that individual is omitted from the mean Froh calculation; for example, if an X chromosome is the focal chromosome being analyzed, all males will be omitted from the mean Froh calculation.  If all individuals are omitted from the mean Froh calculation for this reason, NAN is returned.

This function was developed with advice from Ryan Chaffee.  Thanks, Ryan!

(float$)calcPairHeterozygosity(object<Haplosome>$ haplosome1, object<Haplosome>$ haplosome2, [Ni$ start = NULL], [Ni$ end = NULL], [logical$ infiniteSites = T])

Calculates the heterozygosity for a pair of haplosomes; these will typically be two homologous haplosomes of the same diploid individual, but any two haplosomes associated with the same chromosome may be supplied.

The calculation can be narrowed to apply to only a window – a subrange of the full chromosome – by passing the interval bounds [start, end] for the desired window.  In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window.  The default behavior, with start and end of NULL, provides the haplosome-wide heterozygosity.

The implementation calcPairHeterozygosity(), viewable with functionSource(), treats every mutation as independent in the heterozygosity calculations by default (i.e., with infiniteSites=T).  If mutations are stacked, the heterozygosity calculated therefore depends upon the number of unshared mutations, not the number of differing sites.  Similarly, if multiple Mutation objects exist in different haplosomes at the same site (whether representing different genetic states, or multiple mutational lineages for the same genetic state), each Mutation object is treated separately for purposes of the heterozygosity calculation, just as if they were at different sites.  One could regard these choices as embodying an infinite-sites interpretation of the segregating mutations.  In most biologically realistic models, such genetic states will be quite rare, and so the impact of this choice will be negligible; however, in some models this distinction may be important.  The behavior of calcPairHeterozygosity() can be switched to calculate based upon the number of differing sites, rather than the number of unshared mutations, by passing infiniteSites=F.

(float$)calcPi(object<Haplosome> haplosomes, [No<Mutation> muts = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Calculates π (nucleotide diversity, a metric of genetic diversity) for a vector of haplosomes (containing at least two elements), based upon the mutations in the haplosomes.  π is computed by calculating the mean number of pairwise differences at each site, summing across all sites, and dividing by the number of sites.  Therefore, it is interpretable as the number of differences per site expected between two randomly chosen sequences.  The mathematical formulation (as an estimator of the population parameter θ) is based on work in Nei and Li (1979), Nei and Tajima (1981), and Tajima (1983; equation A3).  The exact formula used here is common in textbooks (e.g., equations 9.1–9.5 in Li 1997, equation 3.3 in Hahn 2018, or equation 2.2 in Coop 2020).

Often haplosomes will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used.  By default, with muts=NULL, the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for muts.

The calculation can be narrowed to apply to only a window – a subrange of the full chromosome – by passing the interval bounds [start, end] for the desired window.  In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window.  The default behavior, with start and end of NULL, provides the haplosome-wide value of π.

The implementation of calcPi(), viewable with functionSource(), treats every mutation as independent in the heterozygosity calculations.  One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations, as with calcHeterozygosity().  Indeed, finite-sites models of π have been derived (Tajima 1996) though are not used here.  In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important.  See calcPairHeterozygosity() for further discussion.  This function was written by Nick Bailey (currently affiliated with CNRS and the Laboratory of Biometry and Evolutionary Biology at University Lyon 1), with helpful input from Peter Ralph and Chase Nelson.

(numeric)calcSFS([Ni$ binCount = NULL], [No<Haplosome> haplosomes = NULL], [No<Mutation> muts = NULL], [string$ metric = "density"], [logical$ fold = F])

Calculates the site frequency spectrum, or SFS, for the mutations specified by muts, within the haplosomes specified by haplosomes.  The site frequency spectrum or SFS (sometimes called the allele frequency spectrum, although some authors distinguish between the two) is essentially a histogram of the frequencies of the mutations within the haplosomes; the first bin spans the lowest range of frequencies (down to a frequency of 0.0, or a count of 1), whereas the last bin spans the highest range of frequencies (up to a frequency of 1.0, or a count equal to number of haplosomes minus one).  The idea was introduced by Watterson (1975), and will be discussed in any population genetics textbook (e.g., A. Cutter, 2019, pp. 50–52).  This histogram can be returned as a float vector of density values for each bin by specifying "density" for metric (the default), or as an integer vector of count values for each bin by specifying "count".

There are two modes of operation for calcSFS().  If a specific number of bins is passed for binCount, then the frequency range [0.0, 1.0] is subdivided into binCount intervals of equal width, and the mutations are tallied into those bins according to their frequencies within the haplosomes to produce the histogram.  In this mode, there will be exactly binCount elements in the returned vector.  Note that either "density" or "count" can be chosen in this mode; you can return the frequency bin tallies as either densities or counts.

In the other mode of operation, chosen with a binCount value of NULL, the bins instead represent the count of the number of occurrences for each mutation, and range from a count of 1 (the bin for mutations that occur only once in the haplosomes, sometimes called “singletons”) up to a count of N-1 where N is the number of haplosomes.  (Note that mutations occurring in all N haplosomes are not included in the tally, since they would not be empirically observable.)  In this mode, there will be exactly N-1 elements in the returned vector.  Again, either "density" or "count" can be chosen in this mode; you can return the count bin tallies as either densities or counts (it’s a bit confusing, but we’re talking about two different kinds of “counts”, the count of the number of times a mutation occurs in the haplosomes versus the count of the number of mutations that were tallied into a particular count bin).

The haplosomes parameter can be either a vector of Haplosome objects or NULL.  If NULL is passed, calcSFS() will calculate the SFS across the whole species, using all non-null haplosomes present (and thus there must be only a single species in the model, since an SFS cannot be calculated across multiple species).  Otherwise, haplosomes can contain any set of haplosomes desired, such as from the individuals of one subpopulation, several subpopulations, or an entire species.  However, they must all belong to the same species, and null haplosomes will be automatically and silently excluded from the set.

The muts parameter can be either a vector of Mutation objects or NULL.  If NULL is passed, calcSFS() will calculate the SFS across all mutations belonging to the focal species (as determined from the species of the haplosomes).  Otherwise, muts can contain any set of mutations desired, such as mutations belonging to a specific mutation type, mutations within a specific range of positions along the chromosome, or all of the mutations in the focal species.

The binCount and metric parameters have already been discussed.  Finally, the fold parameter, if T, “folds” the calculated SFS, adding the first and last bins, the second and next-to-last bins, etc., until the center is reached.  Folding is common when working with empirical data, where one often doesn’t know the “polarity” – which allele at a site is ancestral and which is derived.  Folding solves this problem, because the polarity then doesn’t matter; the tally for a given mutation ends up in the same bin regardless.  If the number of bins is even, folding can be performed without ambiguity; the final number of bins is exactly half the original number of bins, and each final bin is the sum of two original bins.  If the number of bins is odd, the correct treatment of the central bin is somewhat ambiguous.  In calcFST(), the central bin is added to itself – doubled – and the number of bins is equal to half the original number of bins rounded up.  If you would prefer to exclude the central bin altogether – another population treatment – then when the original number of bins is odd, you can simply discard the final value in the returned vector (and, if you wish to work with densities rather than counts, re-normalize the result to sum to 1.0).

The implementation of calcSFS(), viewable with functionSource(), tallies each mutation separately, even if more than one mutation occurs at the same position (or is even stacked with another mutation).  One could regard this choice as embodying an infinite-sites interpretation of the SFS, perhaps; in any case, it follows SLiM’s behavior in other population-genetics utility functions.  In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important.

This function is compatible with multi-chromosome models, in the following sense.  When binCount is specified with an integer value, mutations are binned according to their frequencies, as described above.  In a multi-chromosome model, the haplosomes and mutations used by calcSFS() may be associated with more than one chromosome, and the frequency assessed for each mutation is its frequency specifically within the haplosomes associated with its chromosome (as you would expect).  Mutations occurring in different chromosomes can therefore be tallied together into the same frequency bins, and combined into a single SFS; this produces a meaningful SFS.  (If you want an SFS for just a single chromosome, then of course you can pass just those haplosomes and mutations to calcSFS().)  When binCount is NULL, on the other hand, mutations are binned according to their counts, as described above.  In a multi-chromosome model, it would not make sense to bin counts together from different chromosomes, since those counts might not be on the same scale – the number of haplosomes associated with the various chromosomes might not be equal.  In this case, calcSFS() will raise an error if haplosomes from more than one chromosome are supplied, or if haplosomes is NULL (since it doesn’t know which chromosome to choose).  If you wish to tally according to counts, with binCount=NULL, you must pass in a vector of haplosomes associated with a single chromosome.  (If you know what you are doing and wish to combine counts across multiple chromosomes, you can simply call calcSFS() once per chromosome, and combine the resulting vectors by adding them together.)

Thanks to Ryan Chaffee and Chase Nelson for helpful input.

(float$)calcTajimasD(object<Haplosome> haplosomes, [No<Mutation> muts = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Calculates Tajima’s D (a test of neutrality based on the allele frequency spectrum) for a vector of haplosomes (containing at least four elements), based upon the mutations in the haplosomes.  The mathematical formulation is given in Tajima 1989 (equation 38) and remains unchanged (e.g., equations 2.30 in Durrett 2008, 8.4 in Hahn 2018, and 4.44 in Coop 2020).  Often haplosomes will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used.  By default, with muts=NULL, the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for muts.

The calculation can be narrowed to apply to only a window – a subrange of the full chromosome – by passing the interval bounds [start, end] for the desired window.  In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window.  The default behavior, with start and end of NULL, provides the haplosome-wide Tajima’s D.

If the genetic diversity contained within the haplosomes is insufficient for the calculation, calcTajimasD() may return NAN.  It is up to the caller to detect this with isNAN() and handle it as necessary.

The implementation of calcTajimasD(), viewable with functionSource(), treats every mutation as independent in the heterozygosity calculations.  One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations, as with calcHeterozygosity().  Indeed, Tajima’s D can be modified with finite-sites models of π and θ (Misawa and Tajima 1997) though these are not used here.  In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important.  See calcPairHeterozygosity() for further discussion.  This function was written by Nick Bailey (currently affiliated with CNRS and the Laboratory of Biometry and Evolutionary Biology at University Lyon 1), with helpful input from Peter Ralph.

(float$)calcVA(object<Individual> individuals, io<MutationType>$ mutType)

Calculates VA, the additive genetic variance, among a vector of individuals (containing at least two elements) passed in individuals, in a particular mutation type mutType that represents quantitative trait loci (QTLs) influencing a quantitative phenotypic trait.  The mutType parameter may be either an integer representing the ID of the desired mutation type, or a MutationType object specified directly.

This function assumes that mutations of type mutType encode their effect size upon the quantitative trait in their selectionCoeff property, as is fairly standard in SLiM.  The implementation of calcVA(), which is viewable with functionSource(), is quite simple; if effect sizes are stored elsewhere (such as with setValue()), a new user-defined function following the pattern of calcVA() can easily be written.

(float$)calcWattersonsTheta(object<Haplosome> haplosomes, [No<Mutation> muts = NULL], [Ni$ start = NULL], [Ni$ end = NULL])

Calculates Watterson’s theta (a metric of genetic diversity comparable to heterozygosity) for a vector of haplosomes (containing at least one element), based upon the mutations in the haplosomes.  Often haplosomes will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used.  By default, with muts=NULL, the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for muts.

The calculation can be narrowed to apply to only a window – a subrange of the full chromosome – by passing the interval bounds [start, end] for the desired window.  In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window.  The default behavior, with start and end of NULL, provides the haplosome-wide Watterson’s theta.

The implementation of calcWattersonsTheta(), viewable with functionSource(), treats every mutation as independent in the heterozygosity calculations.  One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations, as with calcHeterozygosity().  In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important.  See calcPairHeterozygosity() for further discussion.

3.4.  Other utilities

(float)summarizeIndividuals(object<Individual> individuals, integer dim, numeric spatialBounds, string$ operation, [Nlif$ empty = 0.0], [logical$ perUnitArea = F], [Ns$ spatiality = NULL])

Returns a vector, matrix, or array that summarizes spatial patterns of information related to the individuals in individuals.  In essence, those individuals are assigned into bins according to their spatial position, and then a summary value for each bin is calculated based upon the individuals each bin contains.  The individuals might be binned in one dimension (resulting in a vector of summary values), in two dimensions (resulting in a matrix), or in three dimensions (resulting in an array).  Typically the spatiality of the result (the dimensions into which the individuals are binned) will match the dimensionality of the model, as indicated by the default value of NULL for the optional spatiality parameter; for example, a two-dimensional ("xy") model would by default produce a two-dimensional matrix as a summary.  However, a spatiality that is more restrictive than the model dimensionality may be passed; for example, in a two-dimensional ("xy") model a spatiality of "y" could be passed to summarize individuals into a vector, rather than a matrix, assigning them to bins based only upon their y position (i.e., the value of their y property).  Whatever spatiality is chosen, the parameter dim provides the dimensions of the desired result, in the same form that the dim() function does: first the number of rows, then the number of columns, and then the number of planes, as needed (see the Eidos manual for discussion of matrices, arrays, and dim()).  The length of dims must match the requested spatiality; for spatiality "xy", for example, dims might be c(50,100) to request that the returned matrix have 50 rows and 100 columns.  The result vector/matrix/array is in the correct orientation to be directly usable as a spatial map, by passing it to the defineSpatialMap() method of Subpopulation.  For further discussion of dimensionality and spatiality, see initializeInteractionType() and InteractionType.

The spatialBounds parameter defines the spatial boundaries within which the individuals are binned.  Typically this is the spatial bounds of a particular subpopulation, within which the individuals reside; for individuals in p1, for example, you would likely pass p1.spatialBounds for this.  However, this is not required; individuals may come from any or all subpopulations in the model, and spatialBounds may be any bounds of non-zero area (if an individual falls outside of the given spatial bounds, it is excluded, as if it were not in individuals at all).  If you have multiple subpopulations that conceptually reside within the same overall coordinate space, for example, that can be accommodated here.  The bounds are supplied in the dimensionality of the model, in the same form as for Subpopulation; for an "xy" model, for example, they are supplied as a four-element vector of the form c(x0, y0, x1, y1) even if the summary is being produced with spatiality "y".  To produce the result, a grid with dimensions defined by dims is conceptually stretched out across the given spatial bounds, such that the centers of the edge and corner grid squares are aligned with the limits of the spatial bounds.  This matches the way that defineSpatialMap() defines its maps.

The particular summary produced depends upon the parameters operation and empty.  Consider a single grid square represented by a single element in the result.  That grid square contains zero or more of the individuals in individuals.  If it contains zero individuals and empty is not NULL, the empty value is used for the result, regardless of operation, providing specific, separate control over the treatment of empty grid squares.  If empty is NULL, this separate control over the treatment of empty grid squares is declined; empty grid squares will be handled through the standard mechanism described next.  In all other cases for the given grid square – when it contains more than zero individuals, or when empty is NULLoperation is executed as an Eidos lambda, a small snippet of code, supplied as a singleton string, that is executed in a manner similar to a function call.  Within the execution of the operation lambda, a constant named individuals is defined to be the focal individuals being evaluated – all of the individuals within that grid square.  This lambda should evaluate to a singleton logical, integer, or float value, comprising the result value for the grid square; these types will all be coerced to float (T being 1 and F being 0).

Two examples may illustrate the use of empty and operation.  To produce a summary indicating presence/absence, simply use the default of 0.0 for empty, and "1.0; " (or "1;", or "T;") for operation.  This will produce 0.0 for empty grid squares, and 1.0 for those that contain at least one individual.  Note that the use of empty is essential here, because operation doesn’t even check whether individuals are present or not.  To produce a summary with a count of the number of individuals in each grid square, again use the default of 0.0 for empty, but now use an operation of "individuals.size();", counting the number of individuals in each grid square.  In this case, empty could be NULL instead and operation would still produce the correct result; but using empty makes summarizeIndividuals() more efficient since it allows the execution of operation to be skipped for those squares.

Lambdas are not limited in their complexity; they can use if, for, etc., and can call methods and functions.  A typical operation to compute the mean phenotype in a quantitative genetic model that stores phenotype values in tagF, for example, would be "mean(individuals.tagF);", and this is still quite simple compared to what is possible.  However, keep in mind that the lambda will be evaluated for every grid cell (or at least those that are non-empty), so efficiency can be a concern, and you may wish to pre-calculate values shared by all of the lambda calls, making them available to your lambda code using defineGlobal() or defineConstant().

There is one last twist, if perUnitArea is T: values are divided by the area (or length, in 1D, or volume, in 3D) that their corresponding grid cell comprises, so that each value is in units of “per unit area” (or “per unit length”, or “per unit volume”).  The total area of the grid is defined by the spatial bounds, and the area of a given grid cell is defined by the portion of the spatial bounds that is within that cell.  This is not the same for all grid cells; grid cells that fall partially outside spatialBounds (because, remember, the centers of the edge/corner grid cells are aligned with the limits of spatialBounds) will have a smaller area inside the bounds.  For an "xy" spatiality summary, for example, corner cells have only a quarter of their area inside spatialBounds, while edge elements have half of their area inside spatialBounds; for purposes of perUnitArea, then, their respective areas are ¼ and ½ the area of an interior grid cell.  By default, perUnitArea is F, and no scaling is performed.  Whether you want perUnitArea to be F or T depends upon whether the summary you are producing is, conceptually, “per unit area”, such as density (individuals per unit area) or local competition strength (total interaction strength per unit area), or is not, such as “mean individual age”, or “maximum tag value”.  For the previous example of counting individuals with an operation of "individuals.size();", a value of F for perUnitArea (the default) will produce a simple count of individuals in each grid square, whereas with T it would produce the density of individuals in each grid square.

(object<Dictionary>$)treeSeqMetadata(string$ filePath, [logical$ userData = T])

Returns a Dictionary containing top-level metadata from the .trees (tree-sequence) file at filePath.  If userData is T (the default), the top-level metadata under the SLiM/user_metadata key is returned; this is the same metadata that can optionally be supplied to treeSeqOutput() in its metadata parameter, so it makes it easy to recover metadata that you attached to the tree sequence when it was saved.  If userData is F, the entire top-level metadata Dictionary object is returned; this can be useful for examining the values of other keys under the SLiM key, or values inside the top-level dictionary itself that might have been placed there by msprime or other software.

This function can be used to read in parameter values or other saved state (tag property values, for example), in order to resuscitate the complete state of a simulation that was written to a .trees file.  It could be used for more esoteric purposes too, such as to search through .trees files in a directory (with the help of the Eidos function filesAtPath()) to find those files that satisfy some metadata criterion.

================================================ FILE: QtSLiM/help.qrc ================================================ help/SLiMHelpCallbacks.html help/SLiMHelpClasses.html help/SLiMHelpFunctions.html help/EidosHelpFunctions.html help/EidosHelpClasses.html help/EidosHelpOperators.html help/EidosHelpStatements.html help/EidosHelpTypes.html help/TickCycle_nonWF.png help/TickCycle_WF.png help/TickCycle_nonWF_MS.png help/TickCycle_WF_MS.png help/ColorChart.png help/PlotSymbols.png ================================================ FILE: QtSLiM/icons.qrc ================================================ icons/AppIcon128.png icons/AppIcon1024.png icons/AppIcon16.png icons/AppIcon32.png icons/AppIcon48.png icons/AppIcon64.png icons/AppIcon256.png icons/AppIcon512.png icons/DocIcon16.png icons/DocIcon32.png icons/DocIcon48.png icons/DocIcon64.png icons/DocIcon128.png icons/DocIcon256.png icons/DocIcon512.png icons/GenericDocIcon16.png icons/GenericDocIcon32.png icons/bug.png icons/bug_DARK.png ================================================ FILE: QtSLiM/main.cpp ================================================ #include "QtSLiMAppDelegate.h" #include "QtSLiMWindow.h" #include "QtSLiMPreferences.h" #include "QtSLiMExtras.h" #include #include #include #include #include #include #include #include "eidos_globals.h" #include "interaction_type.h" #if SLIM_LEAK_CHECKING static void clean_up_leak_false_positives(void) { // This does a little cleanup that helps Valgrind to understand that some things have not been leaked. // I think perhaps unordered_map keeps values in an unaligned manner that Valgrind doesn't see as pointers. InteractionType::DeleteSparseVectorFreeList(); FreeSymbolTablePool(); if (gEidos_RNG_Initialized) Eidos_FreeRNG(); } // Note that we still get some leaks reported, many of which are likely spurious. That seems to be caused by: // https://stackoverflow.com/a/51553776/2752221 // I'd like to incorporate the fix given there, but I'm not sure where I'm supposed to find ... #endif // Force to light mode on macOS // To avoid having to make this a .mm file on macOS, we use the Obj-C runtime directly // See https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html for some background // This is pretty gross, but this is also why Objective-C is cool! :-> // Note that we also have a custom Info.plist file that forces light mode; we try to // force light mode in two different ways. This function is necessary for cmake builds, // which do not use the custom Info.plist at this time. The custom Info.plist is // necessary because this function, for some reason, does not succeed in forcing light // mode when QtSLiM is built with qmake at the command line. // BCH 1/17/2021: We now try to force light mode only when compiled against a Qt version // earlier than 5.15.2 (since Qt did not support dark mode well prior to that version). // Since the double-click install version is built against 5.15.2, this should mean that // most macOS users get dark mode support. The NSRequiresAquaSystemAppearance key in our // QtSLiM_Info.plist file, which was set to to force light mode, has been // removed. As per the above comment, this may mean that macOS builds against a Qt version // earlier than 5.15.2 will not succeed in forcing light mode. For this reason and others, // we now require Qt 5.15.2 when building on macOS (see QtSLiMAppDelegate.cpp). // BCH 4/13/2022: Disabling this macos_ForceLightMode() function altogether. Building against // Qt versions prior to 5.15.2 on macOS is no longer supported at all, and linking against // libobjc.dylib on macOS 12 has gotten complicated for security reasons (trampolines). #ifdef __APPLE__ #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 2)) #if 0 // Include objc headers #include #include static void macos_ForceLightMode(void) { // First we need to make an NSString with value @"NSAppearanceNameAqua"; 1 is NSASCIIStringEncoding Class nsstring_class = objc_lookUpClass("NSString"); SEL selector_stringWithCString_encoding = sel_registerName("stringWithCString:encoding:"); if ((!nsstring_class) || (!selector_stringWithCString_encoding)) return; //std::cout << "nsstring_class == " << nsstring_class << std::endl; //std::cout << "class_getName(nsstring_class) == " << class_getName(nsstring_class) << std::endl; //std::cout << "selector_stringWithCString_encoding == " << sel_getName(selector_stringWithCString_encoding) << std::endl; id aquaString = ((id (*)(id, SEL, const char *, int))objc_msgSend)((id)nsstring_class, selector_stringWithCString_encoding, "NSAppearanceNameAqua", 1); if (!aquaString) return; //std::cout << "aquaString == " << aquaString << std::endl; //std::cout << std::endl; // Next we need to get the named appearance from NSAppearance Class nsappearance_class = objc_lookUpClass("NSAppearance"); SEL selector_appearanceNamed = sel_registerName("appearanceNamed:"); if ((!nsappearance_class) || (!selector_appearanceNamed)) return; //std::cout << "nsappearance_class == " << nsappearance_class << std::endl; //std::cout << "class_getName(nsappearance_class) == " << class_getName(nsappearance_class) << std::endl; //std::cout << "selector_appearanceNamed == " << sel_getName(selector_appearanceNamed) << std::endl; id aquaAppearance = ((id (*)(id, SEL, id))objc_msgSend)((id)nsappearance_class, selector_appearanceNamed, aquaString); if (!aquaAppearance) return; //std::cout << "aquaAppearance == " << aquaAppearance << std::endl; //std::cout << std::endl; // Then get the shared NSApp object Class nsapp_class = objc_lookUpClass("NSApplication"); SEL selector_sharedApplication = sel_registerName("sharedApplication"); if ((!nsapp_class) || (!selector_sharedApplication)) return; //std::cout << "nsapp_class == " << nsapp_class << std::endl; //std::cout << "class_getName(nsapp_class) == " << class_getName(nsapp_class) << std::endl; //std::cout << "selector_sharedApplication == " << sel_getName(selector_sharedApplication) << std::endl; id sharedApplication = ((id (*)(id, SEL))objc_msgSend)((id)nsapp_class, selector_sharedApplication); if (!sharedApplication) return; //std::cout << "sharedApplication == " << sharedApplication << std::endl; //std::cout << std::endl; // Then call setAppearance: on NSApplication to force light mode SEL selector_setAppearance = sel_registerName("setAppearance:"); if (!selector_setAppearance) return; //std::cout << "selector_setAppearance == " << sel_getName(selector_setAppearance) << std::endl; //std::cout << std::endl; // -[NSApplication setAppearance:] was added in 10.14, so we need to check availability first if (class_respondsToSelector(nsapp_class, selector_setAppearance)) ((void (*)(id, SEL, id))objc_msgSend)(sharedApplication, selector_setAppearance, aquaAppearance); } #endif #endif #endif // This function switches the app to a dark theme, regardless of OS settings. This is not the same as // "dark mode" on macOS, and should probably never be used on macOS; it's for Linux, where getting // Qt-based apps to obey the windowing system's preferred theme can be a battle. // Credit is due to Josip Medved at https://www.medo64.com/2020/08/dark-mode-for-qt-application/ #ifndef __APPLE__ static void linux_ForceDarkMode(void) { QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); // Josip writes "Just start with a good style (i.e. Fusion) and adjust its palette." I think he // sets the style to Fusion because some styles don't adjust to a changed palette well; they // just have their own hard-coded palette. Commenting out this line on macOS, for example, // many UI elements look correctly "dark" but scrollers retain their "light" appearance. So, it's // not ideal to override whatever the default style would be, but it seems necessary to guarantee // good results. I've decided to make this subject to a user pref. if (prefs.forceFusionStylePref()) { qApp->setStyle(QStyleFactory::create("Fusion")); } // These numbers are modified from those provided by Josip, to better match the macOS dark mode // appearance for consistency, so that our icons, syntax highlighting colors, etc., work well if (prefs.forceDarkModePref()) { QPalette newPalette; newPalette.setColor(QPalette::Window, QColor( 49, 50, 51)); newPalette.setColor(QPalette::WindowText, QColor(255, 255, 255)); newPalette.setColor(QPalette::Base, QColor( 29, 30, 31)); newPalette.setColor(QPalette::AlternateBase, QColor( 9, 10, 11)); #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) newPalette.setColor(QPalette::PlaceholderText, QColor(101, 101, 101)); #endif newPalette.setColor(QPalette::Text, QColor(255, 255, 255)); newPalette.setColor(QPalette::Button, QColor( 49, 50, 51)); newPalette.setColor(QPalette::ButtonText, QColor(255, 255, 255)); newPalette.setColor(QPalette::BrightText, QColor(255, 255, 255)); newPalette.setColor(QPalette::Highlight, QColor( 22, 86, 114)); newPalette.setColor(QPalette::HighlightedText, QColor(255, 255, 255)); newPalette.setColor(QPalette::Light, QColor( 75, 75, 75)); newPalette.setColor(QPalette::Midlight, QColor( 60, 60, 60)); //QPalette::Button should fall approximately midway here newPalette.setColor(QPalette::Mid, QColor( 35, 35, 35)); newPalette.setColor(QPalette::Dark, QColor( 25, 25, 25)); newPalette.setColor(QPalette::Shadow, QColor( 0, 0, 0)); newPalette.setColor(QPalette::Disabled, QPalette::Text, QColor(101, 101, 101)); newPalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(101, 101, 101)); newPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(101, 101, 101)); qApp->setPalette(newPalette); } } #endif int main(int argc, char *argv[]) { // Check that the run-time Qt version matches the compile-time Qt version if (strcmp(qVersion(), QT_VERSION_STR) != 0) { std::cout << "Run-time Qt version " << qVersion() << " does not match compile-time Qt version " << QT_VERSION_STR << std::endl; exit(EXIT_FAILURE); } // Check for running under ASAN and log to confirm it is enabled; see SLiM.pro to enable it #if defined(__has_feature) # if __has_feature(address_sanitizer) std::cout << "***** ASAN enabled *****" << std::endl; # endif #endif // Start the application QApplication app(argc, argv); QtSLiMAppDelegate appDelegate(nullptr); // Reset the locale to "C" regardless of user locale; see issue #81 { /*{ qDebug() << "QLocale().name() before:" << QLocale().name(); std::locale loc; std::cout << "std::locale name() before : " << loc.name() << std::endl; char *loc_c = setlocale(LC_ALL, NULL); std::cout << "setlocale() name before :" << loc_c << std::endl; }*/ setlocale(LC_ALL, "C"); // Might just do LC_NUMERIC, but let's avoid surprises... QLocale::setDefault(QLocale("C")); /*{ qDebug() << "QLocale().name() after:" << QLocale().name(); std::locale loc; std::cout << "std::locale name() after : " << loc.name() << std::endl; char *loc_c = setlocale(LC_ALL, nullptr); std::cout << "setlocale() name after :" << loc_c << std::endl; }*/ // Test that the locale is working for us; is the decimal separator a period or a comma? double converted_value = strtod("0.5", nullptr); if (fabs(0.5 - converted_value) > 1e-10) { std::cout << "Locale issue: strtod() is not translating numbers according to the C locale."; exit(EXIT_FAILURE); } } // On macOS, force light mode appearance. BCH 1/17/2021: We now try to force light mode only when // compiled against a Qt version earlier than 5.15.2 (since Qt did not support dark mode well prior // to that version). Since the double-click install version is built against 5.15.2, this should // mean that most macOS users get dark mode support. #ifdef __APPLE__ #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 2)) #warning Building on macOS with a Qt version less than 5.15.2; dark/light mode may not work correctly //macos_ForceLightMode(); #endif #endif // On Linux, force dark mode appearance if the user has chosen that. This is Linux-only because // on macOS we follow the macOS dark mode setting, and Qt largely follows it for us. #ifndef __APPLE__ linux_ForceDarkMode(); #endif // Tell Qt to use high-DPI pixmaps for icons; not needed in Qt6, which is always high-DPI #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif // On macOS, turn off the automatic quit on last window close, for Qt 5.15.2. // Builds against older Qt versions will just quit on the last window close, because // QTBUG-86874 and QTBUG-86875 prevent this from working. #ifdef __APPLE__ #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 2)) app.setQuitOnLastWindowClosed(false); #endif #endif // Parse the command line QCommandLineParser parser; parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); parser.setApplicationDescription(QCoreApplication::applicationName()); parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument("file", "The file(s) to open."); parser.process(app); // Open windows QtSLiMWindow *mainWin = nullptr; const QStringList posArgs = parser.positionalArguments(); for (const QString &file : posArgs) { QtSLiMWindow *newWin = new QtSLiMWindow(file); newWin->tile(mainWin); newWin->show(); mainWin = newWin; } if (!mainWin) { QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); if (prefs.appStartupPref() == 1) { // create a new window mainWin = new QtSLiMWindow(QtSLiMWindow::ModelType::WF, /* includeComments */ true); } else if (prefs.appStartupPref() == 2) { // run an open panel, which will return a window to show, or nullptr mainWin = qtSLiMAppDelegate->open(nullptr); // if no file was opened, create a new window after all if (!mainWin) mainWin = new QtSLiMWindow(QtSLiMWindow::ModelType::WF, /* includeComments */ true); } } if (mainWin) { mainWin->show(); // Ensures the main window is visible and exposed on startup QtSLiMMakeWindowVisibleAndExposed(mainWin); } appDelegate.appDidFinishLaunching(mainWin); // Run the event loop int appReturn = app.exec(); #if SLIM_LEAK_CHECKING clean_up_leak_false_positives(); #endif return appReturn; } ================================================ FILE: QtSLiM/recipes/Recipe 10.1 - Temporally varying selection.txt ================================================ // Keywords: environmental change initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // beneficial initializeGenomicElementType("g1", c(m1,m2), c(0.995,0.005)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 2000:3999 mutationEffect(m2) { return 1.0; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 10.2 - Spatially varying selection.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "e", 0.1); // deleterious in p2 initializeGenomicElementType("g1", c(m1,m2), c(0.99,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); p1.setMigrationRates(p2, 0.1); // weak migration p2 -> p1 p2.setMigrationRates(p1, 0.5); // strong migration p1 -> p2 } mutationEffect(m2, p2) { return 1/effect; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis I.txt ================================================ // Keywords: gene interactions initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // epistatic mut 1 initializeMutationType("m3", 0.5, "f", 0.1); // epistatic mut 2 initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); // epistatic locus 1 initializeGenomicElementType("g3", m3, 1); // epistatic locus 2 initializeGenomicElement(g1, 0, 10000); initializeGenomicElement(g2, 10001, 13000); initializeGenomicElement(g1, 13001, 70000); initializeGenomicElement(g3, 70001, 73000); initializeGenomicElement(g1, 73001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m3) { if (individual.countOfMutationsOfType(m2)) return 0.5; else return effect; } ================================================ FILE: QtSLiM/recipes/Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis II.txt ================================================ // Keywords: gene interactions initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // epistatic mut 1 m2.convertToSubstitution = F; initializeMutationType("m3", 0.5, "f", 0.1); // epistatic mut 2 m3.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); // epistatic locus 1 initializeGenomicElementType("g3", m3, 1); // epistatic locus 2 initializeGenomicElement(g1, 0, 10000); initializeGenomicElement(g2, 10001, 13000); initializeGenomicElement(g1, 13001, 70000); initializeGenomicElement(g3, 70001, 73000); initializeGenomicElement(g1, 73001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m3) { if (individual.countOfMutationsOfType(m2)) return 0.5; else return effect; } ================================================ FILE: QtSLiM/recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection I.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // balanced initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m2) { return 1.5 - sim.mutationFrequencies(p1, mut); } ================================================ FILE: QtSLiM/recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection II.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // positive freq. dep. initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m2) { return 1.0 + sim.mutationFrequencies(p1, mut); } ================================================ FILE: QtSLiM/recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection III.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // positive freq. dep. initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m2) { dominance = asInteger(homozygous) * 0.5 + 0.5; return 1.0 + sim.mutationFrequencies(p1, mut) * dominance; } ================================================ FILE: QtSLiM/recipes/Recipe 10.4.2 - Fitness as a function of population composition, Cultural effects on fitness.txt ================================================ // Keywords: culture, non-genetic inheritance initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // lactase-promoting m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(0.99,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 10000 early() { sim.simulationFinished(); } late() { // Assign a cultural group: milk-drinker == T, non-milk-drinker == F p1.individuals.tagL0 = (runif(1000) < 0.5); } mutationEffect(m2) { if (individual.tagL0) return effect; // beneficial for milk-drinkers else return 1.0; // neutral for non-milk-drinkers } ================================================ FILE: QtSLiM/recipes/Recipe 10.4.3 - Fitness as a function of population composition, Kin selection and the green-beard effect.txt ================================================ // Keywords: kin selection, inclusive fitness, selfish gene, greenbeard effect initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.0); // green-beard m2.convertToSubstitution = F; m2.color = "red"; initializeGenomicElementType("g1", m1, 1); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1 late() { target = sample(p1.haplosomes, 100); target.addNewDrawnMutation(m2, 10000); } 1: late() { p1.individuals.tag = 0; for (rep in 1:50) { individuals = sample(p1.individuals, 2); i0 = individuals[0]; i1 = individuals[1]; i0greenbeards = i0.countOfMutationsOfType(m2); i1greenbeards = i1.countOfMutationsOfType(m2); if (i0greenbeards & i1greenbeards) { alleleSum = i0greenbeards + i1greenbeards; i0.tag = i0.tag - alleleSum; // cost to i0 i1.tag = i1.tag + alleleSum * 2; // benefit to i1 } } } mutationEffect(m2) { return 1.0 + individual.tag / 10; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 10.5 - Changing selection coefficients with setSelectionCoeff().txt ================================================ // Keywords: environmental change, temporal change initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 1.0, "f", 0.1); // balanced initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); for (mut in m2muts, freq in freqs) mut.setSelectionCoeff(0.5 - freq); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 10.6 - Varying the dominance coefficient among mutations I.txt ================================================ // Keywords: dominance coefficients, mutation() initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.05); initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } mutation(m2) { mut.setValue("dom", runif(1)); return T; } mutationEffect(m2) { if (homozygous) return 1.0 + mut.selectionCoeff; else return 1.0 + mut.getValue("dom") * mut.selectionCoeff; } 100000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 10.6 - Varying the dominance coefficient among mutations II.txt ================================================ // Keywords: dominance coefficients, mutation() initialize() { initializeMutationRate(1e-7); for (i in 0:10) initializeMutationType(i, i * 0.1, "n", 0.0, 0.02); initializeGenomicElementType("g1", m0, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } mutation(m0) { s = mut.selectionCoeff; d = asInteger(min(floor(abs(s) * 100.0), 10.0)); mut.setMutationType(d); return T; } 100000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 11.1 - Assortative mating.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.setValue("FST", 0.0); sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); p1.setMigrationRates(p2, 0.1); p2.setMigrationRates(p1, 0.1); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } mutationEffect(m2, p2) { return 0.2; } 2000: early() { // tag all individuals with their m2 mutation count inds = sim.subpopulations.individuals; inds.tag = inds.countOfMutationsOfType(m2); // precalculate the mating weights vectors for (subpop in c(p1,p2)) { has_m2 = (subpop.individuals.tag > 0); subpop.setValue("weights1", ifelse(has_m2, 2.0, 1.0)); subpop.setValue("weights2", ifelse(has_m2, 0.5, 1.0)); } } 2000: mateChoice() { if (individual.tag > 0) return weights * sourceSubpop.getValue("weights1"); else return weights * sourceSubpop.getValue("weights2"); } 10000: late() { FST = calcFST(p1.haplosomes, p2.haplosomes); sim.setValue("FST", sim.getValue("FST") + FST); } 19999 late() { cat("Mean FST at equilibrium: " + (sim.getValue("FST") / 10000)); sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 11.2 - Sequential mate search I.txt ================================================ // Keywords: choosy, choosiness, ornament, mate choice initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", -0.025); // ornamental initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1:10001 early() { if (sim.cycle % 1000 == 1) { fixedMuts = sum(sim.substitutions.mutationType == m2); osize = fixedMuts * 2 + p1.individuals.countOfMutationsOfType(m2); catn(sim.cycle + ": Mean ornament size == " + mean(osize)); } } mateChoice() { fixedMuts = sum(sim.substitutions.mutationType == m2); for (attempt in 1:5) { mate = sample(p1.individuals, 1, T, weights); osize = fixedMuts * 2 + mate.countOfMutationsOfType(m2); if (runif(1) < log(osize + 1) * 0.1 + attempt * 0.1) return mate; } return float(0); } ================================================ FILE: QtSLiM/recipes/Recipe 11.2 - Sequential mate search II.txt ================================================ // Keywords: choosy, choosiness, ornament, mate choice initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", -0.025); // ornamental initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1:10001 early() { fixedMuts = sum(sim.substitutions.mutationType == m2); inds = p1.individuals; osize = fixedMuts * 2 + inds.countOfMutationsOfType(m2); inds.tagF = log(osize + 1) * 0.1; if (sim.cycle % 1000 == 1) catn(sim.cycle + ": Mean ornament size == " + mean(osize)); } mateChoice() { for (attempt in 1:5) { mate = sample(p1.individuals, 1, T, weights); if (runif(1) < mate.tagF + attempt * 0.1) return mate; } return float(0); } ================================================ FILE: QtSLiM/recipes/Recipe 11.3 - Gametophytic self-incompatibility.txt ================================================ // Keywords: modifyChild(), plants, pollen initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.0); // S-locus mutations initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElementType("g2", m2, 1.0); initializeGenomicElement(g1, 0, 20000); initializeGenomicElement(g2, 20001, 21000); initializeGenomicElement(g1, 21001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { cat("m1 mutation count: " + sim.countOfMutationsOfType(m1) + "\n"); cat("m2 mutation count: " + sim.countOfMutationsOfType(m2) + "\n"); } modifyChild(p1) { pollenSMuts = child.haploidGenome2.mutationsOfType(m2); styleSMuts1 = parent1.haploidGenome1.mutationsOfType(m2); styleSMuts2 = parent1.haploidGenome2.mutationsOfType(m2); if (identical(pollenSMuts, styleSMuts1)) if (runif(1) < 0.99) return F; if (identical(pollenSMuts, styleSMuts2)) if (runif(1) < 0.99) return F; return T; } ================================================ FILE: QtSLiM/recipes/Recipe 12.1 - Social learning of cultural traits.txt ================================================ // Keywords: non-genetic inheritance initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // lactase-promoting m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(0.99,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); p1.individuals.tagL0 = (runif(1000) < 0.5); } modifyChild() { parentCulture = mean(c(parent1.tagL0, parent2.tagL0)); childCulture = (runif(1) < 0.1 + 0.8 * parentCulture); child.tagL0 = childCulture; return T; } mutationEffect(m2) { if (individual.tagL0) return effect; // beneficial for milk-drinkers else return 1.0; // neutral for non-milk-drinkers } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 12.2 - Lethal epistasis I.txt ================================================ // Keywords: gene interaction initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.5); // mutation A m2.convertToSubstitution = F; initializeMutationType("m3", 0.5, "f", 0.5); // mutation B m3.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1 late() { sample(p1.haplosomes, 20).addNewDrawnMutation(m2, 10000); // add A sample(p1.haplosomes, 20).addNewDrawnMutation(m3, 20000); // add B } modifyChild() { hasMutA = any(child.haplosomes.countOfMutationsOfType(m2) > 0); hasMutB = any(child.haplosomes.countOfMutationsOfType(m3) > 0); if (hasMutA & hasMutB) return F; return T; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 12.2 - Lethal epistasis II.txt ================================================ // Keywords: gene interaction initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.5); // mutation A m2.convertToSubstitution = F; initializeMutationType("m3", 0.5, "f", 0.5); // mutation B m3.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1 late() { sample(p1.haplosomes, 20).addNewDrawnMutation(m2, 10000); // add A sample(p1.haplosomes, 20).addNewDrawnMutation(m3, 20000); // add B } modifyChild() { mutACount = sum(child.haplosomes.countOfMutationsOfType(m2)); mutBCount = sum(child.haplosomes.countOfMutationsOfType(m3)); if ((mutACount == 2) & (mutBCount == 2)) return F; return T; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 12.3 - Simulating gene drive.txt ================================================ // Keywords: migration, dispersal, CRISPR gene drive initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", -0.1); // MCR complex initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { for (i in 0:5) sim.addSubpop(i, 500); for (i in 1:5) sim.subpopulations[i].setMigrationRates(i-1, 0.001); for (i in 0:4) sim.subpopulations[i].setMigrationRates(i+1, 0.1); } 100 late() { p0.haplosomes[0:49].addNewDrawnMutation(m2, 10000); } 100:10000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = any(sim.substitutions.mutationType == m2); cat(ifelse(fixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } mutationEffect(m2) { return 1.5 - subpop.id * 0.15; } 100:10000 modifyChild() { mut = sim.mutationsOfType(m2); if (size(mut) == 1) { hasMutOnChromosome1 = child.haploidGenome1.containsMutations(mut); hasMutOnChromosome2 = child.haploidGenome2.containsMutations(mut); if (hasMutOnChromosome1 & !hasMutOnChromosome2) child.haploidGenome2.addMutations(mut); else if (hasMutOnChromosome2 & !hasMutOnChromosome1) child.haploidGenome1.addMutations(mut); } return T; } ================================================ FILE: QtSLiM/recipes/Recipe 12.4 - Suppressing hermaphroditic selfing.txt ================================================ // Keywords: selfing // NOTE: This model is now obsolete! Is it now recommended // that you use this call in your initialize() callback // instead of following this recipe: // // initializeSLiMOptions(preventIncidentalSelfing=T); // // This recipe still works, but is much slower than using // that configuration flag. initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } modifyChild() { // prevent hermaphroditic selfing if (parent1 == parent2) return F; return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 12.5 - Tracking separate sexes in script.txt ================================================ // Keywords: separate sexes, sexual model, sex chromosomes, sex ratio, Wolbachia initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.individuals.tagL0 = repEach(c(F,T), 250); // f==F, m==T } modifyChild() { if (parent1.tagL0 == parent2.tagL0) return F; child.tagL0 = (runif(1) <= 0.5); return T; } 1: late() { catn("Sex ratio (M:M+F): " + mean(p1.individuals.tagL0)); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 13.1 - Polygenic selection.txt ================================================ // Keywords: quantitative trait, polygenic selection initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.0); // QTLs m2.convertToSubstitution = F; m2.color = "red"; initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); initializeGenomicElement(g1, 0, 20000); initializeGenomicElement(g2, 20001, 30000); initializeGenomicElement(g1, 30001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } fitnessEffect() { phenotype = individual.countOfMutationsOfType(m2); return 1.5 - (phenotype - 10.0)^2 * 0.005; } 5000 late() { print(sim.mutationFrequencies(NULL, sim.mutationsOfType(m2))); } ================================================ FILE: QtSLiM/recipes/Recipe 13.2 - A simple model of variable QTL effect sizes.txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.5); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); initializeGenomicElement(g1, 0, 20000); initializeGenomicElement(g2, 20001, 30000); initializeGenomicElement(g1, 30001, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.5 - (phenotypes - 10.0)^2 * 0.005; if (sim.cycle % 100 == 0) catn(sim.cycle + ": Mean phenotype == " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: QtSLiM/recipes/Recipe 13.3 - A model of discrete QTL effects across multiple chromosomes.txt ================================================ // Keywords: migration, dispersal, QTL, quantitative trait loci initialize() { // neutral mutations in non-coding regions initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); // mutations representing alleles in QTLs scriptForQTLs = "if (runif(1) < 0.5) -1; else 1;"; initializeMutationType("m2", 0.5, "s", scriptForQTLs); initializeGenomicElementType("g2", m2, 1.0); m2.convertToSubstitution = F; m2.mutationStackPolicy = "l"; // set up our chromosome: 10 QTLs, surrounded by neutral regions defineConstant("C", 10); // number of QTLs / chromosomes defineConstant("W", 1000); // size of neutral buffer on each side for (i in 1:C) { initializeChromosome(i, W*2 + 1); initializeGenomicElement(g1, 0, W-1); initializeGenomicElement(g2, W, W); initializeGenomicElement(g1, W+1, W+1 + W-1); initializeMutationRate(1e-6); initializeRecombinationRate(1e-8); } } 1 late() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); // set up migration; comment these out for zero gene flow p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p1, 0.01); // optional: give m2 mutations to everyone, as standing variation individuals = sim.subpopulations.individuals; for (i in 1:C) { g = sim.subpopulations.individuals.haplosomesForChromosomes(i); isPlus = asLogical(rbinom(size(g), 1, 0.5)); g[isPlus].addNewMutation(m2, 1.0, W); g[!isPlus].addNewMutation(m2, -1.0, W); } } mutationEffect(m2) { return 1.0; } 1: late() { // evaluate and save the additive effects of QTLs for (subpop in c(p1,p2)) { inds = subpop.individuals; phenotype = inds.sumOfMutationsOfType(m2); optimum = (subpop == p1 ? 10.0 else -10.0); inds.fitnessScaling = 1.0 + dnorm(optimum - phenotype, 0.0, 5.0); inds.tagF = phenotype; } } mateChoice() { phenotype = individual.tagF; others = sourceSubpop.individuals.tagF; return weights * dnorm(others, phenotype, 5.0); } c(2,2001) early() { cat("-------------------------------\n"); cat("Output for end of cycle " + (sim.cycle - 1) + ":\n\n"); // Output population fitness values cat("p1 mean fitness = " + mean(p1.cachedFitness(NULL)) + "\n"); cat("p2 mean fitness = " + mean(p2.cachedFitness(NULL)) + "\n"); // Output population additive QTL-based phenotypes cat("p1 mean phenotype = " + mean(p1.individuals.tagF) + "\n"); cat("p2 mean phenotype = " + mean(p2.individuals.tagF) + "\n"); // Output frequencies of +1/-1 alleles at the QTLs muts = sim.mutationsOfType(m2); plus = muts[muts.selectionCoeff == 1.0]; minus = muts[muts.selectionCoeff == -1.0]; cat("\nOverall frequencies:\n\n"); for (i in 1:C) { iPlus = plus[plus.chromosome.id == i]; iMinus = minus[minus.chromosome.id == i]; pf = sum(sim.mutationFrequencies(NULL, iPlus)); mf = sum(sim.mutationFrequencies(NULL, iMinus)); pf1 = sum(sim.mutationFrequencies(p1, iPlus)); mf1 = sum(sim.mutationFrequencies(p1, iMinus)); pf2 = sum(sim.mutationFrequencies(p2, iPlus)); mf2 = sum(sim.mutationFrequencies(p2, iMinus)); cat(" QTL " + i + ": f(+) == " + pf + ", f(-) == " + mf + "\n"); cat(" in p1: f(+) == " + pf1 + ", f(-) == " + mf1 + "\n"); cat(" in p2: f(+) == " + pf2 + ", f(-) == " + mf2 + "\n\n"); } } ================================================ FILE: QtSLiM/recipes/Recipe 13.4 - A quantitative genetics model with heritability.txt ================================================ // Keywords: QTLs, quantitative trait loci, heritability, environmental variance, breeding values, additive genetic variance initialize() { defineConstant("h2", 0.1); // target heritability initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 1000); } 1: late() { // sum the additive effects of QTLs inds = sim.subpopulations.individuals; additive = inds.sumOfMutationsOfType(m2); // model environmental variance, according to the target heritability V_A = sd(additive)^2; V_E = (V_A - h2 * V_A) / h2; // from h2 == V_A / (V_A + V_E) env = rnorm(size(inds), 0.0, sqrt(V_E)); // set fitness effects and remember phenotypes phenotypes = additive + env; inds.fitnessScaling = 1.0 + dnorm(10.0 - phenotypes, 0.0, 5.0); inds.tagF = phenotypes; } mutationEffect(m2) { return 1.0; // QTLs are neutral; fitness effects are handled below } 1:100000 late() { if (sim.cycle == 1) cat("Mean phenotype:\n"); meanPhenotype = mean(p1.individuals.tagF); cat(format("%.2f", meanPhenotype)); // Run until we reach the fitness peak if (abs(meanPhenotype - 10.0) > 0.1) { cat(", "); return; } cat("\n\n-------------------------------\n"); cat("QTLs at cycle " + sim.cycle + ":\n\n"); qtls = sim.mutationsOfType(m2); f = sim.mutationFrequencies(NULL, qtls); s = qtls.selectionCoeff; p = qtls.position; o = qtls.originTick; indices = order(f, F); for (i in indices) cat(" " + p[i] + ": s = " + s[i] + ", f == " + f[i] + ", o == " + o[i] + "\n"); sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 13.5 - A QTL-based model with two quantitative phenotypic traits and pleiotropy.txt ================================================ // Keywords: QTL, quantitative trait loci, pleiotropy, M-matrix, live plotting, mutation() function (void)updatePlot(void) { if (exists("slimgui")) { plot = slimgui.createPlot("Adaptive Walk", xrange=c(-10,30), yrange=c(-30,10), xlab="phenotype 1", ylab="phenotype 2"); plot.points(0, 0, symbol=21, color="red", size=3.0); plot.points(20, -20, symbol=21, color="green", size=3.0); plot.text(0, 4, "start", size=15); plot.text(20, -24, "optimum", size=15); plot.lines(HIST[,0], HIST[,1], "black"); plot.points(HIST[,0], HIST[,1], 16, "black", size=0.5); } } initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.0); // QTLs m2.convertToSubstitution = F; m2.color = "red"; // g1 is a neutral region, g2 is a QTL initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElementType("g2", c(m1,m2), c(1.0, 0.1)); // chromosome of length 100 kb with two QTL regions initializeGenomicElement(g1, 0, 39999); initializeGenomicElement(g2, 40000, 49999); initializeGenomicElement(g1, 50000, 79999); initializeGenomicElement(g2, 80000, 89999); initializeGenomicElement(g1, 90000, 99999); initializeRecombinationRate(1e-8); // QTL-related constants used below defineConstant("QTL_mu", c(0, 0)); defineConstant("QTL_cov", 0.25); defineConstant("QTL_sigma", matrix(c(1,QTL_cov,QTL_cov,1), nrow=2)); defineConstant("QTL_optima", c(20, -20)); catn("\nQTL DFE means: "); print(QTL_mu); catn("\nQTL DFE variance-covariance matrix: "); print(QTL_sigma); } 1 late() { sim.addSubpop("p1", 500); defineGlobal("HIST", matrix(c(0.0, 0.0), nrow=1)); updatePlot(); } mutation(m2) { // draw mutational effects for the new m2 mutation effects = rmvnorm(1, QTL_mu, QTL_sigma); mut.setValue("e0", effects[0]); mut.setValue("e1", effects[1]); // remember all drawn effects, for our final output old_effects = sim.getValue("all_effects"); sim.setValue("all_effects", rbind(old_effects, effects)); return T; } late() { for (ind in sim.subpopulations.individuals) { // construct phenotypes from additive effects of QTL mutations muts = ind.haplosomes.mutationsOfType(m2); phenotype0 = size(muts) ? sum(muts.getValue("e0")) else 0.0; phenotype1 = size(muts) ? sum(muts.getValue("e1")) else 0.0; ind.setValue("phenotype0", phenotype0); ind.setValue("phenotype1", phenotype1); // calculate fitness effects effect0 = 1.0 + dnorm(QTL_optima[0] - phenotype0, 0.0, 20.0) * 10.0; effect1 = 1.0 + dnorm(QTL_optima[1] - phenotype1, 0.0, 20.0) * 10.0; ind.fitnessScaling = effect0 * effect1; } } 1:1000000 late() { // output, run every 1000 cycles if (sim.cycle % 1000 != 0) return; // print final phenotypes versus their optima inds = sim.subpopulations.individuals; p0_mean = mean(inds.getValue("phenotype0")); p1_mean = mean(inds.getValue("phenotype1")); catn(); catn("Cycle: " + sim.cycle); catn("Mean phenotype 0: " + p0_mean + " (" + QTL_optima[0] + ")"); catn("Mean phenotype 1: " + p1_mean + " (" + QTL_optima[1] + ")"); // update our plot defineGlobal("HIST", rbind(HIST, c(p0_mean, p1_mean))); updatePlot(); // keep running until we get within 10% of both optima if ((abs(p0_mean - QTL_optima[0]) > abs(0.1 * QTL_optima[0])) | (abs(p1_mean - QTL_optima[1]) > abs(0.1 * QTL_optima[1]))) return; // we are done with the main adaptive walk; print final output // get the QTL mutations and their frequencies m2muts = sim.mutationsOfType(m2); m2freqs = sim.mutationFrequencies(NULL, m2muts); // sort those vectors by frequency o = order(m2freqs, ascending=F); m2muts = m2muts[o]; m2freqs = m2freqs[o]; // get the effect sizes m2e0 = m2muts.getValue("e0"); m2e1 = m2muts.getValue("e1"); // now output a list of the QTL mutations and their effect sizes catn("\nQTL mutations (f: e0, e1):"); for (i in seqAlong(m2muts)) catn(m2freqs[i] + ": " + m2e0[i] + ", " + m2e1[i]); // output covariances fixed_m2 = m2muts[m2freqs == 1.0]; cov_fixed = cov(fixed_m2.getValue("e0"), fixed_m2.getValue("e1")); effects = sim.getValue("all_effects"); cov_drawn = cov(drop(effects[,0]), drop(effects[,1])); catn("\nCovariance of effects among fixed QTLs: " + cov_fixed); catn("\nCovariance of effects specified by the QTL DFE: " + QTL_cov); catn("\nCovariance of effects across all QTL draws: " + cov_drawn); sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 13.6 - A variety of fitness functions I (stabilizing selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); scale = dnorm(5.0, 5.0, 2.0); inds.fitnessScaling = 1.0 + dnorm(phenotypes, 5.0, 2.0) / scale; if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: QtSLiM/recipes/Recipe 13.6 - A variety of fitness functions II (directional selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + phenotypes * 0.12; if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: QtSLiM/recipes/Recipe 13.6 - A variety of fitness functions III (disruptive selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); scale = dnorm(0.0, 0.0, 2.0); inds.fitnessScaling = 1.5 - dnorm(phenotypes, 0.0, 2.0) / scale; if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: QtSLiM/recipes/Recipe 13.6 - A variety of fitness functions IV (truncation selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 5000); } 10000 late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); cat("Phenotypes: " + mean(phenotypes)); } 10001: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = ifelse(phenotypes < 0.0, 0.0, 1.0); if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 15000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: QtSLiM/recipes/Recipe 13.7 - Negative frequency-dependence on a quantitative trait.txt ================================================ // Keywords: quantitative trait, negative frequency-dependence, squashed stabilizing selection initialize() { defineConstant("COMP", T); // is competition enabled? initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1:5000 late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); stabilizing = dnorm(phenotypes, 5.0, 2.0) / dnorm(5.0, 5.0, 2.0); competition = sapply(phenotypes, "sum(dnorm(phenotypes, applyValue, 0.3));"); competition = 1.0 - (competition / size(inds)) / dnorm(0.0, 0.0, 0.3); inds.fitnessScaling = 1.0 + stabilizing * (COMP ? competition else 1.0); if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); if (exists("slimgui")) { // plot the relative densities x1 = -2; x2 = 12; step = 0.5; centers = seq(from=x1, to=x2 + step*0.01, by=step); breaks = seq(from=x1 - step/2, to=x2 + step*0.51, by=step); intervals = findInterval(phenotypes, breaks, allInside=T); counts = tabulate(intervals, length(centers) - 1); density = counts / max(counts); plot_pheno = slimgui.createPlot("Phenotypic Distribution", xrange=c(-0.5, 10.5), yrange=c(-0.05, 1.05), xlab="Phenotypic trait value", ylab="Relative density", width=500, height=250); plot_pheno.axis(1, at=c(0,5,10)); plot_pheno.abline(v=5.0, color="cornflowerblue", lwd=2); plot_pheno.lines(centers, density, lwd=2); // plot the fitness function pheno_vals = seq(-2, 12, by=0.1); stabilizing = dnorm(pheno_vals, 5.0, 2.0) / dnorm(5.0, 5.0, 2.0); competition = sapply(pheno_vals, "sum(dnorm(phenotypes, applyValue, 0.3));"); competition = 1.0 - (competition / size(inds)) / dnorm(0.0, 0.0, 0.3); fitness = 1.0 + stabilizing * (COMP ? competition else 1.0); plot_fit = slimgui.createPlot("Fitness Function", xrange=c(-0.5, 10.5), yrange=c(0.95, 2.05), xlab="Phenotypic trait value", ylab="Fitness", width=500, height=250); plot_fit.axis(1, at=c(0,5,10)); plot_fit.abline(v=5.0, color="cornflowerblue", lwd=2); plot_fit.lines(pheno_vals, fitness, lwd=2); } } ================================================ FILE: QtSLiM/recipes/Recipe 14.1 - Relatedness, inbreeding, and heterozygosity.txt ================================================ // Keywords: initialize() { initializeSLiMOptions(keepPedigrees = T); initializeMutationRate(1e-5); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 100); } mateChoice() { // Prefer relatives as mates return weights * (individual.relatedness(sourceSubpop.individuals) + 0.01); } 1000 late() { // Print mean heterozygosity across the population heterozygosity = calcHeterozygosity(p1.haplosomes); cat("Mean heterozygosity = " + heterozygosity + "\n"); } ================================================ FILE: QtSLiM/recipes/Recipe 14.10 - Modeling transposable elements.txt ================================================ // Keywords: initialize() { defineConstant("L", 1e6); // chromosome length defineConstant("teInitialCount", 100); // initial number of TEs defineConstant("teJumpP", 0.0001); // TE jump probability defineConstant("teDisableP", 0.00005); // disabling mut probability initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); // transposon mutation type; also neutral, but red initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "#FF0000"; // disabled transposon mutation type; dark red initializeMutationType("m3", 0.5, "f", 0.0); m3.convertToSubstitution = F; m3.color = "#700000"; } 1 late() { sim.addSubpop("p1", 500); sim.tag = 0; // the next unique tag value to use for TEs // create some transposons at random positions haplosomes = sim.subpopulations.haplosomes; positions = rdunif(teInitialCount, 0, L-1); for (teIndex in 0:(teInitialCount-1)) { pos = positions[teIndex]; mut = haplosomes.addNewDrawnMutation(m2, pos); mut.tag = sim.tag; sim.tag = sim.tag + 1; } } modifyChild() { // disable transposons with rate teDisableP for (haplosome in child.haplosomes) { tes = haplosome.mutationsOfType(m2); teCount = tes.size(); mutatedCount = teCount ? rpois(1, teCount * teDisableP) else 0; if (mutatedCount) { mutatedTEs = sample(tes, mutatedCount); for (te in mutatedTEs) { all_disabledTEs = sim.mutationsOfType(m3); disabledTE = all_disabledTEs[all_disabledTEs.tag == te.tag]; if (size(disabledTE)) { // use the existing disabled TE mutation haplosome.removeMutations(te); haplosome.addMutations(disabledTE); next; } // make a new disabled TE mutation with the right tag haplosome.removeMutations(te); disabledTE = haplosome.addNewDrawnMutation(m3, te.position); disabledTE.tag = te.tag; } } } return T; } late() { // make active transposons copy themselves with rate teJumpP for (individual in sim.subpopulations.individuals) { for (haplosome in individual.haplosomes) { tes = haplosome.mutationsOfType(m2); teCount = tes.size(); jumpCount = teCount ? rpois(1, teCount * teJumpP) else 0; if (jumpCount) { jumpTEs = sample(tes, jumpCount); for (te in jumpTEs) { // make a new TE mutation pos = rdunif(1, 0, L-1); jumpTE = haplosome.addNewDrawnMutation(m2, pos); jumpTE.tag = sim.tag; sim.tag = sim.tag + 1; } } } } } 5000 late() { // print information on each TE, including the fraction of it disabled all_tes = sortBy(sim.mutationsOfType(m2), "position"); all_disabledTEs = sortBy(sim.mutationsOfType(m3), "position"); haplosomeCount = size(sim.subpopulations.haplosomes); catn("Active TEs:"); for (te in all_tes) { cat(" TE at " + te.position + ": "); active = sim.mutationCounts(NULL, te); disabledTE = all_disabledTEs[all_disabledTEs.tag == te.tag]; if (size(disabledTE) == 0) { disabled = 0; } else { disabled = sim.mutationCounts(NULL, disabledTE); all_disabledTEs = all_disabledTEs[all_disabledTEs != disabledTE]; } total = active + disabled; cat("frequency " + format("%0.3f", total / haplosomeCount) + ", "); catn(round(active / total * 100) + "% active"); } catn("\nCompletely disabled TEs: "); for (te in all_disabledTEs) { freq = sim.mutationFrequencies(NULL, te); cat(" TE at " + te.position + ": "); catn("frequency " + format("%0.3f", freq)); } } ================================================ FILE: QtSLiM/recipes/Recipe 14.11 - Modeling opposite ends of a chromosome I.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeGenomicElement(g1, 9900000, 9999999); initializeRecombinationRate(1.5e-7); } 1 early() { sim.addSubpop("p1", 500); } 200000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.11 - Modeling opposite ends of a chromosome II.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeGenomicElement(g1, 100000, 199999); rates = c(1.5e-7, 0.473642, 1.5e-7); ends = c(99999, 100000, 199999); initializeRecombinationRate(rates, ends); } 1 early() { sim.addSubpop("p1", 500); } 200000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.12 - Visualizing ancestry and admixture with mutation() callbacks.txt ================================================ // Keywords: mutation types, tracking, true local ancestry, admixture, introgression, mutation() initialize() { initializeMutationRate(1e-8); // neutral and beneficial for p1 initializeMutationType("m1", 0.5, "f", 0.0); m1.color = "yellow"; m1.colorSubstitution = "yellow"; initializeMutationType("m2", 0.5, "f", 0.1); m2.color = "red"; m2.colorSubstitution = "red"; // neutral and beneficial for p2 initializeMutationType("m3", 0.5, "f", 0.0); m3.color = "blue"; m3.colorSubstitution = "blue"; initializeMutationType("m4", 0.5, "f", 0.1); m4.color = "green"; m4.colorSubstitution = "green"; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.0001)); initializeGenomicElement(g1, 0, 9999999); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 1000); sim.addSubpop("p2", 1000); } mutation(m1, p2) { // use m3 instead of m1, in p2 mut.setMutationType(m3); return T; } mutation(m2, p2) { // use m4 instead of m2, in p2 mut.setMutationType(m4); return T; } 10000 early() { sim.chromosome.setMutationRate(0.0); p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p1, 0.01); } 15000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.13 - Modeling biallelic loci with a mutation() callback I.txt ================================================ // Keywords: identity by state, uniquing, unique down, back mutation, two alleles initialize() { // an m2 mutation is "A" initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "red"; // an m3 mutation is "a" initializeMutationType("m3", 0.5, "f", 0.0); m3.convertToSubstitution = F; m3.color = "cornflowerblue"; // enforce mutually exclusive replacement c(m2,m3).mutationStackGroup = 1; c(m2,m3).mutationStackPolicy = 'l'; initializeGenomicElementType("g1", c(m2,m3), c(1.0,1.0)); initializeGenomicElement(g1, 0, 99); initializeMutationRate(1e-4); initializeRecombinationRate(0.5); } 1 early() { sim.addSubpop("p1", 100); // create the canonical mutation objects target = p1.haplosomes[0]; target.addNewDrawnMutation(m2, 0:99); defineConstant("MUT2", target.mutations); target.removeMutations(); target.addNewDrawnMutation(m3, 0:99); defineConstant("MUT3", target.mutations); target.removeMutations(); // start homozygous "aa" at every position p1.haplosomes.addMutations(MUT3); // log results log = community.createLogFile("freq.csv", logInterval=10); log.addTick(); log.addMeanSDColumns("freq", "sim.mutationFrequencies(NULL, MUT2);"); } mutation(m2) { // unique down to the canonical m2 mutation return MUT2[mut.position]; } mutation(m3) { // unique down to the canonical m3 mutation return MUT3[mut.position]; } 50000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 14.13 - Modeling biallelic loci with a mutation() callback II.txt ================================================ // Keywords: identity by state, uniquing, unique down, back mutation, two alleles initialize() { // m2 models a biallelic locus; an m2 mutation is "A", // absence of an m2 mutation is "a"; "aa" is neutral initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "red"; // m3 is used for new mutations; new m3 mutations get // uniqued down to the correct biallelic m2 state initializeMutationType("m3", 0.5, "f", 0.0); m3.convertToSubstitution = F; m3.color = "cornflowerblue"; initializeGenomicElementType("g1", m3, 1.0); initializeGenomicElement(g1, 0, 99); initializeMutationRate(1e-4); initializeRecombinationRate(0.5); } 1 early() { sim.addSubpop("p1", 100); // create the permanent m2 mutation objects we will use target = p1.haplosomes[0]; target.addNewDrawnMutation(m2, 0:99); defineConstant("MUT", target.mutations); // then remove them; start with "aa" for all individuals target.removeMutations(); // log results log = community.createLogFile("freq.csv", logInterval=10); log.addTick(); log.addMeanSDColumns("freq", "sim.mutationFrequencies(NULL, MUT);"); } mutation(m3) { // if we already have an m2 mutation at the site, allow // the new m3 mutation; we will remove the stack below if (haplosome.containsMarkerMutation(m2, mut.position)) return T; // no m2 mutation is present, so unique down return MUT[mut.position]; } late() { // implement back-mutations from A to a m3muts = sim.mutationsOfType(m3); // do we have any m3 mutations segregating? // if so, we have m2/m3 stacked mutations to remove if (m3muts.length() > 0) { haplosomes = sim.subpopulations.haplosomes; counts = haplosomes.countOfMutationsOfType(m3); hasStacked = haplosomes[counts > 0]; for (haplosome in hasStacked) { stacked_m3 = haplosome.mutationsOfType(m3); stackPositions = stacked_m3.position; all_m2 = haplosome.mutationsOfType(m2); s = (match(all_m2.position, stackPositions) >= 0); stacked_m2 = all_m2[s]; haplosome.removeMutations(c(stacked_m3, stacked_m2)); } } } 50000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 14.14 - Modeling biallelic loci in script.txt ================================================ // Keywords: identity by state, uniquing, unique down, back mutation, two alleles // biallelicFrequencies() is used by the LogFile function (float)biallelicFrequencies(void) { g = NULL; for (ind in p1.individuals) g = rbind(g, ind.getValue('G1'), ind.getValue('G2')); f = apply(g, 1, "mean(applyValue);"); return f; } initialize() { defineConstant("MU", 1e-4); defineConstant("L", 100); } 1 early() { sim.addSubpop("p1", 100); // all individuals start in the "wild-type" state // genomic state is kept in two vectors, G1 and G2 p1.individuals.setValue("G1", rep(F, L)); p1.individuals.setValue("G2", rep(F, L)); // log results log = community.createLogFile("freq.csv", logInterval=10); log.addTick(); log.addMeanSDColumns("freq", "biallelicFrequencies();"); } modifyChild() { // inherit biallelic loci from parents with recombination and mutation parentG1 = parent1.getValue("G1"); parentG2 = parent1.getValue("G2"); recombined = ifelse(rbinom(L, 1, 0.5) == 0, parentG1, parentG2); mutated = ifelse(rbinom(L, 1, MU) == 1, !recombined, recombined); child.setValue("G1", mutated); parentG1 = parent2.getValue("G1"); parentG2 = parent2.getValue("G2"); recombined = ifelse(rbinom(L, 1, 0.5) == 0, parentG1, parentG2); mutated = ifelse(rbinom(L, 1, MU) == 1, !recombined, recombined); child.setValue("G2", mutated); return T; } 50000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 14.15 - Using runs of homozygosity (ROH) to track inbreeding.txt ================================================ // Keywords: runs of homozygosity, ROH, F_ROH, inbreeding, autozygosity initialize() { initializeMutationRate(5e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e9-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); sim.addSubpop("p2", 100); sim.addSubpop("p3", 100); p1.setMigrationRates(p2, 0.1); p2.setMigrationRates(p1, 0.001); p1.setMigrationRates(p3, 0.001); p3.setMigrationRates(p1, 0.1); // start our histories empty for (subpop in c(p1,p2,p3)) { subpop.setValue("FROH_hist", float(0)); subpop.setValue("FROH_hist_tick", integer(0)); } } 500 early() { p2.setSubpopulationSize(20); } 700 early() { p2.setSubpopulationSize(100); } 1400 early() { p3.setSubpopulationSize(20); } 1600 early() { p3.setSubpopulationSize(500); } 100:2000 late() { // in SLiMgui, make a plot every ten ticks if (exists("slimgui") & (community.tick % 10 == 0)) { plot = slimgui.createPlot("Froh history", xrange=c(1, community.estimatedLastTick()), yrange=c(0.0,0.5), xlab="Tick", ylab="Mean Froh", width=500, height=300); plot.addLegend("topRight"); for (subpop in c(p1,p2,p3), color in c("red", "cornflowerblue", "chartreuse3")) { mean_FROH = calcMeanFroh(subpop.individuals); FROH_hist = c(subpop.getValue("FROH_hist"), mean_FROH); FROH_hist_tick = c(subpop.getValue("FROH_hist_tick"), community.tick); subpop.setValue("FROH_hist", FROH_hist); subpop.setValue("FROH_hist_tick", FROH_hist_tick); plot.lines(x=FROH_hist_tick, y=FROH_hist, color=color, lwd=2.0); plot.legendLineEntry("p" + subpop.id, color, lwd=2.0); } } } ================================================ FILE: QtSLiM/recipes/Recipe 14.16 - Visualizing linkage disequilibrium.txt ================================================ // Keywords: linkage disequilibrium, LD, custom plotting, recombination, calcLD initialize() { setSeed(2180149919968428688); defineConstant("L", 1e7); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.1).color="red"; // used for MUT1 initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 5000 late() { // create the MUT1 mutation that we will track over time mut1 = sample(p1.haplosomes, 1).addNewDrawnMutation(m2, asInteger(L/2)); defineGlobal("MUT1", mut1); } 5000:10000 late() { allMutsMAF = sim.mutations[sim.mutationFrequencies(p1) >= 0.10]; muts = sortBy(allMutsMAF, "position"); if (size(muts) == 0) return; if (!MUT1.isSegregating) { catn("The focal mutation fixed or was lost."); sim.simulationFinished(); return; } ld = calcLD_D(MUT1, muts) * 10; // scale up to make it more visible r = calcLD_Rsquared(MUT1, muts, squared=F); r2 = calcLD_Rsquared(MUT1, muts); p = muts.position; plot = slimgui.createPlot("LD versus R2", xrange=c(0,L-1), yrange=c(-1,1), xlab="tick", ylab="metric", width=800, height=300); plot.abline(v=MUT1.position, color="red", lwd=2); plot.points(p, ld, symbol=16, color="cornflowerblue", size=0.5, alpha=0.1); plot.points(p, r, symbol=16, color="chartreuse3", size=0.5, alpha=0.1); plot.points(p, r2, symbol=16, color="black", size=0.5, alpha=0.1); f = rep(1/201, 201); // running average filter, 201 mutations wide plot.lines(p, filter(ld, f, outside=T), color="cornflowerblue", lwd=2); plot.lines(p, filter(r, f, outside=T), color="chartreuse3", lwd=2); plot.lines(p, filter(r2, f, outside=T), color="black", lwd=2); plot.addLegend("topRight"); plot.legendPointEntry("R2", symbol=16, color="black", size=0.5); plot.legendPointEntry("R", symbol=16, color="chartreuse3", size=0.5); plot.legendPointEntry("LD*10", symbol=16, color="cornflowerblue", size=0.5); } ================================================ FILE: QtSLiM/recipes/Recipe 14.2 - Mortality-based fitness I.txt ================================================ // Keywords: death, survival initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", -0.005); // deleterious m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } mutationEffect(m2) { // convert fecundity-based selection to survival-based selection if (runif(1) < effect) return 1.0; else return 0.0; } 10000 late() { sim.outputMutations(sim.mutationsOfType(m2)); } ================================================ FILE: QtSLiM/recipes/Recipe 14.2 - Mortality-based fitness II.txt ================================================ // Keywords: death, survival initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { // initially, everybody lives sim.subpopulations.individuals.tagL0 = F; // here be dragons sample(sim.subpopulations.individuals, 100).tagL0 = T; } fitnessEffect() { // individuals tagged for death die here if (individual.tagL0) return 0.0; else return 1.0; } 10000 late() { sim.outputFull(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.2 - Mortality-based fitness III.txt ================================================ // Keywords: fitness, death, survival initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { // here be dragons sample(sim.subpopulations.individuals, 100).fitnessScaling = 0.0; } 10000 late() { sim.outputFull(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.3 - Reading initial simulation state from an MS output file I.txt ================================================ // Keywords: MS format initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 20000 late() { p1.outputMSSample(2000, replace=F, filePath="ms.txt"); } ================================================ FILE: QtSLiM/recipes/Recipe 14.3 - Reading initial simulation state from an MS output file II.txt ================================================ // Keywords: MS format initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 1000); // READ MS FORMAT INITIAL STATE lines = readFile("ms.txt"); index = 0; // skip lines until reaching the // line, then skip that line while (lines[index] != "//") index = index + 1; index = index + 1; if (index + 2 + p1.individualCount * 2 > size(lines)) stop("File is too short; terminating."); // next line should be segsites: segsitesLine = lines[index]; index = index + 1; parts = strsplit(segsitesLine); if (size(parts) != 2) stop("Malformed segsites."); if (parts[0] != "segsites:") stop("Missing segsites."); segsites = asInteger(parts[1]); // and next is positions: positionsLine = lines[index]; index = index + 1; parts = strsplit(positionsLine); if (size(parts) != segsites + 1) stop("Malformed positions."); if (parts[0] != "positions:") stop("Missing positions."); positions = asFloat(parts[1:(size(parts)-1)]); // create all mutations in a haplosome in a dummy subpopulation sim.addSubpop("p2", 1); g = p2.haplosomes[0]; L = sim.chromosomes.lastPosition; intPositions = asInteger(round(positions * L)); muts = g.addNewMutation(m1, 0.0, intPositions); // add the appropriate mutations to each haplosome for (g in p1.haplosomes) { f = asLogical(asInteger(strsplit(lines[index], ""))); index = index + 1; g.addMutations(muts[f]); } // remove the dummy subpopulation p2.setSubpopulationSize(0); // (optional) set the tick and cycle to match the save point community.tick = 20000; sim.cycle = 20000; } 30000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.4 - Modeling chromosomal inversions with a recombination() callback.txt ================================================ // Keywords: recombination suppression, inversion, gamete generation, meiosis initialize() { defineConstant("L", 1000000); defineConstant("INV_LENGTH", 500000); defineConstant("INV_START", asInteger(L/2 - INV_LENGTH/2)); defineConstant("INV_END", INV_START + INV_LENGTH - 1); defineConstant("N", 500); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral sites initializeMutationType("m2", 0.5, "f", 0.0); // start marker initializeMutationType("m3", 0.5, "f", 0.0); // end marker c(m2,m3).convertToSubstitution = T; c(m2,m3).color = "red"; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", N); // give some haplosomes an inversion inverted = sample(p1.haplosomes, 100); inverted.addNewDrawnMutation(m2, INV_START); inverted.addNewDrawnMutation(m3, INV_END); } mutationEffect(m2) { // fitness of the inversion is frequency-dependent f = sim.mutationFrequencies(NULL, mut); return 1.0 - (f - 0.5) * 0.2; } 5000 late() { sim.outputFixedMutations(); // Assess fixation inside vs. outside the inversion pos = sim.substitutions.position; catn(sum((pos >= INV_START) & (pos < INV_END)) + " inside."); catn(sum((pos < INV_START) | (pos >= INV_END)) + " outside."); } recombination() { gm1 = haplosome1.containsMarkerMutation(m2, INV_START); gm2 = haplosome2.containsMarkerMutation(m2, INV_START); if (!(gm1 | gm2)) { // homozygote non-inverted return F; } inInv = (breakpoints > INV_START) & (breakpoints <= INV_END); if (sum(inInv) % 2 == 0) { return F; } if (gm1 & gm2) { // homozygote inverted left = (breakpoints == INV_START); right = (breakpoints == INV_END + 1); breakpoints = sort(c(breakpoints[!(left | right)], c(INV_START, INV_END + 1)[c(sum(left) == 0, sum(right) == 0)])); return T; } else { // heterozygote inverted: resample to get an even number of breakpoints // this is *recursive*: it calls this recombination callback again! breakpoints = sim.chromosomes.drawBreakpoints(individual); } return T; } ================================================ FILE: QtSLiM/recipes/Recipe 14.5 - Estimating model parameters with ABC.txt ================================================ // Keywords: Approximate Bayesian computation, MCMC, parameter estimation initialize() { initializeMutationRate(mu); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 999999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000 late() { cat(sim.mutations.size() + "\n"); } ================================================ FILE: QtSLiM/recipes/Recipe 14.6 - Tracking local ancestry along the chromosome.txt ================================================ // Keywords: migration, dispersal, admixture, ancestry, introgression initialize() { defineConstant("Z", 1e9); // last chromosome position defineConstant("I", 1e6); // interval between markers initializeMutationType("m1", 0.5, "f", 0.0); // p1 marker initializeMutationType("m2", 0.5, "f", 0.0); // p2 marker initializeMutationType("m3", 0.5, "f", 0.0); // p3 marker initializeMutationType("m4", 0.5, "e", 0.5); // beneficial c(m1,m2,m3,m4).color = c("red", "green", "blue", "white"); c(m1,m2,m3).convertToSubstitution = F; initializeGenomicElementType("g1", m4, 1.0); initializeGenomicElement(g1, 0, Z); initializeMutationRate(0); initializeRecombinationRate(1e-9); } 1 late() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); sim.addSubpop("p3", 500); // set up markers in each subpopulation, and add a beneficial mutation positions = seq(from=0, to=Z, by=I); defineConstant("M", size(positions)); catn("Modeling " + M + " ancestry markers."); for (subpop in c(p1,p2,p3), muttype in c(m1,m2,m3), symbol in c("M1","M2","M3")) { haplosomes = subpop.haplosomes; muts = haplosomes.addNewDrawnMutation(muttype, positions); defineConstant(symbol, muts); mut = haplosomes.addNewDrawnMutation(m4, integerDiv(Z, 2)); catn("Beneficial mutation: s == " + mut.selectionCoeff); } // set up circular migration between the subpops p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p3, 0.01); p3.setMigrationRates(p1, 0.01); } :100000 late() { if (exists("slimgui")) { plot = slimgui.createPlot("Local ancestry", c(0,1), c(0,1), xlab="Position", ylab="Ancestry fraction", width=700, height=250); plot.addLegend(labelSize=14, graphicsWidth=20); plot.legendLineEntry("p1 ancestry", "red", lwd=3); plot.legendLineEntry("p2 ancestry", "green", lwd=3); plot.legendLineEntry("p3 ancestry", "blue", lwd=3); plot.abline(v=0.5, color="black", lwd=2); for (col in c(m1,m2,m3).color, symbol in c("M1","M2","M3")) { mutlist = executeLambda(symbol + ";"); freqs = sim.mutationFrequencies(NULL, mutlist); plot.lines(seq(0, 1, length=size(freqs)), freqs, color=col, lwd=3); plot.abline(h=mean(freqs), color=col); } } if (sim.countOfMutationsOfType(m4) == 0) sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.7 - Live plotting with R using system().txt ================================================ // Keywords: live plotting initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); if (fileExists("/usr/bin/Rscript")) defineConstant("RSCRIPT", "/usr/bin/Rscript"); else if (fileExists("/usr/local/bin/Rscript")) defineConstant("RSCRIPT", "/usr/local/bin/Rscript"); else stop("Couldn't find Rscript."); } 1 early() { sim.addSubpop("p1", 5000); sim.setValue("fixed", NULL); defineConstant("pngPath", writeTempFile("plot_", ".png", "")); // If we're running in SLiMgui, open a plot window if (exists("slimgui")) slimgui.openDocument(pngPath); } 1: early() { if (sim.cycle % 10 == 0) { count = sim.substitutions.size(); sim.setValue("fixed", c(sim.getValue("fixed"), count)); } if (sim.cycle % 1000 != 0) return; y = sim.getValue("fixed"); rstr = paste('{', 'x <- (1:' + size(y) + ') * 10', 'y <- c(' + paste(y, sep=", ") + ')', 'png(width=4, height=4, units="in", res=72, file="' + pngPath + '")', 'par(mar=c(4.0, 4.0, 1.5, 1.5))', 'plot(x=x, y=y, xlim=c(0, 50000), ylim=c(0, 500), type="l",', 'xlab="Generation", ylab="Fixed mutations", cex.axis=0.95,', 'cex.lab=1.2, mgp=c(2.5, 0.7, 0), col="red", lwd=2,', 'xaxp=c(0, 50000, 2))', 'box()', 'dev.off()', '}', sep="\n"); scriptPath = writeTempFile("plot_", ".R", rstr); system(RSCRIPT, args=scriptPath); deleteFile(scriptPath); } 50000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.8 - Using mutation rate variation to model varying functional density.txt ================================================ // Keywords: mutation rate map, mutation map initialize() { initializeMutationType("m1", 0.5, "f", -0.01); // deleterious initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // Use the mutation rate map to vary functional density ends = c(20000, 30000, 70000, 90000, 99999); densities = c(1e-9, 2e-8, 1e-9, 5e-8, 1e-9); initializeMutationRate(densities, ends); } 1 early() { sim.addSubpop("p1", 500); } 200000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 14.9 - Modeling microsatellites.txt ================================================ // Keywords: initialize() { defineConstant("L", 1e6); // chromosome length defineConstant("msatCount", 10); // number of microsats defineConstant("msatMu", 0.0001); // mutation rate per microsat defineConstant("msatUnique", T); // T = unique msats, F = lineages initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); // microsatellite mutation type; also neutral, but magenta initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "#900090"; } 1 late() { sim.addSubpop("p1", 500); // create some microsatellites at random positions haplosomes = sim.subpopulations.haplosomes; positions = rdunif(msatCount, 0, L-1); repeats = rpois(msatCount, 20) + 5; for (msatIndex in 0:(msatCount-1)) { pos = positions[msatIndex]; mut = haplosomes.addNewDrawnMutation(m2, pos); mut.tag = repeats[msatIndex]; } // remember the microsat positions for later defineConstant("msatPositions", positions); } modifyChild() { // mutate microsatellites with rate msatMu for (haplosome in child.haplosomes) { mutCount = rpois(1, msatMu * msatCount); if (mutCount) { mutSites = sample(msatPositions, mutCount); msats = haplosome.mutationsOfType(m2); for (mutSite in mutSites) { msat = msats[msats.position == mutSite]; repeats = msat.tag; // modify the number of repeats by adding -1 or +1 repeats = repeats + (rdunif(1, 0, 1) * 2 - 1); if (repeats < 5) next; // if we're uniquing microsats, do so now if (msatUnique) { all_msats = sim.mutationsOfType(m2); msatsAtSite = all_msats[all_msats.position == mutSite]; matchingMut = msatsAtSite[msatsAtSite.tag == repeats]; if (matchingMut.size() == 1) { haplosome.removeMutations(msat); haplosome.addMutations(matchingMut); next; } } // make a new mutation with the new repeat count haplosome.removeMutations(msat); msat = haplosome.addNewDrawnMutation(m2, mutSite); msat.tag = repeats; } } } return T; } 10000 late() { // print frequency information for each microsatellite site all_msats = sim.mutationsOfType(m2); for (pos in sort(msatPositions)) { catn("Microsatellite at " + pos + ":"); msatsAtPos = all_msats[all_msats.position == pos]; for (msat in sortBy(msatsAtPos, "tag")) catn(" variant with " + msat.tag + " repeats: " + sim.mutationFrequencies(NULL, msat)); } } ================================================ FILE: QtSLiM/recipes/Recipe 15.1 - A minimal nonWF model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: QtSLiM/recipes/Recipe 15.10 - Recording a pedigree.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 10); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // delete any existing pedigree log files deleteFile("mating.txt"); deleteFile("death.txt"); } reproduction() { // choose a mate and generate an offspring mate = subpop.sampleIndividuals(1); child = subpop.addCrossed(individual, mate); child.tag = sim.tag; sim.tag = sim.tag + 1; // log the mating line = paste(community.tick, individual.tag, mate.tag, child.tag); writeFile("mating.txt", line, append=T); } 1 early() { sim.addSubpop("p1", 10); // provide initial tags and remember the next tag value p1.individuals.tag = 1:10; sim.tag = 11; } early() { // density-dependence p1.fitnessScaling = K / p1.individualCount; } survival() { if (!surviving) { // log the death line = community.tick + " " + individual.tag; writeFile("death.txt", line, append=T); } return NULL; } 100 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.11 - Dynamic population structure in nonWF models.txt ================================================ // Keywords: split, join, vicariance, founder, founding, merge, assimilation, admixture initialize() { initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // each subpopulation reproduces within itself subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { // start with two subpops that grow to different sizes sim.addSubpop("p1", 10).setValue("K", 500); sim.addSubpop("p2", 10).setValue("K", 600); } early() { // density-dependent regulation for each subpop for (subpop in sim.subpopulations) { K = subpop.getValue("K"); subpop.fitnessScaling = K / subpop.individualCount; } } 5000 late() { } // // Join p1 and p2 to form p3 in tick 1000 // 999 late() { // create a zero-size subpop for the join sim.addSubpop("p3", 0).setValue("K", 750); } 1000 reproduction() { // generate juveniles to seed p3 founderCount = rdunif(1, 10, 20); p1_inds = p1.individuals; p2_inds = p2.individuals; all_inds = c(p1_inds, p2_inds); for (i in seqLen(founderCount)) { // select a first parent with equal probabilities parent1 = sample(all_inds, 1); // select a second parent with a bias toward p2 if (rdunif(1) < 0.2) parent2 = sample(p1_inds, 1); else parent2 = sample(p2_inds, 1); // generate the offspring into p3 p3.addCrossed(parent1, parent2); } // we're done, don't run again this tick self.active = 0; } 1000 early() { // get rid of p1 and p2 now c(p1,p2).fitnessScaling = 0.0; } // // Split p3 to form a new founder subpop p4 in 2000 // 1999 late() { // create a zero-size subpop for the split sim.addSubpop("p4", 0).setValue("K", 100); } 2000 reproduction() { // generate juveniles to seed p4 founderCount = rdunif(1, 10, 20); all_inds = p3.individuals; for (i in seqLen(founderCount)) { // select parent1/parent2 with equal probabilities parent1 = sample(all_inds, 1); parent2 = sample(all_inds, 1); // generate the offspring into p4 p4.addCrossed(parent1, parent2); } // we're done, don't run again this tick self.active = 0; } ================================================ FILE: QtSLiM/recipes/Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model I.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, Wright-Fisher, non-overlapping generations, discrete generations initialize() { initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 0.0, "f", -0.5); initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.05)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { K = sim.getValue("K"); // parents are chosen randomly, irrespective of fitness parents1 = p1.sampleIndividuals(K, replace=T); parents2 = p1.sampleIndividuals(K, replace=T); for (i in seqLen(K)) p1.addCrossed(parents1[i], parents2[i]); self.active = 0; } 1 early() { sim.setValue("K", 500); sim.addSubpop("p1", sim.getValue("K")); } early() { // parents die; offspring survive proportional to fitness inds = sim.subpopulations.individuals; inds[inds.age > 0].fitnessScaling = 0.0; } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model II.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, Wright-Fisher, non-overlapping generations, discrete generations initialize() { initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.0, "f", -0.5); c(m1,m2).convertToSubstitution = T; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.05)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { K = sim.getValue("K"); // parents are chosen proportional to fitness inds = p1.individuals; fitness = p1.cachedFitness(NULL); parents1 = sample(inds, K, replace=T, weights=fitness); parents2 = sample(inds, K, replace=T, weights=fitness); for (i in seqLen(K)) p1.addCrossed(parents1[i], parents2[i]); self.active = 0; } 1 early() { sim.setValue("K", 500); sim.addSubpop("p1", sim.getValue("K")); } survival() { // survival is independent of fitness; parents die, offspring live return (individual.age == 0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.13 - Range expansion in a stepping-stone model I.txt ================================================ // Keywords: range expansion, colonization, population spread, migration initialize() { defineConstant("K", 1000); // carrying capacity per subpop defineConstant("N", 10); // number of subpopulations defineConstant("M", 0.01); // migration probability defineConstant("R", 1.04); // mean reproduction (as first parent) initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // individuals reproduce locally, without dispersal litterSize = rpois(1, R); for (i in seqLen(litterSize)) { // generate each offspring with an independently drawn mate mate = subpop.sampleIndividuals(1, exclude=individual); if (mate.size()) subpop.addCrossed(individual, mate); } } 1 early() { // create an initial population of 100 individuals, the rest empty for (i in seqLen(N)) sim.addSubpop(i, (i == 0) ? 100 else 0); } 1 late() { // set up a log file log = community.createLogFile("sim_log.txt", sep="\t", logInterval=10); log.addCycle(); log.addPopulationSize(); log.addMeanSDColumns("size", "sim.subpopulations.individualCount;"); log.addCustomColumn("pop_migrants", "sum(sim.subpopulations.individuals.migrant);"); log.addMeanSDColumns("migrants", "sapply(sim.subpopulations, 'sum(applyValue.individuals.migrant);');"); } early() { inds = sim.subpopulations.individuals; // non-overlapping generations; kill off the parental generation ages = inds.age; inds[ages > 0].fitnessScaling = 0.0; inds = inds[ages == 0]; // pre-plan migration of individuals to adjacent subpops numMigrants = rbinom(1, inds.size(), M); if (numMigrants) { migrants = sample(inds, numMigrants); currentSubpopID = migrants.subpopulation.id; displacement = -1 + rbinom(migrants.size(), 1, 0.5) * 2; // -1 or +1 newSubpopID = currentSubpopID + displacement; actuallyMoving = (newSubpopID >= 0) & (newSubpopID < N); if (sum(actuallyMoving)) { migrants = migrants[actuallyMoving]; newSubpopID = newSubpopID[actuallyMoving]; // do the pre-planned moves into each subpop in bulk for (subpop in sim.subpopulations) subpop.takeMigrants(migrants[newSubpopID == subpop.id]); } } // post-migration density-dependent fitness for each subpop for (subpop in sim.subpopulations) { juvenileCount = sum(subpop.individuals.age == 0); subpop.fitnessScaling = K / juvenileCount; } } 1001 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.13 - Range expansion in a stepping-stone model II.txt ================================================ // Keywords: range expansion, colonization, population spread, migration initialize() { defineConstant("K", 1000); // carrying capacity per subpop defineConstant("N", 10); // number of subpopulations defineConstant("M", 0.01); // migration probability defineConstant("R", 1.04); // mean reproduction (as first parent) initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // individuals reproduce locally, without dispersal litterSize = rpois(1, R); for (i in seqLen(litterSize)) { // generate each offspring with an independently drawn mate mate = subpop.sampleIndividuals(1, exclude=individual); if (mate.size()) subpop.addCrossed(individual, mate); } } 1 early() { // create an initial population of 100 individuals, the rest empty for (i in seqLen(N)) sim.addSubpop(i, (i == 0) ? 100 else 0); } 1 late() { // set up a log file log = community.createLogFile("sim_log.txt", sep="\t", logInterval=10); log.addCycle(); log.addPopulationSize(); log.addMeanSDColumns("size", "sim.subpopulations.individualCount;"); log.addCustomColumn("pop_migrants", "sum(sim.subpopulations.individuals.migrant);"); log.addMeanSDColumns("migrants", "sapply(sim.subpopulations, 'sum(applyValue.individuals.migrant);');"); } early() { // non-overlapping generations; kill off the parental generation inds = sim.subpopulations.individuals; sim.killIndividuals(inds[inds.age > 0]); // pre-migration density-dependent fitness for each subpop for (subpop in sim.subpopulations) subpop.fitnessScaling = K / subpop.individualCount; } survival() { // honor SLiM's survival decision if (!surviving) return NULL; // migrate with probability M if (runif(1) >= M) return NULL; // migrate the focal individual to an adjacent subpop subpops = sim.subpopulations; newSubpopID = subpop.id + (-1 + rbinom(1, 1, 0.5) * 2); // -1 or +1 newSubpop = subpops[subpops.id == newSubpopID]; if (newSubpop.size()) return newSubpop; return NULL; } 1001 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.14 - Logistic population growth with the Beverton-Holt model.txt ================================================ // Keywords: logistic growth, logistic population model, carrying capacity, density dependence initialize() { defineConstant("K", 50000); defineConstant("R", 1.1); defineConstant("M", K / (R - 1)); initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 50); // the "simple model" sim.addSubpop("p2", 50); // Beverton-Holt influencing fecundity sim.addSubpop("p3", 50); // Beverton-Holt influencing survival log = community.createLogFile("sim_log.txt", logInterval=1); log.addCycle(); log.addSubpopulationSize(p1); log.addSubpopulationSize(p2); log.addSubpopulationSize(p3); } reproduction(p1) { // p1 simply reproduces with a mean litter size of R, like p3 litterSize = rpois(1, R); for (i in seqLen(litterSize)) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } reproduction(p2) { // p2 reproduces up to the Beverton-Holt equation's target n_t = subpop.individualCount; n_t_plus_1 = (R * n_t) / (1 + n_t / M); mean_litter_size = n_t_plus_1 / n_t; litterSize = rpois(1, mean_litter_size); for (i in seqLen(litterSize)) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } reproduction(p3) { // p3 simply reproduces with a mean litter size of R, like p1 litterSize = rpois(1, R); for (i in seqLen(litterSize)) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } early() { // p1 uses the "simple model" with non-overlapping generations inds = p1.individuals; inds[inds.age > 0].fitnessScaling = 0.0; n_t_plus_pt5 = sum(inds.age == 0); p1.fitnessScaling = K / n_t_plus_pt5; // p2 has selection only to achieve non-overlapping generations inds = p2.individuals; inds[inds.age > 0].fitnessScaling = 0.0; // p3 uses the Beverton-Holt equation for survival inds = p3.individuals; inds[inds.age > 0].fitnessScaling = 0.0; n_t_plus_pt5 = sum(inds.age == 0); p3.fitnessScaling = 1 / (1 + (n_t_plus_pt5 / R) / M); } 200 late() { // log out the final row before plotting log = community.logFiles; log.logRow(); log.setLogInterval(NULL); // make a final plot if (exists("slimgui")) { cycle_data = slimgui.logFileData(log, "cycle"); p1_data = slimgui.logFileData(log, "p1_num_individuals"); p2_data = slimgui.logFileData(log, "p2_num_individuals"); p3_data = slimgui.logFileData(log, "p3_num_individuals"); plot = slimgui.createPlot("Population Growth", xlab="Generation", ylab="Population size", width=500, height=250); plot.abline(h=50000, color="#999999", lwd=1.0); plot.lines(cycle_data, p1_data, "cornflowerblue", lwd=2); plot.lines(cycle_data, p2_data, "red", lwd=2); plot.lines(cycle_data, p3_data, "chartreuse3", lwd=2); plot.addLegend("topLeft", inset=0, labelSize=13); plot.legendLineEntry("p1", "cornflowerblue", lwd=2); plot.legendLineEntry("p2", "red", lwd=2); plot.legendLineEntry("p3", "chartreuse3", lwd=2); plot.write("Population Growth.pdf"); } } ================================================ FILE: QtSLiM/recipes/Recipe 15.2 - Age structure (a life table model).txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 30); defineConstant("L", c(0.7, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0)); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { if (individual.age > 2) { mate = subpop.sampleIndividuals(1, minAge=3); subpop.addCrossed(individual, mate); } } 1 early() { sim.addSubpop("p1", 10); p1.individuals.age = rdunif(10, min=0, max=7); } early() { // life table based individual mortality inds = p1.individuals; ages = inds.age; mortality = L[ages]; survival = 1 - mortality; inds.fitnessScaling = survival; // density-dependence, factoring in individual mortality p1.fitnessScaling = K / (p1.individualCount * mean(survival)); } late() { // print our age distribution after mortality catn(sim.cycle + ": " + paste(sort(p1.individuals.age))); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.3 - Handling all reproduction at once with big bang reproduction.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { for (s in sim.subpopulations) { for (ind in s.individuals) { s.addCrossed(ind, s.sampleIndividuals(1)); } } self.active = 0; } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: QtSLiM/recipes/Recipe 15.4 - Monogamous mating and variation in litter size.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // randomize the order of p1.individuals parents = sample(p1.individuals, p1.individualCount); // draw monogamous pairs and generate litters for (i in seq(0, p1.individualCount - 2, by=2)) { parent1 = parents[i]; parent2 = parents[i + 1]; p1.addCrossed(parent1, parent2, count=rpois(1, 2.7)); } // disable this callback for this cycle self.active = 0; } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: QtSLiM/recipes/Recipe 15.5 - Beneficial mutations and absolute fitness.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 1.0, "f", 0.5); // dominant beneficial initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { for (i in 1:5) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); } 100 early() { mutant = sample(p1.individuals.haplosomes, 10); mutant.addNewDrawnMutation(m2, 10000); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: QtSLiM/recipes/Recipe 15.6 - A metapopulation extinction-colonization model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, migration, dispersal initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 50); // carrying capacity per subpop defineConstant("N", 10); // number of subpopulations defineConstant("m", 0.01); // migration rate defineConstant("e", 0.1); // subpopulation extinction rate initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { for (i in 1:N) sim.addSubpop(i, (i == 1) ? 10 else 0); } early() { // random migration nIndividuals = sum(sim.subpopulations.individualCount); nMigrants = rpois(1, nIndividuals * m); migrants = sample(sim.subpopulations.individuals, nMigrants); for (migrant in migrants) { do dest = sample(sim.subpopulations, 1); while (dest == migrant.subpopulation); dest.takeMigrants(migrant); } // density-dependence and random extinctions for (subpop in sim.subpopulations) { if (runif(1) < e) sim.killIndividuals(subpop.individuals); else subpop.fitnessScaling = K / subpop.individualCount; } } late() { if (sum(sim.subpopulations.individualCount) == 0) stop("Global extinction in cycle " + sim.cycle + "."); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.7 - Habitat choice.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, migration, dispersal initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 0.5, "e", 0.1); // deleterious in p2 m2.color = "red"; initializeMutationType("m3", 0.5, "e", 0.1); // deleterious in p1 m3.color = "green"; initializeGenomicElementType("g1", c(m1,m2,m3), c(0.98,0.01,0.01)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { dest = sample(sim.subpopulations, 1); dest.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); sim.addSubpop("p2", 10); } early() { // habitat choice inds = sim.subpopulations.individuals; inds_m2 = inds.countOfMutationsOfType(m2); inds_m3 = inds.countOfMutationsOfType(m3); pref_p1 = 0.5 + (inds_m2 - inds_m3) * 0.1; pref_p1 = pmax(pmin(pref_p1, 1.0), 0.0); inertia = ifelse(inds.subpopulation.id == 1, 1.0, 0.0); pref_p1 = pref_p1 * 0.75 + inertia * 0.25; choice = ifelse(runif(inds.size()) < pref_p1, 1, 2); moving = inds[choice != inds.subpopulation.id]; from_p1 = moving[moving.subpopulation == p1]; from_p2 = moving[moving.subpopulation == p2]; p2.takeMigrants(from_p1); p1.takeMigrants(from_p2); } early() { p1.fitnessScaling = K / p1.individualCount; p2.fitnessScaling = K / p2.individualCount; } mutationEffect(m2, p2) { return 1/effect; } mutationEffect(m3, p1) { return 1/effect; } 1000 late() { for (id in 1:2) { subpop = sim.subpopulations[sim.subpopulations.id == id]; s = subpop.individualCount; inds = subpop.individuals; c2 = sum(inds.countOfMutationsOfType(m2)); c3 = sum(inds.countOfMutationsOfType(m3)); catn("subpop " + id + " (" + s + "): " + c2 + " m2, " + c3 + " m3"); } } ================================================ FILE: QtSLiM/recipes/Recipe 15.8 - Evolutionary rescue after environmental change.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, QTL, quantitative trait loci initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); defineConstant("opt1", 0.0); defineConstant("opt2", 10.0); defineConstant("Tdelta", 10000); initializeMutationType("m1", 0.5, "n", 0.0, 1.0); // QTL initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 500); } early() { // QTL-based fitness inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m1); optimum = (sim.cycle < Tdelta) ? opt1 else opt2; deviations = optimum - phenotypes; fitnessFunctionMax = dnorm(0.0, 0.0, 5.0); adaptation = dnorm(deviations, 0.0, 5.0) / fitnessFunctionMax; inds.fitnessScaling = 0.1 + adaptation * 0.9; inds.tagF = phenotypes; // just for output below // density-dependence with a maximum benefit at low density p1.fitnessScaling = min(K / p1.individualCount, 1.5); } mutationEffect(m1) { return 1.0; } late() { if (p1.individualCount == 0) { // stop at extinction catn("Extinction in cycle " + sim.cycle + "."); sim.simulationFinished(); } else { // output the phenotypic mean and pop size phenotypes = p1.individuals.tagF; cat(sim.cycle + ": " + p1.individualCount + " individuals"); cat(", phenotype mean " + mean(phenotypes)); if (size(phenotypes) > 1) cat(" (sd " + sd(phenotypes) + ")"); catn(); } } 20000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 15.9 - Litter size and parental investment.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, sexual, QTL, quantitative trait loci, reproduction() initialize() { initializeSLiMModelType("nonWF"); initializeSex(); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 0.5, "n", 0.0, 0.3); // QTL initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.1)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction(NULL, "F") { mate = subpop.sampleIndividuals(1, sex="M"); if (mate.size()) { qtlValue = individual.tagF; expectedLitterSize = max(0.0, qtlValue + 3); litterSize = rpois(1, expectedLitterSize); penalty = 3.0 / litterSize; for (i in seqLen(litterSize)) { offspring = subpop.addCrossed(individual, mate); offspring.setValue("penalty", rgamma(1, penalty, 20)); } } } 1 early() { sim.addSubpop("p1", 500); p1.individuals.setValue("penalty", 1.0); } early() { // non-overlapping generations inds = sim.subpopulations.individuals; inds[inds.age > 0].fitnessScaling = 0.0; inds = inds[inds.age == 0]; // focus on juveniles // QTL calculations inds.tagF = inds.sumOfMutationsOfType(m2); // parental investment fitness penalties inds.fitnessScaling = inds.getValue("penalty"); // density-dependence for juveniles p1.fitnessScaling = K / size(inds); } mutationEffect(m2) { return 1.0; } late() { // output the phenotypic mean and pop size qtlValues = p1.individuals.tagF; expectedSizes = pmax(0.0, qtlValues + 3); cat(sim.cycle + ": " + p1.individualCount + " individuals"); cat(", mean litter size " + mean(expectedSizes)); catn(); } 20000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 16.1 - Pollen flow.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 200); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // determine how many ovules were fertilized, out of the total fertilizedOvules = rbinom(1, 30, 0.5); // determine the pollen source for each fertilized ovule other = (subpop == p1) ? p2 else p1; pollenSources = ifelse(runif(fertilizedOvules) < 0.99, subpop, other); // generate seeds from each fertilized ovule // the ovule belongs to individual, the pollen comes from source for (source in pollenSources) subpop.addCrossed(individual, source.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); sim.addSubpop("p2", 10); } early() { for (subpop in sim.subpopulations) subpop.fitnessScaling = K / subpop.individualCount; } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 16.10 - Modeling pseudo-autosomal regions (PARs) with addMultiRecombinant().txt ================================================ // Keywords: multiple chromosomes, pseudo-autosomal region (PAR) initialize() { defineConstant("A1_LEN", 2e7); defineConstant("PAR1_LEN", 2771479); defineConstant("PAR2_LEN", 329513); defineConstant("X_LEN", 156040895 - (PAR1_LEN + PAR2_LEN)); defineConstant("Y_LEN", 57227415 - (PAR1_LEN + PAR2_LEN)); defineConstant("MU", 1e-8); defineConstant("R", 1e-7); defineConstant("N", 500); defineConstant("REC", N*10); // start recording at this tick defineConstant("RUNTIME", N*50); // finish at this tick initializeSLiMModelType("nonWF"); initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0).convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); for (id in 1:5, length in c(A1_LEN, PAR1_LEN, X_LEN, Y_LEN, PAR2_LEN), type in c("A","A","X","Y","A"), symbol in c("A1","P1","X","Y","P2")) { chr = initializeChromosome(id, length, type=type, symbol=symbol); initializeGenomicElement(g1); initializeMutationRate(MU); initializeRecombinationRate(R); defineConstant(paste0("CHR_", symbol), chr); } } 1 late() { sim.addSubpop("p1", N); } REC late() { log = community.createLogFile("PAR_Ne.csv", logInterval=10); log.addTick(); log.addCustomColumn("Ne_A1", "estimateNe_Heterozygosity(p1, CHR_A1);"); log.addCustomColumn("Ne_P1", "estimateNe_Heterozygosity(p1, CHR_P1);"); log.addCustomColumn("Ne_X", "estimateNe_Heterozygosity(p1, CHR_X);"); log.addCustomColumn("Ne_Y", "estimateNe_Heterozygosity(p1, CHR_Y);"); log.addCustomColumn("Ne_P2", "estimateNe_Heterozygosity(p1, CHR_P2);"); defineConstant("LOG", log); } reproduction() { for (i in seqLen(N)) { parentF = p1.sampleIndividuals(1, sex="F"); parentM = p1.sampleIndividuals(1, sex="M"); // generate breakpoints for the female parent (X recombines) breaks_F_P1 = CHR_P1.drawBreakpoints(parent=parentF); breaks_F_X = CHR_X.drawBreakpoints(parent=parentF); breaks_F_P2 = CHR_P2.drawBreakpoints(parent=parentF); // generate breakpoints for the male parent (only PARs recombine) breaks_M_P1 = CHR_P1.drawBreakpoints(parent=parentM); breaks_M_P2 = CHR_P2.drawBreakpoints(parent=parentM); // get the haplosomes for each chromosome in each parent strands_F_P1 = parentF.haplosomesForChromosomes(CHR_P1); // 2 strands_F_X = parentF.haplosomesForChromosomes(CHR_X); // 2 strands_F_P2 = parentF.haplosomesForChromosomes(CHR_P2); // 2 strands_M_P1 = parentM.haplosomesForChromosomes(CHR_P1); // 2 strands_M_X = parentM.haplosomesForChromosomes(CHR_X)[0]; // 1 strands_M_Y = parentM.haplosomesForChromosomes(CHR_Y); // 1 strands_M_P2 = parentM.haplosomesForChromosomes(CHR_P2); // 2 // choose initial copy strand indices for PAR1 in both (coin flip) initial_F_P1 = rbinom(1, 1, 0.5); initial_M_P1 = rbinom(1, 1, 0.5); // generate the inheritance dictionary for PAR1 s1_F_P1 = strands_F_P1[initial_F_P1]; s2_F_P1 = strands_F_P1[1 - initial_F_P1]; s1_M_P1 = strands_M_P1[initial_M_P1]; s2_M_P1 = strands_M_P1[1 - initial_M_P1]; pattern = sim.addPatternForRecombinant(CHR_P1, NULL, s1_F_P1, s2_F_P1, breaks_F_P1, s1_M_P1, s2_M_P1, breaks_M_P1, randomizeStrands=F); // the initial strand for the X in the female follows from the // above, because PAR1 is physically linked to the start of the // X; if an odd number of crossovers occurred, switch strands initial_F_X = initial_F_P1; if (size(breaks_F_P1) % 2 == 1) initial_F_X = 1 - initial_F_X; if (runif(1) < R) initial_F_X = 1 - initial_F_X; // do the same for the male, but the "initial strand" is the // X if 0, the Y if 1, and it determines the offspring sex initial_M_XY = initial_M_P1; if (size(breaks_M_P1) % 2 == 1) initial_M_XY = 1 - initial_M_XY; if (runif(1) < R) initial_M_XY = 1 - initial_M_XY; sex = ((initial_M_XY == 0) ? "F" else "M"); // generate the inheritance dictionaries for the X and Y s1_F_X = strands_F_X[initial_F_X]; s2_F_X = strands_F_X[1 - initial_F_X]; if (sex == "F") { sim.addPatternForRecombinant(CHR_X, pattern, s1_F_X, s2_F_X, breaks_F_X, strands_M_X, NULL, NULL, sex=sex, randomizeStrands=F); sim.addPatternForNull(CHR_Y, pattern, sex=sex); } else { sim.addPatternForRecombinant(CHR_X, pattern, s1_F_X, s2_F_X, breaks_F_X, NULL, NULL, NULL, sex=sex, randomizeStrands=F); sim.addPatternForClone(CHR_Y, pattern, parent=parentM, sex=sex); } // and the initial copy strand for PAR2 follows from the above, // because PAR2 is physically linked to the end of the X/Y; // if an odd number of crossovers occurred, switch strands initial_F_P2 = initial_F_X; if (size(breaks_F_X) % 2 == 1) initial_F_P2 = 1 - initial_F_P2; if (runif(1) < R) initial_F_P2 = 1 - initial_F_P2; initial_M_P2 = initial_M_XY; if (runif(1) < R) initial_M_P2 = 1 - initial_M_P2; // generate the inheritance dictionary for PAR2 s1_F_P2 = strands_F_P2[initial_F_P2]; s2_F_P2 = strands_F_P2[1 - initial_F_P2]; s1_M_P2 = strands_M_P2[initial_M_P2]; s2_M_P2 = strands_M_P2[1 - initial_M_P2]; sim.addPatternForRecombinant(CHR_P2, pattern, s1_F_P2, s2_F_P2, breaks_F_P2, s1_M_P2, s2_M_P2, breaks_M_P2, randomizeStrands=F); // finally, generate the offspring following the pattern dictionary subpop.addMultiRecombinant(pattern, sex=sex, parent1=parentF, parent2=parentM, randomizeStrands=F); } self.active = 0; } 2: early() { // non-overlapping generations adults = p1.subsetIndividuals(minAge=1); sim.killIndividuals(adults); } REC:(RUNTIME+1) early() { // plot results that got logged the previous tick (which ended in 0) if ((community.tick % 10 == 1) & exists("slimgui")) { ticks = slimgui.logFileData(LOG, "tick"); Ne_A1 = slimgui.logFileData(LOG, "Ne_A1"); Ne_P1 = slimgui.logFileData(LOG, "Ne_P1"); Ne_X = slimgui.logFileData(LOG, "Ne_X"); Ne_Y = slimgui.logFileData(LOG, "Ne_Y"); Ne_P2 = slimgui.logFileData(LOG, "Ne_P2"); plot = slimgui.createPlot("Ne Estimates", xrange=c(REC, RUNTIME), yrange=c(0, N * 2), xlab="Tick", ylab="Population size", width=1000, height=400); plot.axis(2, at=c(0, N, N*2)); plot.abline(h=N, color="black", lwd=2.0); plot.lines(ticks, Ne_A1, "chartreuse3", lwd=2.0); plot.abline(h=mean(Ne_A1), color="chartreuse3", lwd=1.0); plot.lines(ticks, Ne_P1, "turquoise3", lwd=2.0); plot.abline(h=mean(Ne_P1), color="turquoise3", lwd=1.0); plot.lines(ticks, Ne_X, "red", lwd=2.0); plot.abline(h=mean(Ne_X), color="red", lwd=1.0); plot.lines(ticks, Ne_Y, "orchid2", lwd=2.0); plot.abline(h=mean(Ne_Y), color="orchid2", lwd=1.0); plot.lines(ticks, Ne_P2, "cornflowerblue", lwd=2.0); plot.abline(h=mean(Ne_P2), color="cornflowerblue", lwd=1.0); plot.addLegend("topLeft", labelSize=12); plot.legendLineEntry("N", "black", lwd=2.0); plot.legendLineEntry("Ne (A1)", "chartreuse3", lwd=2.0); plot.legendLineEntry("Ne (P1)", "turquoise3", lwd=2.0); plot.legendLineEntry("Ne (X)", "red", lwd=2.0); plot.legendLineEntry("Ne (Y)", "orchid2", lwd=2.0); plot.legendLineEntry("Ne (P2)", "cornflowerblue", lwd=2.0); if (community.tick == RUNTIME + 1) { plot.write("Ne_EST.pdf"); sim.simulationFinished(); } } } function (float)estimateNe_Heterozygosity(o$ subpop, [No$ chromosome = NULL]) { if (isNULL(chromosome)) { if (size(sim.chromosomes) == 1) chromosome = sim.chromosomes; else stop("ERROR: in a multi-chrom model, a chromosome must be supplied."); } haplosomes = subpop.haplosomesForChromosomes(chromosome, includeNulls=F); pi = calcHeterozygosity(haplosomes); return pi / (4 * MU); } ================================================ FILE: QtSLiM/recipes/Recipe 16.11 - Life-long monogamous mating.txt ================================================ // Keywords: monogamy, monogamous mating, nonWF, non-Wright-Fisher initialize() { defineConstant("K", 500); // carrying capacity defineConstant("R_AGE_M", 3); // minimum age of reproduction (male) defineConstant("R_AGE_F", 4); // minimum age of reproduction (female) defineConstant("FECUN", 0.2); // mean fecundity per female per tick initializeSLiMModelType("nonWF"); initializeSex(); } 1 first() { sim.addSubpop("p1", K); p1.individuals.age = rdunif(K, min=0, max=15); // initial variation in age p1.individuals.tag = -1; // mark all individuals as unmated } first() { // find mated individuals whose mate has died, and mark them as unmated mated_individuals = p1.individuals; mated_individuals = mated_individuals[mated_individuals.tag >= 0]; if (size(mated_individuals) > 0) { tags = mated_individuals.tag; tag_counts = tabulate(tags); tags_to_fix = which(tag_counts == 1); unmated_indices = match(tags_to_fix, tags); mated_individuals[unmated_indices].tag = -1; } // find the next tag value to use for new mating pairs next_tag = max(p1.individuals.tag) + 1; // find unmated individuals that are of reproductive age unmated_F = p1.subsetIndividuals(sex="F", tag=-1, minAge=R_AGE_F); unmated_M = p1.subsetIndividuals(sex="M", tag=-1, minAge=R_AGE_M); // pair individuals randomly; some individuals may be left unpaired pair_count = min(size(unmated_F), size(unmated_M)); unmated_F = sample(unmated_F, pair_count, replace=F); unmated_M = sample(unmated_M, pair_count, replace=F); for (f in unmated_F, m in unmated_M, tag in seqLen(pair_count) + next_tag) { f.tag = tag; m.tag = tag; } } reproduction() { // find the subset of individuals that have a mate mated_F = p1.subsetIndividuals(sex="F"); mated_F = mated_F[mated_F.tag >= 0]; mated_M = p1.subsetIndividuals(sex="M"); mated_M = mated_M[mated_M.tag >= 0]; // look up the male for each female, by tag male_indices = match(mated_F.tag, mated_M.tag); mated_M = mated_M[male_indices]; pair_count = size(mated_F); // produce offspring from each mated pair for (f in mated_F, m in mated_M, c in rpois(pair_count, FECUN), new_tag in seqLen(pair_count)) { // re-tag paired individuals to compact tags down f.tag = new_tag; m.tag = new_tag; offspring = p1.addCrossed(f, m, count=c); offspring.tag = -1; // mark offspring as unmated } self.active = 0; // deactivate for the rest of the tick ("big bang") } early() { // density-dependent population regulation p1.fitnessScaling = K / p1.individualCount; } 10000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 16.2 - Following a pedigree.txt ================================================ // Keywords: nonWF, non-Wright-Fisher function (+)readMatrix(s$ path, [string$ sep = ","]) { if (!fileExists(path)) stop("readMatrix(): File not found at path " + path); df = readCSV(path, colNames=F, sep=sep); m = df.asMatrix(); return m; } initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 10); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // read in the pedigree log files defineConstant("M", readMatrix("mating.txt", sep="")); defineConstant("D", readMatrix("death.txt", sep="")); // extract the ticks for quick lookup defineConstant("Mt", drop(M[,0])); defineConstant("Dt", drop(D[,0])); } reproduction() { // generate all offspring for the tick m = M[Mt == community.tick,]; for (index in seqLen(nrow(m))) { row = m[index,]; ind = subpop.subsetIndividuals(tag=row[,1]); mate = subpop.subsetIndividuals(tag=row[,2]); child = subpop.addCrossed(ind, mate); child.tag = row[,3]; } self.active = 0; } 1 early() { sim.addSubpop("p1", 10); // provide initial tags matching the original model p1.individuals.tag = 1:10; } early() { // execute the predetermined mortality inds = p1.individuals; inds.fitnessScaling = 1.0; d = drop(D[Dt == community.tick, 1]); indices = match(d, inds.tag); inds[indices].fitnessScaling = 0.0; } 100 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 16.3 - Modeling clonal haploid bacteria with horizontal gene transfer.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, clonal, haploid initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 1e5); // carrying capacity defineConstant("L", 1e5); // chromosome length defineConstant("H", 0.001); // HGT probability initializeMutationType("m1", 1.0, "f", 0.0); // neutral (unused) initializeMutationType("m2", 1.0, "f", 0.1); // beneficial initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, L, type="H"); initializeGenomicElement(g1); initializeMutationRate(0); // no mutation initializeRecombinationRate(0); // no recombination } reproduction() { if (runif(1) < H) { // horizontal gene transfer from a randomly chosen individual HGTsource = p1.sampleIndividuals(1, exclude=individual).haplosomes; // draw two distinct locations; redraw if we get a duplicate do breaks = rdunif(2, max=L-1); while (breaks[0] == breaks[1]); // HGT from breaks[0] forward to breaks[1] on a circular chromosome if (breaks[0] > breaks[1]) breaks = c(0, breaks[1], breaks[0]); subpop.addRecombinant(individual.haplosomes, HGTsource, breaks, NULL, NULL, NULL, randomizeStrands=F); } else { // no horizontal gene transfer; clonal replication subpop.addCloned(individual); } } 1 early() { // start from two bacteria with different beneficial mutations sim.addSubpop("p1", 2); h = p1.individuals.haplosomes; h[0].addNewDrawnMutation(m2, asInteger(L * 0.25)); h[1].addNewDrawnMutation(m2, asInteger(L * 0.75)); } early() { // density-dependent population regulation p1.fitnessScaling = K / p1.individualCount; } late() { // detect fixation/loss of the beneficial mutations muts = sim.mutations; freqs = sim.mutationFrequencies(NULL, muts); if (all(freqs == 1.0)) { catn(sim.cycle + ": " + sum(freqs == 1.0) + " fixed."); sim.simulationFinished(); } } 1e6 late() { catn(sim.cycle + ": no result."); } ================================================ FILE: QtSLiM/recipes/Recipe 16.4 - Alternation of generations.txt ================================================ // Keywords: alternation of generations, sporophyte, gametophyte, sperm, eggs, diploid, haploid, mating system, fertilization, meiosis, reproduction() initialize() { defineConstant("K", 500); // carrying capacity (diploid) defineConstant("MU", 1e-7); // mutation rate defineConstant("R", 1e-7); // recombination rate defineConstant("L1", 1e5-1); // chromosome end (length - 1) initializeSLiMModelType("nonWF"); initializeSex(); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L1); initializeRecombinationRate(R); } 1 early() { sim.addSubpop("p1", K); sim.addSubpop("p2", 0); } reproduction(p1) { g_1 = individual.haploidGenome1; g_2 = individual.haploidGenome2; for (meiosisCount in 1:5) { if (individual.sex == "M") { breaks = sim.chromosomes.drawBreakpoints(individual); s_1 = p2.addRecombinant(g_1, g_2, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); s_2 = p2.addRecombinant(g_2, g_1, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); breaks = sim.chromosomes.drawBreakpoints(individual); s_3 = p2.addRecombinant(g_1, g_2, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); s_4 = p2.addRecombinant(g_2, g_1, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); } else if (individual.sex == "F") { e = p2.addRecombinant(g_1, g_2, NULL, NULL, NULL, NULL, "F", randomizeStrands=T); } } } reproduction(p2, "F") { mate = p2.sampleIndividuals(1, sex="M", tagL0=F); mate.tagL0 = T; child = p1.addRecombinant(individual.haploidGenome1, NULL, NULL, mate.haploidGenome1, NULL, NULL); } early() { if (sim.cycle % 2 == 0) { p1.fitnessScaling = 0.0; p2.individuals.tagL0 = F; sim.chromosomes.setMutationRate(0.0); } else { p2.fitnessScaling = 0.0; p1.fitnessScaling = K / p1.individualCount; sim.chromosomes.setMutationRate(MU); } } 10000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 16.5 - Meiotic drive.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, meiotic drive, segregation distortion, intragenomic conflict function (i)driveBreakpoints(o$ gen1, o$ gen2) { // start with default breakpoints generated by the chromosome breaks = sim.chromosomes.drawBreakpoints(); // if both haplosomes have the drive, or neither, then just return gen1has = gen1.containsMarkerMutation(m2, D_pos); gen2has = gen2.containsMarkerMutation(m2, D_pos); if (gen1has == gen2has) return breaks; // will the drive be inherited? do we want it to be? polarity = sum(breaks <= D_pos) % 2; // 0 for gen1, 1 for gen2 polarityI = (gen1has ? 0 else 1); desiredPolarity = (runif(1) < D_prob) ? polarityI else !polarityI; // intervene to produce the outcome we want if (desiredPolarity != polarity) return c(0, breaks); return breaks; } initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); // carrying capacity defineConstant("D_pos", 20000); // meiotic drive allele position defineConstant("D_prob", 0.8); // meiotic drive probability initializeMutationType("m1", 0.5, "f", 0.0); // neutral m1.convertToSubstitution = T; initializeMutationType("m2", 0.1, "f", -0.1); // drive allele m2.color = "red"; m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { m = subpop.sampleIndividuals(1); b1 = driveBreakpoints(individual.haploidGenome1, individual.haploidGenome2); b2 = driveBreakpoints(m.haploidGenome1, m.haploidGenome2); subpop.addRecombinant(individual.haploidGenome1, individual.haploidGenome2, b1, m.haploidGenome1, m.haploidGenome2, b2, randomizeStrands=F); } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } 100 early() { target = sample(p1.haplosomes, 10); target.addNewDrawnMutation(m2, D_pos); } 100:1000 late() { mut = sim.mutationsOfType(m2); if (size(mut) == 0) { catn(sim.cycle + ": LOST"); sim.simulationFinished(); } else if (sim.mutationFrequencies(NULL, mut) == 1.0) { catn(sim.cycle + ": FIXED"); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 16.6 - Sperm storage with a survival() callback.txt ================================================ // Keywords: survival(), sperm storage // This model is loosely based upon a model by Anita Lerch. initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(keepPedigrees=T); initializeSex(); defineConstant("K", 500); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } reproduction(p1) { matureFemales = subpop.subsetIndividuals(sex="F", minAge=7); for (female in matureFemales) { if (female.tag < 0) { // the female has not yet chosen a mate, so choose one now mate = subpop.sampleIndividuals(1, sex="M", minAge=7); } else { // the female has already chosen a mate; look it up by id mate = sim.individualsWithPedigreeIDs(female.tag); } if (mate.size()) { female.tag = mate.pedigreeID; subpop.addCrossed(female, mate, count=rpois(1, 5)); } else { catn(sim.cycle + ": No mate found for tag " + female.tag); } } self.active = 0; } 1 early() { sim.addSubpop("p1", 100); p1inds = p1.individuals; p1inds.age = rdunif(size(p1.individuals), min=0, max=10); p1inds.tag = -1; sim.addSubpop("p1000", 0); // cold storage for dead males } early() { // fix all new female tags; faster to do this vectorized offspringFemales = p1.subsetIndividuals(sex="F", maxAge=0); offspringFemales.tag = -1; // p1 is governed by standard density-dependence p1.fitnessScaling = K / p1.individualCount; // cold storage individuals are kept until unreferenced p1000.individuals.tag = 0; maleRefs = p1.subsetIndividuals(sex="F").tag; maleRefs = maleRefs[maleRefs != -1]; referencedDeadMales = sim.individualsWithPedigreeIDs(maleRefs, p1000); referencedDeadMales.tag = 1; } survival(p1) { // move dying males into cold storage in case they have mated if (!surviving) if (individual.sex == "M") return p1000; return NULL; } survival(p1000) { return (individual.tag == 1); } late() { catn(sim.cycle + ": p1 (" + p1.individualCount + ")" + ", p1000 (" + p1000.individualCount + ")"); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 16.7 - Tracking separate sexes in script, nonWF style.txt ================================================ // Keywords: automixis, parthenogenesis, sex determination, mating systems, sexual types initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); // carrying capacity initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // we focus on the reproduction of the females here if (individual.tagL0 == F) { if (runif(1) < 0.7) { // choose a male mate and produce a son or daughter mate = subpop.sampleIndividuals(1, tagL0=T); offspring = subpop.addCrossed(individual, mate); offspring.tagL0 = (runif(1) <= 0.5); } else { // reproduce through automixis to produce a daughter offspring = subpop.addSelfed(individual); offspring.tagL0 = F; } } } 1 early() { sim.addSubpop("p1", K); // assign random sexes (T = male, F = female) p1.individuals.tagL0 = (runif(p1.individualCount) <= 0.5); } early() { p1.fitnessScaling = K / p1.individualCount; } 1:2000 late() { ratio = mean(p1.individuals.tagL0); catn(sim.cycle + ": " + ratio); } ================================================ FILE: QtSLiM/recipes/Recipe 16.8 - Modeling haplodiploidy with addRecombinant().txt ================================================ // Keywords: mating systems, haplodiploidy, arrhenotoky, bees, wasps, ants, Hymenoptera initialize() { defineConstant("K", 2000); defineConstant("P_OFFSPRING_MALE", 0.8); initializeSLiMModelType("nonWF"); initializeMutationRate(1e-8); initializeMutationType("m1", 0.0, "f", 0.0); m1.convertToSubstitution = T; m1.hemizygousDominanceCoeff = 1.0; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 999999); initializeRecombinationRate(1e-6); initializeSex(); } reproduction(NULL, "F") { gen1 = individual.haploidGenome1; gen2 = individual.haploidGenome2; // decide whether we're generating a haploid male or a diploid female if (rbinom(1, 1, P_OFFSPRING_MALE)) { // didn't find a mate; make a haploid male from an unfertilized egg: // - one haplosome comes from recombination of the female's haplosomes // - the other haplosome is a null haplosome (a placeholder) subpop.addRecombinant(gen1, gen2, NULL, NULL, NULL, NULL, "M", randomizeStrands=T); } else { // found a mate; make a diploid female from a fertilized egg: // - one haplosome comes from recombination of the female's haplosomes // - the other haplosome comes from the mate (a haploid male) mate = subpop.sampleIndividuals(1, sex="M"); subpop.addRecombinant(gen1, gen2, NULL, mate.haploidGenome1, NULL, NULL, "F", randomizeStrands=T); } } 1 early() { // make an initial population with the right genetics mCount = asInteger(K * P_OFFSPRING_MALE); fCount = K - mCount; sim.addSubpop("p1", mCount, sexRatio=1.0, haploid=T); // males sim.addSubpop("p2", fCount, sexRatio=0.0, haploid=F); // females p1.takeMigrants(p2.individuals); p2.removeSubpopulation(); } early() { p1.fitnessScaling = K / p1.individualCount; } 10000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 16.9 - Complex multi-chromosome inheritance with addMultiRecombinant().txt ================================================ // Keywords: multiple chromosomes, inheritance patterns, mating systems initialize() { defineConstant("K", 500); // carrying capacity initializeSLiMModelType("nonWF"); initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); m1.convertToSubstitution = T; ids = 1:7; symbols = c("A", "X", "Y", "P", "Q", "R", "S"); lengths = c(3e6, 2e6, 1e6, 1e6, 1e6, 1e6, 1e6); types = c("A", "X", "Y", "H", "H", "H", "H"); for (id in ids, symbol in symbols, length in lengths, type in types) { initializeChromosome(id, length, type, symbol); initializeMutationRate(1e-7); initializeRecombinationRate(1e-7); initializeGenomicElement(g1); } } reproduction(NULL, "F") { mate = subpop.sampleIndividuals(1, sex="M"); pattern = Dictionary(); sim.addPatternForClone("P", pattern, individual); sim.addPatternForClone("Q", pattern, runif(1) < 0.5 ? individual else mate); sim.addPatternForCross("R", pattern, individual, mate); ind_hapS = individual.haplosomesForChromosomes("S"); mate_hapS = mate.haplosomesForChromosomes("S"); sim.addPatternForRecombinant("S", pattern, ind_hapS, mate_hapS, NULL, NULL, NULL, NULL); subpop.addMultiRecombinant(pattern, parent1=individual, parent2=mate, randomizeStrands=F); } 1 early() { sim.addSubpop("p1", K); } early() { p1.fitnessScaling = K / p1.individualCount; } 1000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.1 - A simple 2D continuous-space model.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); // initial positions are random in ([0,1], [0,1]) p1.individuals.x = runif(p1.individualCount); p1.individuals.y = runif(p1.individualCount); } modifyChild() { // draw a child position near the first parent, within bounds do child.x = parent1.x + rnorm(1, 0, 0.02); while ((child.x < 0.0) | (child.x > 1.0)); do child.y = parent1.y + rnorm(1, 0, 0.02); while ((child.y < 0.0) | (child.y > 1.0)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.10 - A simple biogeographic landscape model.txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial map, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=30.0); i1.setInteractionFunction("n", 5.0, 10.0); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=30.0); i2.setInteractionFunction("n", 1.0, 10.0); } 1 late() { sim.addSubpop("p1", 1000); p1.setSpatialBounds(c(0.0, 0.0, 539.0, 216.0)); // this file is in the recipe archive at http://benhaller.com/slim/SLiM_Recipes.zip mapImage = Image("world_map_540x217.png"); map = p1.defineSpatialMap("world", "xy", 1.0 - mapImage.floatK, valueRange=c(0.0, 1.0), colors=c("#0000CC", "#55FF22")); defineConstant("WORLD", map); // start near a specific map location for (ind in p1.individuals) { ind.x = rnorm(1, 300.0, 1.0); ind.y = rnorm(1, 100.0, 1.0); } } 1: late() { i1.evaluate(p1); inds = sim.subpopulations.individuals; competition = i1.totalOfNeighborStrengths(inds) / size(inds); competition = pmin(competition, 0.99); inds.fitnessScaling = 1.0 - competition; } 2: first() { i2.evaluate(p1); } mateChoice() { return i2.strength(individual); } modifyChild() { do pos = parent1.spatialPosition + rnorm(2, 0, 2.0); while (!p1.pointInBounds(pos)); // prevent dispersal into water if (WORLD.mapValue(pos) == 0.0) return F; child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.11 - Local adaptation on a heterogeneous landscape map.txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial map, reprising boundaries, QTL, quantitative trait loci, spatial competition, spatial mate choice initialize() { defineConstant("SIGMA_C", 0.1); defineConstant("SIGMA_K", 0.5); defineConstant("SIGMA_M", 0.1); defineConstant("N", 500); initializeSLiMOptions(dimensionality="xyz"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.1)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); // competition initializeInteractionType(1, "xyz", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); // mate choice initializeInteractionType(2, "xyz", reciprocal=T, maxDistance=SIGMA_M * 3); i2.setInteractionFunction("n", 1.0, SIGMA_M); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", N); p1.setSpatialBounds(c(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); p1.individuals.setSpatialPosition(p1.pointUniform(N)); p1.individuals.z = 0.0; defineConstant("MAPVALUES", matrix(runif(25, 0, 1), ncol=5)); map = p1.defineSpatialMap("map1", "xy", MAPVALUES, interpolate=T, valueRange=c(0.0, 1.0), colors=c("red", "yellow")); defineConstant("OPTIMUM", map); } modifyChild() { // set offspring position based on parental position do pos = c(parent1.spatialPosition[0:1] + rnorm(2, 0, 0.005), 0.0); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 1: late() { // construct phenotypes and fitness effects from QTLs inds = sim.subpopulations.individuals; phenotype = inds.sumOfMutationsOfType(m2); location = inds.spatialPosition[rep(c(T,T,F), inds.size())]; optimum = OPTIMUM.mapValue(location); inds.fitnessScaling = 1.0 + dnorm(phenotype, optimum, SIGMA_K); inds.z = phenotype; // color individuals according to phenotype inds.color = OPTIMUM.mapColor(phenotype); // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 2: first() { // evaluate mate choice in preparation for reproduction i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.12 - Periodic spatial boundaries.txt ================================================ // Keywords: continuous space, continuous spatial landscape, periodic boundaries initialize() { initializeSLiMOptions(dimensionality="xy", periodicity="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType("i1", "xy", reciprocal=T, maxDistance=0.2); i1.setInteractionFunction("n", 1.0, 0.1); } 1 late() { sim.addSubpop("p1", 2000); p1.individuals.setSpatialPosition(p1.pointUniform(2000)); } late() { i1.evaluate(p1); focus = sample(p1.individuals, 1); s = i1.strength(focus); inds = p1.individuals; for (i in seqAlong(s)) inds[i].color = rgb2color(c(1.0 - s[i], 1.0 - s[i], s[i])); focus.color = "red"; } modifyChild() { pos = parent1.spatialPosition + rnorm(2, 0, 0.02); child.setSpatialPosition(p1.pointPeriodic(pos)); return T; } 1000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.13 - Density-dependent fecundity with summarizeIndividuals().txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial map, density, competition, regulation, fertility, per unit area initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 1000); p1.individuals.setSpatialPosition(p1.pointUniform(1000)); } late() { inds = p1.individuals; bounds = p1.spatialBounds; // make a density map: 0 is empty, 1 is maximum density density = summarizeIndividuals(inds, c(10, 10), bounds, operation="individuals.size();", empty=0.0, perUnitArea=T); density = density / max(density); p1.defineSpatialMap("density", "xy", density, F, range(density), c("black", "orange", "red")); } modifyChild() { pos = parent1.spatialPosition + rnorm(2, 0, 0.01); pos = p1.pointReflected(pos); if (runif(1) < p1.spatialMapValue("density", pos)) return F; child.setSpatialPosition(pos); return T; } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.14 - Directed dispersal with the SpatialMap class.txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial maps, directed dispersal initialize() { defineConstant("K", 1000); initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy", periodicity="xy"); } 1 late() { sim.addSubpop("p1", K); p1.individuals.setSpatialPosition(c(0.0, 0.0)); p1.individuals.color = rainbow(K); do { m = matrix(rbinom(16, 1, 0.2), ncol=4, byrow=T); m = cbind(m, m[,0]); m = rbind(m, m[0,]); } while ((sum(m) == 0) | (sum(m) == 1)); map = p1.defineSpatialMap("habitat", "xy", m, valueRange=c(0,1), colors=c("black", "white")); defineConstant("MAP", map); } 2 late() { MAP.interpolate(15, method="cubic"); } 3 late() { MAP.rescale(); } 4 late() { MAP.smooth(0.3, "n", 0.1); } 5 late() { MAP.rescale(); } 6 late() { MAP.interpolate = T; } 10 late() { p1.individuals.setSpatialPosition(p1.pointUniform(K)); } 11:100000 late() { inds = p1.individuals; pos = inds.spatialPosition; pos = MAP.sampleNearbyPoint(pos, INF, "n", 0.002); inds.setSpatialPosition(pos); } ================================================ FILE: QtSLiM/recipes/Recipe 17.15 - Spatial competition and spatial mate choice in a nonWF model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, periodic boundaries, selfing initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy", periodicity="xy"); defineConstant("K", 300); // carrying capacity defineConstant("S", 0.1); // spatial competition distance initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); } 2: first() { // look for mates i2.evaluate(p1); } reproduction() { // choose our nearest neighbor as a mate, within the max distance mate = i2.nearestNeighbors(individual, 1); for (i in seqLen(rpois(1, 0.1))) { if (mate.size()) offspring = subpop.addCrossed(individual, mate); else offspring = subpop.addSelfed(individual); // set offspring position pos = individual.spatialPosition + rnorm(2, 0, 0.02); offspring.setSpatialPosition(p1.pointPeriodic(pos)); } } 1 early() { sim.addSubpop("p1", 1); // random initial positions p1.individuals.setSpatialPosition(p1.pointUniform(1)); } early() { i1.evaluate(p1); // spatial competition provides density-dependent selection inds = p1.individuals; competition = i1.totalOfNeighborStrengths(inds); competition = (competition + 1) / (PI * S^2); inds.fitnessScaling = K / competition; } late() { // move around a bit for (ind in p1.individuals) { newPos = ind.spatialPosition + runif(2, -0.01, 0.01); ind.setSpatialPosition(p1.pointPeriodic(newPos)); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.16 - A spatial model with carrying-capacity density.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, selfing, spatial competition, spatial mate choice initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy"); defineConstant("K", 300); // carrying-capacity density defineConstant("S", 0.1); // SIGMA_S, the spatial interaction width initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S * 3); i1.setInteractionFunction("n", 1.0, S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); } 2: first() { // look for mates i2.evaluate(p1); } reproduction() { // choose our nearest neighbor as a mate, within the max distance mate = i2.nearestNeighbors(individual, 1); for (i in seqLen(rpois(1, 0.1))) { if (mate.size()) offspring = subpop.addCrossed(individual, mate); else offspring = subpop.addSelfed(individual); // set offspring position do pos = individual.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); offspring.setSpatialPosition(pos); } } 1 early() { sim.addSubpop("p1", 1); // random initial positions p1.individuals.setSpatialPosition(p1.pointUniform(1)); } early() { i1.evaluate(p1); // spatial competition provides density-dependent selection inds = p1.individuals; competition = i1.localPopulationDensity(inds); inds.fitnessScaling = K / competition; } late() { // move around a bit for (ind in p1.individuals) { do newPos = ind.spatialPosition + runif(2, -0.01, 0.01); while (!p1.pointInBounds(newPos)); ind.setSpatialPosition(newPos); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.17 - A spatial epidemiological S-I-R model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, periodic boundaries, spatial competition, spatial mate choice, disease, epidemiology, SIR, S-I-R, infection, epidemic, pandemic initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy", periodicity="xy"); defineConstant("K", 10000); // carrying-capacity density defineConstant("S", 0.01); // SIGMA_S, the competition width defineConstant("HEALTH_S", 0); // susceptible defineConstant("HEALTH_I", 1); // infectious defineConstant("HEALTH_R", 2); // recovered defineConstant("FERTILITY", 0.05); defineConstant("INFECTIVITY", 4); defineConstant("RATE_DEATH", 0.3); defineConstant("RATE_CLEAR", 0.05); defineConstant("MAX_AGE", 100.0); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S * 3); i1.setInteractionFunction("n", 1.0, S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.05); } 2: first() { // look for mates i2.evaluate(p1); } reproduction() { litterSize = rpois(1, FERTILITY); if (litterSize) { mate = i2.nearestNeighbors(individual, 1); if (mate.size()) for (i in seqLen(litterSize)) { offspring = subpop.addCrossed(individual, mate); // set offspring position and state pos = individual.spatialPosition + rnorm(2, 0, 0.005); offspring.setSpatialPosition(p1.pointPeriodic(pos)); offspring.tag = HEALTH_S; } } } 1 early() { sim.addSubpop("p1", K); p1.individuals.setSpatialPosition(p1.pointUniform(K)); p1.individuals.tag = HEALTH_S; } 100 early() { // seed the infection in a susceptible individual target = p1.sampleIndividuals(1, tag=HEALTH_S); target.tag = HEALTH_I; } early() { i1.evaluate(p1); // spatial competition provides density-dependent selection inds = p1.individuals; competition = i1.totalOfNeighborStrengths(inds); competition = (competition + 1) / (2 * PI * S^2); inds.fitnessScaling = K / competition; // age-based mortality; at age 100 mortality is 100% age_mortality = sqrt((MAX_AGE - inds.age) / MAX_AGE); inds.fitnessScaling = inds.fitnessScaling * age_mortality; // SIR model infected = inds[inds.tag == HEALTH_I]; for (ind in infected) { // make contact with random neighbors each cycle contacts = i1.drawByStrength(ind, rpois(1, INFECTIVITY)); for (contact in contacts) { // if the contact is susceptible, they might get infected if (contact.tag == HEALTH_S) { strength = i1.strength(ind, contact); if (runif(1) < strength) contact.tag = HEALTH_I; } } // die with some probability each cycle if (runif(1) < RATE_DEATH) ind.fitnessScaling = 0.0; // recover with some probability each cycle if (runif(1) < RATE_CLEAR) ind.tag = HEALTH_R; } } late() { inds = p1.individuals; // move around a bit for (ind in inds) { newPos = ind.spatialPosition + runif(2, -0.005, 0.005); ind.setSpatialPosition(p1.pointPeriodic(newPos)); } // color according to health status; S=green, I=red, R=blue inds_tags = inds.tag; inds[inds_tags == HEALTH_S].color = "green"; inds[inds_tags == HEALTH_I].color = "red"; inds[inds_tags == HEALTH_R].color = "blue"; } 1:1000 late() { tags = p1.individuals.tag; cat(sum(tags == HEALTH_S) + ", " + sum(tags == HEALTH_I) + ", " + sum(tags == HEALTH_R) + ", "); if ((sum(tags == HEALTH_I) == 0) & (sim.cycle >= 100)) { catn("\nLOST in cycle " + sim.cycle); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 17.18 - A sexual, age-structured spatial model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, selfing, spatial competition, spatial mate choice initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy"); initializeSex(); defineConstant("K", 300); // carrying-capacity density defineConstant("S", 0.1); // SIGMA_S, the spatial interaction width // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S * 3); i1.setInteractionFunction("n", 1.0, S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); i2.setConstraints("receiver", sex="F", minAge=2, maxAge=4); i2.setConstraints("exerter", sex="M", minAge=2); } 1 early() { sim.addSubpop("p1", K); p1.individuals.setSpatialPosition(p1.pointUniform(K)); } 2: first() { // look for mates i2.evaluate(p1); } reproduction(NULL, "F") { // choose our nearest neighbor as a mate, within the max distance mate = i2.nearestInteractingNeighbors(individual, 1); if (mate.size() > 0) subpop.addCrossed(individual, mate, count=rpois(1, 1.5)); } early() { // first, conduct age-related mortality with killIndividuals() inds = p1.individuals; ages = inds.age; inds4 = inds[ages == 4]; inds5 = inds[ages == 5]; inds6 = inds[ages >= 6]; death4 = (runif(inds4.size()) < 0.10); death5 = (runif(inds5.size()) < 0.30); sim.killIndividuals(c(inds4[death4], inds5[death5], inds6)); // disperse prior to density-dependence p1.deviatePositions(NULL, "reprising", INF, "n", 0.02); // spatial competition provides density-dependent selection i1.evaluate(p1); inds = p1.individuals; competition = i1.localPopulationDensity(inds); inds.fitnessScaling = K / competition; } 10000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 17.19 - Modeling indirect competition mediated by resource availability.txt ================================================ // Keywords: resources, foraging, spatial competition, home range, multispecies species all initialize() { initializeSLiMModelType("nonWF"); // Foraging interaction. initializeInteractionType(1, "xy", reciprocal=T, maxDistance=1.5); // Reproduction interaction. initializeInteractionType(2, "xy", reciprocal=T, maxDistance=3, sexSegregation="FM"); } species resourceNode initialize() { initializeSpecies(avatar="🪣", color="cornflowerblue"); initializeSLiMOptions(dimensionality="xy"); } species forager initialize() { initializeSpecies(avatar="🤤", color="red"); initializeSLiMOptions(dimensionality="xy"); initializeSex(); } ticks all 1 early() { // Coordinates for the resource nodes. xs = rep(seq(0.5, 100), 100); ys = repEach(seq(0.5, 100), 100); // Add the resource nodes. resourceNode.addSubpop("p1", 10000); p1.setSpatialBounds(c(0, 0, 100, 100)); p1.individuals.x = xs; p1.individuals.y = ys; p1.individuals.tagF = 10.0; // Initialize the population of foragers. forager.addSubpop("p2", 100000); p2.setSpatialBounds(p1.spatialBounds); p2.individuals.setSpatialPosition(p2.pointUniform(p2.individualCount)); } ticks all 2: first() { // Evaluate the spatial interaction for reproduction. i2.evaluate(p2); } species forager reproduction(NULL, "F") { // Draw the litter size first, and return if it's zero. litterSize = rpois(1, 8); if (litterSize == 0) return; // Draw a random mate from among males in range. mate = i2.drawByStrength(individual, 1, p2); if (size(mate) == 0) return; // Produce the offspring. subpop.addCrossed(individual, mate, count=litterSize); } ticks all 2:100 early() { // Dispersal of new offspring. offspring = p2.subsetIndividuals(maxAge=0); p2.deviatePositions(offspring, "reprising", INF, "n", 1.5); // Evaluate the spatial interaction between resource nodes and foragers. i1.evaluate(c(p2, p1)); // Survival in this model is based entirely on resource availability. p2.individuals.fitnessScaling = 0.0; for (node in p1.individuals) { // Find all foragers within range of the resource node. f = i1.nearestNeighbors(node, p2.individualCount, p2); // Evenly divide resources to all foragers within range. f.fitnessScaling = f.fitnessScaling + node.tagF / size(f); } // In some cases, if the landscape is at very low density, some individuals // might have a fitnessScaling value > 1.0. This value must be capped. p2.individuals.fitnessScaling = pmin(p2.individuals.fitnessScaling, 1.0); } ================================================ FILE: QtSLiM/recipes/Recipe 17.2 - Spatial competition.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries, spatial competition initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // Set up an interaction for spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // initial positions are random in ([0,1], [0,1]) p1.individuals.x = runif(p1.individualCount); p1.individuals.y = runif(p1.individualCount); } 1: late() { // evaluate interactions before fitness calculations i1.evaluate(p1); } fitnessEffect() { // spatial competition totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // draw a child position near the first parent, within bounds do child.x = parent1.x + rnorm(1, 0, 0.02); while ((child.x < 0.0) | (child.x > 1.0)); do child.y = parent1.y + rnorm(1, 0, 0.02); while ((child.y < 0.0) | (child.y > 1.0)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.3 - Boundaries and boundary conditions I (stopping boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, stopping boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Stopping boundary conditions pos = parent1.spatialPosition + rnorm(2, 0, 0.02); child.setSpatialPosition(p1.pointStopped(pos)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.3 - Boundaries and boundary conditions II (reflecting boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, reflecting boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Reflecting boundary conditions pos = parent1.spatialPosition + rnorm(2, 0, 0.02); child.setSpatialPosition(p1.pointReflected(pos)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.3 - Boundaries and boundary conditions III (absorbing boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, absorbing boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Absorbing boundary conditions pos = parent1.spatialPosition + rnorm(2, 0, 0.02); if (!p1.pointInBounds(pos)) return F; child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.3 - Boundaries and boundary conditions IV (reprising boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Reprising boundary conditions do pos = parent1.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.3 - Boundaries and boundary conditions V (dispersal kernels).txt ================================================ // Keywords: continuous space, continuous spatial landscape, boundaries, boundary conditions, dispersal kernel initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { // Dispersal and boundary enforcement p1.deviatePositions(NULL, "reprising", INF, "n", 0.02); // Evaluate for competition i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.4 - Mate choice with a spatial kernel.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); i1.setInteractionFunction("n", 3.0, 0.1); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); i2.setInteractionFunction("n", 1.0, 0.02); } 1 late() { sim.addSubpop("p1", 500); p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); inds = sim.subpopulations.individuals; competition = i1.totalOfNeighborStrengths(inds); inds.fitnessScaling = 1.1 - competition / size(inds); } 2: first() { i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } modifyChild() { do pos = parent1.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.5 - Mate choice with a nearest-neighbor search.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); i1.setInteractionFunction("n", 3.0, 0.1); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); } 1 late() { sim.addSubpop("p1", 500); p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); inds = sim.subpopulations.individuals; competition = i1.totalOfNeighborStrengths(inds); inds.fitnessScaling = 1.1 - competition / size(inds); } 2: first() { i2.evaluate(p1); } mateChoice() { // nearest-neighbor spatial mate choice neighbors = i2.nearestNeighbors(individual, 3); return (size(neighbors) ? sample(neighbors, 1) else float(0)); } modifyChild() { do pos = parent1.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 17.6 - Divergence due to phenotypic competition with an interaction() callback.txt ================================================ // Keywords: QTL, quantitative trait loci, phenotypic competition, interaction() initialize() { defineConstant("OPTIMUM", 5.0); defineConstant("SIGMA_K", 1.0); defineConstant("SIGMA_C", 0.4); defineConstant("NORM", dnorm(0.0, mean=0, sd=SIGMA_C)); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); initializeInteractionType(1, "", reciprocal=T); // competition i1.setInteractionFunction("f", 1.0); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", 500); } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + dnorm(phenotypes, OPTIMUM, SIGMA_K); inds.tagF = phenotypes; // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } interaction(i1) { return dnorm(exerter.tagF, receiver.tagF, SIGMA_C) / NORM; } 1:2001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.tagF; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: QtSLiM/recipes/Recipe 17.7 - Modeling phenotype as a spatial dimension.txt ================================================ // Keywords: QTL, quantitative trait loci, phenotypic competition initialize() { defineConstant("OPTIMUM", 5.0); defineConstant("SIGMA_K", 1.0); defineConstant("SIGMA_C", 0.4); defineConstant("NORM", dnorm(0.0, mean=0, sd=SIGMA_C)); initializeSLiMOptions(dimensionality="x"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); initializeInteractionType(1, "x", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", 500); p1.setSpatialBounds(c(0.0, 10.0)); } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + dnorm(phenotypes, OPTIMUM, SIGMA_K); inds.x = phenotypes; // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 1:2001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.x; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: QtSLiM/recipes/Recipe 17.8 - Sympatric speciation facilitated by assortative mating.txt ================================================ // Keywords: QTL, quantitative trait loci, phenotypic competition initialize() { defineConstant("OPTIMUM", 5.0); defineConstant("SIGMA_K", 1.0); defineConstant("SIGMA_C", 0.4); defineConstant("SIGMA_M", 0.5); defineConstant("NORM", dnorm(0.0, mean=0, sd=SIGMA_C)); initializeSLiMOptions(dimensionality="x"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); // competition initializeInteractionType(1, "x", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); // mate choice initializeInteractionType(2, "x", reciprocal=T, maxDistance=SIGMA_M * 3); i2.setInteractionFunction("n", 1.0, SIGMA_M); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", 500); p1.setSpatialBounds(c(0.0, 10.0)); } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + dnorm(phenotypes, OPTIMUM, SIGMA_K); inds.x = phenotypes; // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 2: first() { // evaluate mate choice in preparation for reproduction i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } 1:2001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.x; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: QtSLiM/recipes/Recipe 17.9 - Speciation due to spatial variation in selection.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries, QTL, quantitative trait loci, spatial competition, phenotypic competition, spatial mate choice initialize() { defineConstant("SIGMA_C", 0.1); defineConstant("SIGMA_K", 0.5); defineConstant("SIGMA_M", 0.1); defineConstant("SLOPE", 1.0); defineConstant("N", 500); initializeSLiMOptions(dimensionality="xyz"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.1)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); // competition initializeInteractionType(1, "xyz", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); // mate choice initializeInteractionType(2, "xyz", reciprocal=T, maxDistance=SIGMA_M * 3); i2.setInteractionFunction("n", 1.0, SIGMA_M); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", N); p1.setSpatialBounds(c(0.0, 0.0, -SLOPE, 1.0, 1.0, SLOPE)); p1.individuals.setSpatialPosition(p1.pointUniform(N)); p1.individuals.z = 0.0; } modifyChild() { // set offspring position based on parental position do pos = c(parent1.spatialPosition[0:1] + rnorm(2, 0, 0.005), 0.0); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); optima = (inds.x - 0.5) * SLOPE; inds.fitnessScaling = 1.0 + dnorm(phenotypes, optima, SIGMA_K); inds.z = phenotypes; // color individuals according to phenotype for (ind in inds) { hue = ((ind.z + SLOPE) / (SLOPE * 2)) * 0.66; ind.color = rgb2color(hsv2rgb(c(hue, 1.0, 1.0))); } // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 2: first() { // evaluate mate choice in preparation for reproduction i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } 1:5001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.z; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: QtSLiM/recipes/Recipe 18.1 - A minimal tree-seq model.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 5000 late() { sim.treeSeqOutput("./overlay.trees"); } // Section 17.2's recipe, which is a Python script that overlays // neutral mutations onto the .trees file saved here, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: QtSLiM/recipes/Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(simplificationRatio=INF, timeUnit="generations"); initializeMutationRate(0); initializeMutationType("m2", 0.5, "f", 1.0); m2.convertToSubstitution = F; initializeGenomicElementType("g1", m2, 1); initializeGenomicElement(g1, 0, 1e6 - 1); initializeRecombinationRate(3e-10); } 1 late() { sim.addSubpop("p1", 1e5); } 100 late() { sample(p1.haplosomes, 1).addNewDrawnMutation(m2, 5e5); } 100:10000 late() { mut = sim.mutationsOfType(m2); if (mut.size() != 1) stop(sim.cycle + ": LOST"); else if (sum(sim.mutationFrequencies(NULL, mut)) == 1.0) { sim.treeSeqOutput("decap.trees"); sim.simulationFinished(); } } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: QtSLiM/recipes/Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import tskit, pyslim import numpy as np import matplotlib.pyplot as plt # Load the .trees file ts = tskit.load("decap.trees") # no simplify! # Calculate tree heights, giving uncoalesced sites the maximum time def tree_heights(ts): heights = np.zeros(ts.num_trees + 1) for tree in ts.trees(): if tree.num_roots > 1: # not fully coalesced heights[tree.index] = ts.metadata['SLiM']['tick'] else: children = tree.children(tree.root) real_root = tree.root if len(children) > 1 else children[0] heights[tree.index] = tree.time(real_root) heights[-1] = heights[-2] # repeat the last entry for plotting with step return heights # Plot tree heights before recapitation breakpoints = list(ts.breakpoints()) heights = tree_heights(ts) plt.step(breakpoints, heights, where='post') plt.show() # Recapitate! recap = pyslim.recapitate(ts, ancestral_Ne=1e5, recombination_rate=3e-10, random_seed=1) recap.dump("recap.trees") # Plot the tree heights after recapitation breakpoints = list(recap.breakpoints()) heights = tree_heights(recap) plt.step(breakpoints, heights, where='post') plt.show() ================================================ FILE: QtSLiM/recipes/Recipe 18.11 - Optimizing tree-sequence simplification.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, simplification initialize() { setSeed(0); initializeTreeSeq(simplificationInterval=1000); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 10000); } 50001 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 18.2 - Overlaying neutral mutations.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording # This is a Python recipe, to be run after the section 17.1 recipe import msprime, tskit ts = tskit.load("./overlay.trees") ts = ts.simplify() for t in ts.trees(): assert t.num_roots == 1, ("not coalesced! on segment {} to {}".format(t.interval[0], t.interval[1])) mutated = msprime.sim_mutations(ts, rate=1e-7, random_seed=1, keep=True) mutated.dump("./overlay_II.trees") ================================================ FILE: QtSLiM/recipes/Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "g", -0.01, 1.0); // deleterious initializeMutationType("m3", 1.0, "f", 0.05); // introduced initializeGenomicElementType("g1", c(m1, m2), c(0.9, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m3, 10000); defineConstant("PATH", tempdir() + "slim_" + simID + ".trees"); sim.outputFull(PATH); } 1000:100000 late() { if (sim.countOfMutationsOfType(m3) == 0) { if (sum(sim.substitutions.mutationType == m3) == 1) { cat(simID + ": FIXED\n"); sim.simulationFinished(); } else { cat(simID + ": LOST - RESTARTING\n"); sim.readFromPopulationFile(PATH); } } } ================================================ FILE: QtSLiM/recipes/Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, conditional sweep initialize() { initializeSLiMOptions(keepPedigrees=T); initializeTreeSeq(); initializeMutationRate(1e-8); initializeMutationType("m2", 0.5, "g", -0.01, 1.0); // deleterious initializeMutationType("m3", 1.0, "f", 0.05); // introduced initializeGenomicElementType("g1", m2, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // assign tag values to be preserved inds = sortBy(sim.subpopulations.individuals, "pedigreeID"); tags = rdunif(size(inds), 0, 100000); inds.tag = tags; // record tag values and pedigree IDs in metadata metadataDict = Dictionary("tags", tags, "ids", inds.pedigreeID); target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m3, 10000); defineConstant("PATH", tempdir() + "slim_" + simID + ".trees"); sim.treeSeqOutput(PATH, metadata=metadataDict); } 1000:100000 late() { if (sim.countOfMutationsOfType(m3) == 0) { if (sum(sim.substitutions.mutationType == m3) == 1) { cat(simID + ": FIXED\n"); sim.treeSeqOutput("slim_" + simID + "_FIXED.trees"); sim.simulationFinished(); } else { cat(simID + ": LOST - RESTARTING\n"); sim.readFromPopulationFile(PATH); metadataDict = treeSeqMetadata(PATH); tags = metadataDict.getValue("tags"); inds = sortBy(sim.subpopulations.individuals, "pedigreeID"); inds.tag = tags; } } } ================================================ FILE: QtSLiM/recipes/Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { defineConstant("N", 10000); // pop size defineConstant("L", 1e8); // total chromosome length defineConstant("L0", 200e3); // between genes defineConstant("L1", 1e3); // gene length initializeTreeSeq(); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8, L-1); initializeMutationType("m2", 0.5, "g", -(5/N), 1.0); initializeGenomicElementType("g2", m2, 1.0); for (start in seq(from=L0, to=L-(L0+L1), by=(L0+L1))) initializeGenomicElement(g2, start, (start+L1)-1); } 1 early() { sim.addSubpop("p1", N); } 10*N late() { sim.treeSeqOutput("./diversity.trees"); } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: QtSLiM/recipes/Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording # This is a Python recipe; note that it runs the SLiM model internally, below import subprocess, tskit import matplotlib.pyplot as plt import numpy as np # Run the SLiM model and load the resulting .trees subprocess.check_output(["slim", "-m", "-s", "0", "./diversity.slim"]) ts = tskit.load("./diversity.trees") ts = ts.simplify() # Measure the tree height at each base position height_for_pos = np.zeros(int(ts.sequence_length)) for tree in ts.trees(): mean_height = np.mean([tree.time(root) for root in tree.roots]) left, right = map(int, tree.interval) height_for_pos[left: right] = mean_height # Convert heights along chromosome into heights at distances from gene height_for_pos = height_for_pos - np.min(height_for_pos) L, L0, L1 = int(1e8), int(200e3), int(1e3) # total length, length between genes, gene length gene_starts = np.arange(L0, L - (L0 + L1) + 1, L0 + L1) gene_ends = gene_starts + L1 - 1 max_d = L0 // 4 height_for_left_dist = np.zeros(max_d) height_for_right_dist = np.zeros(max_d) for d in range(max_d): height_for_left_dist[d] = np.mean(height_for_pos[gene_starts - d - 1]) height_for_right_dist[d] = np.mean(height_for_pos[gene_ends + d + 1]) height_for_distance = np.hstack([height_for_left_dist[::-1], height_for_right_dist]) distances = np.hstack([np.arange(-max_d, 0), np.arange(1, max_d + 1)]) # Make a simple plot plt.plot(distances, height_for_distance) plt.show() ================================================ FILE: QtSLiM/recipes/Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, migration, dispersal initialize() { defineConstant("L", 1e8); initializeTreeSeq(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.1); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); sim.treeSeqRememberIndividuals(sim.subpopulations.individuals); p1.haplosomes.addNewDrawnMutation(m1, asInteger(L * 0.2)); p2.haplosomes.addNewDrawnMutation(m1, asInteger(L * 0.8)); sim.addSubpop("p3", 1000); p3.setMigrationRates(c(p1, p2), c(0.5, 0.5)); } 2 late() { p3.setMigrationRates(c(p1, p2), c(0.0, 0.0)); p1.setSubpopulationSize(0); p2.setSubpopulationSize(0); } 2: late() { if (sim.mutationsOfType(m1).size() == 0) { sim.treeSeqOutput("./admix.trees"); sim.simulationFinished(); } } 10000 late() { stop("Did not reach fixation of beneficial alleles."); } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: QtSLiM/recipes/Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording # This is a Python recipe; note that it runs the SLiM model internally, below import subprocess, tskit import matplotlib.pyplot as plt import numpy as np # Run the SLiM model and load the resulting .trees file subprocess.check_output(["slim", "-m", "-s", "0", "./admix.slim"]) ts = tskit.load("./admix.trees") # Load the .trees file and assess true local ancestry breaks = np.zeros(ts.num_trees + 1) ancestry = np.zeros(ts.num_trees + 1) for tree in ts.trees(): subpop_sum, subpop_weights = 0, 0 for root in tree.roots: leaves_count = tree.num_samples(root) - 1 # subtract one for the root, which is a sample subpop_sum += tree.population(root) * leaves_count subpop_weights += leaves_count breaks[tree.index] = tree.interval[0] ancestry[tree.index] = subpop_sum / subpop_weights breaks[-1] = ts.sequence_length ancestry[-1] = ancestry[-2] # Make a simple plot plt.plot(breaks, ancestry) plt.show() ================================================ FILE: QtSLiM/recipes/Recipe 18.6 - Measuring the coalescence time of a model I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(checkCoalescence=T); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1: late() { if (sim.treeSeqCoalesced()) { catn(sim.cycle + ": COALESCED"); sim.simulationFinished(); } } 100000 late() { catn("NO COALESCENCE BY CYCLE 100000"); } ================================================ FILE: QtSLiM/recipes/Recipe 18.6 - Measuring the coalescence time of a model II.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, deferred scheduling, variable-length burn-in initialize() { initializeTreeSeq(checkCoalescence=T); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1: late() { if (sim.treeSeqCoalesced()) { catn(community.tick + ": COALESCED"); defineConstant("COALESCE", community.tick); community.deregisterScriptBlock(self); } } COALESCE+100 late() { catn(community.tick + ": FINISHED"); sim.simulationFinished(); } 100000 late() { catn("NO COALESCENCE BY CYCLE 100000"); } ================================================ FILE: QtSLiM/recipes/Recipe 18.7 - Analyzing selection coefficients in Python with tskit I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(); initializeMutationRate(1e-10); initializeMutationType("m1", 0.5, "g", 0.1, 0.1); initializeMutationType("m2", 0.5, "g", -0.1, 0.1); initializeGenomicElementType("g1", c(m1, m2), c(1.0, 1.0)); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 20000 late() { sim.treeSeqOutput("./selcoeff.trees"); } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: QtSLiM/recipes/Recipe 18.7 - Analyzing selection coefficients in Python with tskit II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import tskit ts = tskit.load("selcoeff.trees") # selection coefficients of all selected mutations coeffs = [] for mut in ts.mutations(): md = mut.metadata sel = [x["selection_coeff"] for x in md["mutation_list"]] if any([s != 0 for s in sel]): coeffs += sel b = [x for x in coeffs if x > 0] d = [x for x in coeffs if x < 0] print("Beneficial: " + str(len(b)) + ", mean " + str(sum(b) / len(b))) print("Deleterious: " + str(len(d)) + ", mean " + str(sum(d) / len(d))) ================================================ FILE: QtSLiM/recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history I.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import msprime, pyslim ts = msprime.sim_ancestry(samples=5000, population_size=5000, sequence_length=1e8, recombination_rate=1e-8) slim_ts = pyslim.annotate(ts, model_type="WF", tick=1) slim_ts.dump("coalasex.trees") ================================================ FILE: QtSLiM/recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history II.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording // Part I of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ initialize() { initializeTreeSeq(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.1); initializeGenomicElementType("g1", m2, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 late() { sim.readFromPopulationFile("coalasex.trees"); target = sample(sim.subpopulations.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1: late() { if (sim.mutationsOfType(m2).size() == 0) { print(sim.substitutions.size() ? "FIXED" else "LOST"); sim.treeSeqOutput("coalasex_II.trees"); sim.simulationFinished(); } } 2000 early() { sim.simulationFinished(); } // Part III of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: QtSLiM/recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history III.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import tskit ts = tskit.load("coalasex_II.trees").simplify() for tree in ts.trees(): for root in tree.roots: print(tree.time(root)) ================================================ FILE: QtSLiM/recipes/Recipe 18.9 - Starting a sexual nonWF model with a coalescent history I.py ================================================ # Keywords: Python, nonWF, non-Wright-Fisher, tree-sequence recording, tree sequence recording import msprime, pyslim, random import numpy as np ts = msprime.sim_ancestry(samples=5000, population_size=5000, sequence_length=1e8, recombination_rate=1e-8) tables = ts.dump_tables() pyslim.annotate_tables(tables, model_type="nonWF", tick=1) # add sexes and ages individual_metadata = [ind.metadata for ind in tables.individuals] for md in individual_metadata: md["sex"] = random.choice([pyslim.INDIVIDUAL_TYPE_FEMALE, pyslim.INDIVIDUAL_TYPE_MALE]) md["age"] = random.choice([0, 1, 2, 3, 4]) ims = tables.individuals.metadata_schema tables.individuals.packset_metadata( [ims.validate_and_encode_row(md) for md in individual_metadata]) # add selected mutation mut_ind_id = random.choice(range(tables.individuals.num_rows)) mut_node_id = random.choice(np.where(tables.nodes.individual == mut_ind_id)[0]) mut_node = tables.nodes[mut_node_id] mut_metadata = { "mutation_list": [ { "mutation_type": 2, "selection_coeff": 0.1, "subpopulation": mut_node.population, "slim_time": int(tables.metadata['SLiM']['tick'] - mut_node.time), "nucleotide": -1 } ] } site_num = tables.sites.add_row(position=5000, ancestral_state='') tables.mutations.add_row( node=mut_node_id, site=site_num, derived_state='1', time=mut_node.time, metadata=mut_metadata) slim_ts = tables.tree_sequence() slim_ts.dump("coalsex.trees") ================================================ FILE: QtSLiM/recipes/Recipe 18.9 - Starting a sexual nonWF model with a coalescent history II.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, sexual, tree-sequence recording, tree sequence recording, reproduction() // Part I of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ initialize() { initializeSLiMModelType("nonWF"); initializeTreeSeq(); initializeSex(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.1); m2.convertToSubstitution=T; initializeGenomicElementType("g1", m2, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } reproduction(NULL, "F") { subpop.addCrossed(individual, subpop.sampleIndividuals(1, sex="M")); } 1 early() { sim.readFromPopulationFile("coalsex.trees"); } early() { p0.fitnessScaling = 5000 / p0.individualCount; } 1: late() { if (sim.mutationsOfType(m2).size() == 0) { print(sim.substitutions.size() ? "FIXED" else "LOST"); sim.treeSeqOutput("coalsex_II.trees"); sim.simulationFinished(); } } 2000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 19.1 - A simple neutral nucleotide-based model.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 1e6); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-7)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 19.10 - Varying the mutation rate along the chromosome in a nucleotide-based model.txt ================================================ // Keywords: nucleotide-based, hot spot, cold spot, variable mutation rate initialize() { defineConstant("L", 1e5); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); m1.color = "black"; initializeGenomicElementType("g1", m1, 1.0, mmKimura(1.8e-07, 6e-08)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); ends = c(sort(sample(0:(L-2), 99)), L-1); multipliers = rlnorm(100, 0.0, 0.75); initializeHotspotMap(multipliers, ends); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 19.11 - Modeling GC-biased gene conversion (gBGC).txt ================================================ // Keywords: nucleotide-based, gene conversion, GC-bias, GC biased gene conversion, GC content initialize() { defineConstant("L", 1e5); defineConstant("alpha", 2.5e-6); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(alpha)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-5); initializeGeneConversion(0.7, 1500, 0.80, 0.10); } 1 early() { sim.addSubpop("p1", 500); } 1:500001 early() { if (sim.cycle % 1000 == 1) { cat(sim.cycle + ": "); print(nucleotideFrequencies(sim.chromosomes.ancestralNucleotides())); } } ================================================ FILE: QtSLiM/recipes/Recipe 19.12 - Reading VCF files to create nucleotide-based SNPs.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence, VCF file reading, empirical population, SNPs, 1000 Genomes Project // The input files used here can be downloaded from http://benhaller.com/slim/recipe_19_12_files.zip initialize() { initializeSLiMOptions(nucleotideBased=T); length = initializeAncestralNucleotides("hs37d5_chr22_patched.fa"); defineConstant("L", length); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeMutationTypeNuc("m2", 0.5, "f", 0.0); m2.color = "red"; m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(0.0)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 99); p1.haplosomes.readHaplosomesFromVCF("chr22_filtered.recode.vcf", m1); p1.setSubpopulationSize(1000); } 5 late() { mut = sample(sim.mutations, 1); mut.setMutationType(m2); mut.setSelectionCoeff(0.5); } 1:2000 late() { mut = sim.mutationsOfType(m2); if (mut.size()) { f = sim.mutationFrequencies(p1, mut); catn(sim.cycle + ": " + sim.mutations.size() + ", f = " + f); if (f == 1.0) { catn("\nFIXED in cycle " + sim.cycle); catn(sim.substitutions.size() + " substitutions."); catn(paste(sim.substitutions.nucleotide)); sim.simulationFinished(); } } else { catn(sim.cycle + ": " + sim.mutations.size()); } } ================================================ FILE: QtSLiM/recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models I.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence, sequence-based mutation rate initialize() { defineConstant("L", 1e5); initializeSLiMOptions(nucleotideBased=T); initializeTreeSeq(); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-6)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-6); } 1 early() { sim.addSubpop("p1", 1000); } 1000 late() { sim.treeSeqOutput("recipe_nucleotides.trees"); } ================================================ FILE: QtSLiM/recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models II.py ================================================ # Keywords: Python, nucleotide-based, nucleotide sequence, sequence-based mutation rate import tskit, pyslim import numpy as np ts = tskit.load("recipe_nucleotides.trees") M = [[0 for _ in pyslim.NUCLEOTIDES] for _ in pyslim.NUCLEOTIDES] for mut in ts.mutations(): mut_list = mut.metadata["mutation_list"] k = np.argmax([u["slim_time"] for u in mut_list]) derived_nuc = mut_list[k]["nucleotide"] if mut.parent == -1: acgt = ts.reference_sequence.data[int(ts.site(mut.site).position)] parent_nuc = pyslim.NUCLEOTIDES.index(acgt) else: parent_mut = ts.mutation(mut.parent) assert(parent_mut.site == mut.site) parent_nuc = parent_mut.metadata["mutation_list"][0]["nucleotide"] M[parent_nuc][derived_nuc] += 1 print("{}\t{}\t{}".format('ancestral', 'derived', 'count')) for j, a in enumerate(pyslim.NUCLEOTIDES): for k, b in enumerate(pyslim.NUCLEOTIDES): print("{}\t{}\t{}".format(a, b, M[j][k])) ================================================ FILE: QtSLiM/recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models III.py ================================================ # Keywords: Python, nucleotide-based, nucleotide sequence, sequence-based mutation rate import tskit, pyslim import numpy as np ts = tskit.load("recipe_nucleotides.trees") slim_gen = ts.metadata["SLiM"]["tick"] M = np.zeros((4,4,4,4), dtype='int') for mut in ts.mutations(): pos = ts.site(mut.site).position # skip mutations at the end of the sequence if pos > 0 and pos < ts.sequence_length - 1: mut_list = mut.metadata["mutation_list"] k = np.argmax([u["slim_time"] for u in mut_list]) derived_nuc = mut_list[k]["nucleotide"] pretime = mut.time + 1.0 left_nuc = pyslim.nucleotide_at(ts, mut.node, pos - 1, time = pretime) right_nuc = pyslim.nucleotide_at(ts, mut.node, pos + 1, time = pretime) parent_nuc = pyslim.nucleotide_at(ts, mut.node, pos, time = pretime) M[left_nuc, parent_nuc, right_nuc, derived_nuc] += 1 print("{}\t{}\t{}".format('ancestral', 'derived', 'count')) for j0, a0 in enumerate(pyslim.NUCLEOTIDES): for j1, a1 in enumerate(pyslim.NUCLEOTIDES): for j2, a2 in enumerate(pyslim.NUCLEOTIDES): for k, b in enumerate(pyslim.NUCLEOTIDES): print("{}{}{}\t{}{}{}\t{}".format(a0, a1, a2, a0, b, a2, M[j0, j1, j2, k])) ================================================ FILE: QtSLiM/recipes/Recipe 19.14 - Modeling identity by state (IBS) (uniquing mutations with a mutation() callback).txt ================================================ // Keywords: IBS, identity by state, IBD, identity by descent, unique down initialize() { initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(100)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-4 / 3)); initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(1e-3); } 1 early() { sim.addSubpop("p1", 500); } mutation() { m = sim.subsetMutations(position=mut.position, nucleotide=mut.nucleotide); if (m.length()) return m; return T; } 1000 late() { for (pos in 0:99) { muts = sim.subsetMutations(position=pos); nucs = muts.nucleotide; cat(pos + " : " + paste(nucs)); if (size(nucs) != size(unique(nucs))) cat(" DUPLICATES!"); catn(); } } ================================================ FILE: QtSLiM/recipes/Recipe 19.15 - Modeling identity by state (IBS) (uniquing back-mutations to the ancestral state).txt ================================================ // Keywords: IBS, identity by state, IBD, identity by descent, unique down, back-mutation initialize() { initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(100)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-4 / 3)); initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(1e-3); } 1 early() { sim.addSubpop("p1", 500); } mutation() { m = sim.subsetMutations(position=mut.position, nucleotide=mut.nucleotide); if (m.length()) return m; return T; } late() { // unique new mutations down to the ancestral state muts = sim.mutations; new_muts = muts[muts.originTick == community.tick]; back_muts = NULL; for (mut in new_muts) { pos = mut.position; if (mut.nucleotide == sim.chromosomes.ancestralNucleotides(pos, pos)) back_muts = c(back_muts, mut); } if (size(back_muts)) sim.subpopulations.haplosomes.removeMutations(back_muts); } 1000 late() { for (pos in 0:99) { muts = sim.subsetMutations(position=pos); nucs = muts.nucleotide; ancestral = sim.chromosomes.ancestralNucleotides(pos, pos); cat(pos + " : " + paste(nucs)); if (size(nucs) != size(unique(nucs))) cat(" DUPLICATES!"); if (any(nucs == ancestral)) cat(" BACK-MUTATION (" + ancestral + ")!"); catn(); } } ================================================ FILE: QtSLiM/recipes/Recipe 19.2 - Reading an ancestral nucleotide sequence from a FASTA file.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence initialize() { initializeSLiMOptions(nucleotideBased=T); defineConstant("L", initializeAncestralNucleotides("FASTA.txt")); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmKimura(1.8e-07, 6e-08)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 19.3 - Sequence output from nucleotide-based models.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence initialize() { defineConstant("L", 10); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-5)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { c = sim.chromosomes; catn("Ancestral: " + c.ancestralNucleotides()); catn("Ancestral: " + paste(c.ancestralNucleotides(format="char"))); catn("Ancestral: " + paste(c.ancestralNucleotides(format="integer"))); catn("positions: " + paste(0:(L-1))); catn(); sim.addSubpop("p1", 500); } 5000 late() { catn("Fixed: " + paste(sim.substitutions.nucleotide)); catn("Fixed: " + paste(sim.substitutions.nucleotideValue)); catn("positions: " + paste(sim.substitutions.position)); catn(); c = sim.chromosomes; catn("Ancestral: " + c.ancestralNucleotides()); catn("Ancestral: " + paste(c.ancestralNucleotides(format="char"))); catn("Ancestral: " + paste(c.ancestralNucleotides(format="integer"))); catn("positions: " + paste(0:(L-1))); catn(); g = p1.haplosomes[0]; catn("SNPs: " + paste(g.mutations.nucleotide)); catn("SNPs: " + paste(g.mutations.nucleotideValue)); catn("positions: " + paste(g.mutations.position)); catn(); catn("Derived: " + g.nucleotides()); catn("Derived: " + paste(g.nucleotides(format="char"))); catn("Derived: " + paste(g.nucleotides(format="integer"))); catn("positions: " + paste(0:(L-1))); } ================================================ FILE: QtSLiM/recipes/Recipe 19.4 - Back-mutations, independent mutational lineages, and VCF output.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 10); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-5)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); } 5000 late() { g = p1.sampleIndividuals(5).haplosomes; g.outputHaplosomesToVCF(simplifyNucleotides=F); g.outputHaplosomesToVCF(simplifyNucleotides=T); } ================================================ FILE: QtSLiM/recipes/Recipe 19.5 - Modeling elevated CpG mutation rates and equilibrium nucleotide frequencies.txt ================================================ // Keywords: nucleotide-based, sequence-based mutation rate initialize() { defineConstant("L", 1e5); defineConstant("mu", 7.5e-6); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); mm = mm16To256(mmJukesCantor(mu / 3)); xcg = c("ACG", "CCG", "GCG", "TCG"); xcg_codons = nucleotidesToCodons(paste0(xcg)); mm[xcg_codons,3] = mm[xcg_codons,3] * 20; // rates to T cgx = c("CGA", "CGC", "CGG", "CGT"); cgx_codons = nucleotidesToCodons(paste0(cgx)); mm[cgx_codons,0] = mm[cgx_codons,0] * 20; // rates to A initializeGenomicElementType("g1", m1, 1.0, mutationMatrix=mm); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 10); } 1:10000000 early() { if (sim.cycle % 10000 == 1) { cat(sim.cycle + ": "); print(nucleotideFrequencies(sim.chromosomes.ancestralNucleotides())); } } ================================================ FILE: QtSLiM/recipes/Recipe 19.6 - A nucleotide-based model with introduced non-nucleotide-based mutations.txt ================================================ // Keywords: nucleotide-based, non-nucleotide-based, mixed model initialize() { defineConstant("L", 1e5); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.1); m2.convertToSubstitution = F; m2.color = "red"; initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-7)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); sample(p1.haplosomes, 10).addNewDrawnMutation(m2, 20000); } 2000 late() { print(sim.mutationsOfType(m2)); } ================================================ FILE: QtSLiM/recipes/Recipe 19.7 - Using standard SLiM fitness effects with nucleotides (modeling synonymous sites).txt ================================================ // Keywords: nucleotide-based initialize() { initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(3e5)); mm = mmJukesCantor(2.5e-8); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); // neutral initializeMutationTypeNuc("m2", 0.1, "g", -0.03, 0.2); // deleterious initializeGenomicElementType("g1", c(m1,m2), c(3,3), mm); // pos 1/2 initializeGenomicElementType("g2", c(m1,m2), c(5,1), mm); // pos 3 initializeRecombinationRate(1e-8); types = rep(c(g1,g2), 1e5); starts = repEach(seqLen(1e5) * 3, 2) + rep(c(0,2), 1e5); ends = starts + rep(c(1,0), 1e5); initializeGenomicElement(types, starts, ends); } 1 early() { sim.addSubpop("p1", 500); } 1e6 late() { sub = sim.substitutions; pos3 = (sub.position % 3 == 2); pos12 = !pos3; catn(size(sub) + " substitutions occurred."); catn(mean(sub.mutationType == m1)*100 + "% are neutral."); catn(mean(sub.mutationType == m2)*100 + "% are non-neutral."); catn(); catn(size(sub[pos12]) + " substitutions are at position 1 or 2."); catn(mean(sub[pos12].mutationType == m1)*100 + "% are neutral."); catn(mean(sub[pos12].mutationType == m2)*100 + "% are non-neutral."); catn(); catn(size(sub[pos3]) + " substitutions are at position 3."); catn(mean(sub[pos3].mutationType == m1)*100 + "% are neutral."); catn(mean(sub[pos3].mutationType == m2)*100 + "% are non-neutral."); catn(); } ================================================ FILE: QtSLiM/recipes/Recipe 19.8 - Defining sequence-based fitness effects at the nucleotide level.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 1e4); defineConstant("EFF", c(1.0, 0.1, 1.5, 3.0)); initializeSLiMOptions(nucleotideBased=T); seq = randomNucleotides(100) + 'A' + randomNucleotides(1e4 - 101); initializeAncestralNucleotides(seq); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-7)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { if (sum(sim.mutations.position == 100) == 0) s1.active = 0; } s1 fitnessEffect() { nuc1 = individual.haploidGenome1.nucleotides(100, 100, format="integer"); nuc2 = individual.haploidGenome2.nucleotides(100, 100, format="integer"); return EFF[nuc1] * EFF[nuc2]; } 10000 late() { subs = sim.substitutions[sim.substitutions.position == 100]; for (sub in subs) catn("Sub to " + sub.nucleotide + " in " + sub.fixationTick); } ================================================ FILE: QtSLiM/recipes/Recipe 19.9 - Defining sequence-based fitness effects at the amino acid level.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 1e4); defineConstant("TAA", nucleotidesToCodons("TAA")); defineConstant("TAG", nucleotidesToCodons("TAG")); defineConstant("TGA", nucleotidesToCodons("TGA")); defineConstant("STOP", c(TAA, TAG, TGA)); defineConstant("NONSTOP", (0:63)[match(0:63, STOP) < 0]); codons = sample(NONSTOP, 194, replace=T); seq1 = randomNucleotides(253); seq2 = paste0(codonsToNucleotides(codons, format="char")[0:417]); seq3 = randomNucleotides(200); seq4 = paste0(codonsToNucleotides(codons, format="char")[418:581]); seq5 = randomNucleotides(L-1035); seq = seq1 + seq2 + seq3 + seq4 + seq5; catn("Initial AA sequence: " + codonsToAminoAcids(codons)); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(seq); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-6)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } fitnessEffect() { for (g in individual.haplosomes) { seq = g.nucleotides(253, 670) + g.nucleotides(871, 1034); codons = nucleotidesToCodons(seq); if (sum(match(codons, STOP) >= 0)) return 0.0; } return 1.0; } 100000 late() { catn(sim.substitutions.size() + " fixed mutations."); as1 = sim.chromosomes.ancestralNucleotides(253, 670, "integer"); as2 = sim.chromosomes.ancestralNucleotides(871, 1034, "integer"); as = c(as1, as2); codons = nucleotidesToCodons(as); catn("Final AA sequence: " + codonsToAminoAcids(codons)); } ================================================ FILE: QtSLiM/recipes/Recipe 20.1 - A simple multispecies model.txt ================================================ // Keywords: multispecies species sim initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } ticks all 1 early() { sim.addSubpop("p1", 500); } ticks all 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 20.2 - A two-species model.txt ================================================ // Keywords: multispecies species fox initialize() { initializeSpecies(tickModulo=3, tickPhase=5, avatar="🦊"); } species mouse initialize() { initializeSpecies(tickModulo=1, tickPhase=1, avatar="🐭"); } ticks all 1 early() { fox.addSubpop("p1", 50); mouse.addSubpop("p2", 500); } ticks all 2000 late() { fox.outputFixedMutations(); mouse.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 20.3 - A deterministic host-parasitoid model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); } ticks all late() { x1 = p1.individualCount / S; // host density x2 = p2.individualCount / S; // parasitoid density x1′ = x1 * exp(R - x1/K - A*x2); // x1[t+1] x2′ = x1 * (1 - exp(-A*x2)); // x2[t+1] p1.setSubpopulationSize(asInteger(round(S * x1′))); p2.setSubpopulationSize(asInteger(round(S * x2′))); } ticks all 250 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 20.4 - An individual-based host-parasitoid model I.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); initializeSLiMModelType("nonWF"); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); // tag values in host indicate survival (0) or death (1) } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); // tag values in parasitoid count successful hunts } ticks all first() { hosts = host.subpopulations.individuals; parasitoids = parasitoid.subpopulations.individuals; // assess densities x1 = hosts.size() / S; // host density x2 = parasitoids.size() / S; // parasitoid density // hunt: each parasitoid counts its successes, and // each host tracks whether it was killed parasitoids.tag = 0; P_parasitized = 1 - exp(-A * x2); killed = rbinom(hosts.size(), 1, P_parasitized); hosts.tag = killed; // 1 means killed hunters = sample(parasitoids, sum(killed), replace=T); for (hunter in hunters) hunter.tag = hunter.tag + 1; survivors = hosts[killed == 0]; // competition: kill a fraction of survivors; note // that this is based on pre-parasitism density P_survives = exp(-x1 / K); survived = rbinom(survivors.size(), 1, P_survives); dead = survivors[survived == 0]; // 1 means survived dead.tag = 1; // mark as dead } species host reproduction() { // only hosts tagged 0 (survived) get to reproduce if (individual.tag != 0) return; // reproduce each host with a mean of exp(r) offspring litterSize = rpois(1, exp(R)); if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); } // non-overlapping generations: parents die, offspring live species host survival() { return (individual.age == 0); } species parasitoid survival() { return (individual.age == 0); } ticks all 250 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 20.4 - An individual-based host-parasitoid model II.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); initializeSLiMModelType("nonWF"); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); // tag values in host are unused } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); // tag values in parasitoid count successful hunts } species host reproduction() { // reproduce each host with a mean of exp(r) offspring litterSize = rpois(1, exp(R)); if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); } ticks all early() { hosts = host.subpopulations.individuals; parasitoids = parasitoid.subpopulations.individuals; // first, kill off the parental generation host_age = hosts.age; hosts[host_age > 0].fitnessScaling = 0.0; parasitoid_age = parasitoids.age; parasitoids[parasitoid_age > 0].fitnessScaling = 0.0; // narrow down to the juveniles and assess densities hosts = hosts[host_age == 0]; parasitoids = parasitoids[parasitoid_age == 0]; x1 = hosts.size() / S; // host density x2 = parasitoids.size() / S; // parasitoid density // next, hunt; each parasitoid counts its successes parasitoids.tag = 0; P_parasitized = 1 - exp(-A * x2); luck = rbinom(hosts.size(), 1, P_parasitized); dead = hosts[luck == 1]; dead.fitnessScaling = 0.0; hunters = sample(parasitoids, dead.size(), replace=T); for (hunter in hunters) hunter.tag = hunter.tag + 1; hosts = hosts[luck == 0]; // finally, competition kills a fraction of survivors // this is based on pre-parasitism density P_survives = exp(-x1 / K); luck = rbinom(hosts.size(), 1, P_survives); dead = hosts[luck == 0]; dead.fitnessScaling = 0.0; } ticks all 250 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 20.5 - A continuous-space host-parasitoid model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("SIDE", 10); // one side length; square root of S defineConstant("S", SIDE*SIDE); defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); defineConstant("S_P", 0.5); // parasitoid hunting/mating kernel width defineConstant("S_H", 0.2); // host competition/mating kernel width defineConstant("D_H", 0.2); // host dispersal kernel width defineConstant("CROSS_SCRIPT", "subpop.addCrossed(individual, mate);"); initializeSLiMModelType("nonWF"); // parasitoids looking for things (long search distance) initializeInteractionType(1, "xy", maxDistance=S_P); i1.setInteractionFunction("l", 1.0); // hosts looking for things (short search distance) initializeInteractionType(2, "xy", maxDistance=S_H); i2.setInteractionFunction("l", 1.0); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); initializeSLiMOptions(dimensionality="xy"); // tag values in host indicate survival (0) or death (1) } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); initializeSLiMOptions(dimensionality="xy"); // tag values in parasitoid count successful hunts } ticks all 2: first() { host_pop = host.subpopulations; hosts = host_pop.individuals; parasitoid_pop = parasitoid.subpopulations; parasitoids = parasitoid_pop.individuals; // assess densities, per individual i1.evaluate(c(host_pop, parasitoid_pop)); i2.evaluate(host_pop); parasitoid_density_byhost = i1.localPopulationDensity(hosts, parasitoid_pop); // hunt: each parasitoid counts its successes, // and remembers the positions of its prey; // each host tracks whether it was killed parasitoids.tag = 0; parasitoids.setValue("PREY_POS", NULL); P_parasitized_byhost = 1 - exp(-A * parasitoid_density_byhost); killed = (runif(hosts.size()) < P_parasitized_byhost); hosts.tag = asInteger(killed); // T/1 means killed preys = hosts[killed]; for (prey in preys) { hunter = i1.drawByStrength(prey, 1, parasitoid_pop); preyPos = prey.spatialPosition; preyPos = c(hunter.getValue("PREY_POS"), preyPos); hunter.tag = hunter.tag + 1; hunter.setValue("PREY_POS", preyPos); } unhunted = hosts[!killed]; // competition: kill a fraction of unhunted; note // that this is based on pre-parasitism density host_density_by_unhunted = i2.localPopulationDensity(unhunted, host_pop); P_survives_by_unhunted = exp(-host_density_by_unhunted / K); survived = (runif(unhunted.size()) < P_survives_by_unhunted); dead = unhunted[!survived]; // T means survived dead.tag = 1; // mark as dead } species host reproduction() { // only hosts tagged 0 (survived) get to reproduce if (individual.tag != 0) return; // reproduce each host with a mean of exp(r) offspring mate = i2.drawByStrength(individual); if (mate.size()) { litterSize = rpois(1, exp(R)); if (litterSize > 0) { offspring = sapply(seqLen(litterSize), CROSS_SCRIPT); // vectorized set of offspring spatial positions, based // on the first parent position plus dispersal D_H positions = rep(individual.spatialPosition, litterSize); positions = positions + rnorm(litterSize * 2, 0, D_H); positions = p1.pointReflected(positions); offspring.setSpatialPosition(positions); } } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = i1.drawByStrength(individual); if (mate.size()) { offspring = sapply(seqLen(litterSize), CROSS_SCRIPT); // vectorized set of offspring positions to prey positions offspring.setSpatialPosition(individual.getValue("PREY_POS")); } } } ticks all 1 early() { host.addSubpop("p1", N0_host); p1.setSpatialBounds(c(0, 0, SIDE, SIDE)); p1.individuals.setSpatialPosition(p1.pointUniform(N0_host)); parasitoid.addSubpop("p2", N0_parasitoid); p2.setSpatialBounds(c(0, 0, SIDE, SIDE)); p2.individuals.setSpatialPosition(p2.pointUniform(N0_parasitoid)); } // non-overlapping generations: parents die, offspring live species host survival() { return (individual.age == 0); } species parasitoid survival() { return (individual.age == 0); } ticks all 250 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 20.6 - A coevolutionary host-parasitoid trait-matching model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); defineConstant("S_M", 1.0); // parasitoid/host matching width defineConstant("S_S", 2.0); // stabilizing fitness function width initializeSLiMModelType("nonWF"); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); // tag values in host indicate survival (0) or death (1) // tagF values in host are QTL-based additive phenotypes // one short QTL controlling parasitism avoidance initializeMutationType("m1", 0.5, "n", 0.0, 0.1); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 9999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); // tag values in parasitoid count successful hunts // tagF values in parasitoid are QTL-based additive phenotypes // one short QTL controlling host trait matching initializeMutationType("m2", 0.5, "n", 0.0, 0.1); initializeGenomicElementType("g2", m2, 1.0); initializeGenomicElement(g2, 0, 9999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } species host mutationEffect(m1) { return 1.0; } species parasitoid mutationEffect(m2) { return 1.0; } ticks all 2: first() { hosts = host.subpopulations.individuals; parasitoids = parasitoid.subpopulations.individuals; // assess densities x1 = hosts.size() / S; // host density x2 = parasitoids.size() / S; // parasitoid density // assess matches between hosts and the mean parasitoid host_values = hosts.tagF; parasitoid_values = parasitoids.tagF; mean_parasitoid = mean(parasitoid_values); scale = dnorm(0.0, 0.0, S_M); host_match = dnorm(host_values, mean_parasitoid, S_M) / scale; // hunt: each parasitoid counts its successes, and // each host tracks whether it was killed parasitoids.tag = 0; P_parasitized_byhost = 1 - exp(-A * x2 * host_match); killed = rbinom(hosts.size(), 1, P_parasitized_byhost); hosts.tag = killed; // 1 means killed hunters = sapply(hosts[killed == 1], "sample(parasitoids, 1, " + "weights=dnorm(applyValue.tagF - parasitoid_values, 0.0, S_M));"); for (hunter in hunters) hunter.tag = hunter.tag + 1; survivors = hosts[killed == 0]; // competition: kill a fraction of survivors; note // that this is based on pre-parasitism density P_survives = exp(-x1 / K); survived = rbinom(survivors.size(), 1, P_survives); dead = survivors[survived == 0]; // 1 means survived dead.tag = 1; // mark as dead } species host reproduction() { // only hosts tagged 0 (survived) get to reproduce if (individual.tag != 0) return; // reproduce each host with a mean of exp(r) offspring litterSize = rpois(1, exp(R)); if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); log = community.createLogFile("host-parasite log.txt", logInterval=1); log.addTick(); log.addPopulationSize(host); log.addMeanSDColumns("host", "p1.individuals.tagF;"); log.addPopulationSize(parasitoid); log.addMeanSDColumns("parasitoid", "p2.individuals.tagF;"); } ticks all early() { // calculate phenotypes and implement stabilizing selection scale = dnorm(0.0, 0.0, S_S); hosts = host.subpopulations.individuals; phenotypes = hosts.sumOfMutationsOfType(m1); hosts.fitnessScaling = dnorm(phenotypes, 0.0, S_S) / scale; hosts.tagF = phenotypes; parasitoids = parasitoid.subpopulations.individuals; phenotypes = parasitoids.sumOfMutationsOfType(m2); parasitoids.fitnessScaling = dnorm(phenotypes, 0.0, S_S) / scale; parasitoids.tagF = phenotypes; // non-overlapping generations: parents die, offspring live hosts[hosts.age > 0].fitnessScaling = 0.0; parasitoids[parasitoids.age > 0].fitnessScaling = 0.0; } ticks all 10000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 20.7 - A coevolutionary host-parasite matching-allele model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("L", 1); } species host initialize() { initializeSpecies(avatar="🦌"); // one nucleotide controlling infection initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-3)); initializeGenomicElement(g1, 0, L - 1); initializeRecombinationRate(1e-8); } species parasite initialize() { initializeSpecies(avatar="🐛"); // one nucleotide controlling resistance initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m2", 0.5, "f", 0.0); initializeGenomicElementType("g2", m2, 1.0, mmJukesCantor(1e-3)); initializeGenomicElement(g2, 0, L - 1); initializeRecombinationRate(1e-8); } function (float$)nucleotideFreq(o$ species, s$ nuc) { nucs = species.subpopulations.individuals.haplosomes.nucleotides(); return mean(nucs == nuc); } ticks all 1 early() { host.addSubpop("p0", 1000); parasite.addSubpop("p1", 1000); log = community.createLogFile("host-parasite log.txt", logInterval=1); log.addTick(); log.addCustomColumn("hA", "nucleotideFreq(host, 'A');"); log.addCustomColumn("hC", "nucleotideFreq(host, 'C');"); log.addCustomColumn("hG", "nucleotideFreq(host, 'G');"); log.addCustomColumn("hT", "nucleotideFreq(host, 'T');"); log.addCustomColumn("pA", "nucleotideFreq(parasite, 'A');"); log.addCustomColumn("pC", "nucleotideFreq(parasite, 'C');"); log.addCustomColumn("pG", "nucleotideFreq(parasite, 'G');"); log.addCustomColumn("pT", "nucleotideFreq(parasite, 'T');"); } ticks all late() { // each parasite will pick a random host and try to infect it parasites = p1.individuals; chosen_hosts = sample(p0.individuals, size(parasites), replace=T); for (p in parasites, h in chosen_hosts) { // infection depends upon a match (diploid matching-allele model) all_nucleotides = c(p,h).haplosomes.nucleotides(); if (size(unique(all_nucleotides, preserveOrder=F)) == 1) { // parasite and host are both homozygous for the same nucleotide(s) // with a match, the parasite's fitness increases, the host's decreases p.fitnessScaling = 1.5 * p.fitnessScaling; h.fitnessScaling = 0.5 * h.fitnessScaling; } } } ticks all 2000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 20.8 - Within-host reproduction in a host-pathogen model.txt ================================================ // Keywords: multispecies, interaction, life history, timescale, parasite, infection, SIR, S-I-R species all initialize() { initializeSLiMModelType("nonWF"); defineConstant("K_MONKEY", 100); // monkey carrying capacity defineConstant("F_MONKEY", 3.0); // monkey fecundity defineConstant("G_MONKEY", 20); // relative generation timescale defineConstant("P_TRANSMISSION", 0.0001); // probability of transmission defineConstant("SUPPRESSION_μ", 30000); // optimal size for suppression defineConstant("SUPPRESSION_σ", 10000); // width for suppression defineConstant("SUPPRESSION_STRENGTH", 0.4); // strength of suppression defineConstant("DEATH", 100000); // level for certain death } species monkey initialize() { initializeSpecies(tickModulo=G_MONKEY, avatar="🐵", color="tan3"); initializeSLiMOptions(keepPedigrees=T); initializeSex(); // pedigree IDs are used to identify host-pathogen matches // colors: blue == uninfected, yellow -> red == infected, black == dead } species pathogen initialize() { initializeSpecies(avatar="🦠", color="chartreuse3"); // pathogen.tag is a counter of the next subpop ID to use // subpopulation.tag is the pedigree ID of the host for the subpop } ticks all 2: first() { if (p1.individualCount == 0) stop(monkey.avatar + " extinct"); if (pathogen.subpopulations.size() == 0) stop(pathogen.avatar + " extinct"); } species monkey reproduction(p1, "F") { // monkeys reproduce sexually, non-monogamously litterSize = rpois(1, F_MONKEY); for (i in seqLen(litterSize)) { mate = subpop.sampleIndividuals(1, sex="M"); subpop.addCrossed(individual, mate); } } species pathogen reproduction() { // the pathogen reproduces clonally subpop.addCloned(individual); } ticks all 1 early() { monkey.addSubpop("p1", K_MONKEY); // choose initial hosts carrying the infection initial_hosts = p1.sampleIndividuals(5, replace=F); pathogen.tag = 3; for (initial_host in initial_hosts) { // make a pathogen subpop for the host pathogen_subpop = pathogen.addSubpop(pathogen.tag, 1); pathogen_subpop.tag = initial_host.pedigreeID; pathogen.tag = pathogen.tag + 1; } // log some basic output logfile = community.createLogFile("host_pathogen_log.csv", logInterval=1); logfile.addTick(); logfile.addSubpopulationSize(p1); logfile.addPopulationSize(pathogen); logfile.addCustomColumn("host_count", "pathogen.subpopulations.size();"); logfile.addMeanSDColumns("monkeyAge", "p1.individuals.age;"); } ticks monkey early() { // monkey population regulation p1.fitnessScaling = K_MONKEY / p1.individualCount; } ticks all early() { // horizontal transmission if (p1.individualCount > 1) { pathogenSubpops = pathogen.subpopulations; allPathogens = pathogenSubpops.individuals; isTransmitted = (rbinom(allPathogens.size(), 1, P_TRANSMISSION) == 1); moving = allPathogens[isTransmitted]; for (ind in moving) { // figure out which host we are in hostID = ind.subpopulation.tag; currentHost = monkey.individualsWithPedigreeIDs(hostID); // choose a different host and get its ID newHost = p1.sampleIndividuals(1, exclude=currentHost); newHostID = newHost.pedigreeID; // find/create a subpop for the new host and move to it newSubpop = pathogenSubpops[pathogenSubpops.tag == newHostID]; if (newSubpop.size() == 0) { newSubpop = pathogen.addSubpop(pathogen.tag, 0); pathogen.tag = pathogen.tag + 1; newSubpop.tag = newHostID; // need to incorporate the new subpop in case it receives another pathogenSubpops = c(pathogenSubpops, newSubpop); } newSubpop.takeMigrants(ind); } } } ticks all early() { // disease outcomes: either suppression or mortality suppress_scale = SUPPRESSION_STRENGTH / dnorm(0.0, 0.0, SUPPRESSION_σ); for (pathogenSubpop in pathogen.subpopulations) { popsize = asFloat(pathogenSubpop.individualCount); P_supp = (dnorm(popsize, SUPPRESSION_μ, SUPPRESSION_σ) * suppress_scale); P_death = popsize / DEATH; if (runif(1) < P_supp) { // the infection is suppressed; host lives, pathogen dies pathogenSubpop.removeSubpopulation(); } else if (runif(1) < P_death) { // the host has died; kill it and its pathogens host = monkey.individualsWithPedigreeIDs(pathogenSubpop.tag); monkey.killIndividuals(host); pathogenSubpop.removeSubpopulation(); } } } ticks all early() { // color monkeys by their infection level pathogenSubpops = pathogen.subpopulations; for (host in p1.individuals) { hostID = host.pedigreeID; pathogenSubpop = pathogenSubpops[pathogenSubpops.tag == hostID]; if (pathogenSubpop.size() == 1) { pathogenCount = pathogenSubpop.individualCount; hue = max(0.0, 1.0 - pathogenCount / DEATH) * 0.15; host.color = rgb2color(hsv2rgb(c(hue, 1, 1))); } else host.color = "cornflowerblue"; } } species monkey survival(p1) { // when a monkey dies, any pathogens in it die if (!surviving) { hostID = individual.pedigreeID; pathogenSubpops = pathogen.subpopulations; pathogenSubpop = pathogenSubpops[pathogenSubpops.tag == hostID]; pathogenSubpop.removeSubpopulation(); } return NULL; } ticks all 2000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 4.1 - A basic neutral simulation.txt ================================================ // Keywords: // set up a simple neutral simulation initialize() { // set the overall mutation rate initializeMutationRate(1e-7); // m1 mutation type: neutral initializeMutationType("m1", 0.5, "f", 0.0); // g1 genomic element type: uses m1 for all mutations initializeGenomicElementType("g1", m1, 1.0); // uniform chromosome of length 100 kb initializeGenomicElement(g1, 0, 99999); // uniform recombination along the chromosome initializeRecombinationRate(1e-8); } // create a population of 500 individuals 1 early() { sim.addSubpop("p1", 500); } // run to tick 10000 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 4.1.10 - Using symbolic constants for model parameters.txt ================================================ // Keywords: parameter, constant, symbol, global, define initialize() { // define some global constants for parameters defineConstant("MU", 1e-7); defineConstant("L", 1e5); defineConstant("R", 1e-8); defineConstant("N", 500); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L - 1); initializeRecombinationRate(R); } 1 early() { sim.addSubpop("p1", N); } 20*N early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 4.2.1 - Basic output, Entire population.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { sim.outputFull(); } ================================================ FILE: QtSLiM/recipes/Recipe 4.2.2 - Basic output, Random population sample.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 5000 late() { p1.outputSample(10); } 10000 late() { sim.outputFull(); } ================================================ FILE: QtSLiM/recipes/Recipe 4.2.3 - Basic output, Sampling individuals for output.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.01); initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p1, 0.01); } 10000 late() { allIndividuals = sim.subpopulations.individuals; w = asFloat(allIndividuals.countOfMutationsOfType(m2) + 1); sampledIndividuals = sample(allIndividuals, 10, weights=w); sampledIndividuals.haplosomes.outputHaplosomes(); } ================================================ FILE: QtSLiM/recipes/Recipe 4.2.4 - Basic output, Substitutions.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 5000 late() { p1.outputSample(10); } 10000 late() { sim.outputFull(); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 4.2.5 - Basic output, Automatic logging with LogFile.txt ================================================ // Keywords: migration, dispersal, table output, CSV, TSV, FST initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 999999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); sim.addSubpop("p2", 1000); p1.setMigrationRates(p2, 0.001); p2.setMigrationRates(p1, 0.001); log = community.createLogFile("sim_log.txt", logInterval=10); log.addCycle(); log.addCustomColumn("FST", "calcFST(p1.haplosomes, p2.haplosomes);"); } 20000 late() { } ================================================ FILE: QtSLiM/recipes/Recipe 4.2.6 - Basic output, Custom output with Eidos.txt ================================================ // Keywords: MS format initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); } // custom MS-style output from a multi-subpop sample 2000 late() { // obtain a random sample of haplosomes from the whole population h = sample(sim.subpopulations.haplosomes, 10, T); // get the unique mutations in the sample, sorted by position m = sortBy(unique(h.mutations, preserveOrder=F), "position"); // print the number of segregating sites cat("\n\nsegsites: " + size(m) + "\n"); // print the positions positions = format("%.6f", m.position / sim.chromosomes.lastPosition); cat("positions: " + paste(positions, sep=" ") + "\n"); // print the sampled haplosomes for (haplosome in h) { hasMuts = (match(m, haplosome.mutations) >= 0); cat(paste(asInteger(hasMuts), sep="") + "\n"); } } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.1 - Subpopulation size, Instantaneous changes.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 1000 early() { p1.setSubpopulationSize(100); } 2000 early() { p1.setSubpopulationSize(1000); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth I.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000:1099 early() { newSize = asInteger(p1.individualCount * 1.03); p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth II.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000:1099 early() { newSize = asInteger(round(1.03^(sim.cycle - 999) * 100)); p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth III.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000:2000 early() { if (p1.individualCount < 2000) { newSize = asInteger(round(1.03^(sim.cycle - 999) * 100)); p1.setSubpopulationSize(newSize); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth IV.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000: early() { newSize = round(1.03^(sim.cycle - 999) * 100); if (newSize > 2000) newSize = 2000; p1.setSubpopulationSize(asInteger(newSize)); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth V.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000: early() { newSize = asInteger(round(1.03^(sim.cycle - 999) * 100)); if (newSize >= 2000) { newSize = 2000; community.deregisterScriptBlock(self); } p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.4 - Subpopulation size, Cyclical changes.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1500); } early() { newSize = cos((sim.cycle - 1) / 100) * 500 + 1000; p1.setSubpopulationSize(asInteger(newSize)); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.1.5 - Subpopulation size, Context-dependent changes (Muller's Ratchet).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "e", -0.01); m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(1,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } early() { meanFitness = mean(p1.cachedFitness(NULL)); newSize = asInteger(100 * meanFitness); p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.2.1 - Population structure, Adding subpopulations.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 100); sim.addSubpop("p3", 1000); p1.setMigrationRates(c(p2,p3), c(0.2,0.1)); p2.setMigrationRates(c(p1,p3), c(0.8,0.01)); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.2.2 - Population structure, Removing subpopulations.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 100 early() { sim.addSubpop("p2", 100); } 100:150 early() { migrationProgress = (sim.cycle - 100) / 50; p1.setMigrationRates(p2, 0.2 * migrationProgress); p2.setMigrationRates(p1, 0.8 * migrationProgress); } 1000 early() { sim.addSubpop("p3", 10); } 1000:1100 early() { p3Progress = (sim.cycle - 1000) / 100; p3.setSubpopulationSize(asInteger(990 * p3Progress + 10)); p1.setMigrationRates(p3, 0.1 * p3Progress); p2.setMigrationRates(p3, 0.01 * p3Progress); } 2000 early() { p2.setSubpopulationSize(0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.2.3 - Population structure, Splitting subpopulations.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 100 early() { sim.addSubpopSplit("p2", 100, p1); } 100:150 early() { migrationProgress = (sim.cycle - 100) / 50; p1.setMigrationRates(p2, 0.2 * migrationProgress); p2.setMigrationRates(p1, 0.8 * migrationProgress); } 1000 early() { sim.addSubpopSplit("p3", 10, p2); } 1000:1100 early() { p3Progress = (sim.cycle - 1000) / 100; p3.setSubpopulationSize(asInteger(990 * p3Progress + 10)); p1.setMigrationRates(p3, 0.1 * p3Progress); p2.setMigrationRates(p3, 0.01 * p3Progress); } 2000 early() { p2.setSubpopulationSize(0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.2.4 - Population structure, Joining subpopulations.txt ================================================ // Keywords: migration, admixture, merging, combining initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 1000); } 1000 early() { // set up p3 to generate itself entirely from migrants sim.addSubpop("p3", 300); p3.setMigrationRates(c(p1, p2), c(0.75, 0.25)); } 1000 late() { // remove the source subpopulations p3.setMigrationRates(c(p1, p2), c(0.0, 0.0)); p1.setSubpopulationSize(0); p2.setSubpopulationSize(0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.3.1 - Migration and admixture, A linear stepping-stone model.txt ================================================ // Keywords: migration, dispersal initialize() { defineConstant("COUNT", 10); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { for (i in 0:(COUNT-1)) sim.addSubpop(i, 500); subpops = sim.subpopulations; for (i in 1:(COUNT-1)) subpops[i].setMigrationRates(i-1, 0.2); for (i in 0:(COUNT-2)) subpops[i].setMigrationRates(i+1, 0.05); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.3.2 - Migration and admixture, A non-spatial metapopulation.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { subpopCount = 5; for (i in 1:subpopCount) sim.addSubpop(i, 500); for (i in 1:subpopCount) for (j in 1:subpopCount) if (i != j) sim.subpopulations[i-1].setMigrationRates(j, 0.05); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.3.3 - Migration and admixture, A two-dimensional subpopulation matrix.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { metapopSide = 3; // number of subpops along one side of the grid metapopSize = metapopSide * metapopSide; for (i in 1:metapopSize) sim.addSubpop(i, 500); subpops = sim.subpopulations; for (x in 1:metapopSide) for (y in 1:metapopSide) { destID = (x - 1) + (y - 1) * metapopSide + 1; destSubpop = subpops[destID - 1]; if (x > 1) // left to right destSubpop.setMigrationRates(destID - 1, 0.05); if (x < metapopSide) // right to left destSubpop.setMigrationRates(destID + 1, 0.05); if (y > 1) // top to bottom destSubpop.setMigrationRates(destID - metapopSide, 0.05); if (y < metapopSide) // bottom to top destSubpop.setMigrationRates(destID + metapopSide, 0.05); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.3.4 - Migration and admixture, A random, sparse spatial metapopulation.txt ================================================ // Keywords: migration, dispersal, SLiMgui initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.3); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { mSide = 10; // number of subpops along one side of the grid for (i in 1:(mSide * mSide)) sim.addSubpop(i, 500); subpops = sim.subpopulations; for (x in 1:mSide) for (y in 1:mSide) { destID = (x - 1) + (y - 1) * mSide + 1; ds = subpops[destID - 1]; if (x > 1) // left to right ds.setMigrationRates(destID - 1, runif(1, 0.0, 0.05)); if (x < mSide) // right to left ds.setMigrationRates(destID + 1, runif(1, 0.0, 0.05)); if (y > 1) // top to bottom ds.setMigrationRates(destID - mSide, runif(1, 0.0, 0.05)); if (y < mSide) // bottom to top ds.setMigrationRates(destID + mSide, runif(1, 0.0, 0.05)); // set up SLiMgui's population visualization nicely xd = ((x - 1) / (mSide - 1)) * 0.9 + 0.05; yd = ((y - 1) / (mSide - 1)) * 0.9 + 0.05; ds.configureDisplay(c(xd, yd), 0.4); } // remove 25% of the subpopulations subpops[sample(0:99, 25)].setSubpopulationSize(0); // introduce a beneficial mutation target_subpop = sample(sim.subpopulations, 1); sample(target_subpop.haplosomes, 10).addNewDrawnMutation(m2, 20000); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.3.5 - Migration and admixture, Reading a migration matrix from a file.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { for (i in 1:3) sim.addSubpop(i, 1000); subpops = sim.subpopulations; // this file is in the recipe archive at http://benhaller.com/slim/SLiM_Recipes.zip lines = readFile("migration.csv"); lines = lines[substr(lines, 0, 1) != "//"]; for (line in lines) { fields = strsplit(line, ","); i = asInteger(fields[0]); j = asInteger(fields[1]); m = asFloat(fields[2]); if (i != j) { p_i = subpops[subpops.id == i]; p_j = subpops[subpops.id == j]; p_j.setMigrationRates(p_i, m); } } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: QtSLiM/recipes/Recipe 5.4 - The Gravel et al. (2011) model of human evolution I.txt ================================================ // Keywords: migration, dispersal // Model based on Gravel et al. 2011, doi:10.1073/pnas.1019276108 (hereafter "paper") initialize() { initializeMutationRate(2.36e-8); // theta=3813.75 initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 9999); // paper uses 4.04e6, 5007837, etc. initializeRecombinationRate(1e-8); } // INITIALIZE the ancestral African population of size 7310 1 early() { sim.addSubpop("p1", asInteger(round(7310.370867595234))); } // paper rounds to 7310 // END BURN-IN period of 10*N=73104 generations (specific to SLiM recipe); EXPAND the African population // This occurs (5919.131117 generations)*(25 years)=147978 yr ago; paper rounds to 5920 gens (148000 yr) // Thus, simulation should end at generation 1+73104+5919.131117=79024 73105 early() { p1.setSubpopulationSize(asInteger(round(14474.54608753566))); } // paper rounds to 14474 // SPLIT Eurasians (p2) from Africans (p1) and SET UP MIGRATION between them // This occurs 2056.396652 generations (51409.9163 years) ago; paper rounds to 2040 gens (51000 yr) // Relative to beginning, this is generation 79024-2056.396652=76968 76968 early() { sim.addSubpopSplit("p2", asInteger(round(1861.288190027689)), p1); // paper rounds to 1861 p1.setMigrationRates(c(p2), c(15.24422112e-5)); // paper rounds to 15e-5 p2.setMigrationRates(c(p1), c(15.24422112e-5)); // paper rounds to 15e-5 } // SPLIT p2 into European (p2) and East Asian (p3) subpopulations; RESIZE; SET UP MIGRATION between them // This occurs 939.8072428 generations (23495.18107 years) ago; paper rounds to 920 gens (23000 yr) // Relative to beginning, this is generation 79024-939.8072428=78084 78084 early() { sim.addSubpopSplit("p3", asInteger(round(553.8181989)), p2); // paper rounds to 554 p2.setSubpopulationSize(asInteger(round(1032.1046957333444))); // reduce European size; paper rounds to 1032 // Set migration rates for the rest of the simulation p1.setMigrationRates(c(p2, p3), c(2.54332678e-5, 0.7770583877e-5)); // paper rounds to c(2.5e-5, 0.78e-5) p2.setMigrationRates(c(p1, p3), c(2.54332678e-5, 3.115817913e-5)); // paper rounds to c(2.5e-5, 3.11e-5) p3.setMigrationRates(c(p1, p2), c(0.7770583877e-5, 3.115817913e-5)); // paper rounds to c(0.78e-5, 3.11e-5) } // SET UP EXPONENTIAL GROWTH in Europe (p2) and East Asia (p3) // Where N(0) is the base subpopulation size and t = gen - 78084: // N(Europe) should be int(round(N(0) * (1 + 0.003784324268)^t)), i.e., growth is r=0.38% per generation // N(East Asia) should be int(round(N(0) * (1 + 0.004780219543)^t)), i.e., growth is r=0.48% per generation 78084:79024 early() { t = sim.cycle - 78084; p2_size = round(1032.1046957333444 * (1 + 0.003784324268)^t); // paper rounds to N(0)=1032 and r=0.0038 p3_size = round(553.8181989 * (1 + 0.004780219543)^t); // paper rounds to N(0)=554 and r=0.0048 p2.setSubpopulationSize(asInteger(p2_size)); p3.setSubpopulationSize(asInteger(p3_size)); } // OUTPUT AND TERMINATE // Generation 79024 is the present, i.e., 1 initialize + 73104 burn-in + 5919 evolution 79024 late() { p1.outputSample(216); // YRI phase 3 diploid sample of size 108 p2.outputSample(198); // CEU phase 3 diploid sample of size 99 p3.outputSample(206); // CHB phase 3 diploid sample of size 103 } ================================================ FILE: QtSLiM/recipes/Recipe 5.4 - The Gravel et al. (2011) model of human evolution II.txt ================================================ /// # Gravel Model in SLiM /// #### _(with Jump Menu annotations)_ /// initialize() { initializeMutationRate(2.36e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 9999); initializeRecombinationRate(1e-8); } /// /// **Demography:** 1 early() /* create p1 */ { sim.addSubpop("p1", asInteger(round(7310.370867595234))); } 73105 early() /* end burn-in */ { p1.setSubpopulationSize(asInteger(round(14474.54608753566))); } 76968 early() /* split p2 from p1 */ { sim.addSubpopSplit("p2", asInteger(round(1861.288190027689)), p1); p1.setMigrationRates(c(p2), c(15.24422112e-5)); p2.setMigrationRates(c(p1), c(15.24422112e-5)); } 78084 early() /* split p3 from p2 */ { sim.addSubpopSplit("p3", asInteger(round(553.8181989)), p2); p2.setSubpopulationSize(asInteger(round(1032.1046957333444))); p1.setMigrationRates(c(p2, p3), c(2.54332678e-5, 0.7770583877e-5)); p2.setMigrationRates(c(p1, p3), c(2.54332678e-5, 3.115817913e-5)); p3.setMigrationRates(c(p1, p2), c(0.7770583877e-5, 3.115817913e-5)); } 78084:79024 early() /* exponential growth */ { t = sim.cycle - 78084; p2_size = round(1032.1046957333444 * (1 + 0.003784324268)^t); p3_size = round(553.8181989 * (1 + 0.004780219543)^t); p2.setSubpopulationSize(asInteger(p2_size)); p3.setSubpopulationSize(asInteger(p3_size)); } /***/ /** **Final output:** */ 79024 late() { p1.outputSample(216); p2.outputSample(198); p3.outputSample(206); } ================================================ FILE: QtSLiM/recipes/Recipe 5.5 - Rescaling population sizes to improve simulation performance I.txt ================================================ // Keywords: rescale initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", -0.01); initializeGenomicElementType("g1", c(m1,m2), c(0.8,0.2)); initializeGenomicElement(g1, 0, 9999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 50000 early() { p1.setSubpopulationSize(1000); } 55000 early() { p1.setSubpopulationSize(5000); } 60000 late() { p1.outputSample(10); } ================================================ FILE: QtSLiM/recipes/Recipe 5.5 - Rescaling population sizes to improve simulation performance II.txt ================================================ // Keywords: rescale initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", -0.1); initializeGenomicElementType("g1", c(m1,m2), c(0.8,0.2)); initializeGenomicElement(g1, 0, 9999); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 5000 early() { p1.setSubpopulationSize(100); } 5500 early() { p1.setSubpopulationSize(500); } 6000 late() { p1.outputSample(10); } ================================================ FILE: QtSLiM/recipes/Recipe 6.1 - Genomic structure, Part I (Mutation types and fitness effects).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 6.2 - Genomic structure, Part II (Genomic element types).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial initializeGenomicElementType("g1", c(m2,m3,m4), c(2,8,0.1)); // exon initializeGenomicElementType("g2", c(m1,m3), c(9,1)); // intron initializeGenomicElementType("g3", c(m1), 1); // non-coding initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 6.3 - Genomic structure, Part III (Chromosome organization).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial initializeGenomicElementType("g1", c(m2,m3,m4), c(2,8,0.1)); // exon initializeGenomicElementType("g2", c(m1,m3), c(9,1)); // intron initializeGenomicElementType("g3", c(m1), 1); // non-coding // Generate random genes along an approximately 100000-base chromosome base = 0; while (base < 100000) { // make a non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); base = base + nc_length; // make first exon ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; // make additional intron-exon pairs do { in_length = asInteger(rlnorm(1, log(100), log(1.5))) + 10; initializeGenomicElement(g2, base, base + in_length - 1); base = base + in_length; ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; } while (runif(1) < 0.8); // 20% probability of stopping } // final non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); // single recombination rate initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 6.4 - Genomic structure, Part IV (Custom display colors in SLiMgui).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial m1.color = "gray40"; m2.color = "gray40"; m3.color = "red"; m4.color = "green"; m1.colorSubstitution = "gray20"; m2.colorSubstitution = "gray20"; m3.colorSubstitution = "#550000"; m4.colorSubstitution = "#005500"; initializeGenomicElementType("g1", c(m2,m3,m4), c(2,8,0.1)); // exon initializeGenomicElementType("g2", c(m1,m3), c(9,1)); // intron initializeGenomicElementType("g3", c(m1), 1); // non-coding g1.color = "cornflowerblue"; g2.color = "#00009F"; g3.color = "black"; // Generate random genes along an approximately 100000-base chromosome base = 0; while (base < 100000) { // make a non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); base = base + nc_length; // make first exon ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; // make additional intron-exon pairs do { in_length = asInteger(rlnorm(1, log(100), log(1.5))) + 10; initializeGenomicElement(g2, base, base + in_length - 1); base = base + in_length; ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; } while (runif(1) < 0.8); // 20% probability of stopping } // final non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); // single recombination rate initializeRecombinationRate(1e-8); } 1 early() { sim.chromosomes.colorSubstitution = ""; sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.1.1 - Reproduction, Enabling separate sexes.txt ================================================ // Keywords: sexual initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.1.2 - Reproduction, Sex ratios I.txt ================================================ // Keywords: sexual, sex ratio initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500, 0.6); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.1.2 - Reproduction, Sex ratios II.txt ================================================ // Keywords: sexual, sex ratio initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1: early() { p1.setSexRatio(runif(1, 0.3, 0.7)); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.1.3 - Reproduction, Selfing in hermaphroditic populations.txt ================================================ // Keywords: selfing initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.setSelfingRate(0.8); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.1.4 - Reproduction, Cloning I.txt ================================================ // Keywords: clonal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.setCloningRate(0.1); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.1.4 - Reproduction, Cloning II.txt ================================================ // Keywords: clonal, sexual initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.setCloningRate(c(0.5,0.0)); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.2.1 - Recombination, Making a random recombination map.txt ================================================ // Keywords: recombination rate map, recombination map initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); // 1000 random recombination regions ends = c(sort(sample(0:99998, 999)), 99999); rates = runif(1000, 1e-9, 1e-7); initializeRecombinationRate(rates, ends); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.2.2 - Recombination, Reading a recombination map from a file.txt ================================================ // Keywords: recombination rate map, recombination map initialize() { defineConstant("L", 23011544); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); // read Drosophila 2L map from Comeron et al. 2012; this is in the // recipe archive at http://benhaller.com/slim/SLiM_Recipes.zip initializeRecombinationRateFromFile("Comeron_100kb_chr2L.txt", L-1); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.2.3 - Recombination, Unlinked loci.txt ================================================ // Keywords: unlinked loci, free recombination, linkage disequilibrium initialize() { initializeMutationRate(1e-5); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(0.5); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.2.4 - Recombination, Gene conversion.txt ================================================ // Keywords: gene conversion initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeGeneConversion(0.2, 500, 1.0); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.3.1 - Multiple diploid autosomes.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "g", -0.03, 0.2); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElementType("g2", c(m1, m2), c(1,2)); initializeChromosome(1, 1e5); initializeGenomicElement(g1, 0, 1e5-1); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); initializeChromosome(2, 2e5); initializeGenomicElement(g1, 0, 1e5-1); initializeGenomicElement(g2, 1e5, 1e5+5e4-1); initializeGenomicElement(g1, 1e5+5e4, 2e5-1); initializeMutationRate(2e-7); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.3.2 - Clonal haploids and chromosome types.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeMutationType("m1", 1.0, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, type="H"); initializeGenomicElement(g1, 0, 1e5-1); initializeMutationRate(1e-7); initializeRecombinationRate(0.0); } 1 early() { sim.addSubpop("p1", 500); p1.setCloningRate(1.0); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.3.3 - Haploids with recombination.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeMutationType("m1", 1.0, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, 1e5, type="H"); initializeGenomicElement(g1); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.3.4 - Sex-chromosome evolution and null haplosomes.txt ================================================ // Keywords: multiple chromosomes initialize() { defineConstant("X_LEN", 156040895); defineConstant("Y_LEN", 57227415); initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, X_LEN, type="X", symbol="X"); initializeGenomicElement(g1); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); initializeChromosome(2, Y_LEN, type="Y", symbol="Y"); initializeGenomicElement(g1); initializeMutationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.3.5 - Modeling the full human genome.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); // length data: https://www.ncbi.nlm.nih.gov/grc/human/data, // Human Genome Assembly GRCh38.p14, 2022-02-03 ids = 1:24; symbols = c(1:22, "X", "Y"); lengths = c(248956422, 242193529, 198295559, 190214555, 181538259, 170805979, 159345973, 145138636, 138394717, 133797422, 135086622, 133275309, 114364328, 107043718, 101991189, 90338345, 83257441, 80373285, 58617616, 64444167, 46709983, 50818468, 156040895, 57227415); types = c(rep("A", 22), "X", "Y"); for (id in ids, symbol in symbols, length in lengths, type in types) { initializeChromosome(id, length, type, symbol); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // not used for the Y initializeGenomicElement(g1); } } 1 early() { sim.addSubpop("p1", 100); } 1000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.3.6 - A model of bryophytes with UV sex determination.txt ================================================ // Keywords: bryophytes, UV sex chromosomes, UV sex determination, haploid recombination, mosses initialize() { initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); for (id in 1:3, type in c("H", "W", "Y"), symbol in c("A", "U", "V")) { initializeChromosome(id, 1e7, type=type, symbol=symbol); initializeMutationRate(1e-7); initializeGenomicElement(g1); initializeRecombinationRate(1e-8); } } 1 early() { sim.addSubpop("p1", 500); } 20000 late() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 8.3.7 - Output from multiple-chromosome models.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); for (id in 1:3, type in c("A", "X", "Y")) { initializeChromosome(id, 1e6, type=type, symbol=type); initializeGenomicElement(g1); initializeMutationRate(1e-8); initializeRecombinationRate(1e-8); } } 1 early() { sim.addSubpop("p1", 500); } 100 late() { sim.outputFull(); inds = p1.sampleIndividuals(5); inds.outputIndividuals(); inds.outputIndividualsToVCF(); } ================================================ FILE: QtSLiM/recipes/Recipe 9.1 - Introducing adaptive mutations.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000:100000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); cat(ifelse(fixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.10 - Tracking the fate of background mutations.txt ================================================ // Keywords: initialize() { defineConstant("L", 3e6); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.8, "f", 0.5); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); defineConstant("BACKGROUND", target.mutations); mut = target.addNewDrawnMutation(m2, asInteger(L/2)); defineConstant("SWEEP", mut); } 1000: late() { if (!SWEEP.isSegregating & !SWEEP.isFixed) stop("LOST"); } 1500 late() { nonSeg = BACKGROUND[!BACKGROUND.isSegregating]; fixed = nonSeg[nonSeg.isFixed]; lost = nonSeg[!nonSeg.isFixed]; writeFile("fixed.txt", paste(fixed.position, sep=", ")); writeFile("lost.txt", paste(lost.position, sep=", ")); } ================================================ FILE: QtSLiM/recipes/Recipe 9.11 - Effective population size versus census population size.txt ================================================ // Keywords: Ne, parameter estimation initialize() { defineGlobal("N", 1000); defineGlobal("L", 1e7); defineGlobal("MU", 1e-7); defineGlobal("R", 1e-8); defineGlobal("S", 2.0); initializeSLiMOptions(keepPedigrees=T); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", S); // sweep m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(R); } 1 late() { sim.addSubpop("p1", N); p1.setValue("previous_N", p1.individualCount); defineConstant("LOG", community.createLogFile("Ne_log.csv")); LOG.addCycle(); LOG.addCustomColumn("N(t-1)", "p1.getValue('previous_N');"); LOG.addCustomColumn("N(t)", "p1.individualCount;"); LOG.addCustomColumn("freq", "mutTypeFrequency(m2);"); LOG.addCustomColumn("Ne_heterozygosity", "estimateNe_Heterozygosity(p1);"); LOG.addCustomColumn("Ne_inbreeding", "estimateNe_Inbreeding(p1);"); } 2: late() { LOG.logRow(); p1.setValue("previous_N", p1.individualCount); } 10000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, integerDiv(L, 2)); } 20000 late() { sim.simulationFinished(); } function (float)mutTypeFrequency(o$ mutType) { muts = sim.mutationsOfType(mutType); if (muts.size() > 0) return sim.mutationFrequencies(NULL, muts); return NULL; } function (float)estimateNe_Heterozygosity(o$ subpop, [No$ chromosome = NULL]) { if (isNULL(chromosome)) { if (size(sim.chromosomes) == 1) chromosome = sim.chromosomes; else stop("ERROR: in a multi-chrom model, a chromosome must be supplied."); } haplosomes = subpop.haplosomesForChromosomes(chromosome, includeNulls=F); pi = calcHeterozygosity(haplosomes); return pi / (4 * MU); } function (integer)tabulateFecundity(o$ subpop, i$ previous_N) { parentIDs = subpop.individuals.pedigreeParentIDs; rescaledParentIDs = parentIDs - min(parentIDs); return tabulate(rescaledParentIDs, previous_N - 1); } function (float)estimateNe_Inbreeding(o$ subpop) { previous_N = subpop.getValue("previous_N"); k = tabulateFecundity(subpop, previous_N); return (previous_N * mean(k) - 2) / (mean(k) - 1 + var(k) / mean(k)); } ================================================ FILE: QtSLiM/recipes/Recipe 9.12 - Observing the site frequency spectrum (SFS) during selective sweeps.txt ================================================ // Keywords: site frequency spectrum, SFS, population genetics, statistics, custom plotting initialize() { setSeed(4806519412125461529); defineConstant("N", 1000); defineConstant("L", 1e7); defineConstant("MU", 1e-7); defineConstant("BINCOUNT", 100); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.5); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } initialize() { // Calculate the expected SFS with the same bins as our observed SFS. // This is tricky because we need to calculate it for the number of samples // we have (i.e., number of haplosomes), and then re-bin it; maybe there is // an easier way. Note that 10000000 is just a constant large enough to // avoid rounding artifacts in the final curve; it is basically the number // of "mutations" scattered across the original expected SFS in order to // re-bin it to the final bin count. Not sure this math is exactly correct. // It would be great to have a calcExpectedSFS() function built in; if you // know how to do that exactly correctly, for counts as well as density, // then please volunteer! expected = 1 / (1:(2*N-1)); expected = expected / sum(expected); expected = asInteger(round(expected * 10000000)); bins = asInteger(round(repEach(1:(2*N-1), expected) * (BINCOUNT / (2*N-1)))); tallies = tabulate(bins, maxbin=BINCOUNT-1); defineConstant("EXPECTED_SFS", tallies / sum(tallies)); } 1 early() { // make a subpop and start drifting towards equilibrium sim.addSubpop("p1", N); } 20*N early() { // start generating rare m2 sweep mutations g1.setMutationFractions(c(m1, m2), c(1, 0.000001)); } 25*N early() { // stop generating m2 mutations and allow re-equilibration g1.setMutationFractions(m1, 1); } 1:(35*N) late() { updatePlot(); } function (void)updatePlot(void) { if (exists("slimgui")) { // get the empirical population SFS sfs_all = calcSFS(BINCOUNT); sfs_m2 = calcSFS(BINCOUNT, muts=sim.mutationsOfType(m2)); x = seq(from=0.0, to=1.0, length=BINCOUNT+1) + 0.5/BINCOUNT; x = x[0:(length(x)-2)]; // make a plot of the observed vs. expected SFS plot = slimgui.createPlot("Site Frequency Spectrum", xrange=c(0,1), yrange=c(0,1), xlab="Mutation frequency", ylab="Density (sqrt-transformed)"); plot.lines(x=x, y=sqrt(sfs_all), color="black", lwd=3); plot.lines(x=x, y=sqrt(EXPECTED_SFS), color="chartreuse2", lwd=2); plot.lines(x=x, y=sqrt(sfs_m2), color="red", lwd=1); plot.addLegend("topRight"); plot.legendLineEntry("observed", color="black", lwd=3); plot.legendLineEntry("expected", color="chartreuse2", lwd=2); plot.legendLineEntry("m2 (sweeps)", color="red", lwd=2); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.2 - Making sweeps conditional on fixation.txt ================================================ // Keywords: conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { // save this run's identifier, used to save and restore defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // save the state of the simulation sim.outputFull(tempdir() + "slim_" + simID + ".txt"); // introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000:100000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); if (fixed) { cat(simID + ": FIXED\n"); sim.simulationFinished(); } else { cat(simID + ": LOST - RESTARTING\n"); // go back to tick 1000 sim.readFromPopulationFile(tempdir() + "slim_" + simID + ".txt"); // start a newly seeded run setSeed(rdunif(1, 0, asInteger(2^62) - 1)); // re-introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } } } ================================================ FILE: QtSLiM/recipes/Recipe 9.3 - Making sweeps conditional on establishment.txt ================================================ // Keywords: conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { // save this run's identifier, used to save and restore defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // save the state of the simulation sim.outputFull(tempdir() + "slim_" + simID + ".txt"); // introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000: late() { mut = sim.mutationsOfType(m2); if (size(mut) == 1) { if (sim.mutationFrequencies(NULL, mut) > 0.1) { cat(simID + ": ESTABLISHED\n"); community.deregisterScriptBlock(self); } } else { cat(simID + ": LOST - RESTARTING\n"); // go back to tick 1000 sim.readFromPopulationFile(tempdir() + "slim_" + simID + ".txt"); // start a newly seeded run setSeed(rdunif(1, 0, asInteger(2^62) - 1)); // re-introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } } 10000 early() { sim.simulationFinished(); } ================================================ FILE: QtSLiM/recipes/Recipe 9.4 - Partial sweeps.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000:10000 late() { mut = sim.mutationsOfType(m2); if (size(mut) == 0) sim.simulationFinished(); else if (mut.selectionCoeff != 0.0) if (sim.mutationFrequencies(NULL, mut) >= 0.5) mut.setSelectionCoeff(0.0); } ================================================ FILE: QtSLiM/recipes/Recipe 9.5.1 - A soft sweep from recurrent de novo mutations in a large population.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-5); initializeMutationType("m1", 0.45, "f", 0.5); // sweep mutation m1.convertToSubstitution = F; m1.mutationStackPolicy = "f"; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 0); initializeRecombinationRate(0); } 1 early() { sim.addSubpop("p1", 100000); } 1:10000 early() { counts = p1.haplosomes.countOfMutationsOfType(m1); freq = mean(counts > 0); if (freq == 1.0) { cat("\nTotal mutations: " + size(sim.mutations) + "\n\n"); for (mut in sortBy(sim.mutations, "originTick")) { mutFreq = mean(p1.haplosomes.containsMutations(mut)); cat("Origin " + mut.originTick + ": " + mutFreq + "\n"); } sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.5.2 - A soft sweep with a fixed de novo mutation schedule.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.45, "f", 0.5); // sweep mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.tag = 0; // indicate that a mutation has not yet been seen } 1000:1100 late() { if (sim.cycle % 10 == 0) { target = sample(p1.haplosomes, 1); if (target.countOfMutationsOfType(m2) == 0) target.addNewDrawnMutation(m2, 10000); } } 1:10000 late() { if (p1.tag != sim.countOfMutationsOfType(m2)) { if (any(sim.substitutions.mutationType == m2)) { cat("Hard sweep ended in cycle " + sim.cycle + "\n"); sim.simulationFinished(); } else { p1.tag = sim.countOfMutationsOfType(m2); cat("Cycle " + sim.cycle + ": " + p1.tag + " lineage(s)\n"); if ((p1.tag == 0) & (sim.cycle > 1100)) { cat("Sweep failed to establish.\n"); sim.simulationFinished(); } } } if (all(p1.haplosomes.countOfMutationsOfType(m2) > 0)) { cat("Soft sweep ended in cycle " + sim.cycle + "\n"); cat("Frequencies:\n"); print(sim.mutationFrequencies(p1, sim.mutationsOfType(m2))); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.5.3 - A soft sweep with a random de novo mutation schedule.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.1, "f", 0.5); // sweep mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); gens = cumSum(rpois(10, 10)); // make a vector of start gens gens = gens + (1000 - min(gens)); // align to start at 1000 defineConstant("Z", max(gens)); // remember the last gen defineConstant("ADD_GENS", gens); // schedule the add events } ADD_GENS late() { target = sample(p1.haplosomes, 1); mut = sim.mutationsOfType(m2); if (mut.size() > 0) target.addMutations(mut); else target.addNewDrawnMutation(m2, 10000); } 1:10000 late() { if (any(sim.substitutions.mutationType == m2)) { catn("Sweep completed in cycle " + sim.cycle + "."); sim.simulationFinished(); } else if ((sim.countOfMutationsOfType(m2) == 0) & (sim.cycle > Z)) { catn("Soft sweep failed to establish."); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.6.1 - A sweep from standing variation at a random locus.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 1.0, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { muts = sim.mutations; muts = muts[sim.mutationFrequencies(p1, muts) > 0.1]; if (size(muts)) { mut = sample(muts, 1); mut.setSelectionCoeff(0.5); } else { cat("No contender of sufficient frequency found.\n"); } } 1000:10000 late() { if (sum(sim.mutations.selectionCoeff) == 0.0) { if (sum(sim.substitutions.selectionCoeff) == 0.0) cat("Sweep mutation lost in cycle " + sim.cycle + "\n"); else cat("Sweep mutation reached fixation.\n"); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.6.2 - A sweep from standing variation at a predetermined locus.txt ================================================ // Keywords: conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.0); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { // save this run's identifier, used to save and restore defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // save the state of the simulation sim.outputFull(tempdir() + "slim_" + simID + ".txt"); // introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000: late() { mut = sim.mutationsOfType(m2); if (size(mut) == 1) { if (sim.mutationFrequencies(NULL, mut) > 0.1) { cat(simID + ": ESTABLISHED - CONVERTING TO BENEFICIAL\n"); mut.setSelectionCoeff(0.5); community.deregisterScriptBlock(self); } } else { cat(simID + ": LOST BEFORE ESTABLISHMENT - RESTARTING\n"); // go back to tick 1000 sim.readFromPopulationFile(tempdir() + "slim_" + simID + ".txt"); // start a newly seeded run setSeed(rdunif(1, 0, asInteger(2^62) - 1)); // re-introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } } 1000:10000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); cat(simID + ifelse(fixed, ": FIXED\n", ": LOST\n")); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.7 - Adaptive introgression.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { subpopCount = 10; for (i in 1:subpopCount) sim.addSubpop(i, 500); for (i in 2:subpopCount) sim.subpopulations[i-1].setMigrationRates(i-1, 0.01); for (i in 1:(subpopCount-1)) sim.subpopulations[i-1].setMigrationRates(i+1, 0.2); } 100 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 100:100000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); cat(ifelse(fixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/Recipe 9.8 - Fixation probabilities under Hill-Robertson interference.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.05); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 6000 late() { // Calculate the fixation probability for a beneficial mutation s = 0.05; N = 1000; p_fix = (1 - exp(-2 * s)) / (1 - exp(-4 * N * s)); // Calculate the expected number of fixed mutations n_gens = 1000; // first 5000 ticks were burn-in mu = 1e-6; locus_size = 100000; expected = mu * locus_size * n_gens * 2 * N * p_fix; // Figure out the actual number of fixations after burn-in subs = sim.substitutions; actual = sum(subs.fixationTick >= 5000); // Print a summary of our findings cat("P(fix) = " + p_fix + "\n"); cat("Expected fixations: " + expected + "\n"); cat("Actual fixations: " + actual + "\n"); cat("Ratio, actual/expected: " + (actual/expected) + "\n"); } ================================================ FILE: QtSLiM/recipes/Recipe 9.9 - Keeping a reference to a sweep mutation.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); mut = target.addNewDrawnMutation(m2, 10000); defineConstant("SWEEP", mut); } 1000:100000 late() { if (!SWEEP.isSegregating) { cat(ifelse(SWEEP.isFixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } ================================================ FILE: QtSLiM/recipes/_README.txt ================================================ These recipes are all taken directly from SLiM's manual: Haller, B.C., and Messer, P.W. (2016). SLiM: An Evolutionary Simulation Framework. SLiM's manual contains full explanations of each recipe, in the corresponding section; please refer to it for further information. These recipes are provided as separate files for convenience, particularly since copy-paste from the PDF manual often works poorly (but note that SLiMgui now allows you to open these recipes directly from the File menu, also, via the Open Recipe submenu). -------------------------------------------------------------- These recipes were all created by Ben Haller, except recipe 5.4, created by Aaron Sams and Chase W. Nelson. All recipes are copyright (c) 2016-2025 Benjamin C. Haller. All rights reserved. They are products of the Messer Lab, http://messerlab.org/slim/ All of these recipes are a part of SLiM. SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with SLiM. If not, see . -------------------------------------------------------------- The file Comeron_100kb_chr2L.txt provided here is part of the supplemental data from the paper: Comeron, J.M., Ratnappan, R., and Bailin, S. (2012). The many landscapes of recombination in Drosophila melanogaster. PLoS Genetics 8(10), e1002905. This data file is used by recipe 6.1.2. -------------------------------------------------------------- For further information, please feel free to contact us: http://messerlab.org/slim/ Ben Haller, bhaller@benhaller.com Philipp Messer, philipp.messer@gmail.com ================================================ FILE: QtSLiM/recipes.qrc ================================================ recipes/_README.txt recipes/Recipe 4.1 - A basic neutral simulation.txt recipes/Recipe 4.1.10 - Using symbolic constants for model parameters.txt recipes/Recipe 4.2.1 - Basic output, Entire population.txt recipes/Recipe 4.2.2 - Basic output, Random population sample.txt recipes/Recipe 4.2.3 - Basic output, Sampling individuals for output.txt recipes/Recipe 4.2.4 - Basic output, Substitutions.txt recipes/Recipe 4.2.5 - Basic output, Automatic logging with LogFile.txt recipes/Recipe 4.2.6 - Basic output, Custom output with Eidos.txt recipes/Recipe 5.1.1 - Subpopulation size, Instantaneous changes.txt recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth I.txt recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth II.txt recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth III.txt recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth IV.txt recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth V.txt recipes/Recipe 5.1.4 - Subpopulation size, Cyclical changes.txt recipes/Recipe 5.1.5 - Subpopulation size, Context-dependent changes (Muller's Ratchet).txt recipes/Recipe 5.2.1 - Population structure, Adding subpopulations.txt recipes/Recipe 5.2.2 - Population structure, Removing subpopulations.txt recipes/Recipe 5.2.3 - Population structure, Splitting subpopulations.txt recipes/Recipe 5.2.4 - Population structure, Joining subpopulations.txt recipes/Recipe 5.3.1 - Migration and admixture, A linear stepping-stone model.txt recipes/Recipe 5.3.2 - Migration and admixture, A non-spatial metapopulation.txt recipes/Recipe 5.3.3 - Migration and admixture, A two-dimensional subpopulation matrix.txt recipes/Recipe 5.3.4 - Migration and admixture, A random, sparse spatial metapopulation.txt recipes/Recipe 5.3.5 - Migration and admixture, Reading a migration matrix from a file.txt recipes/Recipe 5.4 - The Gravel et al. (2011) model of human evolution I.txt recipes/Recipe 5.4 - The Gravel et al. (2011) model of human evolution II.txt recipes/Recipe 5.5 - Rescaling population sizes to improve simulation performance I.txt recipes/Recipe 5.5 - Rescaling population sizes to improve simulation performance II.txt recipes/Recipe 6.1 - Genomic structure, Part I (Mutation types and fitness effects).txt recipes/Recipe 6.2 - Genomic structure, Part II (Genomic element types).txt recipes/Recipe 6.3 - Genomic structure, Part III (Chromosome organization).txt recipes/Recipe 6.4 - Genomic structure, Part IV (Custom display colors in SLiMgui).txt recipes/Recipe 8.1.1 - Reproduction, Enabling separate sexes.txt recipes/Recipe 8.1.2 - Reproduction, Sex ratios I.txt recipes/Recipe 8.1.2 - Reproduction, Sex ratios II.txt recipes/Recipe 8.1.3 - Reproduction, Selfing in hermaphroditic populations.txt recipes/Recipe 8.1.4 - Reproduction, Cloning I.txt recipes/Recipe 8.1.4 - Reproduction, Cloning II.txt recipes/Recipe 8.2.1 - Recombination, Making a random recombination map.txt recipes/Recipe 8.2.2 - Recombination, Reading a recombination map from a file.txt recipes/Recipe 8.2.3 - Recombination, Unlinked loci.txt recipes/Recipe 8.2.4 - Recombination, Gene conversion.txt recipes/Recipe 8.3.1 - Multiple diploid autosomes.txt recipes/Recipe 8.3.2 - Clonal haploids and chromosome types.txt recipes/Recipe 8.3.3 - Haploids with recombination.txt recipes/Recipe 8.3.4 - Sex-chromosome evolution and null haplosomes.txt recipes/Recipe 8.3.5 - Modeling the full human genome.txt recipes/Recipe 8.3.6 - A model of bryophytes with UV sex determination.txt recipes/Recipe 8.3.7 - Output from multiple-chromosome models.txt recipes/Recipe 9.1 - Introducing adaptive mutations.txt recipes/Recipe 9.2 - Making sweeps conditional on fixation.txt recipes/Recipe 9.3 - Making sweeps conditional on establishment.txt recipes/Recipe 9.4 - Partial sweeps.txt recipes/Recipe 9.5.1 - A soft sweep from recurrent de novo mutations in a large population.txt recipes/Recipe 9.5.2 - A soft sweep with a fixed de novo mutation schedule.txt recipes/Recipe 9.5.3 - A soft sweep with a random de novo mutation schedule.txt recipes/Recipe 9.6.1 - A sweep from standing variation at a random locus.txt recipes/Recipe 9.6.2 - A sweep from standing variation at a predetermined locus.txt recipes/Recipe 9.7 - Adaptive introgression.txt recipes/Recipe 9.8 - Fixation probabilities under Hill-Robertson interference.txt recipes/Recipe 9.9 - Keeping a reference to a sweep mutation.txt recipes/Recipe 9.10 - Tracking the fate of background mutations.txt recipes/Recipe 9.11 - Effective population size versus census population size.txt recipes/Recipe 9.12 - Observing the site frequency spectrum (SFS) during selective sweeps.txt recipes/Recipe 10.1 - Temporally varying selection.txt recipes/Recipe 10.2 - Spatially varying selection.txt recipes/Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis I.txt recipes/Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis II.txt recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection I.txt recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection II.txt recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection III.txt recipes/Recipe 10.4.2 - Fitness as a function of population composition, Cultural effects on fitness.txt recipes/Recipe 10.4.3 - Fitness as a function of population composition, Kin selection and the green-beard effect.txt recipes/Recipe 10.5 - Changing selection coefficients with setSelectionCoeff().txt recipes/Recipe 10.6 - Varying the dominance coefficient among mutations I.txt recipes/Recipe 10.6 - Varying the dominance coefficient among mutations II.txt recipes/Recipe 11.1 - Assortative mating.txt recipes/Recipe 11.2 - Sequential mate search I.txt recipes/Recipe 11.2 - Sequential mate search II.txt recipes/Recipe 11.3 - Gametophytic self-incompatibility.txt recipes/Recipe 12.1 - Social learning of cultural traits.txt recipes/Recipe 12.2 - Lethal epistasis I.txt recipes/Recipe 12.2 - Lethal epistasis II.txt recipes/Recipe 12.3 - Simulating gene drive.txt recipes/Recipe 12.4 - Suppressing hermaphroditic selfing.txt recipes/Recipe 12.5 - Tracking separate sexes in script.txt recipes/Recipe 13.1 - Polygenic selection.txt recipes/Recipe 13.2 - A simple model of variable QTL effect sizes.txt recipes/Recipe 13.3 - A model of discrete QTL effects across multiple chromosomes.txt recipes/Recipe 13.4 - A quantitative genetics model with heritability.txt recipes/Recipe 13.5 - A QTL-based model with two quantitative phenotypic traits and pleiotropy.txt recipes/Recipe 13.6 - A variety of fitness functions I (stabilizing selection).txt recipes/Recipe 13.6 - A variety of fitness functions II (directional selection).txt recipes/Recipe 13.6 - A variety of fitness functions III (disruptive selection).txt recipes/Recipe 13.6 - A variety of fitness functions IV (truncation selection).txt recipes/Recipe 13.7 - Negative frequency-dependence on a quantitative trait.txt recipes/Recipe 14.1 - Relatedness, inbreeding, and heterozygosity.txt recipes/Recipe 14.2 - Mortality-based fitness I.txt recipes/Recipe 14.2 - Mortality-based fitness II.txt recipes/Recipe 14.2 - Mortality-based fitness III.txt recipes/Recipe 14.3 - Reading initial simulation state from an MS output file I.txt recipes/Recipe 14.3 - Reading initial simulation state from an MS output file II.txt recipes/Recipe 14.4 - Modeling chromosomal inversions with a recombination() callback.txt recipes/Recipe 14.5 - Estimating model parameters with ABC.txt recipes/Recipe 14.6 - Tracking local ancestry along the chromosome.txt recipes/Recipe 14.7 - Live plotting with R using system().txt recipes/Recipe 14.8 - Using mutation rate variation to model varying functional density.txt recipes/Recipe 14.9 - Modeling microsatellites.txt recipes/Recipe 14.10 - Modeling transposable elements.txt recipes/Recipe 14.11 - Modeling opposite ends of a chromosome I.txt recipes/Recipe 14.11 - Modeling opposite ends of a chromosome II.txt recipes/Recipe 14.12 - Visualizing ancestry and admixture with mutation() callbacks.txt recipes/Recipe 14.13 - Modeling biallelic loci with a mutation() callback I.txt recipes/Recipe 14.13 - Modeling biallelic loci with a mutation() callback II.txt recipes/Recipe 14.14 - Modeling biallelic loci in script.txt recipes/Recipe 14.15 - Using runs of homozygosity (ROH) to track inbreeding.txt recipes/Recipe 14.16 - Visualizing linkage disequilibrium.txt recipes/Recipe 15.1 - A minimal nonWF model.txt recipes/Recipe 15.2 - Age structure (a life table model).txt recipes/Recipe 15.4 - Monogamous mating and variation in litter size.txt recipes/Recipe 15.5 - Beneficial mutations and absolute fitness.txt recipes/Recipe 15.6 - A metapopulation extinction-colonization model.txt recipes/Recipe 15.7 - Habitat choice.txt recipes/Recipe 15.8 - Evolutionary rescue after environmental change.txt recipes/Recipe 15.9 - Litter size and parental investment.txt recipes/Recipe 15.10 - Recording a pedigree.txt recipes/Recipe 15.11 - Dynamic population structure in nonWF models.txt recipes/Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model I.txt recipes/Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model II.txt recipes/Recipe 15.13 - Range expansion in a stepping-stone model I.txt recipes/Recipe 15.13 - Range expansion in a stepping-stone model II.txt recipes/Recipe 15.14 - Logistic population growth with the Beverton-Holt model.txt recipes/Recipe 16.1 - Pollen flow.txt recipes/Recipe 16.2 - Following a pedigree.txt recipes/Recipe 16.3 - Modeling clonal haploid bacteria with horizontal gene transfer.txt recipes/Recipe 16.4 - Alternation of generations.txt recipes/Recipe 16.5 - Meiotic drive.txt recipes/Recipe 16.6 - Sperm storage with a survival() callback.txt recipes/Recipe 16.7 - Tracking separate sexes in script, nonWF style.txt recipes/Recipe 16.8 - Modeling haplodiploidy with addRecombinant().txt recipes/Recipe 16.9 - Complex multi-chromosome inheritance with addMultiRecombinant().txt recipes/Recipe 16.10 - Modeling pseudo-autosomal regions (PARs) with addMultiRecombinant().txt recipes/Recipe 16.11 - Life-long monogamous mating.txt recipes/Recipe 17.1 - A simple 2D continuous-space model.txt recipes/Recipe 17.2 - Spatial competition.txt recipes/Recipe 17.3 - Boundaries and boundary conditions I (stopping boundaries).txt recipes/Recipe 17.3 - Boundaries and boundary conditions II (reflecting boundaries).txt recipes/Recipe 17.3 - Boundaries and boundary conditions III (absorbing boundaries).txt recipes/Recipe 17.3 - Boundaries and boundary conditions IV (reprising boundaries).txt recipes/Recipe 17.3 - Boundaries and boundary conditions V (dispersal kernels).txt recipes/Recipe 17.4 - Mate choice with a spatial kernel.txt recipes/Recipe 17.5 - Mate choice with a nearest-neighbor search.txt recipes/Recipe 17.6 - Divergence due to phenotypic competition with an interaction() callback.txt recipes/Recipe 17.7 - Modeling phenotype as a spatial dimension.txt recipes/Recipe 17.8 - Sympatric speciation facilitated by assortative mating.txt recipes/Recipe 17.9 - Speciation due to spatial variation in selection.txt recipes/Recipe 17.10 - A simple biogeographic landscape model.txt recipes/Recipe 17.11 - Local adaptation on a heterogeneous landscape map.txt recipes/Recipe 17.12 - Periodic spatial boundaries.txt recipes/Recipe 17.13 - Density-dependent fecundity with summarizeIndividuals().txt recipes/Recipe 17.14 - Directed dispersal with the SpatialMap class.txt recipes/Recipe 17.15 - Spatial competition and spatial mate choice in a nonWF model.txt recipes/Recipe 17.16 - A spatial model with carrying-capacity density.txt recipes/Recipe 17.17 - A spatial epidemiological S-I-R model.txt recipes/Recipe 17.18 - A sexual, age-structured spatial model.txt recipes/Recipe 17.19 - Modeling indirect competition mediated by resource availability.txt recipes/Recipe 18.1 - A minimal tree-seq model.txt recipes/Recipe 18.2 - Overlaying neutral mutations.py recipes/Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt recipes/Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt recipes/Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt recipes/Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py recipes/Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) I.txt recipes/Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) II.py recipes/Recipe 18.6 - Measuring the coalescence time of a model I.txt recipes/Recipe 18.6 - Measuring the coalescence time of a model II.txt recipes/Recipe 18.7 - Analyzing selection coefficients in Python with tskit I.txt recipes/Recipe 18.7 - Analyzing selection coefficients in Python with tskit II.py recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history I.py recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history II.txt recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history III.py recipes/Recipe 18.9 - Starting a sexual nonWF model with a coalescent history I.py recipes/Recipe 18.9 - Starting a sexual nonWF model with a coalescent history II.txt recipes/Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation I.txt recipes/Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation II.py recipes/Recipe 18.11 - Optimizing tree-sequence simplification.txt recipes/Recipe 19.1 - A simple neutral nucleotide-based model.txt recipes/Recipe 19.2 - Reading an ancestral nucleotide sequence from a FASTA file.txt recipes/Recipe 19.3 - Sequence output from nucleotide-based models.txt recipes/Recipe 19.4 - Back-mutations, independent mutational lineages, and VCF output.txt recipes/Recipe 19.5 - Modeling elevated CpG mutation rates and equilibrium nucleotide frequencies.txt recipes/Recipe 19.6 - A nucleotide-based model with introduced non-nucleotide-based mutations.txt recipes/Recipe 19.7 - Using standard SLiM fitness effects with nucleotides (modeling synonymous sites).txt recipes/Recipe 19.8 - Defining sequence-based fitness effects at the nucleotide level.txt recipes/Recipe 19.9 - Defining sequence-based fitness effects at the amino acid level.txt recipes/Recipe 19.10 - Varying the mutation rate along the chromosome in a nucleotide-based model.txt recipes/Recipe 19.11 - Modeling GC-biased gene conversion (gBGC).txt recipes/Recipe 19.12 - Reading VCF files to create nucleotide-based SNPs.txt recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models I.txt recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models II.py recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models III.py recipes/Recipe 19.14 - Modeling identity by state (IBS) (uniquing mutations with a mutation() callback).txt recipes/Recipe 19.15 - Modeling identity by state (IBS) (uniquing back-mutations to the ancestral state).txt recipes/Recipe 20.1 - A simple multispecies model.txt recipes/Recipe 20.2 - A two-species model.txt recipes/Recipe 20.3 - A deterministic host-parasitoid model.txt recipes/Recipe 20.4 - An individual-based host-parasitoid model I.txt recipes/Recipe 20.4 - An individual-based host-parasitoid model II.txt recipes/Recipe 20.5 - A continuous-space host-parasitoid model.txt recipes/Recipe 20.6 - A coevolutionary host-parasitoid trait-matching model.txt recipes/Recipe 20.7 - A coevolutionary host-parasite matching-allele model.txt recipes/Recipe 20.8 - Within-host reproduction in a host-pathogen model.txt ================================================ FILE: README.md ================================================

Screenshot of SLiMgui running on OS X.

SLiM is an evolutionary simulation framework that combines a powerful engine for population genetic simulations with the capability of modeling arbitrarily complex evolutionary scenarios. Simulations are configured via the integrated Eidos scripting language that allows interactive control over practically every aspect of the simulated scenarios. The underlying individual-based simulation engine is highly optimized to enable modeling of entire chromosomes in large populations. We also provide a graphical user interface called SLiMgui on macOS, Linux, and Windows for easy simulation set-up, interactive runtime control, and dynamic visualization of simulation output.

GitHub Actions | Fedora Copr | Conda ---|---|--- ![SLiM on GitHub Actions:](https://github.com/MesserLab/SLiM/workflows/tests/badge.svg) | [![Copr build status](https://copr.fedorainfracloud.org/coprs/bacarson/SLiM-Selection_on_Linked_Mutations/package/SLiM/status_image/last_build.png)](https://copr.fedorainfracloud.org/coprs/bacarson/SLiM-Selection_on_Linked_Mutations/package/SLiM/) | [![Anaconda-Server Badge](https://anaconda.org/conda-forge/slim/badges/platforms.svg)](https://anaconda.org/conda-forge/slim) :construction: This GitHub repository hosts the upstream, development head version of SLiM and SLiMgui. :warning: End users should generally not use these sources; they may contain serious bugs, or may not even compile. :heavy_check_mark: The release version of SLiM and SLiMgui is available at [http://messerlab.org/slim/](http://messerlab.org/slim/). License ---------- Copyright (c) 2016-2025 Benjamin C. Haller. All rights reserved. SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with SLiM. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). Development & Feedback ----------------------------------- SLiM is a product of the Messer Lab, [http://messerlab.org/](http://messerlab.org/), by Benjamin C. Haller and Philipp W. Messer. SLiM is under active development, and our goal is to make it as broadly useful as possible. If you have feedback or feature requests, or if you are interested in contributing to SLiM, please contact Ben Haller at [bhaller@mac.com](mailto:bhaller@mac.com). Please note that Philipp is also looking for graduate students and postdocs. Installation ------------ Looking for Binary Packages / Installers? The following subsections summarize what methods for acquiring SLiM (and SLiMgui) are available. Building from sources is also an option on all platforms; see the next section. Chapter 2 of the SLiM manual contains much more detail on installation and building of SLiM. The manual and other SLiM resources can be found at [http://messerlab.org/slim/](http://messerlab.org/slim/#Downloads). ##### macOS Download and double-click the macOS Installer from the SLiM home page at https://messerlab.org/slim/#Downloads. It will install the `slim` and `eidos` command-line tools, as well as SLiMgui. ##### Linux ###### Arch & Manjaro Any Arch-based distributions *which support the AUR* should be compatible. https://aur.archlinux.org/packages/slim-simulator/ ###### Fedora, Red Hat, openSUSE Derivative distributions are not guaranteed compatibility with these binary packages. Enable the repository for your operating system; you might also try using the source RPM package to rebuild the package for your system to give you an excellent integration for any RPM-based distribution. https://copr.fedorainfracloud.org/coprs/bacarson/SLiM-Selection_on_Linked_Mutations/ ###### Debian & Ubuntu (and any derivatives using dpkg) A shell script using the facilities of `dpkg` is available. It uses the CMake install target to integrate SLiMgui with the desktop environment. It has the advantage over building from source that it will check build dependencies for you, and it will automatically remove build artifacts from `/tmp`. Source the script with `curl` following the instructions in the manual. https://raw.githubusercontent.com/MesserLab/SLiM-Extras/master/installation/DebianUbuntuInstall.sh ##### Windows (10 & 11) ###### Native package (using MSYS2) If you have MSYS2 installed, you can do `pacman -Syu` to update its information (see the SLiM manual for further information). You can then install SLiM and SLiMgui with: `pacman -S mingw-w64-x86_64-slim-simulator` ###### WSL2 installation guide The SLiM manual provides detailed instructions on building and installing SLiM and SLiMgui under the WSL2. Compilation of SLiM from Source ---------------------------------- You can build both SLiM and SLiMgui from sources. This can be useful, in particular, if you wish to run a recent development version of SLiM, rather than the last released version. See chapter 2 of the SLiM manual for more information on building from sources on various platforms. ================================================ FILE: SLiM.pro ================================================ TEMPLATE = subdirs SUBDIRS += \ eidos \ core \ QtSLiM \ gsl \ treerec/tskit \ eidos_zlib eidos.depends = gsl eidos_zlib core.depends = gsl eidos_zlib eidos treerec/tskit QtSLiM.depends = gsl eidos_zlib eidos core treerec/tskit # Uncomment the lines below to enable ASAN (Address Sanitizer), for debugging of memory issues, in every # .pro file project-wide. See https://clang.llvm.org/docs/AddressSanitizer.html for discussion of ASAN # Also set the ASAN_OPTIONS env. variable, in the Run Settings section of the Project tab in Qt Creator, to # strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 # This also enables undefined behavior sanitizing, in conjunction with ASAN, because why not. #CONFIG += sanitizer sanitize_address sanitize_undefined ================================================ FILE: SLiM.spec ================================================ # Cross-distribution SLiM RPM spec. %if %{defined suse_version} %if 0%{?suse_version} < 1600 %global qtNameAndVersion libqt5 %else %global qtNameAndVersion qt6 %endif %endif %if %{defined fedora} %if 0%{?fedora} >= 39 %global qtNameAndVersion qt6 %else %global qtNameAndVersion qt5 %endif %endif %if %{defined rhel} %if 0%{?epel} >= 9 # qt6 is only available through EPEL on RHEL 9 and higher %global qtNameAndVersion qt6 %else %global qtNameAndVersion qt5 %endif %endif Name: SLiM Version: 5.2 Release: 1%{?dist} Summary: an evolutionary simulation framework License: GPLv3+ URL: https://messerlab.org/slim/ Source0: https://github.com/MesserLab/SLiM/archive/refs/tags/v%{version}.tar.gz # Prevent users of the Copr repository from using Simple Login Manager, due to binary file name conflict. Conflicts: slim # This paragraph of the spec file is old and delicate. BuildRequires: cmake # openSUSE Build Requires %if %{defined suse_version} BuildRequires: glew-devel BuildRequires: Mesa-libGL-devel BuildRequires: gcc-c++ BuildRequires: appstream-glib-devel %if 0%{?suse_version} < 1600 BuildRequires: %{qtNameAndVersion}-qtbase-devel %else # only Tumbleweed officially supports Qt6; further, it's "base" not "qtbase" in Tumbleweed. :( BuildRequires: %{qtNameAndVersion}-base-devel %endif %else # if not on openSUSE BuildRequires: %{qtNameAndVersion}-qtbase-devel BuildRequires: libappstream-glib %endif ExclusiveArch: x86_64 # RHEL 8 has the oldest point release of 5.15, and is the oldest RHEL supported. %if 0%{?rhel} == 8 Requires: qt5-qtbase >= 5.15.1 %else Requires: %{qtNameAndVersion}-qtbase %endif %description SLiM is an evolutionary simulation framework that combines a powerful engine for population genetic simulations with the capability of modeling arbitrarily complex evolutionary scenarios. Simulations are configured via the integrated Eidos scripting language that allows interactive control over practically every aspect of the simulated evolutionary scenarios. The underlying individual-based simulation engine is highly optimized to enable modeling of entire chromosomes in large populations. We also provide a graphical user interface on macOS and Linux for easy simulation set-up, interactive runtime control, and dynamical visualization of simulation output. %prep %setup -q %build %if 0%{?rhel} == 8 %if "%_vpath_builddir" != "%_vpath_srcdir" echo "current directory: %(pwd)" echo "source directory: %_vpath_srcdir" echo "build directory: %_vpath_builddir" mkdir -p %_vpath_builddir %else %{warn "The build directory is the same as the source directory on RHEL 8!"} %endif ## Tell CMake where the source directory and the build directory are, directly. %cmake -S %_vpath_srcdir -B %_vpath_builddir -DBUILD_SLIMGUI=ON cd %_vpath_builddir %else # rpmbuild is not running on RHEL 8 %cmake -DBUILD_SLIMGUI=ON %endif %cmake_build %install %if 0%{?rhel} == 8 cmake --install %_vpath_builddir --prefix %{buildroot}/usr %else %cmake_install %endif %files %{_bindir}/eidos %{_bindir}/slim %{_bindir}/SLiMgui %{_datadir}/applications/org.messerlab.slimgui.desktop %{_datadir}/icons/hicolor/scalable/apps/org.messerlab.slimgui.svg %{_datadir}/icons/hicolor/scalable/mimetypes/text-slim.svg %{_datadir}/icons/hicolor/symbolic/apps/org.messerlab.slimgui-symbolic.svg %{_datadir}/metainfo/org.messerlab.slimgui.appdata.xml %{_datadir}/metainfo/org.messerlab.slimgui.metainfo.xml %{_datadir}/mime/packages/org.messerlab.slimgui-mime.xml %changelog * Fri Apr 11 2026 Ben Haller - 5.2-1 - No changes to the package have been made since the last release. - Final candidate 1 for 5.2 release * Fri Sep 12 2025 Ben Haller - 5.1-1 - No changes to the package have been made since the last release. - Final candidate 1 for 5.1 release * Sat Apr 12 2025 Bryce Carson - 5.0-1 - Bump source version to five and use the new /archive/refs/tags/v5.0 path scheme for accessing the tags on GitHub. * Sun Sep 15 2024 Bryce Carson - 4.3-2 - Significant work has been invested into debugging the build of RHEL 8 on COPR. For whatever reason, since 4.0.1-1, we were unable to build on RHEL 8 (or perhaps it was EPEL 8?). Regardless, the ability to build on RHEL 8 and EPEL 8 has been achieved or restored, using conditionals which check what distribution the build is occuring on. These conditionals check the distribution using the defined RPM macros, a reliable system that the operating systems try not to step on each others toes; it'd be nicer if CentOS didn't call itself RHEL, though, but CentOS purposefully tries to be "bug-compatible" (if I recall) with RHEL, yet be slightly upstream of it with RHEL. The buildroot (which is the installation prefix within the CHROOT) and the source and build directories must be manually specified when building on RHEL 8 or EPEL 8 systems (which is RHEL 8 + EPEL [the extra packages for enterprise linux repository] for RHEL 8). I don't know what changed amongst the macros, if anything ever did change, but with 4.0.1-1 we were able to build for EPEL 8 two years ago, and then we weren't when I tried however long ago that issue four-hundred and forty cropped up. This has been resolved with the use of conditionals in the RPM preprocessor (do recall that "if" is not actually a macro) and RPM macros. - Conditionals and macros are used to decide whether to use Qt 6 or Qt 5. * Mon Sep 02 2024 Bryce Carson - 4.3-1 - Changes to the package have occurred. See the following points. - Further version checks for various distributions are introduced to allow cross-distribution packaging and building against Qt5 or Qt6, appropriate to the platform. - An attempt to fix issue 440 is made - See the SLiM release notes on GitHub for information about changes to the packaged software. * Tue Apr 30 2024 Ben Haller - 4.2.2-1 - No changes to the package have been made since the last release. - Ship the fix for the 4.2.1-2 crashing bug as a separate release. - Fix an issue with reading of some VCF files. * Tue Apr 30 2024 Ben Haller - 4.2.1-2 - No changes to the package have been made since the last release. - Another fix for a crashing bug under certain conditions. * Fri Apr 12 2024 Ben Haller - 4.2.1-1 - No changes to the package have been made since the last release. - Fix for a crashing bug under certain conditions. * Wed Mar 20 2024 Bryce Carson - 4.2-1 - No changes to the package have been made since the last release. See the SLiM release notes on GitHub for information about changes to the packaged software. * Mon Dec 4 2023 Bryce Carson - 4.1-1 - Final candidate 1 for 4.1 release - CMake install of package desktop environment data properly implemented - RPM macros adopted * Tue Sep 27 2022 Bryce Carson - 4.0.1-2 - `CMakeLists.txt` improved, so the installation section of the RPM is now simplified. - Data files now exist in `data/`, rather than in the root folder of the software. * Tue Sep 13 2022 Ben Haller - 4.0.1-1 - Final candidate 1 for 4.0.1 release * Tue Aug 23 2022 Bryce Carson - 4.0-2 - Include new changelog entry to identify the date of the new release * Wed Aug 10 2022 Bryce Carson - 4.0-1 - New release * Sat Feb 12 2022 Bryce Carson - 3.7.1-1 - Increment version * Wed Dec 15 2021 Ben Haller - 3.7-1 - Final candidate 1 for 3.7 release - Removed robinhood patch - Removed --parallel for cmake since it was no longer working * Sat Apr 24 2021 Bryce Carson - 3.6-5 - Fixed email address in previous changelog entry. - Included a conflict tag to prevent users of this package from using the conflicting binary in Simple Login Manager. * Sat Mar 20 2021 Bryce Carson - 3.6-4 - Added support for openSUSE (with SUSE Linux Enterprise users possibly able to use the openSUSE RPM). - Cleaned up the changelog. - The `[]` argument to the cmake `--parallel` option was removed, so that Copr uses the default number of concurrent processes (and hopefully the maximum number, rather than hardcoding eight processes). * Wed Mar 3 2021 Bryce Carson - 3.6-3 - Application of patch to allow building on Fedora 34 and Fedora Rawhide. * Wed Mar 3 2021 Bryce Carson - 3.6-2 - Specified required Qt 5.15.2 on Fedora 34. - Added package version in previous changelog entry. * Wed Mar 3 2021 Bryce Carson - 3.6-1 - New package release. - Removed source edits that were addressed upstream. * Sun Jan 31 2021 Bryce Carson - 3.5-6 - spec file improvements; brace expansion used and sorting performed. - FreeDesktop compliance improvements; the organization domain and application name are corrected and are now compliant. - Source modifications allow Gnome Classic to display the proper application name. - The symbolic application icon is now created programmatically from upstream icons in the source, rather than a second source file. * Thu Jan 28 2021 Bryce Carson - 3.5-5 - org.messerlab.slimgui.desktop changed in prep to correct Categories value; fixes desktop integration on Fedora 33 when using Gnome Classic environment. - New symbolic icon included; improves desktop integration on Fedora 33 when using Gnome 3 with Wayland. - Edited the changelog to not refer to the prep stage as a macro, simply "prep", to fix rpmlint warnings. * Thu Jan 14 2021 Bryce Carson - 3.5-4 - org.messerlab.slimgui.desktop changed in prep to correct StartupWMClass and icon; fixes desktop integration on Fedora 33. - Sorted changelog in descending chronological order. * Sun Dec 06 2020 Bryce Carson - 3.5-3 - Updated the requires in the .spec file (and thus the package dependencies) to reflect updates to Qt5 on Fedora 33. - Qt5 5.15.2 now required on Fedora 33. * Sun Dec 06 2020 Bryce Carson - 3.5-2 - Changed the tar command in .spec file to address discrepancy between GitHub archive URI and downloaded source archive. * Sun Dec 06 2020 Bryce Carson - 3.5-1 - Created new release package - Differences from 3.4-8 include removal of necessary source modifications for 3.4 - The source modifications for 3.4 were addressed by the upstream ================================================ FILE: SLiM.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 98024742215D85880025D29C /* FindRecipePanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98024740215D85880025D29C /* FindRecipePanel.xib */; }; 980566E225A7C5B9008D3C7F /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 980566E325A7C5B9008D3C7F /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 980566E425A7C5B9008D3C7F /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 980566E525A7C5B9008D3C7F /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 98076614244934A800F6CBB4 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 98076615244934A800F6CBB4 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 98076617244934A800F6CBB4 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 98076618244934A800F6CBB4 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 9807661C244934A800F6CBB4 /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 9807661D244934A800F6CBB4 /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 9807661E244934A800F6CBB4 /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 9807661F244934A800F6CBB4 /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 98076620244934A800F6CBB4 /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 98076621244934A800F6CBB4 /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 98076622244934A800F6CBB4 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 98076623244934A800F6CBB4 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 98076626244934A800F6CBB4 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 98076627244934A800F6CBB4 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 9807662924493A8F00F6CBB4 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 9807662A24493A8F00F6CBB4 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 9807662B24493E0B00F6CBB4 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 9807662C24493E0B00F6CBB4 /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 9807662D24493E0B00F6CBB4 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 9807662E24493E0B00F6CBB4 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 9807662F24493E0B00F6CBB4 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 9807663024493E0B00F6CBB4 /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 9807663124493E0B00F6CBB4 /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 9807663224493E0B00F6CBB4 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 9807663324493E0B00F6CBB4 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 9807663424493E0B00F6CBB4 /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 9807663524493E0B00F6CBB4 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 9807663624493E0B00F6CBB4 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 9807663724493E0B00F6CBB4 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 9807663824493E0B00F6CBB4 /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 9807663924493E0B00F6CBB4 /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 9807663A24493E0B00F6CBB4 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 9807C0F524BA21B7008CC658 /* slim_test_core.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F324BA21B7008CC658 /* slim_test_core.cpp */; }; 9807C0F624BA21B7008CC658 /* slim_test_core.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F324BA21B7008CC658 /* slim_test_core.cpp */; }; 9807C0F824BA21E3008CC658 /* slim_test_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F724BA21E3008CC658 /* slim_test_other.cpp */; }; 9807C0F924BA21E3008CC658 /* slim_test_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F724BA21E3008CC658 /* slim_test_other.cpp */; }; 98090FA21B1B8B5800791DBF /* show_browser_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA01B1B8B5800791DBF /* show_browser_H.pdf */; }; 98090FA31B1B8B5800791DBF /* show_browser.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA11B1B8B5800791DBF /* show_browser.pdf */; }; 98090FA61B1B978900791DBF /* EidosValueWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98090FA51B1B978900791DBF /* EidosValueWrapper.mm */; }; 9809DFA02550F32500C4E82D /* log_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9809DF9E2550F32500C4E82D /* log_file.cpp */; }; 9809DFA12550F32500C4E82D /* log_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9809DF9E2550F32500C4E82D /* log_file.cpp */; }; 980DD51A1AAE42F900D5B7B8 /* GraphAxisRescaleSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 980DD5181AAE42F900D5B7B8 /* GraphAxisRescaleSheet.xib */; }; 980DD51D1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.mm in Sources */ = {isa = PBXBuildFile; fileRef = 980DD51C1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.mm */; }; 981BAC6A1ACC6E8B0005BE94 /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 981BAC6B1ACC6E8B0005BE94 /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 981DC35028E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 981DC35128E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 981DC35228E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 981DC35328E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 981DC35428E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 981DC35528E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 981DC35628E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 981DC35728E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 981DC35828E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 981DC35928E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 981DC35A28E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 981DC35B28E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 981DC35C28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 981DC35D28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 981DC35E28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 981DC35F28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 981DC36028E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 981DC36128E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 981DC36228E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 981DC36328E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 981DC36428E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 981DC36528E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 981DC36628E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 981DC36728E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 981DC36828E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 981DC36928E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 981DC36A28E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 981DC36B28E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 981DC36C28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 981DC36D28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 981DC36E28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 981DC36F28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 981DC37028E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 981DC37128E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 981DC37228E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 981DC37328E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 981DC37428E27300000ABE91 /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 981DC37528E27300000ABE91 /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 981DC37628E27300000ABE91 /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 981DC37728E27300000ABE91 /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 981DC37828E27300000ABE91 /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 981DC37928E27300000ABE91 /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 981DC37A28E27300000ABE91 /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 981DC37B28E27300000ABE91 /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 981DC37C28E27300000ABE91 /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 981DC37D28E27301000ABE91 /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 981DC37E28E27301000ABE91 /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 981DC37F28E27301000ABE91 /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 981DC38028E27301000ABE91 /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 981DC38128E27301000ABE91 /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 981DC38228E27301000ABE91 /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 981DC38328E27301000ABE91 /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 981DC38428E27301000ABE91 /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 981DC38528E27301000ABE91 /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 9821E2041ABDBC300036EAEA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9821E2031ABDBC300036EAEA /* QuartzCore.framework */; }; 98235682252FDCF50096A745 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98235683252FDCF50096A745 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98235684252FDCF50096A745 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98235685252FDCF50096A745 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98235686252FDCF50096A745 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98235689252FE61A0096A745 /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 9823568A252FE61A0096A745 /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 9823568B252FE61A0096A745 /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 9823568C252FE61A0096A745 /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 9823568D252FE61A0096A745 /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 9825565B1BA32EE80054CB3F /* EidosCocoaExtra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9825565A1BA32EE80054CB3F /* EidosCocoaExtra.mm */; }; 9825565C1BA32EE80054CB3F /* EidosCocoaExtra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9825565A1BA32EE80054CB3F /* EidosCocoaExtra.mm */; }; 982556651BA450980054CB3F /* EidosHelpController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 982556641BA450980054CB3F /* EidosHelpController.mm */; }; 982556661BA450980054CB3F /* EidosHelpController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 982556641BA450980054CB3F /* EidosHelpController.mm */; }; 982556691BA451D00054CB3F /* EidosHelpWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982556671BA451D00054CB3F /* EidosHelpWindow.xib */; }; 9825566A1BA451D00054CB3F /* EidosHelpWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982556671BA451D00054CB3F /* EidosHelpWindow.xib */; }; 9825566C1BA477D60054CB3F /* EidosHelpFunctions.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825566B1BA477D60054CB3F /* EidosHelpFunctions.rtf */; }; 9825566D1BA477D60054CB3F /* EidosHelpFunctions.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825566B1BA477D60054CB3F /* EidosHelpFunctions.rtf */; }; 9825566F1BA4FAD00054CB3F /* SLiMHelpFunctions.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825566E1BA4FAD00054CB3F /* SLiMHelpFunctions.rtf */; }; 982556A01BA5DFEB0054CB3F /* EidosHelpClasses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825569F1BA5DFEB0054CB3F /* EidosHelpClasses.rtf */; }; 982556A11BA5DFEB0054CB3F /* EidosHelpClasses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825569F1BA5DFEB0054CB3F /* EidosHelpClasses.rtf */; }; 982556A31BA5F0810054CB3F /* SLiMHelpClasses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 982556A21BA5F0810054CB3F /* SLiMHelpClasses.rtf */; }; 982556AC1BA8E77C0054CB3F /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 982556AB1BA8E77C0054CB3F /* main.cpp */; }; 982556B01BA8EF720054CB3F /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 982556B11BA8EF760054CB3F /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 982556B21BA8EF790054CB3F /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 982556B31BA8EF7C0054CB3F /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 982556B41BA8EF7F0054CB3F /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 982556B51BA8EF830054CB3F /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 982556B61BA8EF860054CB3F /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 982556B71BA8EF8A0054CB3F /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 982556B81BA8EF8C0054CB3F /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 982556B91BA8EF8F0054CB3F /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 982556BA1BA8EF930054CB3F /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 982556BB1BA8EF960054CB3F /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 982556BC1BA8EF990054CB3F /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 982663541A3BABD300A0CBBF /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 982663531A3BABD300A0CBBF /* main.cpp */; }; 982A9DDE1FCA9FF0007BA3DF /* GraphBarRescaleSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982A9DDC1FCA9FF0007BA3DF /* GraphBarRescaleSheet.xib */; }; 982B50C62704048E006E91BC /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 982B50C72704048E006E91BC /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 982B50C82704048E006E91BC /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 982B50C92704048E006E91BC /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 98321F911B406417007337A3 /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 98321F941B406B67007337A3 /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 98321F951B406B67007337A3 /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 98321F961B406B67007337A3 /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 98330C34294A73AB00B452E2 /* libomp.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 98760EDC28CE5E7600CEBC40 /* libomp.dylib */; }; 98332A9E1FDB98ED00274FF0 /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98332A9F1FDB990400274FF0 /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98332AA01FDB990500274FF0 /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98332AA11FDB990500274FF0 /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98332AB41FDBA1E100274FF0 /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98332AB51FDBA1E400274FF0 /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98332AB61FDBA1E500274FF0 /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98332AB71FDBA1E600274FF0 /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98332AB91FDBA32200274FF0 /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98332ABA1FDBA32500274FF0 /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98332ABB1FDBA32500274FF0 /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98332ABC1FDBA32500274FF0 /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98332AC21FDBA53F00274FF0 /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98332AC31FDBA54600274FF0 /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98332AC41FDBA54600274FF0 /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98332AC51FDBA54700274FF0 /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98332AC71FDBA6B600274FF0 /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98332AC81FDBA74900274FF0 /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98332AC91FDBA74900274FF0 /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98332ACA1FDBA74A00274FF0 /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98332ACE1FDBA81A00274FF0 /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98332ACF1FDBA87D00274FF0 /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98332AD01FDBA87D00274FF0 /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98332AD11FDBA87E00274FF0 /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98332AD71FDBBD1600274FF0 /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98332AD81FDBBE3500274FF0 /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98332AD91FDBBE3600274FF0 /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98332ADA1FDBBE3600274FF0 /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98332ADC1FDBC0D000274FF0 /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98332ADD1FDBC0D700274FF0 /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98332ADE1FDBC0D700274FF0 /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98332ADF1FDBC0D800274FF0 /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98332AE81FDBC1D900274FF0 /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98332AEA1FDBC1D900274FF0 /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98332AED1FDBC29400274FF0 /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98332AEE1FDBC29500274FF0 /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98332AEF1FDBC29500274FF0 /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98332AF11FDBC36300274FF0 /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98332AF21FDBC36300274FF0 /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98332AF31FDBC36400274FF0 /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98332AF71FDBC3F100274FF0 /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98332AF81FDBC42600274FF0 /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98332AF91FDBC42700274FF0 /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98332AFA1FDBC42700274FF0 /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98332AFC1FDBC4B200274FF0 /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98332AFD1FDBC4BB00274FF0 /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98332AFE1FDBC4BB00274FF0 /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98332AFF1FDBC4BC00274FF0 /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98332B031FDBCFC300274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98332B041FDBCFF900274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98332B051FDBCFF900274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98332B061FDBCFF900274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98332B0A1FDBD00800274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98332B0B1FDBD03100274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98332B0C1FDBD03200274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98332B0D1FDBD03200274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98332B111FDBD09800274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98332B121FDBD0F500274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98332B131FDBD0F500274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98332B141FDBD0F600274FF0 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98332B181FDBD13D00274FF0 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 98332B191FDBD16500274FF0 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 98332B1A1FDBD16500274FF0 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 98332B1B1FDBD16600274FF0 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 9836867427CD40CF00683639 /* community.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836867227CD40CF00683639 /* community.cpp */; }; 9836867527CD40CF00683639 /* community.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836867227CD40CF00683639 /* community.cpp */; }; 9836868127CD72E900683639 /* community_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836868027CD72E900683639 /* community_eidos.cpp */; }; 9836868227CD72E900683639 /* community_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836868027CD72E900683639 /* community_eidos.cpp */; }; 984252C3216FA9930019696A /* FindRecipeController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 984252C2216FA9930019696A /* FindRecipeController.mm */; }; 98453F401A75A12700C058CB /* dump_output_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F3E1A75A12700C058CB /* dump_output_H.pdf */; }; 98453F411A75A12700C058CB /* dump_output.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F3F1A75A12700C058CB /* dump_output.pdf */; }; 98453F441A75AABE00C058CB /* syntax_help_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F421A75AABE00C058CB /* syntax_help_H.pdf */; }; 98453F451A75AABE00C058CB /* syntax_help.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F431A75AABE00C058CB /* syntax_help.pdf */; }; 98453F561A76004300C058CB /* open_type_drawer_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4C1A76004300C058CB /* open_type_drawer_H.pdf */; }; 98453F571A76004300C058CB /* open_type_drawer.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4D1A76004300C058CB /* open_type_drawer.pdf */; }; 98453F581A76004300C058CB /* show_fixed_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4E1A76004300C058CB /* show_fixed_H.pdf */; }; 98453F591A76004300C058CB /* show_fixed.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4F1A76004300C058CB /* show_fixed.pdf */; }; 98453F5A1A76004300C058CB /* show_genomicelements_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F501A76004300C058CB /* show_genomicelements_H.pdf */; }; 98453F5B1A76004300C058CB /* show_genomicelements.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F511A76004300C058CB /* show_genomicelements.pdf */; }; 98453F5C1A76004300C058CB /* show_mutations_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F521A76004300C058CB /* show_mutations_H.pdf */; }; 98453F5D1A76004300C058CB /* show_mutations.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F531A76004300C058CB /* show_mutations.pdf */; }; 98453F5E1A76004300C058CB /* show_recombination_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F541A76004300C058CB /* show_recombination_H.pdf */; }; 98453F5F1A76004300C058CB /* show_recombination.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F551A76004300C058CB /* show_recombination.pdf */; }; 98453F621A76041200C058CB /* edit_submenu_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F601A76041200C058CB /* edit_submenu_H.pdf */; }; 98453F631A76041200C058CB /* edit_submenu.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F611A76041200C058CB /* edit_submenu.pdf */; }; 984824EE210B9E8F002402A5 /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 984824F1210B9F23002402A5 /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 984824F2210B9F2C002402A5 /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 984824F3210B9F2D002402A5 /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 984824F4210B9F2D002402A5 /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 984824F5210B9F31002402A5 /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 984824F6210B9F32002402A5 /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 984824F7210B9F32002402A5 /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 984D5FB31E3AF18C00473719 /* EidosTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 984D5FB21E3AF18C00473719 /* EidosTests.mm */; }; 984D5FB51E3AF1F000473719 /* SLiMTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 984D5FB41E3AF1F000473719 /* SLiMTests.mm */; }; 9850D8E32063098E006BFD2E /* tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 9850D8DF2063098E006BFD2E /* tables.c */; }; 9850D8E9206309A0006BFD2E /* tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 9850D8DF2063098E006BFD2E /* tables.c */; }; 985301EC1B72582E001520DF /* change_cloning_rate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985301EB1B72582E001520DF /* change_cloning_rate.pdf */; }; 9854D25F2278B9F8001D43BC /* core.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2562278B9F7001D43BC /* core.c */; }; 9854D2602278B9F8001D43BC /* core.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2562278B9F7001D43BC /* core.c */; }; 9854D2612278B9F8001D43BC /* genotypes.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2572278B9F7001D43BC /* genotypes.c */; }; 9854D2622278B9F8001D43BC /* genotypes.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2572278B9F7001D43BC /* genotypes.c */; }; 9854D2632278B9F8001D43BC /* convert.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2592278B9F8001D43BC /* convert.c */; }; 9854D2642278B9F8001D43BC /* convert.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2592278B9F8001D43BC /* convert.c */; }; 9854D2652278B9F8001D43BC /* stats.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25B2278B9F8001D43BC /* stats.c */; }; 9854D2662278B9F8001D43BC /* stats.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25B2278B9F8001D43BC /* stats.c */; }; 9854D2672278B9F8001D43BC /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25D2278B9F8001D43BC /* trees.c */; }; 9854D2682278B9F8001D43BC /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25D2278B9F8001D43BC /* trees.c */; }; 9857249C1AD08A810047C223 /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 9857249D1AD08A810047C223 /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 985724A11AD34B740047C223 /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 985724A21AD34B740047C223 /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 985724A51AD435310047C223 /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 985724A61AD435310047C223 /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 985724AA1AD4551C0047C223 /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 985724B51AD478630047C223 /* EidosAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 985724B41AD478630047C223 /* EidosAppDelegate.mm */; }; 985724B71AD478630047C223 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 985724B61AD478630047C223 /* main.m */; }; 985724B91AD478630047C223 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 985724B81AD478630047C223 /* Images.xcassets */; }; 985724BC1AD478630047C223 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 985724BA1AD478630047C223 /* MainMenu.xib */; }; 985724CF1AD479010047C223 /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 985724D01AD479010047C223 /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 985724D11AD479010047C223 /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 985724D21AD479010047C223 /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 985724D31AD479010047C223 /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 985724D51AD481070047C223 /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 985724D61AD489AA0047C223 /* check_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2231A71F8AD00FFB083 /* check_H.pdf */; }; 985724D71AD489AA0047C223 /* check.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2241A71F8AD00FFB083 /* check.pdf */; }; 985724D81AD489AA0047C223 /* delete_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2251A71F8AD00FFB083 /* delete_H.pdf */; }; 985724D91AD489AA0047C223 /* delete.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2261A71F8AD00FFB083 /* delete.pdf */; }; 985724DA1AD489AA0047C223 /* syntax_help_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F421A75AABE00C058CB /* syntax_help_H.pdf */; }; 985724DB1AD489AA0047C223 /* syntax_help.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F431A75AABE00C058CB /* syntax_help.pdf */; }; 985724E01AD4C3310047C223 /* execute_script_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DE1AD4C3310047C223 /* execute_script_H.pdf */; }; 985724E11AD4C3310047C223 /* execute_script.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DF1AD4C3310047C223 /* execute_script.pdf */; }; 985724E71AD622880047C223 /* EidosConsoleTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 985724E61AD622880047C223 /* EidosConsoleTextView.mm */; }; 985724EA1AD6B9FE0047C223 /* execute_selection_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E81AD6B9FE0047C223 /* execute_selection_H.pdf */; }; 985724EB1AD6B9FE0047C223 /* execute_selection.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E91AD6B9FE0047C223 /* execute_selection.pdf */; }; 985724F01AD6D4060047C223 /* show_parse_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EC1AD6D4060047C223 /* show_parse_H.pdf */; }; 985724F11AD6D4060047C223 /* show_parse.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724ED1AD6D4060047C223 /* show_parse.pdf */; }; 985724F21AD6D4060047C223 /* show_tokens_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EE1AD6D4060047C223 /* show_tokens_H.pdf */; }; 985724F31AD6D4060047C223 /* show_tokens.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EF1AD6D4060047C223 /* show_tokens.pdf */; }; 985724F61AD6DD470047C223 /* show_execution_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F41AD6DD470047C223 /* show_execution_H.pdf */; }; 985724F71AD6DD470047C223 /* show_execution.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F51AD6DD470047C223 /* show_execution.pdf */; }; 985D1D8B2808B84F00461CFA /* sparse_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985D1D892808B84F00461CFA /* sparse_vector.cpp */; }; 985D1D8C2808B84F00461CFA /* sparse_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985D1D892808B84F00461CFA /* sparse_vector.cpp */; }; 985F3EEA24BA27EC00E712E0 /* slim_test_genetics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EE924BA27EC00E712E0 /* slim_test_genetics.cpp */; }; 985F3EEB24BA27EC00E712E0 /* slim_test_genetics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EE924BA27EC00E712E0 /* slim_test_genetics.cpp */; }; 985F3EED24BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 985F3EEE24BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 985F3EEF24BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 985F3EF024BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 985F3EF224BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 985F3EF324BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 985F3EF424BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 985F3EF524BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 985F3EF724BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 985F3EF824BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 985F3EF924BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 985F3EFA24BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 985F3EFC24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 985F3EFD24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 985F3EFE24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 985F3EFF24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 985F3F0124BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 985F3F0224BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 985F3F0324BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 985F3F0424BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 985F3F0624BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 985F3F0724BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 985F3F0824BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 985F3F0924BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 985F3F0B24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 985F3F0C24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 985F3F0D24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 985F3F0E24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 98606AEE1DED0DCD00821CFF /* mutation_run.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98606AEC1DED0DCD00821CFF /* mutation_run.cpp */; }; 98606AEF1DED0DCD00821CFF /* mutation_run.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98606AEC1DED0DCD00821CFF /* mutation_run.cpp */; }; 986070EA2AACECD600FD6143 /* spatial_kernel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986070E82AACECD600FD6143 /* spatial_kernel.cpp */; }; 986070EB2AACECD600FD6143 /* spatial_kernel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986070E82AACECD600FD6143 /* spatial_kernel.cpp */; }; 986070EC2AACECD600FD6143 /* spatial_kernel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986070E82AACECD600FD6143 /* spatial_kernel.cpp */; }; 986070ED2AACECD600FD6143 /* spatial_kernel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986070E82AACECD600FD6143 /* spatial_kernel.cpp */; }; 986151DE2B1679C20083E68F /* slim_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98800DC21B7EDCB50046F5F9 /* slim_test.cpp */; }; 986151DF2B1679EC0083E68F /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 986151E02B167A080083E68F /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 986151E12B167A0F0083E68F /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 986151E22B167A130083E68F /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 986151E32B167A160083E68F /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 986151E42B167A190083E68F /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 986151E52B167A1C0083E68F /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 986151E62B167A1E0083E68F /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 986151E72B167A210083E68F /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 986151E82B167A280083E68F /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 986151E92B167A2C0083E68F /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 986151EA2B167A2E0083E68F /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 986151EB2B167A320083E68F /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 986151EC2B167A350083E68F /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 986151ED2B167A380083E68F /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 986151EE2B167A3A0083E68F /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 986151EF2B167A3D0083E68F /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 986151F02B167A400083E68F /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 986151F12B167A430083E68F /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 986151F22B167A490083E68F /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 986151F32B167A4D0083E68F /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 986151F42B167A4F0083E68F /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 986151F52B167A520083E68F /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 986151F62B167A550083E68F /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 986151F72B167A580083E68F /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 986151F82B167A5B0083E68F /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 986151F92B167A5E0083E68F /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 986151FA2B167A610083E68F /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 986151FB2B167A640083E68F /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 986151FC2B167A670083E68F /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 986151FD2B167A6A0083E68F /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 986151FE2B167A710083E68F /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 986151FF2B167A7A0083E68F /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 986152002B167A7A0083E68F /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 986152012B167A7A0083E68F /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 986152022B167A7A0083E68F /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 986152032B167A7A0083E68F /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 986152042B167A7A0083E68F /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 986152052B167A7A0083E68F /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 986152062B167A940083E68F /* genotypes.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2572278B9F7001D43BC /* genotypes.c */; }; 986152072B167A940083E68F /* tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 9850D8DF2063098E006BFD2E /* tables.c */; }; 986152082B167A940083E68F /* core.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2562278B9F7001D43BC /* core.c */; }; 986152092B167A940083E68F /* convert.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2592278B9F8001D43BC /* convert.c */; }; 9861520A2B167A940083E68F /* stats.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25B2278B9F8001D43BC /* stats.c */; }; 9861520B2B167A940083E68F /* text_input.c in Sources */ = {isa = PBXBuildFile; fileRef = D0A758F620A4CC9800132D2F /* text_input.c */; }; 9861520C2B167A940083E68F /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25D2278B9F8001D43BC /* trees.c */; }; 9861520D2B167AED0083E68F /* slim_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9898172D1A59750300F7417C /* slim_globals.cpp */; }; 9861520E2B167AED0083E68F /* community_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836868027CD72E900683639 /* community_eidos.cpp */; }; 9861520F2B167AED0083E68F /* mutation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6881A3CCFD0000AD4FC /* mutation.cpp */; }; 986152102B167AED0083E68F /* genomic_element.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6911A3CD4EF000AD4FC /* genomic_element.cpp */; }; 986152112B167AED0083E68F /* polymorphism.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69A1A3CD542000AD4FC /* polymorphism.cpp */; }; 986152122B167AED0083E68F /* spatial_map.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DEB47C2AA632AA00ABE60F /* spatial_map.cpp */; }; 986152132B167AED0083E68F /* species_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AC617924BA34ED0001914C /* species_eidos.cpp */; }; 986152142B167AED0083E68F /* mutation_run.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98606AEC1DED0DCD00821CFF /* mutation_run.cpp */; }; 986152152B167AED0083E68F /* slim_test_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F724BA21E3008CC658 /* slim_test_other.cpp */; }; 986152162B167AED0083E68F /* spatial_kernel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986070E82AACECD600FD6143 /* spatial_kernel.cpp */; }; 986152172B167AED0083E68F /* population.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6AC1A3CD5D3000AD4FC /* population.cpp */; }; 986152182B167AED0083E68F /* slim_eidos_block.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AB59791B2531F10077CB4A /* slim_eidos_block.cpp */; }; 986152192B167AED0083E68F /* slim_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DDAED4221765480038C133 /* slim_functions.cpp */; }; 9861521A2B167AED0083E68F /* individual.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98C92AEB1D0B07A6001C82BC /* individual.cpp */; }; 9861521B2B167AED0083E68F /* species.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9878A93D1A4E57E70007B9D6 /* species.cpp */; }; 9861521C2B167AED0083E68F /* community.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836867227CD40CF00683639 /* community.cpp */; }; 9861521D2B167AED0083E68F /* genomic_element_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6941A3CD51A000AD4FC /* genomic_element_type.cpp */; }; 9861521E2B167AED0083E68F /* substitution.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69D1A3CD551000AD4FC /* substitution.cpp */; }; 9861521F2B167AED0083E68F /* haplosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A61A3CD5A0000AD4FC /* haplosome.cpp */; }; 986152202B167AED0083E68F /* slim_test_genetics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EE924BA27EC00E712E0 /* slim_test_genetics.cpp */; }; 986152212B167AED0083E68F /* slim_test_core.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F324BA21B7008CC658 /* slim_test_core.cpp */; }; 986152222B167AED0083E68F /* subpopulation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A91A3CD5BB000AD4FC /* subpopulation.cpp */; }; 986152232B167AED0083E68F /* chromosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6971A3CD52A000AD4FC /* chromosome.cpp */; }; 986152242B167AED0083E68F /* log_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9809DF9E2550F32500C4E82D /* log_file.cpp */; }; 986152252B167AED0083E68F /* sparse_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985D1D892808B84F00461CFA /* sparse_vector.cpp */; }; 986152262B167AED0083E68F /* mutation_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A68E1A3CD4CF000AD4FC /* mutation_type.cpp */; }; 986152272B167B4E0083E68F /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 986152282B167B4E0083E68F /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 986152292B167B4E0083E68F /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 9861522A2B167B4E0083E68F /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 9861522B2B167B4E0083E68F /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 9861522C2B167B4E0083E68F /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 9861522D2B167B4E0083E68F /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 9861522E2B167B4E0083E68F /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 9861522F2B167B4E0083E68F /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 986152302B167B4E0083E68F /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 986152312B167B4E0083E68F /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 986152322B167B4E0083E68F /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 986152332B167B4E0083E68F /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 986152342B167B4E0083E68F /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 986152352B167B4E0083E68F /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 986152362B167B4E0083E68F /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 986152372B167B4E0083E68F /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 986152382B167B4E0083E68F /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 986152392B167B4E0083E68F /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 9861523A2B167B4E0083E68F /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 9861523B2B167B4E0083E68F /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 9861523C2B167B4E0083E68F /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 9861523D2B167B4E0083E68F /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 9861523E2B167B4E0083E68F /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 9861523F2B167B4E0083E68F /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 986152402B167B4E0083E68F /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 986152412B167B4E0083E68F /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 986152422B167B4E0083E68F /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 986152432B167B4E0083E68F /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 986152442B167B4E0083E68F /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 986152452B167B4E0083E68F /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 986152462B167B4E0083E68F /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 986152472B167B4E0083E68F /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 986152482B167B4E0083E68F /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 986152492B167B4E0083E68F /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 9861524A2B167B4E0083E68F /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 9861524B2B167B4E0083E68F /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 9861524C2B167B4E0083E68F /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 9861524D2B167B4E0083E68F /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 9861524E2B167B4E0083E68F /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 9861524F2B167B4E0083E68F /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 986152502B167B4E0083E68F /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 986152512B167B4E0083E68F /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 986152522B167B4E0083E68F /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 986152532B167B4E0083E68F /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 986152542B167B4E0083E68F /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 986152552B167B4E0083E68F /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 986152562B167B4E0083E68F /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 986152572B167B4E0083E68F /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 986152582B167B4E0083E68F /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 986152592B167B4E0083E68F /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 9861525A2B167B4E0083E68F /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 9861525B2B167B4E0083E68F /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 9861525C2B167B4E0083E68F /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 9861525D2B167B4E0083E68F /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 9861525E2B167B4E0083E68F /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 9861525F2B167B4E0083E68F /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 986152602B167B4E0083E68F /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 986152612B167B4E0083E68F /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 986152622B167B4E0083E68F /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 986152632B167B4E0083E68F /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 986152642B167B4E0083E68F /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 986152652B167B4E0083E68F /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 986152662B167B4E0083E68F /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 986152672B167B4E0083E68F /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 986152682B167B4E0083E68F /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 986152692B167B4E0083E68F /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 9861526A2B167B4E0083E68F /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 9861526B2B167B4E0083E68F /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 9861526C2B167B4E0083E68F /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 9861526D2B167B4E0083E68F /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 9861526E2B167B4E0083E68F /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 9861526F2B167B4E0083E68F /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 986152702B167B4E0083E68F /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 986152712B167B4E0083E68F /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 986152722B167B4E0083E68F /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 986152732B167B4E0083E68F /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 986152742B167B4E0083E68F /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 986152752B167B4E0083E68F /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 986152762B167C620083E68F /* kastore.c in Sources */ = {isa = PBXBuildFile; fileRef = 987D19A4209A53850030D28D /* kastore.c */; }; 986152772B167CBE0083E68F /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 986152782B167D0B0083E68F /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 986926D41AA1337A0000E138 /* graph_submenu_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 986926D21AA1337A0000E138 /* graph_submenu_H.pdf */; }; 986926D51AA1337A0000E138 /* graph_submenu.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 986926D31AA1337A0000E138 /* graph_submenu.pdf */; }; 986926D91AA140550000E138 /* GraphView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926D81AA140550000E138 /* GraphView.mm */; }; 986926DC1AA1429D0000E138 /* GraphWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 986926DA1AA1429D0000E138 /* GraphWindow.xib */; }; 986926DF1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926DE1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.mm */; }; 986926E21AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926E11AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.mm */; }; 986926E51AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926E41AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.mm */; }; 986926E81AA40AFF0000E138 /* GraphView_FitnessOverTime.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926E71AA40AFF0000E138 /* GraphView_FitnessOverTime.mm */; }; 986926EB1AA6B7480000E138 /* GraphView_PopulationVisualization.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926EA1AA6B7480000E138 /* GraphView_PopulationVisualization.mm */; }; 986D73E81B07E89E007FBB70 /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 986D73E91B07E89E007FBB70 /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 986D73EA1B07E89E007FBB70 /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 98729AD82A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98729AD92A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98729ADA2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98729ADB2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98729ADC2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98729ADD2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98729ADE2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98729ADF2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */; }; 98760EDD28CE5E7600CEBC40 /* libomp.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 98760EDC28CE5E7600CEBC40 /* libomp.dylib */; }; 98760EDE28CE5E8200CEBC40 /* libomp.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 98760EDC28CE5E7600CEBC40 /* libomp.dylib */; }; 9876E5F51ED5572C00FF9762 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 9876E5F61ED5573700FF9762 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 9876E5F71ED5573700FF9762 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 9876E5F81ED5573800FF9762 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 9876E5FC1ED5599F00FF9762 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 9876E5FD1ED559A600FF9762 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 9876E5FE1ED559A600FF9762 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 9876E5FF1ED559A700FF9762 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 9876E6011ED55A2500FF9762 /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 9876E6031ED55A7800FF9762 /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 9876E6041ED55A7800FF9762 /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 9876E6051ED55A7900FF9762 /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 9876E6081ED55B4700FF9762 /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 9876E6091ED55B4F00FF9762 /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 9876E60A1ED55B4F00FF9762 /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 9876E60B1ED55B5000FF9762 /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 9876E60D1ED55C0400FF9762 /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 9876E60E1ED55C0B00FF9762 /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 9876E60F1ED55C0C00FF9762 /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 9876E6101ED55C0C00FF9762 /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 9876E6121ED55C6B00FF9762 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 9876E6131ED55C7300FF9762 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 9876E6141ED55C7400FF9762 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 9876E6151ED55C7400FF9762 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 9878A93F1A4E57E70007B9D6 /* species.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9878A93D1A4E57E70007B9D6 /* species.cpp */; }; 987A2A0724033856009A636F /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 987A2A0824033856009A636F /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 987A2A0924033856009A636F /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 987A2A0A24033856009A636F /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 987A700F2AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987A70102AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987A70112AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987A70122AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987A70132AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987A70142AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987A70152AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987A70162AE8032100A049E2 /* laplace.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A700E2AE8032100A049E2 /* laplace.c */; }; 987AD8741B2CBDA70035D6C8 /* show_console_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 987AD8721B2CBDA70035D6C8 /* show_console_H.pdf */; }; 987AD8751B2CBDA70035D6C8 /* show_console.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 987AD8731B2CBDA70035D6C8 /* show_console.pdf */; }; 987AD8761B2CBE010035D6C8 /* show_browser_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA01B1B8B5800791DBF /* show_browser_H.pdf */; }; 987AD8771B2CBE050035D6C8 /* show_browser.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA11B1B8B5800791DBF /* show_browser.pdf */; }; 987AD8781B2CBE280035D6C8 /* execute_script_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DE1AD4C3310047C223 /* execute_script_H.pdf */; }; 987AD8791B2CBE2E0035D6C8 /* execute_script.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DF1AD4C3310047C223 /* execute_script.pdf */; }; 987AD87A1B2CBE330035D6C8 /* execute_selection_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E81AD6B9FE0047C223 /* execute_selection_H.pdf */; }; 987AD87B1B2CBE360035D6C8 /* execute_selection.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E91AD6B9FE0047C223 /* execute_selection.pdf */; }; 987AD87C1B2CBE4B0035D6C8 /* show_parse_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EC1AD6D4060047C223 /* show_parse_H.pdf */; }; 987AD87D1B2CBE4F0035D6C8 /* show_parse.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724ED1AD6D4060047C223 /* show_parse.pdf */; }; 987AD87E1B2CBE520035D6C8 /* show_tokens_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EE1AD6D4060047C223 /* show_tokens_H.pdf */; }; 987AD87F1B2CBE550035D6C8 /* show_tokens.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EF1AD6D4060047C223 /* show_tokens.pdf */; }; 987AD8801B2CBE580035D6C8 /* show_execution_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F41AD6DD470047C223 /* show_execution_H.pdf */; }; 987AD8811B2CBE5B0035D6C8 /* show_execution.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F51AD6DD470047C223 /* show_execution.pdf */; }; 987AD8821B2CBF960035D6C8 /* EidosConsoleTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 985724E61AD622880047C223 /* EidosConsoleTextView.mm */; }; 987AD8831B2CBF9A0035D6C8 /* EidosValueWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98090FA51B1B978900791DBF /* EidosValueWrapper.mm */; }; 987AD8861B2CC0C10035D6C8 /* EidosVariableBrowserController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8851B2CC0C10035D6C8 /* EidosVariableBrowserController.mm */; }; 987AD8871B2CC0C10035D6C8 /* EidosVariableBrowserController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8851B2CC0C10035D6C8 /* EidosVariableBrowserController.mm */; }; 987AD88A1B2CDBB80035D6C8 /* EidosConsoleWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8891B2CDBB80035D6C8 /* EidosConsoleWindowController.mm */; }; 987AD88B1B2CDBB80035D6C8 /* EidosConsoleWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8891B2CDBB80035D6C8 /* EidosConsoleWindowController.mm */; }; 987D19A5209A53850030D28D /* kastore.c in Sources */ = {isa = PBXBuildFile; fileRef = 987D19A4209A53850030D28D /* kastore.c */; }; 987D19A6209A53850030D28D /* kastore.c in Sources */ = {isa = PBXBuildFile; fileRef = 987D19A4209A53850030D28D /* kastore.c */; }; 98800DC41B7EDCB50046F5F9 /* slim_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98800DC21B7EDCB50046F5F9 /* slim_test.cpp */; }; 98800DC51B82B6C60046F5F9 /* slim_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98800DC21B7EDCB50046F5F9 /* slim_test.cpp */; }; 9887946B1EA8804900AE0C8D /* SLiMPDFDocument.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9887946A1EA8804900AE0C8D /* SLiMPDFDocument.mm */; }; 9887946F1EA8808000AE0C8D /* SLiMPDFWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9887946D1EA8808000AE0C8D /* SLiMPDFWindowController.mm */; }; 988794701EA8808000AE0C8D /* SLiMPDFWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9887946E1EA8808000AE0C8D /* SLiMPDFWindow.xib */; }; 988794731EA8C42200AE0C8D /* SLiMPDFView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 988794721EA8C42200AE0C8D /* SLiMPDFView.mm */; }; 988880EC20744EE900E10172 /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 988880ED20744F0100E10172 /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 988880EE20744F0100E10172 /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 988880EF20744F0200E10172 /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 9890D1ED27136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 9890D1EE27136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 9890D1EF27136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 9890D1F027136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 9890D2002713741C001EAE98 /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 9892282B1BAE279700429674 /* EidosHelpOperators.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282A1BAE279700429674 /* EidosHelpOperators.rtf */; }; 9892282C1BAE279700429674 /* EidosHelpOperators.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282A1BAE279700429674 /* EidosHelpOperators.rtf */; }; 9892282E1BAE27AA00429674 /* EidosHelpTypes.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282D1BAE27AA00429674 /* EidosHelpTypes.rtf */; }; 9892282F1BAE27AA00429674 /* EidosHelpTypes.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282D1BAE27AA00429674 /* EidosHelpTypes.rtf */; }; 989228311BAF496C00429674 /* EidosHelpStatements.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 989228301BAF496C00429674 /* EidosHelpStatements.rtf */; }; 989228321BAF496C00429674 /* EidosHelpStatements.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 989228301BAF496C00429674 /* EidosHelpStatements.rtf */; }; 989228341BAFB27300429674 /* SLiMHelpCallbacks.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 989228331BAFB27300429674 /* SLiMHelpCallbacks.rtf */; }; 9893C7DD1CDA2D650029AC94 /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 9893C7DE1CDA2D650029AC94 /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 9893C7DF1CDA2FC10029AC94 /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 9893C7E01CDA2FC20029AC94 /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 9893C7E31CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 9893C7E41CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 9893C7E51CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 9893C7E61CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 9893C7E91CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 9893C7EA1CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 9893C7EB1CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 9893C7EC1CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 989524A91E40AE74007E62FA /* SLiMDocument.mm in Sources */ = {isa = PBXBuildFile; fileRef = 989524A81E40AE74007E62FA /* SLiMDocument.mm */; }; 989790DA1AF3D0E100C6B14C /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 989790DB1AF3D0E100C6B14C /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 989790DC1AF3D0E100C6B14C /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 9898172F1A59750300F7417C /* slim_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9898172D1A59750300F7417C /* slim_globals.cpp */; }; 989A5BE92525304100E7192D /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 989A5BEA2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 989A5BEB2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 989A5BEC2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 989A5BED2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 98A240591B8E3295005C9A30 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98A240581B8E3295005C9A30 /* Cocoa.framework */; }; 98A2405A1B8E32D0005C9A30 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98A240581B8E3295005C9A30 /* Cocoa.framework */; }; 98A2405D1B8E338E005C9A30 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98A2405C1B8E338E005C9A30 /* OpenGL.framework */; }; 98A2FF891D7DF4D7007E3DB8 /* Recipes in Resources */ = {isa = PBXBuildFile; fileRef = 98A2FF881D7DF4D7007E3DB8 /* Recipes */; }; 98A4EC941B67C1CD00CD92FD /* EidosConsoleWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98A4EC921B67C1CD00CD92FD /* EidosConsoleWindow.xib */; }; 98A4EC951B67C1D900CD92FD /* EidosConsoleWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98A4EC921B67C1CD00CD92FD /* EidosConsoleWindow.xib */; }; 98A5134F1B66B69E005A753D /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 98A513501B66B69E005A753D /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 98A513511B66B69E005A753D /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 98A513541B66B6CA005A753D /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 98A513551B66B6CA005A753D /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 98A513561B66B6CA005A753D /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 98AB597B1B2531F10077CB4A /* slim_eidos_block.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AB59791B2531F10077CB4A /* slim_eidos_block.cpp */; }; 98AB597C1B2531F10077CB4A /* slim_eidos_block.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AB59791B2531F10077CB4A /* slim_eidos_block.cpp */; }; 98AC617A24BA34ED0001914C /* species_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AC617924BA34ED0001914C /* species_eidos.cpp */; }; 98AC617B24BA34ED0001914C /* species_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AC617924BA34ED0001914C /* species_eidos.cpp */; }; 98ACDC9D253522B80038703F /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98ACDC9E253522B80038703F /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98ACDC9F253522B80038703F /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98ACDCA0253522B80038703F /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98ACDCA1253522B80038703F /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98C0943E1B7663DF00766A9A /* female_symbol.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98C0943C1B7663DF00766A9A /* female_symbol.pdf */; }; 98C0943F1B7663DF00766A9A /* male_symbol.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98C0943D1B7663DF00766A9A /* male_symbol.pdf */; }; 98C634442EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C634452EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C634462EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C634472EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C634482EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C634492EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C6344A2EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C6344B2EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C6344C2EF9F632003F12A3 /* dirichlet.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C634432EF9F632003F12A3 /* dirichlet.c */; }; 98C821241C7A980000548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98C821251C7A980000548839 /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98C821261C7A980000548839 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98C821271C7A980000548839 /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98C821281C7A980000548839 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98C821291C7A980000548839 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98C8212A1C7A980000548839 /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98C8212B1C7A980000548839 /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98C8212C1C7A980000548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98C8212D1C7A980000548839 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98C8212E1C7A980000548839 /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98C8212F1C7A980000548839 /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98C821301C7A980000548839 /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98C821311C7A980000548839 /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98C821321C7A980000548839 /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98C821331C7A980000548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98C821341C7A980000548839 /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98C821351C7A980000548839 /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98C821361C7A980000548839 /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98C821381C7A980000548839 /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98C821391C7A980000548839 /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98C8213A1C7A980000548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98C8213B1C7A980000548839 /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98C8213C1C7A980000548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98C8213D1C7A980000548839 /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98C8213E1C7A980000548839 /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98C8213F1C7A980000548839 /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98C821401C7A980000548839 /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98C821411C7A980000548839 /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98C821421C7A980000548839 /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98C821431C7A983700548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98C821441C7A983700548839 /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98C821451C7A983700548839 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98C821461C7A983700548839 /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98C821471C7A983700548839 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98C821481C7A983700548839 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98C821491C7A983700548839 /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98C8214A1C7A983700548839 /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98C8214B1C7A983700548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98C8214C1C7A983700548839 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98C8214D1C7A983700548839 /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98C8214E1C7A983700548839 /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98C8214F1C7A983700548839 /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98C821501C7A983700548839 /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98C821511C7A983700548839 /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98C821521C7A983700548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98C821531C7A983700548839 /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98C821541C7A983700548839 /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98C821551C7A983700548839 /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98C821561C7A983700548839 /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98C821571C7A983700548839 /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98C821581C7A983700548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98C821591C7A983700548839 /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98C8215A1C7A983700548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98C8215B1C7A983700548839 /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98C8215C1C7A983700548839 /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98C8215D1C7A983700548839 /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98C8215E1C7A983700548839 /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98C8215F1C7A983700548839 /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98C821601C7A983700548839 /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98C821611C7A983800548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98C821621C7A983800548839 /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98C821631C7A983800548839 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98C821641C7A983800548839 /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98C821651C7A983800548839 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98C821661C7A983800548839 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98C821671C7A983800548839 /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98C821681C7A983800548839 /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98C821691C7A983800548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98C8216A1C7A983800548839 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98C8216B1C7A983800548839 /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98C8216C1C7A983800548839 /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98C8216D1C7A983800548839 /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98C8216E1C7A983800548839 /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98C8216F1C7A983800548839 /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98C821701C7A983800548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98C821711C7A983800548839 /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98C821721C7A983800548839 /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98C821731C7A983800548839 /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98C821741C7A983800548839 /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98C821751C7A983800548839 /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98C821761C7A983800548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98C821771C7A983800548839 /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98C821781C7A983800548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98C821791C7A983800548839 /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98C8217A1C7A983800548839 /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98C8217B1C7A983800548839 /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98C8217C1C7A983800548839 /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98C8217D1C7A983800548839 /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98C8217E1C7A983800548839 /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98C8217F1C7A983800548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98C821801C7A983800548839 /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98C821811C7A983800548839 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98C821821C7A983800548839 /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98C821831C7A983800548839 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98C821841C7A983800548839 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98C821851C7A983800548839 /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98C821861C7A983800548839 /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98C821871C7A983800548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98C821881C7A983800548839 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98C821891C7A983800548839 /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98C8218A1C7A983800548839 /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98C8218B1C7A983800548839 /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98C8218C1C7A983800548839 /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98C8218D1C7A983800548839 /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98C8218E1C7A983800548839 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98C8218F1C7A983800548839 /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98C821901C7A983800548839 /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98C821911C7A983800548839 /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98C821921C7A983800548839 /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98C821931C7A983800548839 /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98C821941C7A983800548839 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98C821951C7A983800548839 /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98C821961C7A983800548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98C821971C7A983800548839 /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98C821981C7A983800548839 /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98C821991C7A983800548839 /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98C8219A1C7A983800548839 /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98C8219B1C7A983800548839 /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98C8219C1C7A983800548839 /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98C8219E1C7A99B200548839 /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98C8219F1C7A99B200548839 /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98C821A01C7A99B200548839 /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98C821A11C7A99B200548839 /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98C821A31C7A99F000548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98C821A41C7A99F000548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98C821A51C7A99F000548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98C821A61C7A99F000548839 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98C821A91C7A9B1600548839 /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98C821AA1C7A9B1600548839 /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98C821AB1C7A9B1600548839 /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98C821AC1C7A9B1600548839 /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98C821AD1C7A9B1600548839 /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98C821AE1C7A9B1600548839 /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98C821AF1C7A9B1600548839 /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98C821B01C7A9B1600548839 /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98C821B21C7A9B9F00548839 /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98C821B31C7A9B9F00548839 /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98C821B41C7A9B9F00548839 /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98C821B51C7A9B9F00548839 /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98C92AED1D0B07A6001C82BC /* individual.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98C92AEB1D0B07A6001C82BC /* individual.cpp */; }; 98C92AEE1D0B07A6001C82BC /* individual.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98C92AEB1D0B07A6001C82BC /* individual.cpp */; }; 98CEFCEC2AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCED2AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCEE2AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCEF2AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCF02AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCF12AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCF22AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCF32AAFABAA00D2C9B4 /* spline2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */; }; 98CEFCF42AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFCF52AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFCF62AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFCF72AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFCF82AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFCF92AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFCFA2AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFCFB2AAFABAA00D2C9B4 /* bilinear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */; }; 98CEFD102AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD112AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD122AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD132AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD142AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD152AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD162AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD172AAFABAA00D2C9B4 /* interp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */; }; 98CEFD182AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD192AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD1A2AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD1B2AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD1C2AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD1D2AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD1E2AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD1F2AAFABAA00D2C9B4 /* bicubic.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */; }; 98CEFD202AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD212AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD222AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD232AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD242AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD252AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD262AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD272AAFABAA00D2C9B4 /* akima.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */; }; 98CEFD302AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD312AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD322AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD332AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD342AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD352AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD362AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD372AAFABAA00D2C9B4 /* spline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE22AAFABAA00D2C9B4 /* spline.c */; }; 98CEFD482AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD492AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD4A2AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD4B2AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD4C2AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD4D2AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD4E2AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD4F2AAFABAA00D2C9B4 /* interp2d.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */; }; 98CEFD502AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD512AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD522AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD532AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD542AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD552AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD562AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD572AAFABAA00D2C9B4 /* linear.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCE62AAFABAA00D2C9B4 /* linear.c */; }; 98CEFD642AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD652AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD662AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD672AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD682AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD692AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD6A2AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD6B2AAFABAA00D2C9B4 /* accel.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */; }; 98CEFD6C2AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD6D2AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD6E2AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD6F2AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD702AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD712AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD722AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD732AAFABAA00D2C9B4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */; }; 98CEFD752AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD762AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD772AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD782AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD792AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD7A2AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD7B2AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD7C2AAFB12F00D2C9B4 /* cspline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD742AAFB12F00D2C9B4 /* cspline.c */; }; 98CEFD7E2AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD7F2AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD802AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD812AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD822AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD832AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD842AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD852AAFB23E00D2C9B4 /* tridiag.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */; }; 98CEFD882AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CEFD892AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CEFD8A2AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CEFD8B2AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CEFD8C2AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CEFD8D2AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CEFD8E2AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CEFD8F2AAFB4F000D2C9B4 /* view.c in Sources */ = {isa = PBXBuildFile; fileRef = 98CEFD872AAFB4EF00D2C9B4 /* view.c */; }; 98CF264F1E42DBE200E392D8 /* slim.iconset in Resources */ = {isa = PBXBuildFile; fileRef = 98CF264E1E42DBE200E392D8 /* slim.iconset */; }; 98CF26521E4353FE00E392D8 /* SLiMDocumentController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98CF26511E4353FE00E392D8 /* SLiMDocumentController.mm */; }; 98CF51F5294A3FC900557BBA /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 98CF51F6294A3FC900557BBA /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 98CF51F7294A3FC900557BBA /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 98CF51F8294A3FC900557BBA /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 98CF51F9294A3FC900557BBA /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98CF51FA294A3FC900557BBA /* slim_eidos_block.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AB59791B2531F10077CB4A /* slim_eidos_block.cpp */; }; 98CF51FB294A3FC900557BBA /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 98CF51FC294A3FC900557BBA /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98CF51FD294A3FC900557BBA /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 98CF51FE294A3FC900557BBA /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98CF51FF294A3FC900557BBA /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 98CF5200294A3FC900557BBA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C1BA1A6F537B00FFB083 /* main.m */; }; 98CF5201294A3FC900557BBA /* EidosValueWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98090FA51B1B978900791DBF /* EidosValueWrapper.mm */; }; 98CF5202294A3FC900557BBA /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98CF5203294A3FC900557BBA /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98CF5204294A3FC900557BBA /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 98CF5205294A3FC900557BBA /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98CF5206294A3FC900557BBA /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25D2278B9F8001D43BC /* trees.c */; }; 98CF5207294A3FC900557BBA /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 98CF5208294A3FC900557BBA /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 98CF5209294A3FC900557BBA /* GraphView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926D81AA140550000E138 /* GraphView.mm */; }; 98CF520A294A3FC900557BBA /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98CF520B294A3FC900557BBA /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 98CF520C294A3FC900557BBA /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98CF520D294A3FC900557BBA /* interaction_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DB3D6D1E6122AE00E2C200 /* interaction_type.cpp */; }; 98CF520E294A3FC900557BBA /* subpopulation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A91A3CD5BB000AD4FC /* subpopulation.cpp */; }; 98CF520F294A3FC900557BBA /* community.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836867227CD40CF00683639 /* community.cpp */; }; 98CF5210294A3FC900557BBA /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 98CF5211294A3FC900557BBA /* EidosTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D218C41B2E213200156FC3 /* EidosTextView.mm */; }; 98CF5212294A3FC900557BBA /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98CF5213294A3FC900557BBA /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 98CF5214294A3FC900557BBA /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98CF5215294A3FC900557BBA /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98CF5216294A3FC900557BBA /* tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 9850D8DF2063098E006BFD2E /* tables.c */; }; 98CF5217294A3FC900557BBA /* species_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AC617924BA34ED0001914C /* species_eidos.cpp */; }; 98CF5218294A3FC900557BBA /* slim_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DDAED4221765480038C133 /* slim_functions.cpp */; }; 98CF5219294A3FC900557BBA /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 98CF521A294A3FC900557BBA /* FindRecipeController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 984252C2216FA9930019696A /* FindRecipeController.mm */; }; 98CF521B294A3FC900557BBA /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 98CF521C294A3FC900557BBA /* mutation_run.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98606AEC1DED0DCD00821CFF /* mutation_run.cpp */; }; 98CF521D294A3FC900557BBA /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 98CF521E294A3FC900557BBA /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 98CF521F294A3FC900557BBA /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 98CF5220294A3FC900557BBA /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98CF5221294A3FC900557BBA /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98CF5222294A3FC900557BBA /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98CF5223294A3FC900557BBA /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98CF5224294A3FC900557BBA /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 98CF5225294A3FC900557BBA /* slim_test_genetics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EE924BA27EC00E712E0 /* slim_test_genetics.cpp */; }; 98CF5226294A3FC900557BBA /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98CF5227294A3FC900557BBA /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98CF5228294A3FC900557BBA /* genomic_element.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6911A3CD4EF000AD4FC /* genomic_element.cpp */; }; 98CF5229294A3FC900557BBA /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98CF522A294A3FC900557BBA /* substitution.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69D1A3CD551000AD4FC /* substitution.cpp */; }; 98CF522B294A3FC900557BBA /* core.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2562278B9F7001D43BC /* core.c */; }; 98CF522C294A3FC900557BBA /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 98CF522D294A3FC900557BBA /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98CF522E294A3FC900557BBA /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98CF522F294A3FC900557BBA /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 98CF5230294A3FC900557BBA /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 98CF5231294A3FC900557BBA /* EidosHelpController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 982556641BA450980054CB3F /* EidosHelpController.mm */; }; 98CF5232294A3FC900557BBA /* community_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836868027CD72E900683639 /* community_eidos.cpp */; }; 98CF5233294A3FC900557BBA /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98CF5234294A3FC900557BBA /* slim_test_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F724BA21E3008CC658 /* slim_test_other.cpp */; }; 98CF5235294A3FC900557BBA /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 98CF5236294A3FC900557BBA /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98CF5237294A3FC900557BBA /* GraphView_MutationLossTimeHistogram.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926E11AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.mm */; }; 98CF5238294A3FC900557BBA /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98CF5239294A3FC900557BBA /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98CF523A294A3FC900557BBA /* individual.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98C92AEB1D0B07A6001C82BC /* individual.cpp */; }; 98CF523B294A3FC900557BBA /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 98CF523C294A3FC900557BBA /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98CF523D294A3FC900557BBA /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 98CF523E294A3FC900557BBA /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98CF523F294A3FC900557BBA /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 98CF5240294A3FC900557BBA /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 98CF5241294A3FC900557BBA /* CocoaExtra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C21B1A718EFD00FFB083 /* CocoaExtra.mm */; }; 98CF5242294A3FC900557BBA /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98CF5243294A3FC900557BBA /* slim_test_core.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F324BA21B7008CC658 /* slim_test_core.cpp */; }; 98CF5244294A3FC900557BBA /* EidosCocoaExtra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9825565A1BA32EE80054CB3F /* EidosCocoaExtra.mm */; }; 98CF5245294A3FC900557BBA /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98CF5246294A3FC900557BBA /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98CF5247294A3FC900557BBA /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 98CF5248294A3FC900557BBA /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 98CF5249294A3FC900557BBA /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 98CF524A294A3FC900557BBA /* SLiMPDFDocument.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9887946A1EA8804900AE0C8D /* SLiMPDFDocument.mm */; }; 98CF524B294A3FC900557BBA /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 98CF524C294A3FC900557BBA /* log_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9809DF9E2550F32500C4E82D /* log_file.cpp */; }; 98CF524D294A3FC900557BBA /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 98CF524E294A3FC900557BBA /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98CF524F294A3FC900557BBA /* population.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6AC1A3CD5D3000AD4FC /* population.cpp */; }; 98CF5250294A3FC900557BBA /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 98CF5251294A3FC900557BBA /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 98CF5252294A3FC900557BBA /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98CF5254294A3FC900557BBA /* GraphView_FitnessOverTime.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926E71AA40AFF0000E138 /* GraphView_FitnessOverTime.mm */; }; 98CF5255294A3FC900557BBA /* SLiMWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C2031A701D5A00FFB083 /* SLiMWindowController.mm */; }; 98CF5256294A3FC900557BBA /* PopulationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C2061A704EA700FFB083 /* PopulationView.mm */; }; 98CF5257294A3FC900557BBA /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98CF5258294A3FC900557BBA /* GraphView_PopulationVisualization.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926EA1AA6B7480000E138 /* GraphView_PopulationVisualization.mm */; }; 98CF5259294A3FC900557BBA /* EidosPrettyprinter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D524681F2EB4DD005AD9A6 /* EidosPrettyprinter.mm */; }; 98CF525A294A3FC900557BBA /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98CF525B294A3FC900557BBA /* genomic_element_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6941A3CD51A000AD4FC /* genomic_element_type.cpp */; }; 98CF525C294A3FC900557BBA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C1B81A6F537B00FFB083 /* AppDelegate.mm */; }; 98CF525D294A3FC900557BBA /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98CF525E294A3FC900557BBA /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98CF525F294A3FC900557BBA /* slim_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98800DC21B7EDCB50046F5F9 /* slim_test.cpp */; }; 98CF5260294A3FC900557BBA /* sparse_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985D1D892808B84F00461CFA /* sparse_vector.cpp */; }; 98CF5261294A3FC900557BBA /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 98CF5262294A3FC900557BBA /* mutation_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A68E1A3CD4CF000AD4FC /* mutation_type.cpp */; }; 98CF5263294A3FC900557BBA /* convert.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2592278B9F8001D43BC /* convert.c */; }; 98CF5264294A3FC900557BBA /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 98CF5265294A3FC900557BBA /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 98CF5266294A3FC900557BBA /* polymorphism.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69A1A3CD542000AD4FC /* polymorphism.cpp */; }; 98CF5267294A3FC900557BBA /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98CF5268294A3FC900557BBA /* species.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9878A93D1A4E57E70007B9D6 /* species.cpp */; }; 98CF526A294A3FC900557BBA /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98CF526B294A3FC900557BBA /* text_input.c in Sources */ = {isa = PBXBuildFile; fileRef = D0A758F620A4CC9800132D2F /* text_input.c */; }; 98CF526C294A3FC900557BBA /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98CF526D294A3FC900557BBA /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 98CF526E294A3FC900557BBA /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 98CF526F294A3FC900557BBA /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 98CF5270294A3FC900557BBA /* ChromosomeView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C2091A7086FF00FFB083 /* ChromosomeView.mm */; }; 98CF5272294A3FC900557BBA /* EidosConsoleTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 985724E61AD622880047C223 /* EidosConsoleTextView.mm */; }; 98CF5273294A3FC900557BBA /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 98CF5274294A3FC900557BBA /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98CF5275294A3FC900557BBA /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98CF5276294A3FC900557BBA /* SLiMPDFWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9887946D1EA8808000AE0C8D /* SLiMPDFWindowController.mm */; }; 98CF5277294A3FC900557BBA /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98CF5278294A3FC900557BBA /* GraphView_MutationFrequencyTrajectory.mm in Sources */ = {isa = PBXBuildFile; fileRef = 980DD51C1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.mm */; }; 98CF5279294A3FC900557BBA /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 98CF527A294A3FC900557BBA /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 98CF527B294A3FC900557BBA /* SLiMDocument.mm in Sources */ = {isa = PBXBuildFile; fileRef = 989524A81E40AE74007E62FA /* SLiMDocument.mm */; }; 98CF527C294A3FC900557BBA /* EidosConsoleWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8891B2CDBB80035D6C8 /* EidosConsoleWindowController.mm */; }; 98CF527D294A3FC900557BBA /* SLiMDocumentController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98CF26511E4353FE00E392D8 /* SLiMDocumentController.mm */; }; 98CF527E294A3FC900557BBA /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98CF527F294A3FC900557BBA /* GraphView_MutationFixationTimeHistogram.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926E41AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.mm */; }; 98CF5280294A3FC900557BBA /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 98CF5281294A3FC900557BBA /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98CF5282294A3FC900557BBA /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98CF5283294A3FC900557BBA /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 98CF5284294A3FC900557BBA /* kastore.c in Sources */ = {isa = PBXBuildFile; fileRef = 987D19A4209A53850030D28D /* kastore.c */; }; 98CF5285294A3FC900557BBA /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 98CF5286294A3FC900557BBA /* chromosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6971A3CD52A000AD4FC /* chromosome.cpp */; }; 98CF5287294A3FC900557BBA /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 98CF5288294A3FC900557BBA /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98CF5289294A3FC900557BBA /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 98CF528A294A3FC900557BBA /* GraphView_MutationFrequencySpectra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 986926DE1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.mm */; }; 98CF528B294A3FC900557BBA /* genotypes.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2572278B9F7001D43BC /* genotypes.c */; }; 98CF528C294A3FC900557BBA /* SLiMPDFView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 988794721EA8C42200AE0C8D /* SLiMPDFView.mm */; }; 98CF528D294A3FC900557BBA /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 98CF528E294A3FC900557BBA /* haplosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A61A3CD5A0000AD4FC /* haplosome.cpp */; }; 98CF528F294A3FC900557BBA /* stats.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25B2278B9F8001D43BC /* stats.c */; }; 98CF5290294A3FC900557BBA /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 98CF5291294A3FC900557BBA /* EidosVariableBrowserController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8851B2CC0C10035D6C8 /* EidosVariableBrowserController.mm */; }; 98CF5292294A3FC900557BBA /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 98CF5293294A3FC900557BBA /* slim_gui.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98EBCD1221F3CFC600B385CF /* slim_gui.mm */; }; 98CF5294294A3FC900557BBA /* mutation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6881A3CCFD0000AD4FC /* mutation.cpp */; }; 98CF5295294A3FC900557BBA /* slim_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9898172D1A59750300F7417C /* slim_globals.cpp */; }; 98CF5296294A3FC900557BBA /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 98CF5297294A3FC900557BBA /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 98CF5298294A3FC900557BBA /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98CF5299294A3FC900557BBA /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 98CF529A294A3FC900557BBA /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 98CF529B294A3FC900557BBA /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98CF529C294A3FC900557BBA /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98CF529D294A3FC900557BBA /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98CF529E294A3FC900557BBA /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98CF529F294A3FC900557BBA /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98CF52A0294A3FC900557BBA /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98CF52A1294A3FC900557BBA /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 98CF52A3294A3FC900557BBA /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98A2405C1B8E338E005C9A30 /* OpenGL.framework */; }; 98CF52A4294A3FC900557BBA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98A240581B8E3295005C9A30 /* Cocoa.framework */; }; 98CF52A5294A3FC900557BBA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9821E2031ABDBC300036EAEA /* QuartzCore.framework */; }; 98CF52A6294A3FC900557BBA /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98D4C2151A7187E200FFB083 /* Quartz.framework */; }; 98CF52A7294A3FC900557BBA /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98D4C20F1A716E0E00FFB083 /* WebKit.framework */; }; 98CF52A9294A3FC900557BBA /* profile.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98EF4AB51ECDA5EA00CCDB09 /* profile.pdf */; }; 98CF52AA294A3FC900557BBA /* Recipes in Resources */ = {isa = PBXBuildFile; fileRef = 98A2FF881D7DF4D7007E3DB8 /* Recipes */; }; 98CF52AB294A3FC900557BBA /* play_step.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F31A70040400FFB083 /* play_step.pdf */; }; 98CF52AC294A3FC900557BBA /* FindRecipePanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98024740215D85880025D29C /* FindRecipePanel.xib */; }; 98CF52AD294A3FC900557BBA /* slim.iconset in Resources */ = {isa = PBXBuildFile; fileRef = 98CF264E1E42DBE200E392D8 /* slim.iconset */; }; 98CF52AE294A3FC900557BBA /* show_parse.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724ED1AD6D4060047C223 /* show_parse.pdf */; }; 98CF52AF294A3FC900557BBA /* open_type_drawer_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4C1A76004300C058CB /* open_type_drawer_H.pdf */; }; 98CF52B0294A3FC900557BBA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1BC1A6F537B00FFB083 /* Images.xcassets */; }; 98CF52B1294A3FC900557BBA /* recycle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F71A70064800FFB083 /* recycle.pdf */; }; 98CF52B2294A3FC900557BBA /* check.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2241A71F8AD00FFB083 /* check.pdf */; }; 98CF52B3294A3FC900557BBA /* remove_subpop.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1EC1A6FEAB500FFB083 /* remove_subpop.pdf */; }; 98CF52B4294A3FC900557BBA /* EidosHelpWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982556671BA451D00054CB3F /* EidosHelpWindow.xib */; }; 98CF52B5294A3FC900557BBA /* execute_selection.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E91AD6B9FE0047C223 /* execute_selection.pdf */; }; 98CF52B6294A3FC900557BBA /* profile_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98EF4AB41ECDA5EA00CCDB09 /* profile_H.pdf */; }; 98CF52B7294A3FC900557BBA /* dump_output.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F3F1A75A12700C058CB /* dump_output.pdf */; }; 98CF52B8294A3FC900557BBA /* show_mutations_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F521A76004300C058CB /* show_mutations_H.pdf */; }; 98CF52B9294A3FC900557BBA /* dump_output_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F3E1A75A12700C058CB /* dump_output_H.pdf */; }; 98CF52BA294A3FC900557BBA /* GraphBarRescaleSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982A9DDC1FCA9FF0007BA3DF /* GraphBarRescaleSheet.xib */; }; 98CF52BB294A3FC900557BBA /* show_genomicelements_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F501A76004300C058CB /* show_genomicelements_H.pdf */; }; 98CF52BC294A3FC900557BBA /* SLiMHelpFunctions.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825566E1BA4FAD00054CB3F /* SLiMHelpFunctions.rtf */; }; 98CF52BD294A3FC900557BBA /* edit_submenu.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F611A76041200C058CB /* edit_submenu.pdf */; }; 98CF52BE294A3FC900557BBA /* prettyprint_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524611F2E6AFB005AD9A6 /* prettyprint_H.pdf */; }; 98CF52BF294A3FC900557BBA /* EidosHelpStatements.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 989228301BAF496C00429674 /* EidosHelpStatements.rtf */; }; 98CF52C0294A3FC900557BBA /* change_sex_ratio.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1E51A6FD8D600FFB083 /* change_sex_ratio.pdf */; }; 98CF52C1294A3FC900557BBA /* ProfileReport.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98EA965A1ECC2541006BA35B /* ProfileReport.xib */; }; 98CF52C2294A3FC900557BBA /* show_console_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 987AD8721B2CBDA70035D6C8 /* show_console_H.pdf */; }; 98CF52C3294A3FC900557BBA /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1BE1A6F537B00FFB083 /* MainMenu.xib */; }; 98CF52C4294A3FC900557BBA /* show_execution.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F51AD6DD470047C223 /* show_execution.pdf */; }; 98CF52C5294A3FC900557BBA /* change_cloning_rate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985301EB1B72582E001520DF /* change_cloning_rate.pdf */; }; 98CF52C6294A3FC900557BBA /* female_symbol.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98C0943C1B7663DF00766A9A /* female_symbol.pdf */; }; 98CF52C7294A3FC900557BBA /* split_subpop.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1EF1A6FED4000FFB083 /* split_subpop.pdf */; }; 98CF52C8294A3FC900557BBA /* show_recombination.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F551A76004300C058CB /* show_recombination.pdf */; }; 98CF52C9294A3FC900557BBA /* GraphWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 986926DA1AA1429D0000E138 /* GraphWindow.xib */; }; 98CF52CA294A3FC900557BBA /* GraphAxisRescaleSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 980DD5181AAE42F900D5B7B8 /* GraphAxisRescaleSheet.xib */; }; 98CF52CB294A3FC900557BBA /* EidosHelpOperators.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282A1BAE279700429674 /* EidosHelpOperators.rtf */; }; 98CF52CC294A3FC900557BBA /* add_subpop.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1EB1A6FEAB500FFB083 /* add_subpop.pdf */; }; 98CF52CE294A3FC900557BBA /* EidosHelpTypes.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282D1BAE27AA00429674 /* EidosHelpTypes.rtf */; }; 98CF52CF294A3FC900557BBA /* check_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2231A71F8AD00FFB083 /* check_H.pdf */; }; 98CF52D0294A3FC900557BBA /* show_execution_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F41AD6DD470047C223 /* show_execution_H.pdf */; }; 98CF52D1294A3FC900557BBA /* SLiMHelpClasses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 982556A21BA5F0810054CB3F /* SLiMHelpClasses.rtf */; }; 98CF52D2294A3FC900557BBA /* show_fixed.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4F1A76004300C058CB /* show_fixed.pdf */; }; 98CF52D3294A3FC900557BBA /* syntax_help_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F421A75AABE00C058CB /* syntax_help_H.pdf */; }; 98CF52D4294A3FC900557BBA /* graph_submenu_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 986926D21AA1337A0000E138 /* graph_submenu_H.pdf */; }; 98CF52D5294A3FC900557BBA /* show_parse_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EC1AD6D4060047C223 /* show_parse_H.pdf */; }; 98CF52D6294A3FC900557BBA /* show_browser.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA11B1B8B5800791DBF /* show_browser.pdf */; }; 98CF52D7294A3FC900557BBA /* EidosConsoleWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98A4EC921B67C1CD00CD92FD /* EidosConsoleWindow.xib */; }; 98CF52D8294A3FC900557BBA /* SLiMPDFWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9887946E1EA8808000AE0C8D /* SLiMPDFWindow.xib */; }; 98CF52D9294A3FC900557BBA /* prettyprint.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524621F2E6AFB005AD9A6 /* prettyprint.pdf */; }; 98CF52DA294A3FC900557BBA /* syntax_help.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F431A75AABE00C058CB /* syntax_help.pdf */; }; 98CF52DB294A3FC900557BBA /* change_selfing_ratio.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1E71A6FDE6E00FFB083 /* change_selfing_ratio.pdf */; }; 98CF52DC294A3FC900557BBA /* change_folder.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98DD5F002155B857009062EE /* change_folder.pdf */; }; 98CF52DD294A3FC900557BBA /* open_type_drawer.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4D1A76004300C058CB /* open_type_drawer.pdf */; }; 98CF52DF294A3FC900557BBA /* EidosHelpClasses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825569F1BA5DFEB0054CB3F /* EidosHelpClasses.rtf */; }; 98CF52E0294A3FC900557BBA /* show_tokens.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EF1AD6D4060047C223 /* show_tokens.pdf */; }; 98CF52E1294A3FC900557BBA /* edit_submenu_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F601A76041200C058CB /* edit_submenu_H.pdf */; }; 98CF52E2294A3FC900557BBA /* AboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C20B1A715F6100FFB083 /* AboutWindow.xib */; }; 98CF52E3294A3FC900557BBA /* play_step_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1FA1A700DC100FFB083 /* play_step_H.pdf */; }; 98CF52E4294A3FC900557BBA /* show_recombination_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F541A76004300C058CB /* show_recombination_H.pdf */; }; 98CF52E5294A3FC900557BBA /* EidosHelpFunctions.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825566B1BA477D60054CB3F /* EidosHelpFunctions.rtf */; }; 98CF52E6294A3FC900557BBA /* male_symbol.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98C0943D1B7663DF00766A9A /* male_symbol.pdf */; }; 98CF52E7294A3FC900557BBA /* recycle_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1FB1A700DC100FFB083 /* recycle_H.pdf */; }; 98CF52E8294A3FC900557BBA /* show_mutations.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F531A76004300C058CB /* show_mutations.pdf */; }; 98CF52E9294A3FC900557BBA /* execute_selection_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E81AD6B9FE0047C223 /* execute_selection_H.pdf */; }; 98CF52EA294A3FC900557BBA /* change_folder_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98DD5F012155B857009062EE /* change_folder_H.pdf */; }; 98CF52EB294A3FC900557BBA /* play_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F91A700DC100FFB083 /* play_H.pdf */; }; 98CF52ED294A3FC900557BBA /* SLiMHelpCallbacks.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 989228331BAFB27300429674 /* SLiMHelpCallbacks.rtf */; }; 98CF52EE294A3FC900557BBA /* execute_script.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DF1AD4C3310047C223 /* execute_script.pdf */; }; 98CF52EF294A3FC900557BBA /* graph_submenu.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 986926D31AA1337A0000E138 /* graph_submenu.pdf */; }; 98CF52F0294A3FC900557BBA /* show_fixed_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F4E1A76004300C058CB /* show_fixed_H.pdf */; }; 98CF52F1294A3FC900557BBA /* execute_script_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DE1AD4C3310047C223 /* execute_script_H.pdf */; }; 98CF52F2294A3FC900557BBA /* SLiMWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1FF1A70192A00FFB083 /* SLiMWindow.xib */; }; 98CF52F3294A3FC900557BBA /* show_browser_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA01B1B8B5800791DBF /* show_browser_H.pdf */; }; 98CF52F4294A3FC900557BBA /* play.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F41A70040400FFB083 /* play.pdf */; }; 98CF52F5294A3FC900557BBA /* change_migration.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F11A6FEEAB00FFB083 /* change_migration.pdf */; }; 98CF52F6294A3FC900557BBA /* show_console.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 987AD8731B2CBDA70035D6C8 /* show_console.pdf */; }; 98CF52F7294A3FC900557BBA /* show_tokens_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EE1AD6D4060047C223 /* show_tokens_H.pdf */; }; 98CF52F8294A3FC900557BBA /* delete.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2261A71F8AD00FFB083 /* delete.pdf */; }; 98CF52F9294A3FC900557BBA /* delete_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2251A71F8AD00FFB083 /* delete_H.pdf */; }; 98CF52FA294A3FC900557BBA /* Tips in Resources */ = {isa = PBXBuildFile; fileRef = 98F65D551DF14DA40058BD29 /* Tips */; }; 98CF52FB294A3FC900557BBA /* change_size.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1E91A6FE7FC00FFB083 /* change_size.pdf */; }; 98CF52FC294A3FC900557BBA /* show_genomicelements.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F511A76004300C058CB /* show_genomicelements.pdf */; }; 98CF533B294A4B5300557BBA /* libomp.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 98760EDC28CE5E7600CEBC40 /* libomp.dylib */; }; 98CF5399294A714200557BBA /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98CF539A294A714200557BBA /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98CF539B294A714200557BBA /* eidos_functions_matrices.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */; }; 98CF539C294A714200557BBA /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 98CF539D294A714200557BBA /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98CF539E294A714200557BBA /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 98CF539F294A714200557BBA /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98CF53A0294A714200557BBA /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98CF53A1294A714200557BBA /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 98CF53A2294A714200557BBA /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 98CF53A3294A714200557BBA /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 98CF53A4294A714200557BBA /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98CF53A5294A714200557BBA /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98CF53A6294A714200557BBA /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98CF53A7294A714200557BBA /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 98CF53A8294A714200557BBA /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 98CF53A9294A714200557BBA /* eidos_functions_stats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */; }; 98CF53AA294A714200557BBA /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98CF53AB294A714200557BBA /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98CF53AC294A714200557BBA /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98CF53AD294A714200557BBA /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 98CF53AE294A714200557BBA /* eidos_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */; }; 98CF53AF294A714200557BBA /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 98CF53B0294A714200557BBA /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 98CF53B1294A714200557BBA /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98CF53B2294A714200557BBA /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98CF53B3294A714200557BBA /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 98CF53B4294A714200557BBA /* eidos_functions_strings.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */; }; 98CF53B5294A714200557BBA /* eidos_functions_colors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */; }; 98CF53B6294A714200557BBA /* eidos_functions_files.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */; }; 98CF53B7294A714200557BBA /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 98CF53B8294A714200557BBA /* EidosCocoaExtra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9825565A1BA32EE80054CB3F /* EidosCocoaExtra.mm */; }; 98CF53B9294A714200557BBA /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 98CF53BA294A714200557BBA /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98CF53BB294A714200557BBA /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98CF53BC294A714200557BBA /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 98CF53BD294A714200557BBA /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98CF53BE294A714200557BBA /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 98CF53BF294A714200557BBA /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98CF53C0294A714200557BBA /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98CF53C1294A714200557BBA /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98CF53C2294A714200557BBA /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 98CF53C3294A714200557BBA /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 98CF53C4294A714200557BBA /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 98CF53C5294A714200557BBA /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98CF53C6294A714200557BBA /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98CF53C7294A714200557BBA /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 98CF53C8294A714200557BBA /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98CF53C9294A714200557BBA /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 98CF53CA294A714200557BBA /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98CF53CB294A714200557BBA /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98CF53CC294A714200557BBA /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 98CF53CD294A714200557BBA /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98CF53CE294A714200557BBA /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98CF53CF294A714200557BBA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 985724B61AD478630047C223 /* main.m */; }; 98CF53D0294A714200557BBA /* eidos_functions_values.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */; }; 98CF53D1294A714200557BBA /* EidosValueWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98090FA51B1B978900791DBF /* EidosValueWrapper.mm */; }; 98CF53D2294A714200557BBA /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98CF53D3294A714200557BBA /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 98CF53D4294A714200557BBA /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98CF53D5294A714200557BBA /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98CF53D6294A714200557BBA /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98CF53D7294A714200557BBA /* EidosTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D218C41B2E213200156FC3 /* EidosTextView.mm */; }; 98CF53D8294A714200557BBA /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98CF53D9294A714200557BBA /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98CF53DA294A714200557BBA /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98CF53DB294A714200557BBA /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98CF53DC294A714200557BBA /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 98CF53DD294A714200557BBA /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98CF53DE294A714200557BBA /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98CF53DF294A714200557BBA /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98CF53E0294A714200557BBA /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98CF53E1294A714200557BBA /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 98CF53E2294A714200557BBA /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98CF53E3294A714200557BBA /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 98CF53E4294A714200557BBA /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 98CF53E5294A714200557BBA /* eidos_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */; }; 98CF53E6294A714200557BBA /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 98CF53E7294A714200557BBA /* EidosHelpController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 982556641BA450980054CB3F /* EidosHelpController.mm */; }; 98CF53E8294A714200557BBA /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98CF53E9294A714200557BBA /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 98CF53EA294A714200557BBA /* EidosVariableBrowserController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8851B2CC0C10035D6C8 /* EidosVariableBrowserController.mm */; }; 98CF53EB294A714200557BBA /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 98CF53EC294A714200557BBA /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98CF53ED294A714200557BBA /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 98CF53EE294A714200557BBA /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98CF53EF294A714200557BBA /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 98CF53F0294A714200557BBA /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98CF53F1294A714200557BBA /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 98CF53F2294A714200557BBA /* EidosAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 985724B41AD478630047C223 /* EidosAppDelegate.mm */; }; 98CF53F3294A714200557BBA /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 98CF53F4294A714200557BBA /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 98CF53F5294A714200557BBA /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 98CF53F6294A714200557BBA /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98CF53F7294A714200557BBA /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 98CF53F8294A714200557BBA /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98CF53F9294A714200557BBA /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 98CF53FA294A714200557BBA /* EidosConsoleTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 985724E61AD622880047C223 /* EidosConsoleTextView.mm */; }; 98CF53FB294A714200557BBA /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 98CF53FC294A714200557BBA /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 98CF53FD294A714200557BBA /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 98CF53FE294A714200557BBA /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98CF53FF294A714200557BBA /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98CF5400294A714200557BBA /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 98CF5401294A714200557BBA /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 98CF5402294A714200557BBA /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 98CF5403294A714200557BBA /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 98CF5404294A714200557BBA /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 98CF5405294A714200557BBA /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98CF5406294A714200557BBA /* EidosConsoleWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 987AD8891B2CDBB80035D6C8 /* EidosConsoleWindowController.mm */; }; 98CF5407294A714200557BBA /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 98CF5408294A714200557BBA /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98CF5409294A714200557BBA /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98CF540A294A714200557BBA /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98CF540B294A714200557BBA /* EidosPrettyprinter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D524681F2EB4DD005AD9A6 /* EidosPrettyprinter.mm */; }; 98CF540C294A714200557BBA /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 98CF540D294A714200557BBA /* eidos_functions_distributions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */; }; 98CF540E294A714200557BBA /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98CF5410294A714200557BBA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98A240581B8E3295005C9A30 /* Cocoa.framework */; }; 98CF5412294A714200557BBA /* show_parse_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EC1AD6D4060047C223 /* show_parse_H.pdf */; }; 98CF5413294A714200557BBA /* show_parse.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724ED1AD6D4060047C223 /* show_parse.pdf */; }; 98CF5414294A714200557BBA /* execute_script.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DF1AD4C3310047C223 /* execute_script.pdf */; }; 98CF5415294A714200557BBA /* check_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2231A71F8AD00FFB083 /* check_H.pdf */; }; 98CF5416294A714200557BBA /* EidosHelpOperators.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282A1BAE279700429674 /* EidosHelpOperators.rtf */; }; 98CF5417294A714200557BBA /* execute_script_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724DE1AD4C3310047C223 /* execute_script_H.pdf */; }; 98CF5418294A714200557BBA /* EidosHelpStatements.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 989228301BAF496C00429674 /* EidosHelpStatements.rtf */; }; 98CF5419294A714200557BBA /* EidosHelpFunctions.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825566B1BA477D60054CB3F /* EidosHelpFunctions.rtf */; }; 98CF541A294A714200557BBA /* EidosHelpTypes.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9892282D1BAE27AA00429674 /* EidosHelpTypes.rtf */; }; 98CF541B294A714200557BBA /* prettyprint_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524611F2E6AFB005AD9A6 /* prettyprint_H.pdf */; }; 98CF541C294A714200557BBA /* EidosHelpClasses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9825569F1BA5DFEB0054CB3F /* EidosHelpClasses.rtf */; }; 98CF541D294A714200557BBA /* show_browser_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA01B1B8B5800791DBF /* show_browser_H.pdf */; }; 98CF541E294A714200557BBA /* EidosHelpWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982556671BA451D00054CB3F /* EidosHelpWindow.xib */; }; 98CF541F294A714200557BBA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 985724B81AD478630047C223 /* Images.xcassets */; }; 98CF5420294A714200557BBA /* show_execution.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F51AD6DD470047C223 /* show_execution.pdf */; }; 98CF5421294A714200557BBA /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 985724BA1AD478630047C223 /* MainMenu.xib */; }; 98CF5422294A714200557BBA /* syntax_help_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F421A75AABE00C058CB /* syntax_help_H.pdf */; }; 98CF5423294A714200557BBA /* EidosAboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98EFE6321ADD92BC00CBEC78 /* EidosAboutWindow.xib */; }; 98CF5424294A714200557BBA /* delete.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2261A71F8AD00FFB083 /* delete.pdf */; }; 98CF5425294A714200557BBA /* delete_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2251A71F8AD00FFB083 /* delete_H.pdf */; }; 98CF5426294A714200557BBA /* syntax_help.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98453F431A75AABE00C058CB /* syntax_help.pdf */; }; 98CF5427294A714200557BBA /* show_tokens.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EF1AD6D4060047C223 /* show_tokens.pdf */; }; 98CF5428294A714200557BBA /* EidosConsoleWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98A4EC921B67C1CD00CD92FD /* EidosConsoleWindow.xib */; }; 98CF5429294A714200557BBA /* check.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2241A71F8AD00FFB083 /* check.pdf */; }; 98CF542A294A714200557BBA /* prettyprint.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524621F2E6AFB005AD9A6 /* prettyprint.pdf */; }; 98CF542B294A714200557BBA /* execute_selection.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E91AD6B9FE0047C223 /* execute_selection.pdf */; }; 98CF542C294A714200557BBA /* show_tokens_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724EE1AD6D4060047C223 /* show_tokens_H.pdf */; }; 98CF542D294A714200557BBA /* execute_selection_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724E81AD6B9FE0047C223 /* execute_selection_H.pdf */; }; 98CF542E294A714200557BBA /* show_execution_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 985724F41AD6DD470047C223 /* show_execution_H.pdf */; }; 98CF542F294A714200557BBA /* show_browser.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98090FA11B1B8B5800791DBF /* show_browser.pdf */; }; 98D218C51B2E213200156FC3 /* EidosTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D218C41B2E213200156FC3 /* EidosTextView.mm */; }; 98D218C61B2E213200156FC3 /* EidosTextView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D218C41B2E213200156FC3 /* EidosTextView.mm */; }; 98D4C1B91A6F537B00FFB083 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C1B81A6F537B00FFB083 /* AppDelegate.mm */; }; 98D4C1BB1A6F537B00FFB083 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C1BA1A6F537B00FFB083 /* main.m */; }; 98D4C1BD1A6F537B00FFB083 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1BC1A6F537B00FFB083 /* Images.xcassets */; }; 98D4C1C01A6F537B00FFB083 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1BE1A6F537B00FFB083 /* MainMenu.xib */; }; 98D4C1D31A6F541200FFB083 /* slim_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9898172D1A59750300F7417C /* slim_globals.cpp */; }; 98D4C1D41A6F541700FFB083 /* species.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9878A93D1A4E57E70007B9D6 /* species.cpp */; }; 98D4C1D61A6F541F00FFB083 /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 98D4C1D81A6F542600FFB083 /* mutation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6881A3CCFD0000AD4FC /* mutation.cpp */; }; 98D4C1D91A6F542900FFB083 /* mutation_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A68E1A3CD4CF000AD4FC /* mutation_type.cpp */; }; 98D4C1DB1A6F542F00FFB083 /* genomic_element.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6911A3CD4EF000AD4FC /* genomic_element.cpp */; }; 98D4C1DC1A6F543200FFB083 /* genomic_element_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6941A3CD51A000AD4FC /* genomic_element_type.cpp */; }; 98D4C1DD1A6F543600FFB083 /* chromosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6971A3CD52A000AD4FC /* chromosome.cpp */; }; 98D4C1DE1A6F543900FFB083 /* polymorphism.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69A1A3CD542000AD4FC /* polymorphism.cpp */; }; 98D4C1DF1A6F543C00FFB083 /* substitution.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69D1A3CD551000AD4FC /* substitution.cpp */; }; 98D4C1E11A6F544500FFB083 /* haplosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A61A3CD5A0000AD4FC /* haplosome.cpp */; }; 98D4C1E21A6F544800FFB083 /* subpopulation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A91A3CD5BB000AD4FC /* subpopulation.cpp */; }; 98D4C1E31A6F544B00FFB083 /* population.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6AC1A3CD5D3000AD4FC /* population.cpp */; }; 98D4C1E61A6FD8D600FFB083 /* change_sex_ratio.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1E51A6FD8D600FFB083 /* change_sex_ratio.pdf */; }; 98D4C1E81A6FDE6E00FFB083 /* change_selfing_ratio.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1E71A6FDE6E00FFB083 /* change_selfing_ratio.pdf */; }; 98D4C1EA1A6FE7FC00FFB083 /* change_size.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1E91A6FE7FC00FFB083 /* change_size.pdf */; }; 98D4C1ED1A6FEAB500FFB083 /* add_subpop.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1EB1A6FEAB500FFB083 /* add_subpop.pdf */; }; 98D4C1EE1A6FEAB500FFB083 /* remove_subpop.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1EC1A6FEAB500FFB083 /* remove_subpop.pdf */; }; 98D4C1F01A6FED4000FFB083 /* split_subpop.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1EF1A6FED4000FFB083 /* split_subpop.pdf */; }; 98D4C1F21A6FEEAB00FFB083 /* change_migration.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F11A6FEEAB00FFB083 /* change_migration.pdf */; }; 98D4C1F51A70040400FFB083 /* play_step.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F31A70040400FFB083 /* play_step.pdf */; }; 98D4C1F61A70040400FFB083 /* play.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F41A70040400FFB083 /* play.pdf */; }; 98D4C1F81A70064800FFB083 /* recycle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F71A70064800FFB083 /* recycle.pdf */; }; 98D4C1FC1A700DC100FFB083 /* play_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1F91A700DC100FFB083 /* play_H.pdf */; }; 98D4C1FD1A700DC100FFB083 /* play_step_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1FA1A700DC100FFB083 /* play_step_H.pdf */; }; 98D4C1FE1A700DC100FFB083 /* recycle_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1FB1A700DC100FFB083 /* recycle_H.pdf */; }; 98D4C2011A70192A00FFB083 /* SLiMWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C1FF1A70192A00FFB083 /* SLiMWindow.xib */; }; 98D4C2041A701D5A00FFB083 /* SLiMWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C2031A701D5A00FFB083 /* SLiMWindowController.mm */; }; 98D4C2071A704EA700FFB083 /* PopulationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C2061A704EA700FFB083 /* PopulationView.mm */; }; 98D4C20A1A7086FF00FFB083 /* ChromosomeView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C2091A7086FF00FFB083 /* ChromosomeView.mm */; }; 98D4C20D1A715F6100FFB083 /* AboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C20B1A715F6100FFB083 /* AboutWindow.xib */; }; 98D4C2101A716E0E00FFB083 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98D4C20F1A716E0E00FFB083 /* WebKit.framework */; }; 98D4C2161A7187E200FFB083 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98D4C2151A7187E200FFB083 /* Quartz.framework */; }; 98D4C21C1A718EFD00FFB083 /* CocoaExtra.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D4C21B1A718EFD00FFB083 /* CocoaExtra.mm */; }; 98D4C2271A71F8AD00FFB083 /* check_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2231A71F8AD00FFB083 /* check_H.pdf */; }; 98D4C2281A71F8AD00FFB083 /* check.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2241A71F8AD00FFB083 /* check.pdf */; }; 98D4C2291A71F8AD00FFB083 /* delete_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2251A71F8AD00FFB083 /* delete_H.pdf */; }; 98D4C22A1A71F8AD00FFB083 /* delete.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D4C2261A71F8AD00FFB083 /* delete.pdf */; }; 98D524631F2E6AFB005AD9A6 /* prettyprint_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524611F2E6AFB005AD9A6 /* prettyprint_H.pdf */; }; 98D524641F2E6AFB005AD9A6 /* prettyprint.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524621F2E6AFB005AD9A6 /* prettyprint.pdf */; }; 98D524651F2E6B08005AD9A6 /* prettyprint_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524611F2E6AFB005AD9A6 /* prettyprint_H.pdf */; }; 98D524661F2E6B0B005AD9A6 /* prettyprint.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98D524621F2E6AFB005AD9A6 /* prettyprint.pdf */; }; 98D524691F2EB4DD005AD9A6 /* EidosPrettyprinter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D524681F2EB4DD005AD9A6 /* EidosPrettyprinter.mm */; }; 98D5246A1F2EB4DD005AD9A6 /* EidosPrettyprinter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98D524681F2EB4DD005AD9A6 /* EidosPrettyprinter.mm */; }; 98D7D65C2AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D65D2AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D65E2AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D65F2AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D6602AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D6612AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D6622AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D6632AB24C40002AFE34 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D65B2AB24C40002AFE34 /* tdist.c */; }; 98D7D6652AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7D6662AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7D6672AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7D6682AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7D6692AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7D66A2AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7D66B2AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7D66C2AB24CBC002AFE34 /* chisq.c in Sources */ = {isa = PBXBuildFile; fileRef = 98D7D6642AB24CBC002AFE34 /* chisq.c */; }; 98D7EB8528CE557C00DEAAC4 /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98D7EB8628CE557C00DEAAC4 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 98D7EB8728CE557C00DEAAC4 /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 98D7EB8828CE557C00DEAAC4 /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98D7EB8928CE557C00DEAAC4 /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98D7EB8A28CE557C00DEAAC4 /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 98D7EB8B28CE557C00DEAAC4 /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98D7EB8C28CE557C00DEAAC4 /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98D7EB8D28CE557C00DEAAC4 /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98D7EB8E28CE557C00DEAAC4 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98D7EB8F28CE557C00DEAAC4 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98D7EB9028CE557C00DEAAC4 /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98D7EB9128CE557C00DEAAC4 /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98D7EB9228CE557C00DEAAC4 /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 98D7EB9328CE557C00DEAAC4 /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 98D7EB9428CE557C00DEAAC4 /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98D7EB9528CE557C00DEAAC4 /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 98D7EB9628CE557C00DEAAC4 /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98D7EB9728CE557C00DEAAC4 /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 98D7EB9828CE557C00DEAAC4 /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98D7EB9928CE557C00DEAAC4 /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 98D7EB9A28CE557C00DEAAC4 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 98D7EB9B28CE557C00DEAAC4 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98D7EB9C28CE557C00DEAAC4 /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 98D7EB9D28CE557C00DEAAC4 /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 98D7EB9E28CE557C00DEAAC4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98D7EB9F28CE557C00DEAAC4 /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98D7EBA028CE557C00DEAAC4 /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 98D7EBA128CE557C00DEAAC4 /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 98D7EBA228CE557C00DEAAC4 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 98D7EBA328CE557C00DEAAC4 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 98D7EBA428CE557C00DEAAC4 /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98D7EBA528CE557C00DEAAC4 /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98D7EBA628CE557C00DEAAC4 /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98D7EBA728CE557C00DEAAC4 /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 98D7EBA828CE557C00DEAAC4 /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98D7EBA928CE557C00DEAAC4 /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 98D7EBAA28CE557C00DEAAC4 /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 98D7EBAB28CE557C00DEAAC4 /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98D7EBAC28CE557C00DEAAC4 /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98D7EBAD28CE557C00DEAAC4 /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98D7EBAE28CE557C00DEAAC4 /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 98D7EBAF28CE557C00DEAAC4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98D7EBB028CE557C00DEAAC4 /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 98D7EBB128CE557C00DEAAC4 /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98D7EBB228CE557C00DEAAC4 /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 98D7EBB328CE557C00DEAAC4 /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98D7EBB428CE557C00DEAAC4 /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 98D7EBB528CE557C00DEAAC4 /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98D7EBB628CE557C00DEAAC4 /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 98D7EBB728CE557C00DEAAC4 /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 98D7EBB828CE557C00DEAAC4 /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 98D7EBB928CE557C00DEAAC4 /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98D7EBBA28CE557C00DEAAC4 /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98D7EBBB28CE557C00DEAAC4 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98D7EBBC28CE557C00DEAAC4 /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 98D7EBBD28CE557C00DEAAC4 /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98D7EBBE28CE557C00DEAAC4 /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 98D7EBBF28CE557C00DEAAC4 /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 98D7EBC028CE557C00DEAAC4 /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 98D7EBC128CE557C00DEAAC4 /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98D7EBC228CE557C00DEAAC4 /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 98D7EBC328CE557C00DEAAC4 /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 98D7EBC428CE557C00DEAAC4 /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 98D7EBC528CE557C00DEAAC4 /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98D7EBC628CE557C00DEAAC4 /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98D7EBC728CE557C00DEAAC4 /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98D7EBC828CE557C00DEAAC4 /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98D7EBC928CE557C00DEAAC4 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98D7EBCA28CE557C00DEAAC4 /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98D7EBCB28CE557C00DEAAC4 /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98D7EBCC28CE557C00DEAAC4 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98D7EBCD28CE557C00DEAAC4 /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 98D7EBCE28CE557C00DEAAC4 /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 98D7EBCF28CE557C00DEAAC4 /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 98D7EBD028CE557C00DEAAC4 /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 98D7EBD128CE557C00DEAAC4 /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98D7EBD228CE557C00DEAAC4 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 98D7EBD328CE557C00DEAAC4 /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 98D7EBD428CE557C00DEAAC4 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 98D7EBD528CE557C00DEAAC4 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98D7EBD628CE557C00DEAAC4 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98D7EBD728CE557C00DEAAC4 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 98D7EBD828CE557C00DEAAC4 /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98D7EBD928CE557C00DEAAC4 /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 98D7EBDA28CE557C00DEAAC4 /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98D7EBDB28CE557C00DEAAC4 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 982556AB1BA8E77C0054CB3F /* main.cpp */; }; 98D7EBDC28CE557C00DEAAC4 /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 98D7EBDD28CE557C00DEAAC4 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 98D7EBDE28CE557C00DEAAC4 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98D7EBDF28CE557C00DEAAC4 /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98D7EBE028CE557C00DEAAC4 /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 98D7EBE128CE557C00DEAAC4 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98D7EBE228CE557C00DEAAC4 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98D7EBE328CE557C00DEAAC4 /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98D7EBE428CE557C00DEAAC4 /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 98D7EBE528CE557C00DEAAC4 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 98D7EBE628CE557C00DEAAC4 /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98D7EBE728CE557C00DEAAC4 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98D7EBE828CE557C00DEAAC4 /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 98D7ECA128CE58FC00DEAAC4 /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 98D7ECA228CE58FC00DEAAC4 /* eidos_type_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */; }; 98D7ECA328CE58FC00DEAAC4 /* eidos_call_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */; }; 98D7ECA428CE58FC00DEAAC4 /* dgemv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ADB1FDBC0D000274FF0 /* dgemv.c */; }; 98D7ECA528CE58FC00DEAAC4 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B0F1FDBD09800274FF0 /* init.c */; }; 98D7ECA628CE58FC00DEAAC4 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FA1C7A980000548839 /* gamma.c */; }; 98D7ECA728CE58FC00DEAAC4 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FB1C7A980000548839 /* gauss.c */; }; 98D7ECA828CE58FC00DEAAC4 /* eidos_test_operators_arithmetic.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */; }; 98D7ECA928CE58FC00DEAAC4 /* eidos_test_functions_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */; }; 98D7ECAA28CE58FC00DEAAC4 /* log.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211A1C7A980000548839 /* log.c */; }; 98D7ECAB28CE58FC00DEAAC4 /* slim_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98800DC21B7EDCB50046F5F9 /* slim_test.cpp */; }; 98D7ECAC28CE58FC00DEAAC4 /* expint.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E60C1ED55C0400FF9762 /* expint.c */; }; 98D7ECAD28CE58FC00DEAAC4 /* elementary.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210C1C7A980000548839 /* elementary.c */; }; 98D7ECAE28CE58FC00DEAAC4 /* mvgauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332A9D1FDB98ED00274FF0 /* mvgauss.c */; }; 98D7ECAF28CE58FC00DEAAC4 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B161FDBD13D00274FF0 /* copy.c */; }; 98D7ECB028CE58FC00DEAAC4 /* eidos_class_Image.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235688252FE61A0096A745 /* eidos_class_Image.cpp */; }; 98D7ECB128CE58FC00DEAAC4 /* shuffle.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821B11C7A9B9F00548839 /* shuffle.c */; }; 98D7ECB228CE58FC00DEAAC4 /* stats.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25B2278B9F8001D43BC /* stats.c */; }; 98D7ECB328CE58FC00DEAAC4 /* mutation_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A68E1A3CD4CF000AD4FC /* mutation_type.cpp */; }; 98D7ECB428CE58FC00DEAAC4 /* mutation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6881A3CCFD0000AD4FC /* mutation.cpp */; }; 98D7ECB528CE58FC00DEAAC4 /* coerce.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821201C7A980000548839 /* coerce.c */; }; 98D7ECB628CE58FC00DEAAC4 /* dtrmv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB81FDBA32200274FF0 /* dtrmv.c */; }; 98D7ECB728CE58FC00DEAAC4 /* gamma_inc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6001ED55A2500FF9762 /* gamma_inc.c */; }; 98D7ECB828CE58FC00DEAAC4 /* slim_test_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F724BA21E3008CC658 /* slim_test_other.cpp */; }; 98D7ECB928CE58FC00DEAAC4 /* vector.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC61FDBA6B600274FF0 /* vector.c */; }; 98D7ECBA28CE58FC00DEAAC4 /* eidos_script.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */; }; 98D7ECBB28CE58FC00DEAAC4 /* gaussinv.c in Sources */ = {isa = PBXBuildFile; fileRef = 987A2A0524033856009A636F /* gaussinv.c */; }; 98D7ECBC28CE58FC00DEAAC4 /* erfc.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6071ED55B4700FF9762 /* erfc.c */; }; 98D7ECBD28CE58FC00DEAAC4 /* gausszig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FC1C7A980000548839 /* gausszig.c */; }; 98D7ECBE28CE58FC00DEAAC4 /* community_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836868027CD72E900683639 /* community_eidos.cpp */; }; 98D7ECBF28CE58FC00DEAAC4 /* ddot.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824ED210B9E8F002402A5 /* ddot.c */; }; 98D7ECC028CE58FC00DEAAC4 /* species.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9878A93D1A4E57E70007B9D6 /* species.cpp */; }; 98D7ECC128CE58FC00DEAAC4 /* eidos_beep.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */; }; 98D7ECC228CE58FC00DEAAC4 /* fdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 980566E125A7C5B9008D3C7F /* fdist.c */; }; 98D7ECC328CE58FC00DEAAC4 /* matrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AFB1FDBC4B200274FF0 /* matrix.c */; }; 98D7ECC428CE58FC00DEAAC4 /* submatrix.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE51FDBC1D900274FF0 /* submatrix.c */; }; 98D7ECC528CE58FC00DEAAC4 /* psi.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211C1C7A980000548839 /* psi.c */; }; 98D7ECC628CE58FC00DEAAC4 /* binomial_tpe.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F81C7A980000548839 /* binomial_tpe.c */; }; 98D7ECC728CE58FC00DEAAC4 /* eidos_test_functions_math.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */; }; 98D7ECC828CE58FC00DEAAC4 /* haplosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A61A3CD5A0000AD4FC /* haplosome.cpp */; }; 98D7ECC928CE58FC00DEAAC4 /* zutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076609244934A800F6CBB4 /* zutil.c */; }; 98D7ECCA28CE58FC00DEAAC4 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D25D2278B9F8001D43BC /* trees.c */; }; 98D7ECCB28CE58FC00DEAAC4 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B011FDBCFC300274FF0 /* init.c */; }; 98D7ECCC28CE58FC00DEAAC4 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EC1C7A980000548839 /* stream.c */; }; 98D7ECCD28CE58FC00DEAAC4 /* slim_test_core.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9807C0F324BA21B7008CC658 /* slim_test_core.cpp */; }; 98D7ECCE28CE58FC00DEAAC4 /* eidos_ast_node.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */; }; 98D7ECCF28CE58FC00DEAAC4 /* chromosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6971A3CD52A000AD4FC /* chromosome.cpp */; }; 98D7ECD028CE58FC00DEAAC4 /* eidos_test_functions_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */; }; 98D7ECD128CE58FC00DEAAC4 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E91C7A980000548839 /* error.c */; }; 98D7ECD228CE58FC00DEAAC4 /* tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 9850D8DF2063098E006BFD2E /* tables.c */; }; 98D7ECD328CE58FC00DEAAC4 /* slim_eidos_block.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AB59791B2531F10077CB4A /* slim_eidos_block.cpp */; }; 98D7ECD428CE58FC00DEAAC4 /* convert.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2592278B9F8001D43BC /* convert.c */; }; 98D7ECD528CE58FC00DEAAC4 /* eidos_test_operators_other.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */; }; 98D7ECD628CE58FC00DEAAC4 /* eidos_test_functions_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */; }; 98D7ECD728CE58FC00DEAAC4 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076605244934A800F6CBB4 /* deflate.c */; }; 98D7ECD828CE58FC00DEAAC4 /* gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821101C7A980000548839 /* gamma.c */; }; 98D7ECD928CE58FC00DEAAC4 /* nbinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 982B50C52704048E006E91BC /* nbinomial.c */; }; 98D7ECDA28CE58FC00DEAAC4 /* poisson.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821001C7A980000548839 /* poisson.c */; }; 98D7ECDB28CE58FC00DEAAC4 /* sparse_vector.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985D1D892808B84F00461CFA /* sparse_vector.cpp */; }; 98D7ECDC28CE58FC00DEAAC4 /* population.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6AC1A3CD5D3000AD4FC /* population.cpp */; }; 98D7ECDD28CE58FC00DEAAC4 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E6111ED55C6B00FF9762 /* beta.c */; }; 98D7ECDE28CE58FC00DEAAC4 /* rowcol.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AE31FDBC1D900274FF0 /* rowcol.c */; }; 98D7ECDF28CE58FC00DEAAC4 /* taus.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821071C7A980000548839 /* taus.c */; }; 98D7ECE028CE58FC00DEAAC4 /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 98D7ECE128CE58FC00DEAAC4 /* geometric.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A81C7A9B1600548839 /* geometric.c */; }; 98D7ECE228CE58FC00DEAAC4 /* eidos_token.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98A5134D1B66B69E005A753D /* eidos_token.cpp */; }; 98D7ECE328CE58FC00DEAAC4 /* core.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2562278B9F7001D43BC /* core.c */; }; 98D7ECE428CE58FC00DEAAC4 /* minmax.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8219D1C7A99B200548839 /* minmax.c */; }; 98D7ECE528CE58FC00DEAAC4 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660F244934A800F6CBB4 /* trees.c */; }; 98D7ECE628CE58FC00DEAAC4 /* xerbla.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AC11FDBA53F00274FF0 /* xerbla.c */; }; 98D7ECE728CE58FC00DEAAC4 /* eidos_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A71AD4551C0047C223 /* eidos_test.cpp */; }; 98D7ECE828CE58FC00DEAAC4 /* blas.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AB31FDBA1E100274FF0 /* blas.c */; }; 98D7ECE928CE58FC00DEAAC4 /* log_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9809DF9E2550F32500C4E82D /* log_file.cpp */; }; 98D7ECEA28CE58FC00DEAAC4 /* eidos_class_Dictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */; }; 98D7ECEB28CE58FC00DEAAC4 /* individual.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98C92AEB1D0B07A6001C82BC /* individual.cpp */; }; 98D7ECEC28CE58FC00DEAAC4 /* GitSHA1_Xcode.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DC983D289986B300160DD8 /* GitSHA1_Xcode.cpp */; }; 98D7ECED28CE58FC00DEAAC4 /* eidos_class_Object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */; }; 98D7ECEE28CE58FC00DEAAC4 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98235681252FDCF50096A745 /* lodepng.cpp */; }; 98D7ECEF28CE58FC00DEAAC4 /* mutation_run.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98606AEC1DED0DCD00821CFF /* mutation_run.cpp */; }; 98D7ECF028CE58FC00DEAAC4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E51C7A980000548839 /* inline.c */; }; 98D7ECF128CE58FC00DEAAC4 /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 98D7ECF228CE58FC00DEAAC4 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076603244934A800F6CBB4 /* compress.c */; }; 98D7ECF328CE58FC00DEAAC4 /* genomic_element_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6941A3CD51A000AD4FC /* genomic_element_type.cpp */; }; 98D7ECF428CE58FC00DEAAC4 /* discrete.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A71C7A9B1600548839 /* discrete.c */; }; 98D7ECF528CE58FC00DEAAC4 /* exponential.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F91C7A980000548839 /* exponential.c */; }; 98D7ECF628CE58FC00DEAAC4 /* eidos_value.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985724A31AD435310047C223 /* eidos_value.cpp */; }; 98D7ECF728CE58FC00DEAAC4 /* eidos_type_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */; }; 98D7ECF828CE58FC00DEAAC4 /* eidos_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249F1AD34B740047C223 /* eidos_functions.cpp */; }; 98D7ECF928CE58FC00DEAAC4 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211B1C7A980000548839 /* pow_int.c */; }; 98D7ECFA28CE58FC00DEAAC4 /* weibull.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821011C7A980000548839 /* weibull.c */; }; 98D7ECFB28CE58FC00DEAAC4 /* fdiv.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821211C7A980000548839 /* fdiv.c */; }; 98D7ECFC28CE58FC00DEAAC4 /* cholesky.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AD51FDBBD1600274FF0 /* cholesky.c */; }; 98D7ECFD28CE58FC00DEAAC4 /* kastore.c in Sources */ = {isa = PBXBuildFile; fileRef = 987D19A4209A53850030D28D /* kastore.c */; }; 98D7ECFE28CE58FC00DEAAC4 /* tdist.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5F41ED5572C00FF9762 /* tdist.c */; }; 98D7ECFF28CE58FC00DEAAC4 /* polymorphism.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69A1A3CD542000AD4FC /* polymorphism.cpp */; }; 98D7ED0028CE58FC00DEAAC4 /* eidos_class_TestElement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */; }; 98D7ED0128CE58FC00DEAAC4 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 982663531A3BABD300A0CBBF /* main.cpp */; }; 98D7ED0228CE58FC00DEAAC4 /* slim_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DDAED4221765480038C133 /* slim_functions.cpp */; }; 98D7ED0328CE58FC00DEAAC4 /* gauss.c in Sources */ = {isa = PBXBuildFile; fileRef = 9876E5FB1ED5599F00FF9762 /* gauss.c */; }; 98D7ED0428CE58FC00DEAAC4 /* cauchy.c in Sources */ = {isa = PBXBuildFile; fileRef = 988880EB20744EE800E10172 /* cauchy.c */; }; 98D7ED0528CE58FC00DEAAC4 /* substitution.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69D1A3CD551000AD4FC /* substitution.cpp */; }; 98D7ED0628CE58FC00DEAAC4 /* genotypes.c in Sources */ = {isa = PBXBuildFile; fileRef = 9854D2572278B9F7001D43BC /* genotypes.c */; }; 98D7ED0728CE58FC00DEAAC4 /* exp.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8210F1C7A980000548839 /* exp.c */; }; 98D7ED0828CE58FC00DEAAC4 /* gzwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660E244934A800F6CBB4 /* gzwrite.c */; }; 98D7ED0928CE58FC00DEAAC4 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 98076612244934A800F6CBB4 /* adler32.c */; }; 98D7ED0A28CE58FC00DEAAC4 /* trig.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211D1C7A980000548839 /* trig.c */; }; 98D7ED0B28CE58FC00DEAAC4 /* eidos_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98321F921B406B67007337A3 /* eidos_globals.cpp */; }; 98D7ED0C28CE58FC00DEAAC4 /* slim_test_genetics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3EE924BA27EC00E712E0 /* slim_test_genetics.cpp */; }; 98D7ED0D28CE58FC00DEAAC4 /* multinomial.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FF1C7A980000548839 /* multinomial.c */; }; 98D7ED0E28CE58FC00DEAAC4 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332B081FDBD00800274FF0 /* init.c */; }; 98D7ED0F28CE58FC00DEAAC4 /* math.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820E61C7A980000548839 /* math.c */; }; 98D7ED1028CE58FC00DEAAC4 /* genomic_element.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6911A3CD4EF000AD4FC /* genomic_element.cpp */; }; 98D7ED1128CE58FC00DEAAC4 /* slim_globals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9898172D1A59750300F7417C /* slim_globals.cpp */; }; 98D7ED1228CE58FC00DEAAC4 /* interaction_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DB3D6D1E6122AE00E2C200 /* interaction_type.cpp */; }; 98D7ED1328CE58FC00DEAAC4 /* subpopulation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A91A3CD5BB000AD4FC /* subpopulation.cpp */; }; 98D7ED1428CE58FC00DEAAC4 /* swap.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332AF51FDBC3F100274FF0 /* swap.c */; }; 98D7ED1528CE58FC00DEAAC4 /* gzlib.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807660D244934A800F6CBB4 /* gzlib.c */; }; 98D7ED1628CE58FC00DEAAC4 /* species_eidos.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98AC617924BA34ED0001914C /* species_eidos.cpp */; }; 98D7ED1728CE58FC00DEAAC4 /* mt.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821051C7A980000548839 /* mt.c */; }; 98D7ED1828CE58FC00DEAAC4 /* eidos_interpreter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */; }; 98D7ED1928CE58FC00DEAAC4 /* inline.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821041C7A980000548839 /* inline.c */; }; 98D7ED1A28CE58FC00DEAAC4 /* message.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820EB1C7A980000548839 /* message.c */; }; 98D7ED1B28CE58FC00DEAAC4 /* dtrsv.c in Sources */ = {isa = PBXBuildFile; fileRef = 984824F0210B9F23002402A5 /* dtrsv.c */; }; 98D7ED1C28CE58FC00DEAAC4 /* infnan.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821231C7A980000548839 /* infnan.c */; }; 98D7ED1D28CE58FC00DEAAC4 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 9807662824493A8F00F6CBB4 /* crc32.c */; }; 98D7ED1E28CE58FC00DEAAC4 /* zeta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C8211E1C7A980000548839 /* zeta.c */; }; 98D7ED1F28CE58FC00DEAAC4 /* eidos_class_DataFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */; }; 98D7ED2028CE58FC00DEAAC4 /* rng.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821061C7A980000548839 /* rng.c */; }; 98D7ED2128CE58FC00DEAAC4 /* oper.c in Sources */ = {isa = PBXBuildFile; fileRef = 98332ACC1FDBA81A00274FF0 /* oper.c */; }; 98D7ED2228CE58FC00DEAAC4 /* beta.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820F71C7A980000548839 /* beta.c */; }; 98D7ED2328CE58FC00DEAAC4 /* text_input.c in Sources */ = {isa = PBXBuildFile; fileRef = D0A758F620A4CC9800132D2F /* text_input.c */; }; 98D7ED2428CE58FC00DEAAC4 /* pow_int.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C821A21C7A99F000548839 /* pow_int.c */; }; 98D7ED2528CE58FC00DEAAC4 /* eidos_test_operators_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */; }; 98D7ED2628CE58FC00DEAAC4 /* lognormal.c in Sources */ = {isa = PBXBuildFile; fileRef = 98C820FE1C7A980000548839 /* lognormal.c */; }; 98D7ED2728CE58FC00DEAAC4 /* community.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9836867227CD40CF00683639 /* community.cpp */; }; 98DB3D6F1E6122AE00E2C200 /* interaction_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DB3D6D1E6122AE00E2C200 /* interaction_type.cpp */; }; 98DB3D701E6122AE00E2C200 /* interaction_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DB3D6D1E6122AE00E2C200 /* interaction_type.cpp */; }; 98DB3D711E6122AE00E2C200 /* interaction_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DB3D6D1E6122AE00E2C200 /* interaction_type.cpp */; }; 98DC9841289986B300160DD8 /* GitSHA1_Xcode.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DC983D289986B300160DD8 /* GitSHA1_Xcode.cpp */; }; 98DD5F022155B857009062EE /* change_folder.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98DD5F002155B857009062EE /* change_folder.pdf */; }; 98DD5F032155B857009062EE /* change_folder_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98DD5F012155B857009062EE /* change_folder_H.pdf */; }; 98DDAED6221765480038C133 /* slim_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DDAED4221765480038C133 /* slim_functions.cpp */; }; 98DDAED7221765480038C133 /* slim_functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DDAED4221765480038C133 /* slim_functions.cpp */; }; 98DE4C131B6F9657004FDF5F /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 98DE4C141B6F9657004FDF5F /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 98DE4C151B6F9657004FDF5F /* eidos_property_signature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */; }; 98DEB47E2AA632AA00ABE60F /* spatial_map.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DEB47C2AA632AA00ABE60F /* spatial_map.cpp */; }; 98DEB47F2AA632AA00ABE60F /* spatial_map.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DEB47C2AA632AA00ABE60F /* spatial_map.cpp */; }; 98DEB4802AA632AA00ABE60F /* spatial_map.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DEB47C2AA632AA00ABE60F /* spatial_map.cpp */; }; 98DEB4812AA632AA00ABE60F /* spatial_map.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98DEB47C2AA632AA00ABE60F /* spatial_map.cpp */; }; 98E684402B694F09000B3B65 /* plot.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98E6843E2B694F09000B3B65 /* plot.mm */; }; 98E684412B694F09000B3B65 /* plot.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98E6843E2B694F09000B3B65 /* plot.mm */; }; 98E9A68A1A3CCFD0000AD4FC /* mutation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6881A3CCFD0000AD4FC /* mutation.cpp */; }; 98E9A6901A3CD4CF000AD4FC /* mutation_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A68E1A3CD4CF000AD4FC /* mutation_type.cpp */; }; 98E9A6931A3CD4EF000AD4FC /* genomic_element.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6911A3CD4EF000AD4FC /* genomic_element.cpp */; }; 98E9A6961A3CD51A000AD4FC /* genomic_element_type.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6941A3CD51A000AD4FC /* genomic_element_type.cpp */; }; 98E9A6991A3CD52A000AD4FC /* chromosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6971A3CD52A000AD4FC /* chromosome.cpp */; }; 98E9A69C1A3CD542000AD4FC /* polymorphism.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69A1A3CD542000AD4FC /* polymorphism.cpp */; }; 98E9A69F1A3CD551000AD4FC /* substitution.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A69D1A3CD551000AD4FC /* substitution.cpp */; }; 98E9A6A81A3CD5A0000AD4FC /* haplosome.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A61A3CD5A0000AD4FC /* haplosome.cpp */; }; 98E9A6AB1A3CD5BB000AD4FC /* subpopulation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6A91A3CD5BB000AD4FC /* subpopulation.cpp */; }; 98E9A6AE1A3CD5D3000AD4FC /* population.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6AC1A3CD5D3000AD4FC /* population.cpp */; }; 98E9A6B81A3CE35E000AD4FC /* eidos_rng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */; }; 98EA965C1ECC2541006BA35B /* ProfileReport.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98EA965A1ECC2541006BA35B /* ProfileReport.xib */; }; 98EBCD1421F3CFC600B385CF /* slim_gui.mm in Sources */ = {isa = PBXBuildFile; fileRef = 98EBCD1221F3CFC600B385CF /* slim_gui.mm */; }; 98EDB4CF2E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D02E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D12E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D22E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D32E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D42E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D52E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D62E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4D72E652F4A00CC8798 /* lu.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4CE2E652F4A00CC8798 /* lu.c */; }; 98EDB4E32E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4E42E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4E52E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4E62E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4E72E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4E82E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4E92E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4EA2E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4EB2E65366E00CC8798 /* daxpy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4E22E65366E00CC8798 /* daxpy.c */; }; 98EDB4EE2E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4EF2E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F02E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F12E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F22E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F32E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F42E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F52E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F62E65389600CC8798 /* init.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4ED2E65389600CC8798 /* init.c */; }; 98EDB4F82E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB4F92E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB4FA2E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB4FB2E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB4FC2E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB4FD2E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB4FE2E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB4FF2E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB5002E6538F200CC8798 /* permutation.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB4F72E6538F200CC8798 /* permutation.c */; }; 98EDB5022E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5032E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5042E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5052E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5062E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5072E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5082E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5092E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB50A2E65399600CC8798 /* permute.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5012E65399600CC8798 /* permute.c */; }; 98EDB5112E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5122E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5132E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5142E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5152E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5162E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5172E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5182E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EDB5192E65410C00CC8798 /* copy.c in Sources */ = {isa = PBXBuildFile; fileRef = 98EDB5102E65410C00CC8798 /* copy.c */; }; 98EF4AB61ECDA5EA00CCDB09 /* profile_H.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98EF4AB41ECDA5EA00CCDB09 /* profile_H.pdf */; }; 98EF4AB71ECDA5EA00CCDB09 /* profile.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 98EF4AB51ECDA5EA00CCDB09 /* profile.pdf */; }; 98EFE62F1ADB611100CBEC78 /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 98EFE6301ADB611100CBEC78 /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 98EFE6311ADB611100CBEC78 /* eidos_symbol_table.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */; }; 98EFE6341ADD92BC00CBEC78 /* EidosAboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98EFE6321ADD92BC00CBEC78 /* EidosAboutWindow.xib */; }; 98F65D561DF14DA50058BD29 /* Tips in Resources */ = {isa = PBXBuildFile; fileRef = 98F65D551DF14DA40058BD29 /* Tips */; }; D0A758F720A4CC9800132D2F /* text_input.c in Sources */ = {isa = PBXBuildFile; fileRef = D0A758F620A4CC9800132D2F /* text_input.c */; }; D0A758F920A4CCCF00132D2F /* text_input.c in Sources */ = {isa = PBXBuildFile; fileRef = D0A758F620A4CC9800132D2F /* text_input.c */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 984D5FAD1E3AF0D200473719 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 982663481A3BABD300A0CBBF /* Project object */; proxyType = 1; remoteGlobalIDString = 98D4C1B21A6F537B00FFB083; remoteInfo = SLiMgui; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 982556A71BA8E77B0054CB3F /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; 9826634E1A3BABD300A0CBBF /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; 98D7EBEA28CE557C00DEAAC4 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; 98D7ED2928CE58FC00DEAAC4 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 98024741215D85880025D29C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/FindRecipePanel.xib; sourceTree = ""; }; 980566E125A7C5B9008D3C7F /* fdist.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = fdist.c; sourceTree = ""; }; 98076602244934A800F6CBB4 /* zutil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = zutil.h; sourceTree = ""; }; 98076603244934A800F6CBB4 /* compress.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = compress.c; sourceTree = ""; }; 98076604244934A800F6CBB4 /* ChangeLog */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ChangeLog; sourceTree = ""; }; 98076605244934A800F6CBB4 /* deflate.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = deflate.c; sourceTree = ""; }; 98076606244934A800F6CBB4 /* trees.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = trees.h; sourceTree = ""; }; 98076608244934A800F6CBB4 /* README */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README; sourceTree = ""; }; 98076609244934A800F6CBB4 /* zutil.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = zutil.c; sourceTree = ""; }; 9807660A244934A800F6CBB4 /* deflate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = deflate.h; sourceTree = ""; }; 9807660B244934A800F6CBB4 /* gzguts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gzguts.h; sourceTree = ""; }; 9807660C244934A800F6CBB4 /* zlib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = zlib.h; sourceTree = ""; }; 9807660D244934A800F6CBB4 /* gzlib.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = gzlib.c; sourceTree = ""; }; 9807660E244934A800F6CBB4 /* gzwrite.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = gzwrite.c; sourceTree = ""; }; 9807660F244934A800F6CBB4 /* trees.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = trees.c; sourceTree = ""; }; 98076611244934A800F6CBB4 /* crc32.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = crc32.h; sourceTree = ""; }; 98076612244934A800F6CBB4 /* adler32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = adler32.c; sourceTree = ""; }; 98076613244934A800F6CBB4 /* zconf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = zconf.h; sourceTree = ""; }; 9807662824493A8F00F6CBB4 /* crc32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = crc32.c; sourceTree = ""; }; 9807C0F324BA21B7008CC658 /* slim_test_core.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = slim_test_core.cpp; sourceTree = ""; }; 9807C0F724BA21E3008CC658 /* slim_test_other.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = slim_test_other.cpp; sourceTree = ""; }; 98090FA01B1B8B5800791DBF /* show_browser_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_browser_H.pdf; sourceTree = ""; }; 98090FA11B1B8B5800791DBF /* show_browser.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_browser.pdf; sourceTree = ""; }; 98090FA41B1B978900791DBF /* EidosValueWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosValueWrapper.h; sourceTree = ""; }; 98090FA51B1B978900791DBF /* EidosValueWrapper.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosValueWrapper.mm; sourceTree = ""; }; 9809DF9E2550F32500C4E82D /* log_file.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = log_file.cpp; sourceTree = ""; }; 9809DF9F2550F32500C4E82D /* log_file.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = log_file.h; sourceTree = ""; }; 9809F8BD24F32B3E00D312E4 /* eidos_tinycolormap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_tinycolormap.h; sourceTree = ""; }; 980B6A6428CE642D0075B192 /* eidos_multi.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = eidos_multi.entitlements; sourceTree = ""; }; 980B6A6528CE64310075B192 /* slim_multi.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = slim_multi.entitlements; sourceTree = ""; }; 980DD5191AAE42F900D5B7B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/GraphAxisRescaleSheet.xib; sourceTree = ""; }; 980DD51B1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphView_MutationFrequencyTrajectory.h; sourceTree = ""; }; 980DD51C1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphView_MutationFrequencyTrajectory.mm; sourceTree = ""; }; 98186DB8254A8B1600F9118C /* robin_hood.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = robin_hood.h; sourceTree = ""; }; 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_script.cpp; sourceTree = ""; }; 981BAC691ACC6E8B0005BE94 /* eidos_script.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_script.h; sourceTree = ""; }; 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_files.cpp; sourceTree = ""; }; 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_math.cpp; sourceTree = ""; }; 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_colors.cpp; sourceTree = ""; }; 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_matrices.cpp; sourceTree = ""; }; 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_values.cpp; sourceTree = ""; }; 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_distributions.cpp; sourceTree = ""; }; 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_strings.cpp; sourceTree = ""; }; 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_other.cpp; sourceTree = ""; }; 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions_stats.cpp; sourceTree = ""; }; 9821E2031ABDBC300036EAEA /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 98235681252FDCF50096A745 /* lodepng.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = lodepng.cpp; sourceTree = ""; }; 98235687252FDD120096A745 /* lodepng.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = lodepng.h; sourceTree = ""; }; 98235688252FE61A0096A745 /* eidos_class_Image.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_class_Image.cpp; sourceTree = ""; }; 9823568E252FE62F0096A745 /* eidos_class_Image.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_class_Image.h; sourceTree = ""; }; 9825565A1BA32EE80054CB3F /* EidosCocoaExtra.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosCocoaExtra.mm; sourceTree = ""; }; 9825565D1BA32F030054CB3F /* EidosCocoaExtra.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EidosCocoaExtra.h; sourceTree = ""; }; 9825565E1BA358EC0054CB3F /* EidosVariableBrowserControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosVariableBrowserControllerDelegate.h; sourceTree = ""; }; 982556621BA35DF00054CB3F /* EidosConsoleTextViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EidosConsoleTextViewDelegate.h; sourceTree = ""; }; 982556631BA450980054CB3F /* EidosHelpController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosHelpController.h; sourceTree = ""; }; 982556641BA450980054CB3F /* EidosHelpController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosHelpController.mm; sourceTree = ""; }; 982556681BA451D00054CB3F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/EidosHelpWindow.xib; sourceTree = ""; }; 9825566B1BA477D60054CB3F /* EidosHelpFunctions.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = EidosHelpFunctions.rtf; sourceTree = ""; }; 9825566E1BA4FAD00054CB3F /* SLiMHelpFunctions.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = SLiMHelpFunctions.rtf; sourceTree = ""; }; 9825569F1BA5DFEB0054CB3F /* EidosHelpClasses.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = EidosHelpClasses.rtf; sourceTree = ""; }; 982556A21BA5F0810054CB3F /* SLiMHelpClasses.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = SLiMHelpClasses.rtf; sourceTree = ""; }; 982556A91BA8E77B0054CB3F /* eidos */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = eidos; sourceTree = BUILT_PRODUCTS_DIR; }; 982556AB1BA8E77C0054CB3F /* main.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = ""; }; 982663501A3BABD300A0CBBF /* slim */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = slim; sourceTree = BUILT_PRODUCTS_DIR; }; 982663531A3BABD300A0CBBF /* main.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = ""; }; 982A9DDD1FCA9FF0007BA3DF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/GraphBarRescaleSheet.xib; sourceTree = ""; }; 982B50C52704048E006E91BC /* nbinomial.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = nbinomial.c; sourceTree = ""; }; 98321F921B406B67007337A3 /* eidos_globals.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_globals.cpp; sourceTree = ""; }; 98321F931B406B67007337A3 /* eidos_globals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_globals.h; sourceTree = ""; }; 98330C48294AB34300B452E2 /* eidos_test_parallel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_test_parallel.h; sourceTree = ""; }; 98332A9D1FDB98ED00274FF0 /* mvgauss.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = mvgauss.c; sourceTree = ""; }; 98332AA51FDB997E00274FF0 /* gsl_matrix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_matrix.h; path = matrix/gsl_matrix.h; sourceTree = ""; }; 98332AA71FDB99AA00274FF0 /* gsl_vector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_vector.h; path = vector/gsl_vector.h; sourceTree = ""; }; 98332AA81FDB9B3F00274FF0 /* gsl_vector_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_vector_double.h; path = vector/gsl_vector_double.h; sourceTree = ""; }; 98332AA91FDB9B5100274FF0 /* gsl_matrix_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_matrix_double.h; path = matrix/gsl_matrix_double.h; sourceTree = ""; }; 98332AAB1FDB9C7C00274FF0 /* gsl_block_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_block_double.h; path = block/gsl_block_double.h; sourceTree = ""; }; 98332AAC1FDB9C7C00274FF0 /* gsl_block.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_block.h; path = block/gsl_block.h; sourceTree = ""; }; 98332AAD1FDB9C7C00274FF0 /* gsl_check_range.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_check_range.h; path = block/gsl_check_range.h; sourceTree = ""; }; 98332AAF1FDB9E1500274FF0 /* gsl_blas.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_blas.h; path = blas/gsl_blas.h; sourceTree = ""; }; 98332AB01FDB9FA600274FF0 /* gsl_blas_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_blas_types.h; path = blas/gsl_blas_types.h; sourceTree = ""; }; 98332AB21FDBA00000274FF0 /* gsl_cblas.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_cblas.h; path = cblas/gsl_cblas.h; sourceTree = ""; }; 98332AB31FDBA1E100274FF0 /* blas.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = blas.c; path = blas/blas.c; sourceTree = ""; }; 98332AB81FDBA32200274FF0 /* dtrmv.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = dtrmv.c; path = cblas/dtrmv.c; sourceTree = ""; }; 98332ABD1FDBA36500274FF0 /* cblas.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = cblas.h; path = cblas/cblas.h; sourceTree = ""; }; 98332ABE1FDBA3A700274FF0 /* error_cblas_l2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = error_cblas_l2.h; path = cblas/error_cblas_l2.h; sourceTree = ""; }; 98332ABF1FDBA3D700274FF0 /* error_cblas.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = error_cblas.h; path = cblas/error_cblas.h; sourceTree = ""; }; 98332AC01FDBA41100274FF0 /* source_trmv_r.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = source_trmv_r.h; path = cblas/source_trmv_r.h; sourceTree = ""; }; 98332AC11FDBA53F00274FF0 /* xerbla.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = xerbla.c; path = cblas/xerbla.c; sourceTree = ""; }; 98332AC61FDBA6B600274FF0 /* vector.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = vector.c; path = vector/vector.c; sourceTree = ""; }; 98332ACB1FDBA81A00274FF0 /* oper_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = oper_source.inc; path = vector/oper_source.inc; sourceTree = ""; }; 98332ACC1FDBA81A00274FF0 /* oper.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = oper.c; path = vector/oper.c; sourceTree = ""; }; 98332AD21FDBA8B500274FF0 /* templates_off.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = templates_off.h; sourceTree = ""; }; 98332AD31FDBA8B500274FF0 /* templates_on.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = templates_on.h; sourceTree = ""; }; 98332AD51FDBBD1600274FF0 /* cholesky.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cholesky.c; path = linalg/cholesky.c; sourceTree = ""; }; 98332AD61FDBBD1600274FF0 /* gsl_linalg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_linalg.h; path = linalg/gsl_linalg.h; sourceTree = ""; }; 98332ADB1FDBC0D000274FF0 /* dgemv.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = dgemv.c; path = cblas/dgemv.c; sourceTree = ""; }; 98332AE01FDBC11000274FF0 /* source_gemv_r.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = source_gemv_r.h; path = cblas/source_gemv_r.h; sourceTree = ""; }; 98332AE31FDBC1D900274FF0 /* rowcol.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = rowcol.c; path = matrix/rowcol.c; sourceTree = ""; }; 98332AE41FDBC1D900274FF0 /* submatrix_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = submatrix_source.inc; path = matrix/submatrix_source.inc; sourceTree = ""; }; 98332AE51FDBC1D900274FF0 /* submatrix.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = submatrix.c; path = matrix/submatrix.c; sourceTree = ""; }; 98332AEB1FDBC1F600274FF0 /* rowcol_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = rowcol_source.inc; path = matrix/rowcol_source.inc; sourceTree = ""; }; 98332AF01FDBC30100274FF0 /* view.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = view.h; path = matrix/view.h; sourceTree = ""; }; 98332AF41FDBC3F100274FF0 /* swap_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = swap_source.inc; path = matrix/swap_source.inc; sourceTree = ""; }; 98332AF51FDBC3F100274FF0 /* swap.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = swap.c; path = matrix/swap.c; sourceTree = ""; }; 98332AFB1FDBC4B200274FF0 /* matrix.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = matrix.c; path = matrix/matrix.c; sourceTree = ""; }; 98332B001FDBCFC300274FF0 /* init_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = init_source.inc; path = matrix/init_source.inc; sourceTree = ""; }; 98332B011FDBCFC300274FF0 /* init.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = init.c; path = matrix/init.c; sourceTree = ""; }; 98332B071FDBD00800274FF0 /* init_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = init_source.inc; path = vector/init_source.inc; sourceTree = ""; }; 98332B081FDBD00800274FF0 /* init.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = init.c; path = vector/init.c; sourceTree = ""; }; 98332B0E1FDBD09800274FF0 /* init_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = init_source.inc; path = block/init_source.inc; sourceTree = ""; }; 98332B0F1FDBD09800274FF0 /* init.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = init.c; path = block/init.c; sourceTree = ""; }; 98332B151FDBD13D00274FF0 /* copy_source.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = copy_source.inc; path = matrix/copy_source.inc; sourceTree = ""; }; 98332B161FDBD13D00274FF0 /* copy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = copy.c; path = matrix/copy.c; sourceTree = ""; }; 9836867227CD40CF00683639 /* community.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = community.cpp; sourceTree = ""; }; 9836867327CD40CF00683639 /* community.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = community.h; sourceTree = ""; }; 9836868027CD72E900683639 /* community_eidos.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = community_eidos.cpp; sourceTree = ""; }; 984252C1216FA9930019696A /* FindRecipeController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FindRecipeController.h; sourceTree = ""; }; 984252C2216FA9930019696A /* FindRecipeController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FindRecipeController.mm; sourceTree = ""; }; 98453F3E1A75A12700C058CB /* dump_output_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = dump_output_H.pdf; sourceTree = ""; }; 98453F3F1A75A12700C058CB /* dump_output.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = dump_output.pdf; sourceTree = ""; }; 98453F421A75AABE00C058CB /* syntax_help_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = syntax_help_H.pdf; sourceTree = ""; }; 98453F431A75AABE00C058CB /* syntax_help.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = syntax_help.pdf; sourceTree = ""; }; 98453F4C1A76004300C058CB /* open_type_drawer_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = open_type_drawer_H.pdf; sourceTree = ""; }; 98453F4D1A76004300C058CB /* open_type_drawer.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = open_type_drawer.pdf; sourceTree = ""; }; 98453F4E1A76004300C058CB /* show_fixed_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_fixed_H.pdf; sourceTree = ""; }; 98453F4F1A76004300C058CB /* show_fixed.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_fixed.pdf; sourceTree = ""; }; 98453F501A76004300C058CB /* show_genomicelements_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_genomicelements_H.pdf; sourceTree = ""; }; 98453F511A76004300C058CB /* show_genomicelements.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_genomicelements.pdf; sourceTree = ""; }; 98453F521A76004300C058CB /* show_mutations_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_mutations_H.pdf; sourceTree = ""; }; 98453F531A76004300C058CB /* show_mutations.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_mutations.pdf; sourceTree = ""; }; 98453F541A76004300C058CB /* show_recombination_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_recombination_H.pdf; sourceTree = ""; }; 98453F551A76004300C058CB /* show_recombination.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_recombination.pdf; sourceTree = ""; }; 98453F601A76041200C058CB /* edit_submenu_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = edit_submenu_H.pdf; sourceTree = ""; }; 98453F611A76041200C058CB /* edit_submenu.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = edit_submenu.pdf; sourceTree = ""; }; 984824ED210B9E8F002402A5 /* ddot.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ddot.c; path = cblas/ddot.c; sourceTree = ""; }; 984824EF210B9EE6002402A5 /* source_dot_r.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = source_dot_r.h; path = cblas/source_dot_r.h; sourceTree = ""; }; 984824F0210B9F23002402A5 /* dtrsv.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = dtrsv.c; path = cblas/dtrsv.c; sourceTree = ""; }; 984824F8210B9F63002402A5 /* source_trsv_r.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = source_trsv_r.h; path = cblas/source_trsv_r.h; sourceTree = ""; }; 984D5FA81E3AF0D200473719 /* EidosSLiMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EidosSLiMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 984D5FAC1E3AF0D200473719 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 984D5FB21E3AF18C00473719 /* EidosTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosTests.mm; sourceTree = ""; }; 984D5FB41E3AF1F000473719 /* SLiMTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SLiMTests.mm; sourceTree = ""; }; 984F20AF298087850079D4D2 /* slim_test_parallel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = slim_test_parallel.h; sourceTree = ""; }; 9850D8DF2063098E006BFD2E /* tables.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = tables.c; path = treerec/tskit/tables.c; sourceTree = ""; }; 985301EB1B72582E001520DF /* change_cloning_rate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = change_cloning_rate.pdf; sourceTree = ""; }; 9854D2562278B9F7001D43BC /* core.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = core.c; path = treerec/tskit/core.c; sourceTree = ""; }; 9854D2572278B9F7001D43BC /* genotypes.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = genotypes.c; path = treerec/tskit/genotypes.c; sourceTree = ""; }; 9854D2582278B9F7001D43BC /* genotypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = genotypes.h; path = treerec/tskit/genotypes.h; sourceTree = ""; }; 9854D2592278B9F8001D43BC /* convert.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = convert.c; path = treerec/tskit/convert.c; sourceTree = ""; }; 9854D25A2278B9F8001D43BC /* convert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = convert.h; path = treerec/tskit/convert.h; sourceTree = ""; }; 9854D25B2278B9F8001D43BC /* stats.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = stats.c; path = treerec/tskit/stats.c; sourceTree = ""; }; 9854D25C2278B9F8001D43BC /* stats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = stats.h; path = treerec/tskit/stats.h; sourceTree = ""; }; 9854D25D2278B9F8001D43BC /* trees.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = trees.c; path = treerec/tskit/trees.c; sourceTree = ""; }; 9854D25E2278B9F8001D43BC /* core.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = core.h; path = treerec/tskit/core.h; sourceTree = ""; }; 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_interpreter.cpp; sourceTree = ""; }; 9857249B1AD08A810047C223 /* eidos_interpreter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_interpreter.h; sourceTree = ""; }; 9857249F1AD34B740047C223 /* eidos_functions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_functions.cpp; sourceTree = ""; }; 985724A01AD34B740047C223 /* eidos_functions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_functions.h; sourceTree = ""; }; 985724A31AD435310047C223 /* eidos_value.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_value.cpp; sourceTree = ""; }; 985724A41AD435310047C223 /* eidos_value.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_value.h; sourceTree = ""; }; 985724A71AD4551C0047C223 /* eidos_test.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test.cpp; sourceTree = ""; }; 985724A81AD4551C0047C223 /* eidos_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_test.h; sourceTree = ""; }; 985724AF1AD478630047C223 /* EidosScribe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EidosScribe.app; sourceTree = BUILT_PRODUCTS_DIR; }; 985724B21AD478630047C223 /* EidosScribe-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "EidosScribe-Info.plist"; sourceTree = ""; }; 985724B31AD478630047C223 /* EidosAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EidosAppDelegate.h; sourceTree = ""; }; 985724B41AD478630047C223 /* EidosAppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosAppDelegate.mm; sourceTree = ""; }; 985724B61AD478630047C223 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 985724B81AD478630047C223 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 985724BB1AD478630047C223 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 985724DE1AD4C3310047C223 /* execute_script_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = execute_script_H.pdf; sourceTree = ""; }; 985724DF1AD4C3310047C223 /* execute_script.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = execute_script.pdf; sourceTree = ""; }; 985724E51AD622880047C223 /* EidosConsoleTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosConsoleTextView.h; sourceTree = ""; }; 985724E61AD622880047C223 /* EidosConsoleTextView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosConsoleTextView.mm; sourceTree = ""; }; 985724E81AD6B9FE0047C223 /* execute_selection_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = execute_selection_H.pdf; sourceTree = ""; }; 985724E91AD6B9FE0047C223 /* execute_selection.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = execute_selection.pdf; sourceTree = ""; }; 985724EC1AD6D4060047C223 /* show_parse_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_parse_H.pdf; sourceTree = ""; }; 985724ED1AD6D4060047C223 /* show_parse.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_parse.pdf; sourceTree = ""; }; 985724EE1AD6D4060047C223 /* show_tokens_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_tokens_H.pdf; sourceTree = ""; }; 985724EF1AD6D4060047C223 /* show_tokens.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_tokens.pdf; sourceTree = ""; }; 985724F41AD6DD470047C223 /* show_execution_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_execution_H.pdf; sourceTree = ""; }; 985724F51AD6DD470047C223 /* show_execution.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_execution.pdf; sourceTree = ""; }; 9857E33A1BB58DAE00F1C8A9 /* eidos_intrusive_ptr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_intrusive_ptr.h; sourceTree = ""; }; 985A11861BBA07CB009EE1FF /* eidos_object_pool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_object_pool.h; sourceTree = ""; }; 985D1D892808B84F00461CFA /* sparse_vector.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = sparse_vector.cpp; sourceTree = ""; }; 985D1D8A2808B84F00461CFA /* sparse_vector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sparse_vector.h; sourceTree = ""; }; 985DCFD52B1ED7340025B0D5 /* PARALLEL */ = {isa = PBXFileReference; lastKnownFileType = text; path = PARALLEL; sourceTree = ""; }; 985F3EE924BA27EC00E712E0 /* slim_test_genetics.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = slim_test_genetics.cpp; sourceTree = ""; }; 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test_operators_other.cpp; sourceTree = ""; }; 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test_functions_other.cpp; sourceTree = ""; }; 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test_functions_math.cpp; sourceTree = ""; }; 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test_functions_vector.cpp; sourceTree = ""; }; 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test_operators_arithmetic.cpp; sourceTree = ""; }; 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test_operators_comparison.cpp; sourceTree = ""; }; 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_test_functions_statistics.cpp; sourceTree = ""; }; 986021081A3BB504001BDCFE /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 98606AEC1DED0DCD00821CFF /* mutation_run.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = mutation_run.cpp; sourceTree = ""; }; 98606AED1DED0DCD00821CFF /* mutation_run.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mutation_run.h; sourceTree = ""; }; 986070E82AACECD600FD6143 /* spatial_kernel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = spatial_kernel.cpp; sourceTree = ""; }; 986070E92AACECD600FD6143 /* spatial_kernel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = spatial_kernel.h; sourceTree = ""; }; 986764A92089066A00E81B2E /* tables.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tables.h; path = treerec/tskit/tables.h; sourceTree = ""; }; 986926D21AA1337A0000E138 /* graph_submenu_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = graph_submenu_H.pdf; sourceTree = ""; }; 986926D31AA1337A0000E138 /* graph_submenu.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = graph_submenu.pdf; sourceTree = ""; }; 986926D71AA140550000E138 /* GraphView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphView.h; sourceTree = ""; }; 986926D81AA140550000E138 /* GraphView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphView.mm; sourceTree = ""; }; 986926DB1AA1429D0000E138 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/GraphWindow.xib; sourceTree = ""; }; 986926DD1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphView_MutationFrequencySpectra.h; sourceTree = ""; }; 986926DE1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphView_MutationFrequencySpectra.mm; sourceTree = ""; }; 986926E01AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphView_MutationLossTimeHistogram.h; sourceTree = ""; }; 986926E11AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphView_MutationLossTimeHistogram.mm; sourceTree = ""; }; 986926E31AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphView_MutationFixationTimeHistogram.h; sourceTree = ""; }; 986926E41AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphView_MutationFixationTimeHistogram.mm; sourceTree = ""; }; 986926E61AA40AFF0000E138 /* GraphView_FitnessOverTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphView_FitnessOverTime.h; sourceTree = ""; }; 986926E71AA40AFF0000E138 /* GraphView_FitnessOverTime.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphView_FitnessOverTime.mm; sourceTree = ""; }; 986926E91AA6B7480000E138 /* GraphView_PopulationVisualization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphView_PopulationVisualization.h; sourceTree = ""; }; 986926EA1AA6B7480000E138 /* GraphView_PopulationVisualization.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GraphView_PopulationVisualization.mm; sourceTree = ""; }; 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_call_signature.cpp; sourceTree = ""; }; 986D73E71B07E89E007FBB70 /* eidos_call_signature.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_call_signature.h; sourceTree = ""; }; 98729ACD2A87A93500E81662 /* eidos_sorting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_sorting.h; sourceTree = ""; }; 98729ACE2A87AAB700E81662 /* eidos_sorting.inc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.pascal; path = eidos_sorting.inc; sourceTree = ""; }; 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_sorting.cpp; sourceTree = ""; }; 9873B12328CE26CD00582D83 /* eidos_openmp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_openmp.h; sourceTree = ""; }; 98760EDC28CE5E7600CEBC40 /* libomp.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libomp.dylib; path = /usr/local/lib/libomp.dylib; sourceTree = ""; }; 9876E5F31ED5572C00FF9762 /* gsl_cdf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gsl_cdf.h; path = cdf/gsl_cdf.h; sourceTree = ""; }; 9876E5F41ED5572C00FF9762 /* tdist.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = tdist.c; path = cdf/tdist.c; sourceTree = ""; }; 9876E5F91ED557B000FF9762 /* beta_inc.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = beta_inc.inc; path = cdf/beta_inc.inc; sourceTree = ""; }; 9876E5FB1ED5599F00FF9762 /* gauss.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = gauss.c; path = cdf/gauss.c; sourceTree = ""; }; 9876E6001ED55A2500FF9762 /* gamma_inc.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = gamma_inc.c; sourceTree = ""; }; 9876E6021ED55A6E00FF9762 /* gsl_sf_erf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_erf.h; sourceTree = ""; }; 9876E6061ED55AD600FF9762 /* gsl_sf_expint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_expint.h; sourceTree = ""; }; 9876E6071ED55B4700FF9762 /* erfc.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = erfc.c; sourceTree = ""; }; 9876E60C1ED55C0400FF9762 /* expint.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = expint.c; sourceTree = ""; }; 9876E6111ED55C6B00FF9762 /* beta.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = beta.c; sourceTree = ""; }; 9878A93D1A4E57E70007B9D6 /* species.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = species.cpp; sourceTree = ""; }; 9878A93E1A4E57E70007B9D6 /* species.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = species.h; sourceTree = ""; }; 987A2A0524033856009A636F /* gaussinv.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = gaussinv.c; path = cdf/gaussinv.c; sourceTree = ""; }; 987A2A0624033856009A636F /* rat_eval.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rat_eval.h; path = cdf/rat_eval.h; sourceTree = ""; }; 987A700E2AE8032100A049E2 /* laplace.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = laplace.c; sourceTree = ""; }; 987AD8721B2CBDA70035D6C8 /* show_console_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_console_H.pdf; sourceTree = ""; }; 987AD8731B2CBDA70035D6C8 /* show_console.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = show_console.pdf; sourceTree = ""; }; 987AD8841B2CC0C10035D6C8 /* EidosVariableBrowserController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosVariableBrowserController.h; sourceTree = ""; }; 987AD8851B2CC0C10035D6C8 /* EidosVariableBrowserController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosVariableBrowserController.mm; sourceTree = ""; }; 987AD8881B2CDBB80035D6C8 /* EidosConsoleWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosConsoleWindowController.h; sourceTree = ""; }; 987AD8891B2CDBB80035D6C8 /* EidosConsoleWindowController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosConsoleWindowController.mm; sourceTree = ""; }; 987D19A3209A53850030D28D /* kastore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = kastore.h; path = treerec/tskit/kastore/kastore.h; sourceTree = ""; }; 987D19A4209A53850030D28D /* kastore.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = kastore.c; path = treerec/tskit/kastore/kastore.c; sourceTree = ""; }; 987D30EA2EF482AA0096621B /* eidos_simd.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_simd.h; sourceTree = ""; }; 98800DC21B7EDCB50046F5F9 /* slim_test.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = slim_test.cpp; sourceTree = ""; }; 98800DC31B7EDCB50046F5F9 /* slim_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = slim_test.h; sourceTree = ""; }; 98833AEA1C7A9E8D0072DBAC /* _README */ = {isa = PBXFileReference; lastKnownFileType = text; path = _README; sourceTree = ""; }; 98833AEB1C7A9E8D0072DBAC /* AUTHORS */ = {isa = PBXFileReference; lastKnownFileType = text; path = AUTHORS; sourceTree = ""; }; 98833AEC1C7A9E8D0072DBAC /* COPYING */ = {isa = PBXFileReference; lastKnownFileType = text; path = COPYING; sourceTree = ""; }; 98833AED1C7A9E8D0072DBAC /* README */ = {isa = PBXFileReference; lastKnownFileType = text; path = README; sourceTree = ""; }; 98833AEE1C7A9E8D0072DBAC /* THANKS */ = {isa = PBXFileReference; lastKnownFileType = text; path = THANKS; sourceTree = ""; }; 988794691EA8804900AE0C8D /* SLiMPDFDocument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLiMPDFDocument.h; sourceTree = ""; }; 9887946A1EA8804900AE0C8D /* SLiMPDFDocument.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SLiMPDFDocument.mm; sourceTree = ""; }; 9887946C1EA8808000AE0C8D /* SLiMPDFWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLiMPDFWindowController.h; sourceTree = ""; }; 9887946D1EA8808000AE0C8D /* SLiMPDFWindowController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SLiMPDFWindowController.mm; sourceTree = ""; }; 9887946E1EA8808000AE0C8D /* SLiMPDFWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SLiMPDFWindow.xib; sourceTree = ""; }; 988794711EA8C42200AE0C8D /* SLiMPDFView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLiMPDFView.h; sourceTree = ""; }; 988794721EA8C42200AE0C8D /* SLiMPDFView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SLiMPDFView.mm; sourceTree = ""; }; 988880EB20744EE800E10172 /* cauchy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = cauchy.c; sourceTree = ""; }; 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_class_DataFrame.cpp; sourceTree = ""; }; 9890D1EC27136BB7001EAE98 /* eidos_class_DataFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_class_DataFrame.h; sourceTree = ""; }; 9892282A1BAE279700429674 /* EidosHelpOperators.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = EidosHelpOperators.rtf; sourceTree = ""; }; 9892282D1BAE27AA00429674 /* EidosHelpTypes.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = EidosHelpTypes.rtf; sourceTree = ""; }; 989228301BAF496C00429674 /* EidosHelpStatements.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = EidosHelpStatements.rtf; sourceTree = ""; }; 989228331BAFB27300429674 /* SLiMHelpCallbacks.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = SLiMHelpCallbacks.rtf; sourceTree = ""; }; 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_beep.cpp; sourceTree = ""; }; 9893C7DC1CDA2D650029AC94 /* eidos_beep.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_beep.h; sourceTree = ""; }; 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_type_table.cpp; sourceTree = ""; }; 9893C7E21CDFCF0D0029AC94 /* eidos_type_table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_type_table.h; sourceTree = ""; }; 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_type_interpreter.cpp; sourceTree = ""; }; 9893C7E81CDFE9870029AC94 /* eidos_type_interpreter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_type_interpreter.h; sourceTree = ""; }; 989524A71E40AE74007E62FA /* SLiMDocument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLiMDocument.h; sourceTree = ""; }; 989524A81E40AE74007E62FA /* SLiMDocument.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SLiMDocument.mm; sourceTree = ""; }; 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_class_TestElement.cpp; sourceTree = ""; }; 989790D91AF3D0E100C6B14C /* eidos_class_TestElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_class_TestElement.h; sourceTree = ""; }; 9898172D1A59750300F7417C /* slim_globals.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = slim_globals.cpp; sourceTree = ""; }; 9898172E1A59750300F7417C /* slim_globals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = slim_globals.h; sourceTree = ""; }; 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_class_Dictionary.cpp; sourceTree = ""; }; 989F495127DBB7D600B07B7D /* QtSLiM */ = {isa = PBXFileReference; lastKnownFileType = folder; path = QtSLiM; sourceTree = ""; }; 98A02F931BA1E3E1002DDE54 /* EidosTextViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EidosTextViewDelegate.h; sourceTree = ""; }; 98A02F941BA1E648002DDE54 /* EidosConsoleWindowControllerDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EidosConsoleWindowControllerDelegate.h; sourceTree = ""; }; 98A240581B8E3295005C9A30 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 98A2405C1B8E338E005C9A30 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; 98A2FF881D7DF4D7007E3DB8 /* Recipes */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Recipes; path = SLiMgui/Recipes; sourceTree = ""; }; 98A4EC931B67C1CD00CD92FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/EidosConsoleWindow.xib; sourceTree = ""; }; 98A5134D1B66B69E005A753D /* eidos_token.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_token.cpp; sourceTree = ""; }; 98A5134E1B66B69E005A753D /* eidos_token.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_token.h; sourceTree = ""; }; 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_ast_node.cpp; sourceTree = ""; }; 98A513531B66B6CA005A753D /* eidos_ast_node.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_ast_node.h; sourceTree = ""; }; 98AB59791B2531F10077CB4A /* slim_eidos_block.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = slim_eidos_block.cpp; sourceTree = ""; }; 98AB597A1B2531F10077CB4A /* slim_eidos_block.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = slim_eidos_block.h; sourceTree = ""; }; 98AC617924BA34ED0001914C /* species_eidos.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = species_eidos.cpp; sourceTree = ""; }; 98ACDC9B2534CA120038703F /* eidos_class_Dictionary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_class_Dictionary.h; sourceTree = ""; }; 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_class_Object.cpp; sourceTree = ""; }; 98ACDCA2253522D40038703F /* eidos_class_Object.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_class_Object.h; sourceTree = ""; }; 98AF84641CB307300030D1EB /* VERSIONS */ = {isa = PBXFileReference; lastKnownFileType = text; path = VERSIONS; sourceTree = ""; }; 98B8AF8625A2BE7200C95D66 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = ""; }; 98B8AF8725A2BE7200C95D66 /* json_fwd.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json_fwd.hpp; sourceTree = ""; }; 98C0943C1B7663DF00766A9A /* female_symbol.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = female_symbol.pdf; sourceTree = ""; }; 98C0943D1B7663DF00766A9A /* male_symbol.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = male_symbol.pdf; sourceTree = ""; }; 98C634432EF9F632003F12A3 /* dirichlet.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = dirichlet.c; sourceTree = ""; }; 98C820E11C7A980000548839 /* build.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = build.h; sourceTree = ""; }; 98C820E31C7A980000548839 /* gsl_complex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_complex.h; sourceTree = ""; }; 98C820E41C7A980000548839 /* gsl_complex_math.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_complex_math.h; sourceTree = ""; }; 98C820E51C7A980000548839 /* inline.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = inline.c; sourceTree = ""; }; 98C820E61C7A980000548839 /* math.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; path = math.c; sourceTree = ""; }; 98C820E71C7A980000548839 /* config.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = config.h; sourceTree = ""; }; 98C820E91C7A980000548839 /* error.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = error.c; sourceTree = ""; }; 98C820EA1C7A980000548839 /* gsl_message.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_message.h; sourceTree = ""; }; 98C820EB1C7A980000548839 /* message.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = message.c; sourceTree = ""; }; 98C820EC1C7A980000548839 /* stream.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = stream.c; sourceTree = ""; }; 98C820ED1C7A980000548839 /* gsl_errno.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_errno.h; sourceTree = ""; }; 98C820EE1C7A980000548839 /* gsl_inline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_inline.h; sourceTree = ""; }; 98C820EF1C7A980000548839 /* gsl_machine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_machine.h; sourceTree = ""; }; 98C820F01C7A980000548839 /* gsl_math.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_math.h; sourceTree = ""; }; 98C820F11C7A980000548839 /* gsl_minmax.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_minmax.h; sourceTree = ""; }; 98C820F21C7A980000548839 /* gsl_nan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_nan.h; sourceTree = ""; }; 98C820F31C7A980000548839 /* gsl_pow_int.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_pow_int.h; sourceTree = ""; }; 98C820F41C7A980000548839 /* gsl_precision.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_precision.h; sourceTree = ""; }; 98C820F51C7A980000548839 /* gsl_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_types.h; sourceTree = ""; }; 98C820F71C7A980000548839 /* beta.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = beta.c; sourceTree = ""; }; 98C820F81C7A980000548839 /* binomial_tpe.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = binomial_tpe.c; sourceTree = ""; }; 98C820F91C7A980000548839 /* exponential.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = exponential.c; sourceTree = ""; }; 98C820FA1C7A980000548839 /* gamma.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = gamma.c; sourceTree = ""; }; 98C820FB1C7A980000548839 /* gauss.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = gauss.c; sourceTree = ""; }; 98C820FC1C7A980000548839 /* gausszig.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = gausszig.c; sourceTree = ""; }; 98C820FD1C7A980000548839 /* gsl_randist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_randist.h; sourceTree = ""; }; 98C820FE1C7A980000548839 /* lognormal.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lognormal.c; sourceTree = ""; }; 98C820FF1C7A980000548839 /* multinomial.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = multinomial.c; sourceTree = ""; }; 98C821001C7A980000548839 /* poisson.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = poisson.c; sourceTree = ""; }; 98C821011C7A980000548839 /* weibull.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = weibull.c; sourceTree = ""; }; 98C821031C7A980000548839 /* gsl_rng.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_rng.h; sourceTree = ""; }; 98C821041C7A980000548839 /* inline.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = inline.c; sourceTree = ""; }; 98C821051C7A980000548839 /* mt.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = mt.c; sourceTree = ""; }; 98C821061C7A980000548839 /* rng.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = rng.c; sourceTree = ""; }; 98C821071C7A980000548839 /* taus.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = taus.c; sourceTree = ""; }; 98C821091C7A980000548839 /* cheb_eval.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; path = cheb_eval.inc; sourceTree = ""; }; 98C8210A1C7A980000548839 /* chebyshev.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = chebyshev.h; sourceTree = ""; }; 98C8210B1C7A980000548839 /* check.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = check.h; sourceTree = ""; }; 98C8210C1C7A980000548839 /* elementary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = elementary.c; sourceTree = ""; }; 98C8210D1C7A980000548839 /* error.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = error.h; sourceTree = ""; }; 98C8210E1C7A980000548839 /* eval.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eval.h; sourceTree = ""; }; 98C8210F1C7A980000548839 /* exp.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = exp.c; sourceTree = ""; }; 98C821101C7A980000548839 /* gamma.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = gamma.c; sourceTree = ""; }; 98C821111C7A980000548839 /* gsl_sf_elementary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_elementary.h; sourceTree = ""; }; 98C821121C7A980000548839 /* gsl_sf_exp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_exp.h; sourceTree = ""; }; 98C821131C7A980000548839 /* gsl_sf_gamma.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_gamma.h; sourceTree = ""; }; 98C821141C7A980000548839 /* gsl_sf_log.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_log.h; sourceTree = ""; }; 98C821151C7A980000548839 /* gsl_sf_pow_int.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_pow_int.h; sourceTree = ""; }; 98C821161C7A980000548839 /* gsl_sf_psi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_psi.h; sourceTree = ""; }; 98C821171C7A980000548839 /* gsl_sf_result.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_result.h; sourceTree = ""; }; 98C821181C7A980000548839 /* gsl_sf_trig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_trig.h; sourceTree = ""; }; 98C821191C7A980000548839 /* gsl_sf_zeta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sf_zeta.h; sourceTree = ""; }; 98C8211A1C7A980000548839 /* log.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = log.c; sourceTree = ""; }; 98C8211B1C7A980000548839 /* pow_int.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pow_int.c; sourceTree = ""; }; 98C8211C1C7A980000548839 /* psi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = psi.c; sourceTree = ""; }; 98C8211D1C7A980000548839 /* trig.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = trig.c; sourceTree = ""; }; 98C8211E1C7A980000548839 /* zeta.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = zeta.c; sourceTree = ""; }; 98C821201C7A980000548839 /* coerce.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = coerce.c; sourceTree = ""; }; 98C821211C7A980000548839 /* fdiv.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = fdiv.c; sourceTree = ""; }; 98C821221C7A980000548839 /* gsl_sys.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_sys.h; sourceTree = ""; }; 98C821231C7A980000548839 /* infnan.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = infnan.c; sourceTree = ""; }; 98C8219D1C7A99B200548839 /* minmax.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = minmax.c; sourceTree = ""; }; 98C821A21C7A99F000548839 /* pow_int.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pow_int.c; sourceTree = ""; }; 98C821A71C7A9B1600548839 /* discrete.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = discrete.c; sourceTree = ""; }; 98C821A81C7A9B1600548839 /* geometric.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = geometric.c; sourceTree = ""; }; 98C821B11C7A9B9F00548839 /* shuffle.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = shuffle.c; sourceTree = ""; }; 98C92AEB1D0B07A6001C82BC /* individual.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = individual.cpp; sourceTree = ""; }; 98C92AEC1D0B07A6001C82BC /* individual.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = individual.h; sourceTree = ""; }; 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = spline2d.c; sourceTree = ""; }; 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = bilinear.c; sourceTree = ""; }; 98CEFCD82AAFABAA00D2C9B4 /* gsl_interp2d.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_interp2d.h; sourceTree = ""; }; 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = interp.c; sourceTree = ""; }; 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = bicubic.c; sourceTree = ""; }; 98CEFCDC2AAFABAA00D2C9B4 /* gsl_interp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_interp.h; sourceTree = ""; }; 98CEFCDD2AAFABAA00D2C9B4 /* integ_eval.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = integ_eval.h; sourceTree = ""; }; 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = akima.c; sourceTree = ""; }; 98CEFCE12AAFABAA00D2C9B4 /* gsl_spline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_spline.h; sourceTree = ""; }; 98CEFCE22AAFABAA00D2C9B4 /* spline.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = spline.c; sourceTree = ""; }; 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = interp2d.c; sourceTree = ""; }; 98CEFCE62AAFABAA00D2C9B4 /* linear.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = linear.c; sourceTree = ""; }; 98CEFCE82AAFABAA00D2C9B4 /* gsl_spline2d.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_spline2d.h; sourceTree = ""; }; 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = accel.c; sourceTree = ""; }; 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = inline.c; sourceTree = ""; }; 98CEFD742AAFB12F00D2C9B4 /* cspline.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = cspline.c; sourceTree = ""; }; 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = tridiag.c; path = linalg/tridiag.c; sourceTree = ""; }; 98CEFD862AAFB24700D2C9B4 /* tridiag.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = tridiag.h; path = linalg/tridiag.h; sourceTree = ""; }; 98CEFD872AAFB4EF00D2C9B4 /* view.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = view.c; path = vector/view.c; sourceTree = ""; }; 98CEFD902AAFB55500D2C9B4 /* view.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = view.h; path = vector/view.h; sourceTree = ""; }; 98CEFD912AAFB58100D2C9B4 /* view_source.inc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.pascal; name = view_source.inc; path = vector/view_source.inc; sourceTree = ""; }; 98CF264E1E42DBE200E392D8 /* slim.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; path = slim.iconset; sourceTree = ""; }; 98CF26501E4353FE00E392D8 /* SLiMDocumentController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLiMDocumentController.h; sourceTree = ""; }; 98CF26511E4353FE00E392D8 /* SLiMDocumentController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SLiMDocumentController.mm; sourceTree = ""; }; 98CF5301294A3FC900557BBA /* SLiMguiLegacy_multi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SLiMguiLegacy_multi.app; sourceTree = BUILT_PRODUCTS_DIR; }; 98CF5302294A3FC900557BBA /* SLiMguiLegacy_multi-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "SLiMguiLegacy_multi-Info.plist"; path = "SLiMgui/SLiMguiLegacy_multi-Info.plist"; sourceTree = SOURCE_ROOT; }; 98CF5346294A5F3900557BBA /* SLiMguiLegacy_multi.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SLiMguiLegacy_multi.entitlements; sourceTree = ""; }; 98CF5434294A714200557BBA /* EidosScribe_multi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EidosScribe_multi.app; sourceTree = BUILT_PRODUCTS_DIR; }; 98CF5435294A714200557BBA /* EidosScribe_multi-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "EidosScribe_multi-Info.plist"; path = "EidosScribe/EidosScribe_multi-Info.plist"; sourceTree = SOURCE_ROOT; }; 98CF5451294A725300557BBA /* EidosScribe_multi.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = EidosScribe_multi.entitlements; path = EidosScribe/EidosScribe_multi.entitlements; sourceTree = SOURCE_ROOT; }; 98D218C31B2E213200156FC3 /* EidosTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosTextView.h; sourceTree = ""; }; 98D218C41B2E213200156FC3 /* EidosTextView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosTextView.mm; sourceTree = ""; }; 98D2C3B21F91C56E00E5EB3A /* eidos_test_builtins.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = eidos_test_builtins.h; sourceTree = ""; }; 98D4C1B31A6F537B00FFB083 /* SLiMguiLegacy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SLiMguiLegacy.app; sourceTree = BUILT_PRODUCTS_DIR; }; 98D4C1B61A6F537B00FFB083 /* SLiMguiLegacy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "SLiMguiLegacy-Info.plist"; path = "SLiMgui/SLiMguiLegacy-Info.plist"; sourceTree = SOURCE_ROOT; }; 98D4C1B71A6F537B00FFB083 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 98D4C1B81A6F537B00FFB083 /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = ""; }; 98D4C1BA1A6F537B00FFB083 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 98D4C1BC1A6F537B00FFB083 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 98D4C1BF1A6F537B00FFB083 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 98D4C1E51A6FD8D600FFB083 /* change_sex_ratio.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = change_sex_ratio.pdf; sourceTree = ""; }; 98D4C1E71A6FDE6E00FFB083 /* change_selfing_ratio.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = change_selfing_ratio.pdf; sourceTree = ""; }; 98D4C1E91A6FE7FC00FFB083 /* change_size.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = change_size.pdf; sourceTree = ""; }; 98D4C1EB1A6FEAB500FFB083 /* add_subpop.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = add_subpop.pdf; sourceTree = ""; }; 98D4C1EC1A6FEAB500FFB083 /* remove_subpop.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = remove_subpop.pdf; sourceTree = ""; }; 98D4C1EF1A6FED4000FFB083 /* split_subpop.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = split_subpop.pdf; sourceTree = ""; }; 98D4C1F11A6FEEAB00FFB083 /* change_migration.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = change_migration.pdf; sourceTree = ""; }; 98D4C1F31A70040400FFB083 /* play_step.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = play_step.pdf; sourceTree = ""; }; 98D4C1F41A70040400FFB083 /* play.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = play.pdf; sourceTree = ""; }; 98D4C1F71A70064800FFB083 /* recycle.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = recycle.pdf; sourceTree = ""; }; 98D4C1F91A700DC100FFB083 /* play_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = play_H.pdf; sourceTree = ""; }; 98D4C1FA1A700DC100FFB083 /* play_step_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = play_step_H.pdf; sourceTree = ""; }; 98D4C1FB1A700DC100FFB083 /* recycle_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = recycle_H.pdf; sourceTree = ""; }; 98D4C2001A70192A00FFB083 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/SLiMWindow.xib; sourceTree = ""; }; 98D4C2021A701D5A00FFB083 /* SLiMWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLiMWindowController.h; sourceTree = ""; }; 98D4C2031A701D5A00FFB083 /* SLiMWindowController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SLiMWindowController.mm; sourceTree = ""; }; 98D4C2051A704EA700FFB083 /* PopulationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopulationView.h; sourceTree = ""; }; 98D4C2061A704EA700FFB083 /* PopulationView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PopulationView.mm; sourceTree = ""; }; 98D4C2081A7086FF00FFB083 /* ChromosomeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChromosomeView.h; sourceTree = ""; }; 98D4C2091A7086FF00FFB083 /* ChromosomeView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ChromosomeView.mm; sourceTree = ""; }; 98D4C20C1A715F6100FFB083 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AboutWindow.xib; sourceTree = ""; }; 98D4C20F1A716E0E00FFB083 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 98D4C2131A71838400FFB083 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/HelpWindow.xib; sourceTree = ""; }; 98D4C2151A7187E200FFB083 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; 98D4C21A1A718EFD00FFB083 /* CocoaExtra.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CocoaExtra.h; sourceTree = ""; }; 98D4C21B1A718EFD00FFB083 /* CocoaExtra.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CocoaExtra.mm; sourceTree = ""; }; 98D4C2231A71F8AD00FFB083 /* check_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = check_H.pdf; sourceTree = ""; }; 98D4C2241A71F8AD00FFB083 /* check.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = check.pdf; sourceTree = ""; }; 98D4C2251A71F8AD00FFB083 /* delete_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = delete_H.pdf; sourceTree = ""; }; 98D4C2261A71F8AD00FFB083 /* delete.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = delete.pdf; sourceTree = ""; }; 98D524611F2E6AFB005AD9A6 /* prettyprint_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = prettyprint_H.pdf; sourceTree = ""; }; 98D524621F2E6AFB005AD9A6 /* prettyprint.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = prettyprint.pdf; sourceTree = ""; }; 98D524671F2EB4DD005AD9A6 /* EidosPrettyprinter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EidosPrettyprinter.h; sourceTree = ""; }; 98D524681F2EB4DD005AD9A6 /* EidosPrettyprinter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EidosPrettyprinter.mm; sourceTree = ""; }; 98D7D65B2AB24C40002AFE34 /* tdist.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = tdist.c; sourceTree = ""; }; 98D7D6642AB24CBC002AFE34 /* chisq.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = chisq.c; sourceTree = ""; }; 98D7EBEE28CE557C00DEAAC4 /* eidos_multi */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = eidos_multi; sourceTree = BUILT_PRODUCTS_DIR; }; 98D7ED2D28CE58FC00DEAAC4 /* slim_multi */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = slim_multi; sourceTree = BUILT_PRODUCTS_DIR; }; 98D957882EB53494008314C1 /* pcg_extras.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = pcg_extras.hpp; sourceTree = ""; }; 98D957892EB53494008314C1 /* pcg_random.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = pcg_random.hpp; sourceTree = ""; }; 98DB3D6D1E6122AE00E2C200 /* interaction_type.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = interaction_type.cpp; sourceTree = ""; }; 98DB3D6E1E6122AE00E2C200 /* interaction_type.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = interaction_type.h; sourceTree = ""; }; 98DC9838289986B300160DD8 /* GitSHA1.cpp.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GitSHA1.cpp.in; sourceTree = ""; }; 98DC9839289986B300160DD8 /* GetGitRevisionDescription.cmake */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GetGitRevisionDescription.cmake; sourceTree = ""; }; 98DC983A289986B300160DD8 /* GetGitRevisionDescription.cmake.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GetGitRevisionDescription.cmake.in; sourceTree = ""; }; 98DC983B289986B300160DD8 /* AboutTheseModules.cmake */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = AboutTheseModules.cmake; sourceTree = ""; }; 98DC983C289986B300160DD8 /* GitSHA1.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GitSHA1.h; sourceTree = ""; }; 98DC983D289986B300160DD8 /* GitSHA1_Xcode.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GitSHA1_Xcode.cpp; sourceTree = ""; }; 98DC983E289986B300160DD8 /* LICENSE_1_0.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE_1_0.txt; sourceTree = ""; }; 98DC983F289986B300160DD8 /* README.markdown */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.markdown; sourceTree = ""; }; 98DC9840289986B300160DD8 /* _README.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = _README.txt; sourceTree = ""; }; 98DC98512899919A00160DD8 /* GitSHA1_qmake.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = GitSHA1_qmake.cpp; sourceTree = ""; }; 98DD5F002155B857009062EE /* change_folder.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = change_folder.pdf; sourceTree = ""; }; 98DD5F012155B857009062EE /* change_folder_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = change_folder_H.pdf; sourceTree = ""; }; 98DDAED4221765480038C133 /* slim_functions.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = slim_functions.cpp; sourceTree = ""; }; 98DDAED5221765480038C133 /* slim_functions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = slim_functions.h; sourceTree = ""; }; 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_property_signature.cpp; sourceTree = ""; }; 98DE4C121B6F9657004FDF5F /* eidos_property_signature.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_property_signature.h; sourceTree = ""; }; 98DEB47C2AA632AA00ABE60F /* spatial_map.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = spatial_map.cpp; sourceTree = ""; }; 98DEB47D2AA632AA00ABE60F /* spatial_map.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = spatial_map.h; sourceTree = ""; }; 98E6843E2B694F09000B3B65 /* plot.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = plot.mm; sourceTree = ""; }; 98E6843F2B694F09000B3B65 /* plot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = plot.h; sourceTree = ""; }; 98E9A6881A3CCFD0000AD4FC /* mutation.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = mutation.cpp; sourceTree = ""; }; 98E9A6891A3CCFD0000AD4FC /* mutation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mutation.h; sourceTree = ""; }; 98E9A68E1A3CD4CF000AD4FC /* mutation_type.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = mutation_type.cpp; sourceTree = ""; }; 98E9A68F1A3CD4CF000AD4FC /* mutation_type.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mutation_type.h; sourceTree = ""; }; 98E9A6911A3CD4EF000AD4FC /* genomic_element.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = genomic_element.cpp; sourceTree = ""; }; 98E9A6921A3CD4EF000AD4FC /* genomic_element.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = genomic_element.h; sourceTree = ""; }; 98E9A6941A3CD51A000AD4FC /* genomic_element_type.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = genomic_element_type.cpp; sourceTree = ""; }; 98E9A6951A3CD51A000AD4FC /* genomic_element_type.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = genomic_element_type.h; sourceTree = ""; }; 98E9A6971A3CD52A000AD4FC /* chromosome.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = chromosome.cpp; sourceTree = ""; }; 98E9A6981A3CD52A000AD4FC /* chromosome.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = chromosome.h; sourceTree = ""; }; 98E9A69A1A3CD542000AD4FC /* polymorphism.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = polymorphism.cpp; sourceTree = ""; }; 98E9A69B1A3CD542000AD4FC /* polymorphism.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = polymorphism.h; sourceTree = ""; }; 98E9A69D1A3CD551000AD4FC /* substitution.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = substitution.cpp; sourceTree = ""; }; 98E9A69E1A3CD551000AD4FC /* substitution.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = substitution.h; sourceTree = ""; }; 98E9A6A61A3CD5A0000AD4FC /* haplosome.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = haplosome.cpp; sourceTree = ""; }; 98E9A6A71A3CD5A0000AD4FC /* haplosome.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = haplosome.h; sourceTree = ""; }; 98E9A6A91A3CD5BB000AD4FC /* subpopulation.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = subpopulation.cpp; sourceTree = ""; }; 98E9A6AA1A3CD5BB000AD4FC /* subpopulation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = subpopulation.h; sourceTree = ""; }; 98E9A6AC1A3CD5D3000AD4FC /* population.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = population.cpp; sourceTree = ""; }; 98E9A6AD1A3CD5D3000AD4FC /* population.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = population.h; sourceTree = ""; }; 98E9A6B21A3CE299000AD4FC /* TO_DO */ = {isa = PBXFileReference; lastKnownFileType = text; path = TO_DO; sourceTree = ""; }; 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_rng.cpp; sourceTree = ""; }; 98E9A6B71A3CE35E000AD4FC /* eidos_rng.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_rng.h; sourceTree = ""; }; 98EA965B1ECC2541006BA35B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ProfileReport.xib; sourceTree = ""; }; 98EBCD1221F3CFC600B385CF /* slim_gui.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = slim_gui.mm; sourceTree = ""; }; 98EBCD1321F3CFC600B385CF /* slim_gui.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = slim_gui.h; sourceTree = ""; }; 98EDB4CE2E652F4A00CC8798 /* lu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = lu.c; path = linalg/lu.c; sourceTree = ""; }; 98EDB4DF2E65300500CC8798 /* gsl_permute_vector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_permute_vector.h; sourceTree = ""; }; 98EDB4E02E65326800CC8798 /* gsl_permutation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_permutation.h; sourceTree = ""; }; 98EDB4E12E65340200CC8798 /* gsl_permute_vector_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gsl_permute_vector_double.h; sourceTree = ""; }; 98EDB4E22E65366E00CC8798 /* daxpy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = daxpy.c; path = cblas/daxpy.c; sourceTree = ""; }; 98EDB4EC2E65381500CC8798 /* source_axpy_r.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = source_axpy_r.h; path = cblas/source_axpy_r.h; sourceTree = ""; }; 98EDB4ED2E65389600CC8798 /* init.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = init.c; sourceTree = ""; }; 98EDB4F72E6538F200CC8798 /* permutation.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = permutation.c; sourceTree = ""; }; 98EDB5012E65399600CC8798 /* permute.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = permute.c; sourceTree = ""; }; 98EDB50B2E6539A500CC8798 /* permute_source.inc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.pascal; path = permute_source.inc; sourceTree = ""; }; 98EDB50C2E653A0300CC8798 /* gsl_permute.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = gsl_permute.h; sourceTree = ""; }; 98EDB50D2E653A5300CC8798 /* gsl_permute_double.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = gsl_permute_double.h; sourceTree = ""; }; 98EDB50E2E653BA400CC8798 /* gsl_permute_matrix_double.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = gsl_permute_matrix_double.h; sourceTree = ""; }; 98EDB50F2E653BA400CC8798 /* gsl_permute_matrix.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = gsl_permute_matrix.h; sourceTree = ""; }; 98EDB5102E65410C00CC8798 /* copy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = copy.c; path = vector/copy.c; sourceTree = ""; }; 98EDB51A2E65412C00CC8798 /* copy_source.inc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.pascal; name = copy_source.inc; path = vector/copy_source.inc; sourceTree = ""; }; 98EF4AB41ECDA5EA00CCDB09 /* profile_H.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = profile_H.pdf; sourceTree = ""; }; 98EF4AB51ECDA5EA00CCDB09 /* profile.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = profile.pdf; sourceTree = ""; }; 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = eidos_symbol_table.cpp; sourceTree = ""; }; 98EFE62E1ADB611100CBEC78 /* eidos_symbol_table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = eidos_symbol_table.h; sourceTree = ""; }; 98EFE6331ADD92BC00CBEC78 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/EidosAboutWindow.xib; sourceTree = ""; }; 98F65D551DF14DA40058BD29 /* Tips */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Tips; path = SLiMgui/Tips; sourceTree = ""; }; 98FF384D228AFDD900A96440 /* SLiMgui.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SLiMgui.entitlements; sourceTree = ""; }; 98FF384E228AFE1E00A96440 /* EidosScribe.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EidosScribe.entitlements; sourceTree = ""; }; D05AC1EA20B47EC400FFD319 /* text_input.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = text_input.h; path = treerec/tskit/text_input.h; sourceTree = ""; }; D0A758F620A4CC9800132D2F /* text_input.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = text_input.c; path = treerec/tskit/text_input.c; sourceTree = ""; }; D0A758F820A4CCC900132D2F /* trees.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = trees.h; path = treerec/tskit/trees.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 982556A61BA8E77B0054CB3F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 9826634D1A3BABD300A0CBBF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 984D5FA51E3AF0D200473719 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 985724AC1AD478630047C223 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 98A240591B8E3295005C9A30 /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 98CF52A2294A3FC900557BBA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 98CF52A3294A3FC900557BBA /* OpenGL.framework in Frameworks */, 98CF52A4294A3FC900557BBA /* Cocoa.framework in Frameworks */, 98CF52A5294A3FC900557BBA /* QuartzCore.framework in Frameworks */, 98CF52A6294A3FC900557BBA /* Quartz.framework in Frameworks */, 98CF52A7294A3FC900557BBA /* WebKit.framework in Frameworks */, 98CF533B294A4B5300557BBA /* libomp.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 98CF540F294A714200557BBA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 98CF5410294A714200557BBA /* Cocoa.framework in Frameworks */, 98330C34294A73AB00B452E2 /* libomp.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 98D4C1B01A6F537B00FFB083 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 98A2405D1B8E338E005C9A30 /* OpenGL.framework in Frameworks */, 98A2405A1B8E32D0005C9A30 /* Cocoa.framework in Frameworks */, 9821E2041ABDBC300036EAEA /* QuartzCore.framework in Frameworks */, 98D4C2161A7187E200FFB083 /* Quartz.framework in Frameworks */, 98D4C2101A716E0E00FFB083 /* WebKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 98D7EBE928CE557C00DEAAC4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 98760EDD28CE5E7600CEBC40 /* libomp.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 98D7ED2828CE58FC00DEAAC4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 98760EDE28CE5E8200CEBC40 /* libomp.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 98076601244934A800F6CBB4 /* eidos_zlib */ = { isa = PBXGroup; children = ( 98076608244934A800F6CBB4 /* README */, 98076604244934A800F6CBB4 /* ChangeLog */, 9807660C244934A800F6CBB4 /* zlib.h */, 98076613244934A800F6CBB4 /* zconf.h */, 98076611244934A800F6CBB4 /* crc32.h */, 9807662824493A8F00F6CBB4 /* crc32.c */, 98076602244934A800F6CBB4 /* zutil.h */, 98076609244934A800F6CBB4 /* zutil.c */, 98076606244934A800F6CBB4 /* trees.h */, 9807660F244934A800F6CBB4 /* trees.c */, 9807660A244934A800F6CBB4 /* deflate.h */, 98076605244934A800F6CBB4 /* deflate.c */, 9807660B244934A800F6CBB4 /* gzguts.h */, 98076603244934A800F6CBB4 /* compress.c */, 9807660D244934A800F6CBB4 /* gzlib.c */, 9807660E244934A800F6CBB4 /* gzwrite.c */, 98076612244934A800F6CBB4 /* adler32.c */, ); path = eidos_zlib; sourceTree = ""; }; 981DC34328E26F09000ABE91 /* dependencies */ = { isa = PBXGroup; children = ( 9857E33A1BB58DAE00F1C8A9 /* eidos_intrusive_ptr.h */, 985A11861BBA07CB009EE1FF /* eidos_object_pool.h */, 9809F8BD24F32B3E00D312E4 /* eidos_tinycolormap.h */, 98729ACD2A87A93500E81662 /* eidos_sorting.h */, 98729AD72A87DFBE00E81662 /* eidos_sorting.cpp */, 98729ACE2A87AAB700E81662 /* eidos_sorting.inc */, 9893C7DC1CDA2D650029AC94 /* eidos_beep.h */, 9893C7DB1CDA2D650029AC94 /* eidos_beep.cpp */, 98B8AF8725A2BE7200C95D66 /* json_fwd.hpp */, 98B8AF8625A2BE7200C95D66 /* json.hpp */, 98235687252FDD120096A745 /* lodepng.h */, 98235681252FDCF50096A745 /* lodepng.cpp */, 98186DB8254A8B1600F9118C /* robin_hood.h */, 98D957882EB53494008314C1 /* pcg_extras.hpp */, 98D957892EB53494008314C1 /* pcg_random.hpp */, ); name = dependencies; sourceTree = ""; }; 981DC34428E26F10000ABE91 /* unit tests */ = { isa = PBXGroup; children = ( 989790D91AF3D0E100C6B14C /* eidos_class_TestElement.h */, 989790D81AF3D0E100C6B14C /* eidos_class_TestElement.cpp */, 985724A81AD4551C0047C223 /* eidos_test.h */, 985724A71AD4551C0047C223 /* eidos_test.cpp */, 985F3F0024BA307200E712E0 /* eidos_test_operators_arithmetic.cpp */, 985F3F0524BA310100E712E0 /* eidos_test_operators_comparison.cpp */, 985F3EEC24BA2A5D00E712E0 /* eidos_test_operators_other.cpp */, 985F3EF624BA2DD300E712E0 /* eidos_test_functions_math.cpp */, 985F3F0A24BA31D900E712E0 /* eidos_test_functions_statistics.cpp */, 985F3EFB24BA2F1500E712E0 /* eidos_test_functions_vector.cpp */, 985F3EF124BA2A8C00E712E0 /* eidos_test_functions_other.cpp */, 98D2C3B21F91C56E00E5EB3A /* eidos_test_builtins.h */, 98330C48294AB34300B452E2 /* eidos_test_parallel.h */, ); name = "unit tests"; sourceTree = ""; }; 981DC34528E26F17000ABE91 /* Eidos classes */ = { isa = PBXGroup; children = ( 98ACDCA2253522D40038703F /* eidos_class_Object.h */, 98ACDC9C253522B80038703F /* eidos_class_Object.cpp */, 98ACDC9B2534CA120038703F /* eidos_class_Dictionary.h */, 989A5BE82525304100E7192D /* eidos_class_Dictionary.cpp */, 9890D1EC27136BB7001EAE98 /* eidos_class_DataFrame.h */, 9890D1EB27136BB7001EAE98 /* eidos_class_DataFrame.cpp */, 9823568E252FE62F0096A745 /* eidos_class_Image.h */, 98235688252FE61A0096A745 /* eidos_class_Image.cpp */, ); name = "Eidos classes"; sourceTree = ""; }; 981DC34628E26F1E000ABE91 /* Eidos functions */ = { isa = PBXGroup; children = ( 985724A01AD34B740047C223 /* eidos_functions.h */, 9857249F1AD34B740047C223 /* eidos_functions.cpp */, 981DC34828E26F8A000ABE91 /* eidos_functions_math.cpp */, 981DC34F28E26F8B000ABE91 /* eidos_functions_stats.cpp */, 981DC34C28E26F8A000ABE91 /* eidos_functions_distributions.cpp */, 981DC34B28E26F8A000ABE91 /* eidos_functions_values.cpp */, 981DC34D28E26F8A000ABE91 /* eidos_functions_strings.cpp */, 981DC34A28E26F8A000ABE91 /* eidos_functions_matrices.cpp */, 981DC34728E26F8A000ABE91 /* eidos_functions_files.cpp */, 981DC34928E26F8A000ABE91 /* eidos_functions_colors.cpp */, 981DC34E28E26F8B000ABE91 /* eidos_functions_other.cpp */, ); name = "Eidos functions"; sourceTree = ""; }; 982556AA1BA8E77C0054CB3F /* eidostool */ = { isa = PBXGroup; children = ( 982556AB1BA8E77C0054CB3F /* main.cpp */, ); path = eidostool; sourceTree = ""; }; 982663471A3BABD300A0CBBF = { isa = PBXGroup; children = ( 985DCFD52B1ED7340025B0D5 /* PARALLEL */, 98E9A6B21A3CE299000AD4FC /* TO_DO */, 986021081A3BB504001BDCFE /* LICENSE */, 98AF84641CB307300030D1EB /* VERSIONS */, 98DC9837289986B300160DD8 /* cmake */, 98C820E01C7A980000548839 /* gsl */, 98BCF14F200C5FDB00CB2837 /* treerec */, 98076601244934A800F6CBB4 /* eidos_zlib */, 9857249E1AD34B120047C223 /* eidos */, 982663521A3BABD300A0CBBF /* core */, 982556AA1BA8E77C0054CB3F /* eidostool */, 985724B01AD478630047C223 /* EidosScribe */, 98D4C1B41A6F537B00FFB083 /* SLiMgui */, 98A2FF881D7DF4D7007E3DB8 /* Recipes */, 98F65D551DF14DA40058BD29 /* Tips */, 989F495127DBB7D600B07B7D /* QtSLiM */, 98A2405B1B8E32F4005C9A30 /* Shared Frameworks */, 984D5FA91E3AF0D200473719 /* EidosSLiMTests */, 982663511A3BABD300A0CBBF /* Products */, 98760EDB28CE5E7600CEBC40 /* Frameworks */, 980B6A6428CE642D0075B192 /* eidos_multi.entitlements */, 980B6A6528CE64310075B192 /* slim_multi.entitlements */, ); sourceTree = ""; }; 982663511A3BABD300A0CBBF /* Products */ = { isa = PBXGroup; children = ( 982663501A3BABD300A0CBBF /* slim */, 98D4C1B31A6F537B00FFB083 /* SLiMguiLegacy.app */, 985724AF1AD478630047C223 /* EidosScribe.app */, 982556A91BA8E77B0054CB3F /* eidos */, 984D5FA81E3AF0D200473719 /* EidosSLiMTests.xctest */, 98D7EBEE28CE557C00DEAAC4 /* eidos_multi */, 98D7ED2D28CE58FC00DEAAC4 /* slim_multi */, 98CF5301294A3FC900557BBA /* SLiMguiLegacy_multi.app */, 98CF5434294A714200557BBA /* EidosScribe_multi.app */, ); name = Products; sourceTree = ""; }; 982663521A3BABD300A0CBBF /* core */ = { isa = PBXGroup; children = ( 982663531A3BABD300A0CBBF /* main.cpp */, 9898172E1A59750300F7417C /* slim_globals.h */, 9898172D1A59750300F7417C /* slim_globals.cpp */, 98DDAED5221765480038C133 /* slim_functions.h */, 98DDAED4221765480038C133 /* slim_functions.cpp */, 98AB597A1B2531F10077CB4A /* slim_eidos_block.h */, 98AB59791B2531F10077CB4A /* slim_eidos_block.cpp */, 9836867327CD40CF00683639 /* community.h */, 9836867227CD40CF00683639 /* community.cpp */, 9836868027CD72E900683639 /* community_eidos.cpp */, 9878A93E1A4E57E70007B9D6 /* species.h */, 9878A93D1A4E57E70007B9D6 /* species.cpp */, 98AC617924BA34ED0001914C /* species_eidos.cpp */, 98E9A6981A3CD52A000AD4FC /* chromosome.h */, 98E9A6971A3CD52A000AD4FC /* chromosome.cpp */, 98E9A6AD1A3CD5D3000AD4FC /* population.h */, 98E9A6AC1A3CD5D3000AD4FC /* population.cpp */, 98E9A6AA1A3CD5BB000AD4FC /* subpopulation.h */, 98E9A6A91A3CD5BB000AD4FC /* subpopulation.cpp */, 98C92AEC1D0B07A6001C82BC /* individual.h */, 98C92AEB1D0B07A6001C82BC /* individual.cpp */, 98E9A6A71A3CD5A0000AD4FC /* haplosome.h */, 98E9A6A61A3CD5A0000AD4FC /* haplosome.cpp */, 98606AED1DED0DCD00821CFF /* mutation_run.h */, 98606AEC1DED0DCD00821CFF /* mutation_run.cpp */, 98E9A6891A3CCFD0000AD4FC /* mutation.h */, 98E9A6881A3CCFD0000AD4FC /* mutation.cpp */, 98E9A69E1A3CD551000AD4FC /* substitution.h */, 98E9A69D1A3CD551000AD4FC /* substitution.cpp */, 98E9A6921A3CD4EF000AD4FC /* genomic_element.h */, 98E9A6911A3CD4EF000AD4FC /* genomic_element.cpp */, 98E9A6951A3CD51A000AD4FC /* genomic_element_type.h */, 98E9A6941A3CD51A000AD4FC /* genomic_element_type.cpp */, 98E9A68F1A3CD4CF000AD4FC /* mutation_type.h */, 98E9A68E1A3CD4CF000AD4FC /* mutation_type.cpp */, 98E9A69B1A3CD542000AD4FC /* polymorphism.h */, 98E9A69A1A3CD542000AD4FC /* polymorphism.cpp */, 985D1D8A2808B84F00461CFA /* sparse_vector.h */, 985D1D892808B84F00461CFA /* sparse_vector.cpp */, 986070E92AACECD600FD6143 /* spatial_kernel.h */, 986070E82AACECD600FD6143 /* spatial_kernel.cpp */, 98DB3D6E1E6122AE00E2C200 /* interaction_type.h */, 98DB3D6D1E6122AE00E2C200 /* interaction_type.cpp */, 98DEB47D2AA632AA00ABE60F /* spatial_map.h */, 98DEB47C2AA632AA00ABE60F /* spatial_map.cpp */, 9809DF9F2550F32500C4E82D /* log_file.h */, 9809DF9E2550F32500C4E82D /* log_file.cpp */, 98800DC31B7EDCB50046F5F9 /* slim_test.h */, 98800DC21B7EDCB50046F5F9 /* slim_test.cpp */, 9807C0F324BA21B7008CC658 /* slim_test_core.cpp */, 985F3EE924BA27EC00E712E0 /* slim_test_genetics.cpp */, 9807C0F724BA21E3008CC658 /* slim_test_other.cpp */, 984F20AF298087850079D4D2 /* slim_test_parallel.h */, ); path = core; sourceTree = ""; }; 98332AA41FDB996800274FF0 /* matrix */ = { isa = PBXGroup; children = ( 98332AA51FDB997E00274FF0 /* gsl_matrix.h */, 98332AA91FDB9B5100274FF0 /* gsl_matrix_double.h */, 98332AF01FDBC30100274FF0 /* view.h */, 98332AFB1FDBC4B200274FF0 /* matrix.c */, 98332B151FDBD13D00274FF0 /* copy_source.inc */, 98332B161FDBD13D00274FF0 /* copy.c */, 98332B001FDBCFC300274FF0 /* init_source.inc */, 98332B011FDBCFC300274FF0 /* init.c */, 98332AEB1FDBC1F600274FF0 /* rowcol_source.inc */, 98332AE31FDBC1D900274FF0 /* rowcol.c */, 98332AE41FDBC1D900274FF0 /* submatrix_source.inc */, 98332AE51FDBC1D900274FF0 /* submatrix.c */, 98332AF41FDBC3F100274FF0 /* swap_source.inc */, 98332AF51FDBC3F100274FF0 /* swap.c */, ); name = matrix; sourceTree = ""; }; 98332AA61FDB998700274FF0 /* vector */ = { isa = PBXGroup; children = ( 98332AA71FDB99AA00274FF0 /* gsl_vector.h */, 98332AA81FDB9B3F00274FF0 /* gsl_vector_double.h */, 98332AC61FDBA6B600274FF0 /* vector.c */, 98EDB51A2E65412C00CC8798 /* copy_source.inc */, 98EDB5102E65410C00CC8798 /* copy.c */, 98332B071FDBD00800274FF0 /* init_source.inc */, 98332B081FDBD00800274FF0 /* init.c */, 98332ACB1FDBA81A00274FF0 /* oper_source.inc */, 98332ACC1FDBA81A00274FF0 /* oper.c */, 98CEFD912AAFB58100D2C9B4 /* view_source.inc */, 98CEFD902AAFB55500D2C9B4 /* view.h */, 98CEFD872AAFB4EF00D2C9B4 /* view.c */, ); name = vector; sourceTree = ""; }; 98332AAA1FDB9C6D00274FF0 /* block */ = { isa = PBXGroup; children = ( 98332AAB1FDB9C7C00274FF0 /* gsl_block_double.h */, 98332AAC1FDB9C7C00274FF0 /* gsl_block.h */, 98332AAD1FDB9C7C00274FF0 /* gsl_check_range.h */, 98332B0E1FDBD09800274FF0 /* init_source.inc */, 98332B0F1FDBD09800274FF0 /* init.c */, ); name = block; sourceTree = ""; }; 98332AAE1FDB9E0000274FF0 /* blas */ = { isa = PBXGroup; children = ( 98332AB31FDBA1E100274FF0 /* blas.c */, 98332AAF1FDB9E1500274FF0 /* gsl_blas.h */, 98332AB01FDB9FA600274FF0 /* gsl_blas_types.h */, ); name = blas; sourceTree = ""; }; 98332AB11FDB9FF000274FF0 /* cblas */ = { isa = PBXGroup; children = ( 98332AB21FDBA00000274FF0 /* gsl_cblas.h */, 98332ABD1FDBA36500274FF0 /* cblas.h */, 98332ABF1FDBA3D700274FF0 /* error_cblas.h */, 98332ABE1FDBA3A700274FF0 /* error_cblas_l2.h */, 98EDB4EC2E65381500CC8798 /* source_axpy_r.h */, 984824EF210B9EE6002402A5 /* source_dot_r.h */, 98332AE01FDBC11000274FF0 /* source_gemv_r.h */, 98332AC01FDBA41100274FF0 /* source_trmv_r.h */, 984824F8210B9F63002402A5 /* source_trsv_r.h */, 98332AC11FDBA53F00274FF0 /* xerbla.c */, 98EDB4E22E65366E00CC8798 /* daxpy.c */, 984824ED210B9E8F002402A5 /* ddot.c */, 98332ADB1FDBC0D000274FF0 /* dgemv.c */, 98332AB81FDBA32200274FF0 /* dtrmv.c */, 984824F0210B9F23002402A5 /* dtrsv.c */, ); name = cblas; sourceTree = ""; }; 98332AD41FDBBCC600274FF0 /* linalg */ = { isa = PBXGroup; children = ( 98332AD61FDBBD1600274FF0 /* gsl_linalg.h */, 98332AD51FDBBD1600274FF0 /* cholesky.c */, 98EDB4CE2E652F4A00CC8798 /* lu.c */, 98CEFD862AAFB24700D2C9B4 /* tridiag.h */, 98CEFD7D2AAFB23E00D2C9B4 /* tridiag.c */, ); name = linalg; sourceTree = ""; }; 984D5FA91E3AF0D200473719 /* EidosSLiMTests */ = { isa = PBXGroup; children = ( 984D5FB41E3AF1F000473719 /* SLiMTests.mm */, 984D5FB21E3AF18C00473719 /* EidosTests.mm */, 984D5FAC1E3AF0D200473719 /* Info.plist */, ); path = EidosSLiMTests; sourceTree = ""; }; 9857249E1AD34B120047C223 /* eidos */ = { isa = PBXGroup; children = ( 9873B12328CE26CD00582D83 /* eidos_openmp.h */, 987D30EA2EF482AA0096621B /* eidos_simd.h */, 98E9A6B71A3CE35E000AD4FC /* eidos_rng.h */, 98E9A6B61A3CE35E000AD4FC /* eidos_rng.cpp */, 98321F931B406B67007337A3 /* eidos_globals.h */, 98321F921B406B67007337A3 /* eidos_globals.cpp */, 98A5134E1B66B69E005A753D /* eidos_token.h */, 98A5134D1B66B69E005A753D /* eidos_token.cpp */, 98A513531B66B6CA005A753D /* eidos_ast_node.h */, 98A513521B66B6CA005A753D /* eidos_ast_node.cpp */, 981BAC691ACC6E8B0005BE94 /* eidos_script.h */, 981BAC681ACC6E8B0005BE94 /* eidos_script.cpp */, 98EFE62E1ADB611100CBEC78 /* eidos_symbol_table.h */, 98EFE62D1ADB611100CBEC78 /* eidos_symbol_table.cpp */, 985724A41AD435310047C223 /* eidos_value.h */, 985724A31AD435310047C223 /* eidos_value.cpp */, 986D73E71B07E89E007FBB70 /* eidos_call_signature.h */, 986D73E61B07E89E007FBB70 /* eidos_call_signature.cpp */, 98DE4C121B6F9657004FDF5F /* eidos_property_signature.h */, 98DE4C111B6F9657004FDF5F /* eidos_property_signature.cpp */, 9857249B1AD08A810047C223 /* eidos_interpreter.h */, 9857249A1AD08A810047C223 /* eidos_interpreter.cpp */, 9893C7E21CDFCF0D0029AC94 /* eidos_type_table.h */, 9893C7E11CDFCF0D0029AC94 /* eidos_type_table.cpp */, 9893C7E81CDFE9870029AC94 /* eidos_type_interpreter.h */, 9893C7E71CDFE9870029AC94 /* eidos_type_interpreter.cpp */, 981DC34628E26F1E000ABE91 /* Eidos functions */, 981DC34528E26F17000ABE91 /* Eidos classes */, 981DC34428E26F10000ABE91 /* unit tests */, 981DC34328E26F09000ABE91 /* dependencies */, ); path = eidos; sourceTree = ""; }; 985724B01AD478630047C223 /* EidosScribe */ = { isa = PBXGroup; children = ( 985724B31AD478630047C223 /* EidosAppDelegate.h */, 985724B41AD478630047C223 /* EidosAppDelegate.mm */, 9825565D1BA32F030054CB3F /* EidosCocoaExtra.h */, 9825565A1BA32EE80054CB3F /* EidosCocoaExtra.mm */, 98D218C31B2E213200156FC3 /* EidosTextView.h */, 98A02F931BA1E3E1002DDE54 /* EidosTextViewDelegate.h */, 98D218C41B2E213200156FC3 /* EidosTextView.mm */, 985724E51AD622880047C223 /* EidosConsoleTextView.h */, 982556621BA35DF00054CB3F /* EidosConsoleTextViewDelegate.h */, 985724E61AD622880047C223 /* EidosConsoleTextView.mm */, 987AD8881B2CDBB80035D6C8 /* EidosConsoleWindowController.h */, 98A02F941BA1E648002DDE54 /* EidosConsoleWindowControllerDelegate.h */, 987AD8891B2CDBB80035D6C8 /* EidosConsoleWindowController.mm */, 987AD8841B2CC0C10035D6C8 /* EidosVariableBrowserController.h */, 9825565E1BA358EC0054CB3F /* EidosVariableBrowserControllerDelegate.h */, 987AD8851B2CC0C10035D6C8 /* EidosVariableBrowserController.mm */, 98090FA41B1B978900791DBF /* EidosValueWrapper.h */, 98090FA51B1B978900791DBF /* EidosValueWrapper.mm */, 982556631BA450980054CB3F /* EidosHelpController.h */, 982556641BA450980054CB3F /* EidosHelpController.mm */, 98D524671F2EB4DD005AD9A6 /* EidosPrettyprinter.h */, 98D524681F2EB4DD005AD9A6 /* EidosPrettyprinter.mm */, 985724B81AD478630047C223 /* Images.xcassets */, 985724BA1AD478630047C223 /* MainMenu.xib */, 98EFE6321ADD92BC00CBEC78 /* EidosAboutWindow.xib */, 98A4EC921B67C1CD00CD92FD /* EidosConsoleWindow.xib */, 982556671BA451D00054CB3F /* EidosHelpWindow.xib */, 9825566B1BA477D60054CB3F /* EidosHelpFunctions.rtf */, 9825569F1BA5DFEB0054CB3F /* EidosHelpClasses.rtf */, 9892282A1BAE279700429674 /* EidosHelpOperators.rtf */, 9892282D1BAE27AA00429674 /* EidosHelpTypes.rtf */, 989228301BAF496C00429674 /* EidosHelpStatements.rtf */, 98A4EC911B67C02000CD92FD /* buttons */, 985724B11AD478630047C223 /* Supporting Files */, ); path = EidosScribe; sourceTree = ""; }; 985724B11AD478630047C223 /* Supporting Files */ = { isa = PBXGroup; children = ( 985724B21AD478630047C223 /* EidosScribe-Info.plist */, 98CF5435294A714200557BBA /* EidosScribe_multi-Info.plist */, 98FF384E228AFE1E00A96440 /* EidosScribe.entitlements */, 98CF5451294A725300557BBA /* EidosScribe_multi.entitlements */, 985724B61AD478630047C223 /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; 986926D61AA13D340000E138 /* Graphing */ = { isa = PBXGroup; children = ( 98E6843F2B694F09000B3B65 /* plot.h */, 98E6843E2B694F09000B3B65 /* plot.mm */, 986926D71AA140550000E138 /* GraphView.h */, 986926D81AA140550000E138 /* GraphView.mm */, 986926DD1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.h */, 986926DE1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.mm */, 986926E01AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.h */, 986926E11AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.mm */, 986926E31AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.h */, 986926E41AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.mm */, 986926E61AA40AFF0000E138 /* GraphView_FitnessOverTime.h */, 986926E71AA40AFF0000E138 /* GraphView_FitnessOverTime.mm */, 986926E91AA6B7480000E138 /* GraphView_PopulationVisualization.h */, 986926EA1AA6B7480000E138 /* GraphView_PopulationVisualization.mm */, 980DD51B1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.h */, 980DD51C1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.mm */, 986926DA1AA1429D0000E138 /* GraphWindow.xib */, 980DD5181AAE42F900D5B7B8 /* GraphAxisRescaleSheet.xib */, 982A9DDC1FCA9FF0007BA3DF /* GraphBarRescaleSheet.xib */, ); name = Graphing; sourceTree = ""; }; 98760EDB28CE5E7600CEBC40 /* Frameworks */ = { isa = PBXGroup; children = ( 98760EDC28CE5E7600CEBC40 /* libomp.dylib */, ); name = Frameworks; sourceTree = ""; }; 9876E5F21ED5571C00FF9762 /* cdf */ = { isa = PBXGroup; children = ( 987A2A0524033856009A636F /* gaussinv.c */, 987A2A0624033856009A636F /* rat_eval.h */, 9876E5F31ED5572C00FF9762 /* gsl_cdf.h */, 9876E5F91ED557B000FF9762 /* beta_inc.inc */, 9876E5FB1ED5599F00FF9762 /* gauss.c */, 9876E5F41ED5572C00FF9762 /* tdist.c */, ); name = cdf; sourceTree = ""; }; 987D19A2209A535E0030D28D /* kastore */ = { isa = PBXGroup; children = ( 987D19A3209A53850030D28D /* kastore.h */, 987D19A4209A53850030D28D /* kastore.c */, ); name = kastore; sourceTree = ""; }; 988794681EA87FC300AE0C8D /* PDF */ = { isa = PBXGroup; children = ( 988794691EA8804900AE0C8D /* SLiMPDFDocument.h */, 9887946A1EA8804900AE0C8D /* SLiMPDFDocument.mm */, 9887946C1EA8808000AE0C8D /* SLiMPDFWindowController.h */, 9887946D1EA8808000AE0C8D /* SLiMPDFWindowController.mm */, 988794711EA8C42200AE0C8D /* SLiMPDFView.h */, 988794721EA8C42200AE0C8D /* SLiMPDFView.mm */, 9887946E1EA8808000AE0C8D /* SLiMPDFWindow.xib */, ); name = PDF; sourceTree = ""; }; 98A2405B1B8E32F4005C9A30 /* Shared Frameworks */ = { isa = PBXGroup; children = ( 98A240581B8E3295005C9A30 /* Cocoa.framework */, ); name = "Shared Frameworks"; sourceTree = ""; }; 98A4EC911B67C02000CD92FD /* buttons */ = { isa = PBXGroup; children = ( 98090FA01B1B8B5800791DBF /* show_browser_H.pdf */, 98090FA11B1B8B5800791DBF /* show_browser.pdf */, 98D4C2231A71F8AD00FFB083 /* check_H.pdf */, 98D4C2241A71F8AD00FFB083 /* check.pdf */, 98D524611F2E6AFB005AD9A6 /* prettyprint_H.pdf */, 98D524621F2E6AFB005AD9A6 /* prettyprint.pdf */, 98D4C2251A71F8AD00FFB083 /* delete_H.pdf */, 98D4C2261A71F8AD00FFB083 /* delete.pdf */, 985724DE1AD4C3310047C223 /* execute_script_H.pdf */, 985724DF1AD4C3310047C223 /* execute_script.pdf */, 985724E81AD6B9FE0047C223 /* execute_selection_H.pdf */, 985724E91AD6B9FE0047C223 /* execute_selection.pdf */, 98453F421A75AABE00C058CB /* syntax_help_H.pdf */, 98453F431A75AABE00C058CB /* syntax_help.pdf */, 985724EC1AD6D4060047C223 /* show_parse_H.pdf */, 985724ED1AD6D4060047C223 /* show_parse.pdf */, 985724EE1AD6D4060047C223 /* show_tokens_H.pdf */, 985724EF1AD6D4060047C223 /* show_tokens.pdf */, 985724F41AD6DD470047C223 /* show_execution_H.pdf */, 985724F51AD6DD470047C223 /* show_execution.pdf */, ); path = buttons; sourceTree = ""; }; 98BCF14F200C5FDB00CB2837 /* treerec */ = { isa = PBXGroup; children = ( 987D19A2209A535E0030D28D /* kastore */, 9854D25A2278B9F8001D43BC /* convert.h */, 9854D2592278B9F8001D43BC /* convert.c */, 9854D25E2278B9F8001D43BC /* core.h */, 9854D2562278B9F7001D43BC /* core.c */, 9854D2582278B9F7001D43BC /* genotypes.h */, 9854D2572278B9F7001D43BC /* genotypes.c */, 9854D25C2278B9F8001D43BC /* stats.h */, 9854D25B2278B9F8001D43BC /* stats.c */, D05AC1EA20B47EC400FFD319 /* text_input.h */, D0A758F620A4CC9800132D2F /* text_input.c */, 986764A92089066A00E81B2E /* tables.h */, 9850D8DF2063098E006BFD2E /* tables.c */, D0A758F820A4CCC900132D2F /* trees.h */, 9854D25D2278B9F8001D43BC /* trees.c */, ); name = treerec; sourceTree = ""; }; 98C820E01C7A980000548839 /* gsl */ = { isa = PBXGroup; children = ( 98833AEA1C7A9E8D0072DBAC /* _README */, 98833AEB1C7A9E8D0072DBAC /* AUTHORS */, 98833AEC1C7A9E8D0072DBAC /* COPYING */, 98833AED1C7A9E8D0072DBAC /* README */, 98833AEE1C7A9E8D0072DBAC /* THANKS */, 98C820E11C7A980000548839 /* build.h */, 98C820E71C7A980000548839 /* config.h */, 98C820ED1C7A980000548839 /* gsl_errno.h */, 98C820EE1C7A980000548839 /* gsl_inline.h */, 98C820EF1C7A980000548839 /* gsl_machine.h */, 98C820F01C7A980000548839 /* gsl_math.h */, 98C820F11C7A980000548839 /* gsl_minmax.h */, 98C820F21C7A980000548839 /* gsl_nan.h */, 98C820F31C7A980000548839 /* gsl_pow_int.h */, 98C820F41C7A980000548839 /* gsl_precision.h */, 98C820F51C7A980000548839 /* gsl_types.h */, 98332AD21FDBA8B500274FF0 /* templates_off.h */, 98332AD31FDBA8B500274FF0 /* templates_on.h */, 98332AAE1FDB9E0000274FF0 /* blas */, 98332AAA1FDB9C6D00274FF0 /* block */, 98332AB11FDB9FF000274FF0 /* cblas */, 9876E5F21ED5571C00FF9762 /* cdf */, 98C820E21C7A980000548839 /* complex */, 98C820E81C7A980000548839 /* err */, 98CEFCD32AAFABAA00D2C9B4 /* interpolation */, 98332AD41FDBBCC600274FF0 /* linalg */, 98332AA41FDB996800274FF0 /* matrix */, 98EDB4DE2E65300500CC8798 /* permutation */, 98C820F61C7A980000548839 /* randist */, 98C821021C7A980000548839 /* rng */, 98C821081C7A980000548839 /* specfunc */, 98C8211F1C7A980000548839 /* sys */, 98332AA61FDB998700274FF0 /* vector */, ); path = gsl; sourceTree = ""; }; 98C820E21C7A980000548839 /* complex */ = { isa = PBXGroup; children = ( 98C820E31C7A980000548839 /* gsl_complex.h */, 98C820E41C7A980000548839 /* gsl_complex_math.h */, 98C820E51C7A980000548839 /* inline.c */, 98C820E61C7A980000548839 /* math.c */, ); path = complex; sourceTree = ""; }; 98C820E81C7A980000548839 /* err */ = { isa = PBXGroup; children = ( 98C820EA1C7A980000548839 /* gsl_message.h */, 98C820E91C7A980000548839 /* error.c */, 98C820EB1C7A980000548839 /* message.c */, 98C820EC1C7A980000548839 /* stream.c */, ); path = err; sourceTree = ""; }; 98C820F61C7A980000548839 /* randist */ = { isa = PBXGroup; children = ( 98C820FD1C7A980000548839 /* gsl_randist.h */, 98C820F71C7A980000548839 /* beta.c */, 98C820F81C7A980000548839 /* binomial_tpe.c */, 988880EB20744EE800E10172 /* cauchy.c */, 98D7D6642AB24CBC002AFE34 /* chisq.c */, 98C634432EF9F632003F12A3 /* dirichlet.c */, 98C821A71C7A9B1600548839 /* discrete.c */, 98C820F91C7A980000548839 /* exponential.c */, 980566E125A7C5B9008D3C7F /* fdist.c */, 98C820FA1C7A980000548839 /* gamma.c */, 98C820FB1C7A980000548839 /* gauss.c */, 98C820FC1C7A980000548839 /* gausszig.c */, 98C821A81C7A9B1600548839 /* geometric.c */, 987A700E2AE8032100A049E2 /* laplace.c */, 98C820FE1C7A980000548839 /* lognormal.c */, 98C820FF1C7A980000548839 /* multinomial.c */, 98332A9D1FDB98ED00274FF0 /* mvgauss.c */, 982B50C52704048E006E91BC /* nbinomial.c */, 98C821001C7A980000548839 /* poisson.c */, 98C821B11C7A9B9F00548839 /* shuffle.c */, 98D7D65B2AB24C40002AFE34 /* tdist.c */, 98C821011C7A980000548839 /* weibull.c */, ); path = randist; sourceTree = ""; }; 98C821021C7A980000548839 /* rng */ = { isa = PBXGroup; children = ( 98C821031C7A980000548839 /* gsl_rng.h */, 98C821041C7A980000548839 /* inline.c */, 98C821051C7A980000548839 /* mt.c */, 98C821061C7A980000548839 /* rng.c */, 98C821071C7A980000548839 /* taus.c */, ); path = rng; sourceTree = ""; }; 98C821081C7A980000548839 /* specfunc */ = { isa = PBXGroup; children = ( 98C8210A1C7A980000548839 /* chebyshev.h */, 98C8210B1C7A980000548839 /* check.h */, 98C8210D1C7A980000548839 /* error.h */, 98C8210E1C7A980000548839 /* eval.h */, 98C821111C7A980000548839 /* gsl_sf_elementary.h */, 9876E6021ED55A6E00FF9762 /* gsl_sf_erf.h */, 98C821121C7A980000548839 /* gsl_sf_exp.h */, 9876E6061ED55AD600FF9762 /* gsl_sf_expint.h */, 98C821131C7A980000548839 /* gsl_sf_gamma.h */, 98C821141C7A980000548839 /* gsl_sf_log.h */, 98C821151C7A980000548839 /* gsl_sf_pow_int.h */, 98C821161C7A980000548839 /* gsl_sf_psi.h */, 98C821171C7A980000548839 /* gsl_sf_result.h */, 98C821181C7A980000548839 /* gsl_sf_trig.h */, 98C821191C7A980000548839 /* gsl_sf_zeta.h */, 98C821091C7A980000548839 /* cheb_eval.inc */, 9876E6111ED55C6B00FF9762 /* beta.c */, 98C8210C1C7A980000548839 /* elementary.c */, 9876E6071ED55B4700FF9762 /* erfc.c */, 98C8210F1C7A980000548839 /* exp.c */, 9876E60C1ED55C0400FF9762 /* expint.c */, 98C821101C7A980000548839 /* gamma.c */, 9876E6001ED55A2500FF9762 /* gamma_inc.c */, 98C8211A1C7A980000548839 /* log.c */, 98C8211B1C7A980000548839 /* pow_int.c */, 98C8211C1C7A980000548839 /* psi.c */, 98C8211D1C7A980000548839 /* trig.c */, 98C8211E1C7A980000548839 /* zeta.c */, ); path = specfunc; sourceTree = ""; }; 98C8211F1C7A980000548839 /* sys */ = { isa = PBXGroup; children = ( 98C821221C7A980000548839 /* gsl_sys.h */, 98C821201C7A980000548839 /* coerce.c */, 98C821211C7A980000548839 /* fdiv.c */, 98C821231C7A980000548839 /* infnan.c */, 98C8219D1C7A99B200548839 /* minmax.c */, 98C821A21C7A99F000548839 /* pow_int.c */, ); path = sys; sourceTree = ""; }; 98CEFCD32AAFABAA00D2C9B4 /* interpolation */ = { isa = PBXGroup; children = ( 98CEFCD42AAFABAA00D2C9B4 /* spline2d.c */, 98CEFCD52AAFABAA00D2C9B4 /* bilinear.c */, 98CEFCD82AAFABAA00D2C9B4 /* gsl_interp2d.h */, 98CEFCDA2AAFABAA00D2C9B4 /* interp.c */, 98CEFD742AAFB12F00D2C9B4 /* cspline.c */, 98CEFCDB2AAFABAA00D2C9B4 /* bicubic.c */, 98CEFCDC2AAFABAA00D2C9B4 /* gsl_interp.h */, 98CEFCDD2AAFABAA00D2C9B4 /* integ_eval.h */, 98CEFCDE2AAFABAA00D2C9B4 /* akima.c */, 98CEFCE12AAFABAA00D2C9B4 /* gsl_spline.h */, 98CEFCE22AAFABAA00D2C9B4 /* spline.c */, 98CEFCE52AAFABAA00D2C9B4 /* interp2d.c */, 98CEFCE62AAFABAA00D2C9B4 /* linear.c */, 98CEFCE82AAFABAA00D2C9B4 /* gsl_spline2d.h */, 98CEFCEA2AAFABAA00D2C9B4 /* accel.c */, 98CEFCEB2AAFABAA00D2C9B4 /* inline.c */, ); path = interpolation; sourceTree = ""; }; 98D4C1B41A6F537B00FFB083 /* SLiMgui */ = { isa = PBXGroup; children = ( 98EBCD1321F3CFC600B385CF /* slim_gui.h */, 98EBCD1221F3CFC600B385CF /* slim_gui.mm */, 986926D61AA13D340000E138 /* Graphing */, 988794681EA87FC300AE0C8D /* PDF */, 98D4C21A1A718EFD00FFB083 /* CocoaExtra.h */, 98D4C21B1A718EFD00FFB083 /* CocoaExtra.mm */, 98D4C1B71A6F537B00FFB083 /* AppDelegate.h */, 98D4C1B81A6F537B00FFB083 /* AppDelegate.mm */, 989524A71E40AE74007E62FA /* SLiMDocument.h */, 989524A81E40AE74007E62FA /* SLiMDocument.mm */, 98CF26501E4353FE00E392D8 /* SLiMDocumentController.h */, 98CF26511E4353FE00E392D8 /* SLiMDocumentController.mm */, 98D4C2021A701D5A00FFB083 /* SLiMWindowController.h */, 98D4C2031A701D5A00FFB083 /* SLiMWindowController.mm */, 98D4C2051A704EA700FFB083 /* PopulationView.h */, 98D4C2061A704EA700FFB083 /* PopulationView.mm */, 98D4C2081A7086FF00FFB083 /* ChromosomeView.h */, 98D4C2091A7086FF00FFB083 /* ChromosomeView.mm */, 984252C1216FA9930019696A /* FindRecipeController.h */, 984252C2216FA9930019696A /* FindRecipeController.mm */, 98D4C1BE1A6F537B00FFB083 /* MainMenu.xib */, 98D4C1FF1A70192A00FFB083 /* SLiMWindow.xib */, 98D4C20B1A715F6100FFB083 /* AboutWindow.xib */, 98D4C2121A71838400FFB083 /* HelpWindow.xib */, 98EA965A1ECC2541006BA35B /* ProfileReport.xib */, 98024740215D85880025D29C /* FindRecipePanel.xib */, 9825566E1BA4FAD00054CB3F /* SLiMHelpFunctions.rtf */, 982556A21BA5F0810054CB3F /* SLiMHelpClasses.rtf */, 989228331BAFB27300429674 /* SLiMHelpCallbacks.rtf */, 98D4C1E41A6FD8D600FFB083 /* buttons */, 98D4C1B51A6F537B00FFB083 /* Supporting Files */, ); path = SLiMgui; sourceTree = ""; }; 98D4C1B51A6F537B00FFB083 /* Supporting Files */ = { isa = PBXGroup; children = ( 98D4C1BC1A6F537B00FFB083 /* Images.xcassets */, 98CF264E1E42DBE200E392D8 /* slim.iconset */, 98D4C1B61A6F537B00FFB083 /* SLiMguiLegacy-Info.plist */, 98CF5302294A3FC900557BBA /* SLiMguiLegacy_multi-Info.plist */, 98FF384D228AFDD900A96440 /* SLiMgui.entitlements */, 98CF5346294A5F3900557BBA /* SLiMguiLegacy_multi.entitlements */, 98D4C1BA1A6F537B00FFB083 /* main.m */, 98A2405C1B8E338E005C9A30 /* OpenGL.framework */, 98D4C2151A7187E200FFB083 /* Quartz.framework */, 9821E2031ABDBC300036EAEA /* QuartzCore.framework */, 98D4C20F1A716E0E00FFB083 /* WebKit.framework */, ); name = "Supporting Files"; sourceTree = ""; }; 98D4C1E41A6FD8D600FFB083 /* buttons */ = { isa = PBXGroup; children = ( 98DD5F012155B857009062EE /* change_folder_H.pdf */, 98DD5F002155B857009062EE /* change_folder.pdf */, 98C0943C1B7663DF00766A9A /* female_symbol.pdf */, 98C0943D1B7663DF00766A9A /* male_symbol.pdf */, 987AD8721B2CBDA70035D6C8 /* show_console_H.pdf */, 987AD8731B2CBDA70035D6C8 /* show_console.pdf */, 986926D21AA1337A0000E138 /* graph_submenu_H.pdf */, 986926D31AA1337A0000E138 /* graph_submenu.pdf */, 98453F4C1A76004300C058CB /* open_type_drawer_H.pdf */, 98453F4D1A76004300C058CB /* open_type_drawer.pdf */, 98453F601A76041200C058CB /* edit_submenu_H.pdf */, 98453F611A76041200C058CB /* edit_submenu.pdf */, 98453F4E1A76004300C058CB /* show_fixed_H.pdf */, 98453F4F1A76004300C058CB /* show_fixed.pdf */, 98453F501A76004300C058CB /* show_genomicelements_H.pdf */, 98453F511A76004300C058CB /* show_genomicelements.pdf */, 98453F521A76004300C058CB /* show_mutations_H.pdf */, 98453F531A76004300C058CB /* show_mutations.pdf */, 98453F541A76004300C058CB /* show_recombination_H.pdf */, 98453F551A76004300C058CB /* show_recombination.pdf */, 98453F3E1A75A12700C058CB /* dump_output_H.pdf */, 98453F3F1A75A12700C058CB /* dump_output.pdf */, 98D4C1F31A70040400FFB083 /* play_step.pdf */, 98D4C1FA1A700DC100FFB083 /* play_step_H.pdf */, 98D4C1F41A70040400FFB083 /* play.pdf */, 98D4C1F91A700DC100FFB083 /* play_H.pdf */, 98EF4AB51ECDA5EA00CCDB09 /* profile.pdf */, 98EF4AB41ECDA5EA00CCDB09 /* profile_H.pdf */, 98D4C1F71A70064800FFB083 /* recycle.pdf */, 98D4C1FB1A700DC100FFB083 /* recycle_H.pdf */, 98D4C1F11A6FEEAB00FFB083 /* change_migration.pdf */, 98D4C1E71A6FDE6E00FFB083 /* change_selfing_ratio.pdf */, 985301EB1B72582E001520DF /* change_cloning_rate.pdf */, 98D4C1E51A6FD8D600FFB083 /* change_sex_ratio.pdf */, 98D4C1E91A6FE7FC00FFB083 /* change_size.pdf */, 98D4C1EB1A6FEAB500FFB083 /* add_subpop.pdf */, 98D4C1EC1A6FEAB500FFB083 /* remove_subpop.pdf */, 98D4C1EF1A6FED4000FFB083 /* split_subpop.pdf */, ); path = buttons; sourceTree = ""; }; 98DC9837289986B300160DD8 /* cmake */ = { isa = PBXGroup; children = ( 98DC9838289986B300160DD8 /* GitSHA1.cpp.in */, 98DC9839289986B300160DD8 /* GetGitRevisionDescription.cmake */, 98DC983A289986B300160DD8 /* GetGitRevisionDescription.cmake.in */, 98DC983B289986B300160DD8 /* AboutTheseModules.cmake */, 98DC983C289986B300160DD8 /* GitSHA1.h */, 98DC983D289986B300160DD8 /* GitSHA1_Xcode.cpp */, 98DC98512899919A00160DD8 /* GitSHA1_qmake.cpp */, 98DC983E289986B300160DD8 /* LICENSE_1_0.txt */, 98DC983F289986B300160DD8 /* README.markdown */, 98DC9840289986B300160DD8 /* _README.txt */, ); path = cmake; sourceTree = ""; }; 98EDB4DE2E65300500CC8798 /* permutation */ = { isa = PBXGroup; children = ( 98EDB50C2E653A0300CC8798 /* gsl_permute.h */, 98EDB50D2E653A5300CC8798 /* gsl_permute_double.h */, 98EDB50F2E653BA400CC8798 /* gsl_permute_matrix.h */, 98EDB50E2E653BA400CC8798 /* gsl_permute_matrix_double.h */, 98EDB4E12E65340200CC8798 /* gsl_permute_vector_double.h */, 98EDB4E02E65326800CC8798 /* gsl_permutation.h */, 98EDB4DF2E65300500CC8798 /* gsl_permute_vector.h */, 98EDB4ED2E65389600CC8798 /* init.c */, 98EDB5012E65399600CC8798 /* permute.c */, 98EDB50B2E6539A500CC8798 /* permute_source.inc */, 98EDB4F72E6538F200CC8798 /* permutation.c */, ); path = permutation; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 982556A81BA8E77B0054CB3F /* eidos */ = { isa = PBXNativeTarget; buildConfigurationList = 982556AD1BA8E77C0054CB3F /* Build configuration list for PBXNativeTarget "eidos" */; buildPhases = ( 982556A51BA8E77B0054CB3F /* Sources */, 982556A61BA8E77B0054CB3F /* Frameworks */, 982556A71BA8E77B0054CB3F /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); name = eidos; productName = eidos; productReference = 982556A91BA8E77B0054CB3F /* eidos */; productType = "com.apple.product-type.tool"; }; 9826634F1A3BABD300A0CBBF /* SLiM */ = { isa = PBXNativeTarget; buildConfigurationList = 982663571A3BABD300A0CBBF /* Build configuration list for PBXNativeTarget "SLiM" */; buildPhases = ( 9826634C1A3BABD300A0CBBF /* Sources */, 9826634D1A3BABD300A0CBBF /* Frameworks */, 9826634E1A3BABD300A0CBBF /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); name = SLiM; productName = SLiM; productReference = 982663501A3BABD300A0CBBF /* slim */; productType = "com.apple.product-type.tool"; }; 984D5FA71E3AF0D200473719 /* EidosSLiMTests */ = { isa = PBXNativeTarget; buildConfigurationList = 984D5FB11E3AF0D200473719 /* Build configuration list for PBXNativeTarget "EidosSLiMTests" */; buildPhases = ( 984D5FA41E3AF0D200473719 /* Sources */, 984D5FA51E3AF0D200473719 /* Frameworks */, 984D5FA61E3AF0D200473719 /* Resources */, ); buildRules = ( ); dependencies = ( 984D5FAE1E3AF0D200473719 /* PBXTargetDependency */, ); name = EidosSLiMTests; productName = EidosSLiMTests; productReference = 984D5FA81E3AF0D200473719 /* EidosSLiMTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 985724AE1AD478630047C223 /* EidosScribe */ = { isa = PBXNativeTarget; buildConfigurationList = 985724C91AD478640047C223 /* Build configuration list for PBXNativeTarget "EidosScribe" */; buildPhases = ( 985724AB1AD478630047C223 /* Sources */, 985724AC1AD478630047C223 /* Frameworks */, 985724AD1AD478630047C223 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = EidosScribe; productName = SLiMscribe; productReference = 985724AF1AD478630047C223 /* EidosScribe.app */; productType = "com.apple.product-type.application"; }; 98CF51F3294A3FC900557BBA /* SLiMguiLegacy_multi */ = { isa = PBXNativeTarget; buildConfigurationList = 98CF52FE294A3FC900557BBA /* Build configuration list for PBXNativeTarget "SLiMguiLegacy_multi" */; buildPhases = ( 98CF51F4294A3FC900557BBA /* Sources */, 98CF52A2294A3FC900557BBA /* Frameworks */, 98CF52A8294A3FC900557BBA /* Resources */, ); buildRules = ( ); dependencies = ( ); name = SLiMguiLegacy_multi; productName = SLiMgui; productReference = 98CF5301294A3FC900557BBA /* SLiMguiLegacy_multi.app */; productType = "com.apple.product-type.application"; }; 98CF5397294A714200557BBA /* EidosScribe_multi */ = { isa = PBXNativeTarget; buildConfigurationList = 98CF5431294A714200557BBA /* Build configuration list for PBXNativeTarget "EidosScribe_multi" */; buildPhases = ( 98CF5398294A714200557BBA /* Sources */, 98CF540F294A714200557BBA /* Frameworks */, 98CF5411294A714200557BBA /* Resources */, ); buildRules = ( ); dependencies = ( ); name = EidosScribe_multi; productName = SLiMscribe; productReference = 98CF5434294A714200557BBA /* EidosScribe_multi.app */; productType = "com.apple.product-type.application"; }; 98D4C1B21A6F537B00FFB083 /* SLiMguiLegacy */ = { isa = PBXNativeTarget; buildConfigurationList = 98D4C1CD1A6F537C00FFB083 /* Build configuration list for PBXNativeTarget "SLiMguiLegacy" */; buildPhases = ( 98D4C1AF1A6F537B00FFB083 /* Sources */, 98D4C1B01A6F537B00FFB083 /* Frameworks */, 98D4C1B11A6F537B00FFB083 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = SLiMguiLegacy; productName = SLiMgui; productReference = 98D4C1B31A6F537B00FFB083 /* SLiMguiLegacy.app */; productType = "com.apple.product-type.application"; }; 98D7EB8328CE557C00DEAAC4 /* eidos_multi */ = { isa = PBXNativeTarget; buildConfigurationList = 98D7EBEB28CE557C00DEAAC4 /* Build configuration list for PBXNativeTarget "eidos_multi" */; buildPhases = ( 98D7EB8428CE557C00DEAAC4 /* Sources */, 98D7EBE928CE557C00DEAAC4 /* Frameworks */, 98D7EBEA28CE557C00DEAAC4 /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); name = eidos_multi; productName = eidos; productReference = 98D7EBEE28CE557C00DEAAC4 /* eidos_multi */; productType = "com.apple.product-type.tool"; }; 98D7EC9F28CE58FC00DEAAC4 /* slim_multi */ = { isa = PBXNativeTarget; buildConfigurationList = 98D7ED2A28CE58FC00DEAAC4 /* Build configuration list for PBXNativeTarget "slim_multi" */; buildPhases = ( 98D7ECA028CE58FC00DEAAC4 /* Sources */, 98D7ED2828CE58FC00DEAAC4 /* Frameworks */, 98D7ED2928CE58FC00DEAAC4 /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); name = slim_multi; productName = SLiM; productReference = 98D7ED2D28CE58FC00DEAAC4 /* slim_multi */; productType = "com.apple.product-type.tool"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 982663481A3BABD300A0CBBF /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1210; ORGANIZATIONNAME = "Messer Lab, http://messerlab.org/software/"; TargetAttributes = { 982556A81BA8E77B0054CB3F = { CreatedOnToolsVersion = 6.4; }; 9826634F1A3BABD300A0CBBF = { CreatedOnToolsVersion = 6.1.1; }; 984D5FA71E3AF0D200473719 = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = C5P6M43RZ7; ProvisioningStyle = Automatic; TestTargetID = 98D4C1B21A6F537B00FFB083; }; 985724AE1AD478630047C223 = { CreatedOnToolsVersion = 6.1.1; SystemCapabilities = { com.apple.HardenedRuntime = { enabled = 1; }; }; }; 98D4C1B21A6F537B00FFB083 = { CreatedOnToolsVersion = 6.1.1; SystemCapabilities = { com.apple.HardenedRuntime = { enabled = 1; }; }; }; }; }; buildConfigurationList = 9826634B1A3BABD300A0CBBF /* Build configuration list for PBXProject "SLiM" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 982663471A3BABD300A0CBBF; productRefGroup = 982663511A3BABD300A0CBBF /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 9826634F1A3BABD300A0CBBF /* SLiM */, 98D4C1B21A6F537B00FFB083 /* SLiMguiLegacy */, 98CF51F3294A3FC900557BBA /* SLiMguiLegacy_multi */, 985724AE1AD478630047C223 /* EidosScribe */, 98CF5397294A714200557BBA /* EidosScribe_multi */, 982556A81BA8E77B0054CB3F /* eidos */, 984D5FA71E3AF0D200473719 /* EidosSLiMTests */, 98D7EB8328CE557C00DEAAC4 /* eidos_multi */, 98D7EC9F28CE58FC00DEAAC4 /* slim_multi */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 984D5FA61E3AF0D200473719 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 985724AD1AD478630047C223 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 985724F01AD6D4060047C223 /* show_parse_H.pdf in Resources */, 985724F11AD6D4060047C223 /* show_parse.pdf in Resources */, 985724E11AD4C3310047C223 /* execute_script.pdf in Resources */, 985724D61AD489AA0047C223 /* check_H.pdf in Resources */, 9892282C1BAE279700429674 /* EidosHelpOperators.rtf in Resources */, 985724E01AD4C3310047C223 /* execute_script_H.pdf in Resources */, 989228321BAF496C00429674 /* EidosHelpStatements.rtf in Resources */, 9825566D1BA477D60054CB3F /* EidosHelpFunctions.rtf in Resources */, 9892282F1BAE27AA00429674 /* EidosHelpTypes.rtf in Resources */, 98D524631F2E6AFB005AD9A6 /* prettyprint_H.pdf in Resources */, 982556A11BA5DFEB0054CB3F /* EidosHelpClasses.rtf in Resources */, 98090FA21B1B8B5800791DBF /* show_browser_H.pdf in Resources */, 9825566A1BA451D00054CB3F /* EidosHelpWindow.xib in Resources */, 985724B91AD478630047C223 /* Images.xcassets in Resources */, 985724F71AD6DD470047C223 /* show_execution.pdf in Resources */, 985724BC1AD478630047C223 /* MainMenu.xib in Resources */, 985724DA1AD489AA0047C223 /* syntax_help_H.pdf in Resources */, 98EFE6341ADD92BC00CBEC78 /* EidosAboutWindow.xib in Resources */, 985724D91AD489AA0047C223 /* delete.pdf in Resources */, 985724D81AD489AA0047C223 /* delete_H.pdf in Resources */, 985724DB1AD489AA0047C223 /* syntax_help.pdf in Resources */, 985724F31AD6D4060047C223 /* show_tokens.pdf in Resources */, 98A4EC941B67C1CD00CD92FD /* EidosConsoleWindow.xib in Resources */, 985724D71AD489AA0047C223 /* check.pdf in Resources */, 98D524641F2E6AFB005AD9A6 /* prettyprint.pdf in Resources */, 985724EB1AD6B9FE0047C223 /* execute_selection.pdf in Resources */, 985724F21AD6D4060047C223 /* show_tokens_H.pdf in Resources */, 985724EA1AD6B9FE0047C223 /* execute_selection_H.pdf in Resources */, 985724F61AD6DD470047C223 /* show_execution_H.pdf in Resources */, 98090FA31B1B8B5800791DBF /* show_browser.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98CF52A8294A3FC900557BBA /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 98CF52A9294A3FC900557BBA /* profile.pdf in Resources */, 98CF52AA294A3FC900557BBA /* Recipes in Resources */, 98CF52AB294A3FC900557BBA /* play_step.pdf in Resources */, 98CF52AC294A3FC900557BBA /* FindRecipePanel.xib in Resources */, 98CF52AD294A3FC900557BBA /* slim.iconset in Resources */, 98CF52AE294A3FC900557BBA /* show_parse.pdf in Resources */, 98CF52AF294A3FC900557BBA /* open_type_drawer_H.pdf in Resources */, 98CF52B0294A3FC900557BBA /* Images.xcassets in Resources */, 98CF52B1294A3FC900557BBA /* recycle.pdf in Resources */, 98CF52B2294A3FC900557BBA /* check.pdf in Resources */, 98CF52B3294A3FC900557BBA /* remove_subpop.pdf in Resources */, 98CF52B4294A3FC900557BBA /* EidosHelpWindow.xib in Resources */, 98CF52B5294A3FC900557BBA /* execute_selection.pdf in Resources */, 98CF52B6294A3FC900557BBA /* profile_H.pdf in Resources */, 98CF52B7294A3FC900557BBA /* dump_output.pdf in Resources */, 98CF52B8294A3FC900557BBA /* show_mutations_H.pdf in Resources */, 98CF52B9294A3FC900557BBA /* dump_output_H.pdf in Resources */, 98CF52BA294A3FC900557BBA /* GraphBarRescaleSheet.xib in Resources */, 98CF52BB294A3FC900557BBA /* show_genomicelements_H.pdf in Resources */, 98CF52BC294A3FC900557BBA /* SLiMHelpFunctions.rtf in Resources */, 98CF52BD294A3FC900557BBA /* edit_submenu.pdf in Resources */, 98CF52BE294A3FC900557BBA /* prettyprint_H.pdf in Resources */, 98CF52BF294A3FC900557BBA /* EidosHelpStatements.rtf in Resources */, 98CF52C0294A3FC900557BBA /* change_sex_ratio.pdf in Resources */, 98CF52C1294A3FC900557BBA /* ProfileReport.xib in Resources */, 98CF52C2294A3FC900557BBA /* show_console_H.pdf in Resources */, 98CF52C3294A3FC900557BBA /* MainMenu.xib in Resources */, 98CF52C4294A3FC900557BBA /* show_execution.pdf in Resources */, 98CF52C5294A3FC900557BBA /* change_cloning_rate.pdf in Resources */, 98CF52C6294A3FC900557BBA /* female_symbol.pdf in Resources */, 98CF52C7294A3FC900557BBA /* split_subpop.pdf in Resources */, 98CF52C8294A3FC900557BBA /* show_recombination.pdf in Resources */, 98CF52C9294A3FC900557BBA /* GraphWindow.xib in Resources */, 98CF52CA294A3FC900557BBA /* GraphAxisRescaleSheet.xib in Resources */, 98CF52CB294A3FC900557BBA /* EidosHelpOperators.rtf in Resources */, 98CF52CC294A3FC900557BBA /* add_subpop.pdf in Resources */, 98CF52CE294A3FC900557BBA /* EidosHelpTypes.rtf in Resources */, 98CF52CF294A3FC900557BBA /* check_H.pdf in Resources */, 98CF52D0294A3FC900557BBA /* show_execution_H.pdf in Resources */, 98CF52D1294A3FC900557BBA /* SLiMHelpClasses.rtf in Resources */, 98CF52D2294A3FC900557BBA /* show_fixed.pdf in Resources */, 98CF52D3294A3FC900557BBA /* syntax_help_H.pdf in Resources */, 98CF52D4294A3FC900557BBA /* graph_submenu_H.pdf in Resources */, 98CF52D5294A3FC900557BBA /* show_parse_H.pdf in Resources */, 98CF52D6294A3FC900557BBA /* show_browser.pdf in Resources */, 98CF52D7294A3FC900557BBA /* EidosConsoleWindow.xib in Resources */, 98CF52D8294A3FC900557BBA /* SLiMPDFWindow.xib in Resources */, 98CF52D9294A3FC900557BBA /* prettyprint.pdf in Resources */, 98CF52DA294A3FC900557BBA /* syntax_help.pdf in Resources */, 98CF52DB294A3FC900557BBA /* change_selfing_ratio.pdf in Resources */, 98CF52DC294A3FC900557BBA /* change_folder.pdf in Resources */, 98CF52DD294A3FC900557BBA /* open_type_drawer.pdf in Resources */, 98CF52DF294A3FC900557BBA /* EidosHelpClasses.rtf in Resources */, 98CF52E0294A3FC900557BBA /* show_tokens.pdf in Resources */, 98CF52E1294A3FC900557BBA /* edit_submenu_H.pdf in Resources */, 98CF52E2294A3FC900557BBA /* AboutWindow.xib in Resources */, 98CF52E3294A3FC900557BBA /* play_step_H.pdf in Resources */, 98CF52E4294A3FC900557BBA /* show_recombination_H.pdf in Resources */, 98CF52E5294A3FC900557BBA /* EidosHelpFunctions.rtf in Resources */, 98CF52E6294A3FC900557BBA /* male_symbol.pdf in Resources */, 98CF52E7294A3FC900557BBA /* recycle_H.pdf in Resources */, 98CF52E8294A3FC900557BBA /* show_mutations.pdf in Resources */, 98CF52E9294A3FC900557BBA /* execute_selection_H.pdf in Resources */, 98CF52EA294A3FC900557BBA /* change_folder_H.pdf in Resources */, 98CF52EB294A3FC900557BBA /* play_H.pdf in Resources */, 98CF52ED294A3FC900557BBA /* SLiMHelpCallbacks.rtf in Resources */, 98CF52EE294A3FC900557BBA /* execute_script.pdf in Resources */, 98CF52EF294A3FC900557BBA /* graph_submenu.pdf in Resources */, 98CF52F0294A3FC900557BBA /* show_fixed_H.pdf in Resources */, 98CF52F1294A3FC900557BBA /* execute_script_H.pdf in Resources */, 98CF52F2294A3FC900557BBA /* SLiMWindow.xib in Resources */, 98CF52F3294A3FC900557BBA /* show_browser_H.pdf in Resources */, 98CF52F4294A3FC900557BBA /* play.pdf in Resources */, 98CF52F5294A3FC900557BBA /* change_migration.pdf in Resources */, 98CF52F6294A3FC900557BBA /* show_console.pdf in Resources */, 98CF52F7294A3FC900557BBA /* show_tokens_H.pdf in Resources */, 98CF52F8294A3FC900557BBA /* delete.pdf in Resources */, 98CF52F9294A3FC900557BBA /* delete_H.pdf in Resources */, 98CF52FA294A3FC900557BBA /* Tips in Resources */, 98CF52FB294A3FC900557BBA /* change_size.pdf in Resources */, 98CF52FC294A3FC900557BBA /* show_genomicelements.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98CF5411294A714200557BBA /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 98CF5412294A714200557BBA /* show_parse_H.pdf in Resources */, 98CF5413294A714200557BBA /* show_parse.pdf in Resources */, 98CF5414294A714200557BBA /* execute_script.pdf in Resources */, 98CF5415294A714200557BBA /* check_H.pdf in Resources */, 98CF5416294A714200557BBA /* EidosHelpOperators.rtf in Resources */, 98CF5417294A714200557BBA /* execute_script_H.pdf in Resources */, 98CF5418294A714200557BBA /* EidosHelpStatements.rtf in Resources */, 98CF5419294A714200557BBA /* EidosHelpFunctions.rtf in Resources */, 98CF541A294A714200557BBA /* EidosHelpTypes.rtf in Resources */, 98CF541B294A714200557BBA /* prettyprint_H.pdf in Resources */, 98CF541C294A714200557BBA /* EidosHelpClasses.rtf in Resources */, 98CF541D294A714200557BBA /* show_browser_H.pdf in Resources */, 98CF541E294A714200557BBA /* EidosHelpWindow.xib in Resources */, 98CF541F294A714200557BBA /* Images.xcassets in Resources */, 98CF5420294A714200557BBA /* show_execution.pdf in Resources */, 98CF5421294A714200557BBA /* MainMenu.xib in Resources */, 98CF5422294A714200557BBA /* syntax_help_H.pdf in Resources */, 98CF5423294A714200557BBA /* EidosAboutWindow.xib in Resources */, 98CF5424294A714200557BBA /* delete.pdf in Resources */, 98CF5425294A714200557BBA /* delete_H.pdf in Resources */, 98CF5426294A714200557BBA /* syntax_help.pdf in Resources */, 98CF5427294A714200557BBA /* show_tokens.pdf in Resources */, 98CF5428294A714200557BBA /* EidosConsoleWindow.xib in Resources */, 98CF5429294A714200557BBA /* check.pdf in Resources */, 98CF542A294A714200557BBA /* prettyprint.pdf in Resources */, 98CF542B294A714200557BBA /* execute_selection.pdf in Resources */, 98CF542C294A714200557BBA /* show_tokens_H.pdf in Resources */, 98CF542D294A714200557BBA /* execute_selection_H.pdf in Resources */, 98CF542E294A714200557BBA /* show_execution_H.pdf in Resources */, 98CF542F294A714200557BBA /* show_browser.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98D4C1B11A6F537B00FFB083 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 98EF4AB71ECDA5EA00CCDB09 /* profile.pdf in Resources */, 98A2FF891D7DF4D7007E3DB8 /* Recipes in Resources */, 98D4C1F51A70040400FFB083 /* play_step.pdf in Resources */, 98024742215D85880025D29C /* FindRecipePanel.xib in Resources */, 98CF264F1E42DBE200E392D8 /* slim.iconset in Resources */, 987AD87D1B2CBE4F0035D6C8 /* show_parse.pdf in Resources */, 98453F561A76004300C058CB /* open_type_drawer_H.pdf in Resources */, 98D4C1BD1A6F537B00FFB083 /* Images.xcassets in Resources */, 98D4C1F81A70064800FFB083 /* recycle.pdf in Resources */, 98D4C2281A71F8AD00FFB083 /* check.pdf in Resources */, 98D4C1EE1A6FEAB500FFB083 /* remove_subpop.pdf in Resources */, 982556691BA451D00054CB3F /* EidosHelpWindow.xib in Resources */, 987AD87B1B2CBE360035D6C8 /* execute_selection.pdf in Resources */, 98EF4AB61ECDA5EA00CCDB09 /* profile_H.pdf in Resources */, 98453F411A75A12700C058CB /* dump_output.pdf in Resources */, 98453F5C1A76004300C058CB /* show_mutations_H.pdf in Resources */, 98453F401A75A12700C058CB /* dump_output_H.pdf in Resources */, 982A9DDE1FCA9FF0007BA3DF /* GraphBarRescaleSheet.xib in Resources */, 98453F5A1A76004300C058CB /* show_genomicelements_H.pdf in Resources */, 9825566F1BA4FAD00054CB3F /* SLiMHelpFunctions.rtf in Resources */, 98453F631A76041200C058CB /* edit_submenu.pdf in Resources */, 98D524651F2E6B08005AD9A6 /* prettyprint_H.pdf in Resources */, 989228311BAF496C00429674 /* EidosHelpStatements.rtf in Resources */, 98D4C1E61A6FD8D600FFB083 /* change_sex_ratio.pdf in Resources */, 98EA965C1ECC2541006BA35B /* ProfileReport.xib in Resources */, 987AD8741B2CBDA70035D6C8 /* show_console_H.pdf in Resources */, 98D4C1C01A6F537B00FFB083 /* MainMenu.xib in Resources */, 987AD8811B2CBE5B0035D6C8 /* show_execution.pdf in Resources */, 985301EC1B72582E001520DF /* change_cloning_rate.pdf in Resources */, 98C0943E1B7663DF00766A9A /* female_symbol.pdf in Resources */, 98D4C1F01A6FED4000FFB083 /* split_subpop.pdf in Resources */, 98453F5F1A76004300C058CB /* show_recombination.pdf in Resources */, 986926DC1AA1429D0000E138 /* GraphWindow.xib in Resources */, 980DD51A1AAE42F900D5B7B8 /* GraphAxisRescaleSheet.xib in Resources */, 9892282B1BAE279700429674 /* EidosHelpOperators.rtf in Resources */, 98D4C1ED1A6FEAB500FFB083 /* add_subpop.pdf in Resources */, 9892282E1BAE27AA00429674 /* EidosHelpTypes.rtf in Resources */, 98D4C2271A71F8AD00FFB083 /* check_H.pdf in Resources */, 987AD8801B2CBE580035D6C8 /* show_execution_H.pdf in Resources */, 982556A31BA5F0810054CB3F /* SLiMHelpClasses.rtf in Resources */, 98453F591A76004300C058CB /* show_fixed.pdf in Resources */, 98453F441A75AABE00C058CB /* syntax_help_H.pdf in Resources */, 986926D41AA1337A0000E138 /* graph_submenu_H.pdf in Resources */, 987AD87C1B2CBE4B0035D6C8 /* show_parse_H.pdf in Resources */, 987AD8771B2CBE050035D6C8 /* show_browser.pdf in Resources */, 98A4EC951B67C1D900CD92FD /* EidosConsoleWindow.xib in Resources */, 988794701EA8808000AE0C8D /* SLiMPDFWindow.xib in Resources */, 98D524661F2E6B0B005AD9A6 /* prettyprint.pdf in Resources */, 98453F451A75AABE00C058CB /* syntax_help.pdf in Resources */, 98D4C1E81A6FDE6E00FFB083 /* change_selfing_ratio.pdf in Resources */, 98DD5F022155B857009062EE /* change_folder.pdf in Resources */, 98453F571A76004300C058CB /* open_type_drawer.pdf in Resources */, 982556A01BA5DFEB0054CB3F /* EidosHelpClasses.rtf in Resources */, 987AD87F1B2CBE550035D6C8 /* show_tokens.pdf in Resources */, 98453F621A76041200C058CB /* edit_submenu_H.pdf in Resources */, 98D4C20D1A715F6100FFB083 /* AboutWindow.xib in Resources */, 98D4C1FD1A700DC100FFB083 /* play_step_H.pdf in Resources */, 98453F5E1A76004300C058CB /* show_recombination_H.pdf in Resources */, 9825566C1BA477D60054CB3F /* EidosHelpFunctions.rtf in Resources */, 98C0943F1B7663DF00766A9A /* male_symbol.pdf in Resources */, 98D4C1FE1A700DC100FFB083 /* recycle_H.pdf in Resources */, 98453F5D1A76004300C058CB /* show_mutations.pdf in Resources */, 987AD87A1B2CBE330035D6C8 /* execute_selection_H.pdf in Resources */, 98DD5F032155B857009062EE /* change_folder_H.pdf in Resources */, 98D4C1FC1A700DC100FFB083 /* play_H.pdf in Resources */, 989228341BAFB27300429674 /* SLiMHelpCallbacks.rtf in Resources */, 987AD8791B2CBE2E0035D6C8 /* execute_script.pdf in Resources */, 986926D51AA1337A0000E138 /* graph_submenu.pdf in Resources */, 98453F581A76004300C058CB /* show_fixed_H.pdf in Resources */, 987AD8781B2CBE280035D6C8 /* execute_script_H.pdf in Resources */, 98D4C2011A70192A00FFB083 /* SLiMWindow.xib in Resources */, 987AD8761B2CBE010035D6C8 /* show_browser_H.pdf in Resources */, 98D4C1F61A70040400FFB083 /* play.pdf in Resources */, 98D4C1F21A6FEEAB00FFB083 /* change_migration.pdf in Resources */, 987AD8751B2CBDA70035D6C8 /* show_console.pdf in Resources */, 987AD87E1B2CBE520035D6C8 /* show_tokens_H.pdf in Resources */, 98D4C22A1A71F8AD00FFB083 /* delete.pdf in Resources */, 98D4C2291A71F8AD00FFB083 /* delete_H.pdf in Resources */, 98F65D561DF14DA50058BD29 /* Tips in Resources */, 98D4C1EA1A6FE7FC00FFB083 /* change_size.pdf in Resources */, 98453F5B1A76004300C058CB /* show_genomicelements.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 982556A51BA8E77B0054CB3F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 98C8219A1C7A983800548839 /* coerce.c in Sources */, 9807663724493E0B00F6CBB4 /* compress.c in Sources */, 987A70142AE8032100A049E2 /* laplace.c in Sources */, 981DC36728E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */, 98CEFD712AAFABAA00D2C9B4 /* inline.c in Sources */, 982556B61BA8EF860054CB3F /* eidos_value.cpp in Sources */, 98C821801C7A983800548839 /* math.c in Sources */, 98ACDCA0253522B80038703F /* eidos_class_Object.cpp in Sources */, 982556B21BA8EF790054CB3F /* eidos_token.cpp in Sources */, 98332ADA1FDBBE3600274FF0 /* cholesky.c in Sources */, 98332AA11FDB990500274FF0 /* mvgauss.c in Sources */, 98332AC51FDBA54700274FF0 /* xerbla.c in Sources */, 981DC36B28E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */, 981DC36F28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */, 98332B141FDBD0F600274FF0 /* init.c in Sources */, 981DC35728E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */, 981DC35328E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */, 98C821881C7A983800548839 /* gauss.c in Sources */, 98C821B51C7A9B9F00548839 /* shuffle.c in Sources */, 98CEFCF92AAFABAA00D2C9B4 /* bilinear.c in Sources */, 98C821861C7A983800548839 /* exponential.c in Sources */, 98D7D6612AB24C40002AFE34 /* tdist.c in Sources */, 9876E60B1ED55B5000FF9762 /* erfc.c in Sources */, 981DC35F28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */, 98C634492EF9F632003F12A3 /* dirichlet.c in Sources */, 9876E6101ED55C0C00FF9762 /* expint.c in Sources */, 98C821981C7A983800548839 /* trig.c in Sources */, 982556BC1BA8EF990054CB3F /* eidos_test.cpp in Sources */, 98332AD11FDBA87E00274FF0 /* oper.c in Sources */, 9823568C252FE61A0096A745 /* eidos_class_Image.cpp in Sources */, 98332AFA1FDBC42700274FF0 /* swap.c in Sources */, 989A5BEC2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */, 9807663624493E0B00F6CBB4 /* deflate.c in Sources */, 98CEFD7A2AAFB12F00D2C9B4 /* cspline.c in Sources */, 98C821811C7A983800548839 /* error.c in Sources */, 98EDB5162E65410C00CC8798 /* copy.c in Sources */, 985F3F0424BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */, 98CEFD4D2AAFABAA00D2C9B4 /* interp2d.c in Sources */, 98CEFD552AAFABAA00D2C9B4 /* linear.c in Sources */, 982556B01BA8EF720054CB3F /* eidos_rng.cpp in Sources */, 98EDB4FD2E6538F200CC8798 /* permutation.c in Sources */, 98C8218E1C7A983800548839 /* inline.c in Sources */, 98C821B01C7A9B1600548839 /* geometric.c in Sources */, 98CEFD152AAFABAA00D2C9B4 /* interp.c in Sources */, 982556B31BA8EF7C0054CB3F /* eidos_ast_node.cpp in Sources */, 9893C7DE1CDA2D650029AC94 /* eidos_beep.cpp in Sources */, 9807663A24493E0B00F6CBB4 /* adler32.c in Sources */, 98D7D66A2AB24CBC002AFE34 /* chisq.c in Sources */, 9876E5FF1ED559A700FF9762 /* gauss.c in Sources */, 98C821991C7A983800548839 /* zeta.c in Sources */, 98C8218B1C7A983800548839 /* multinomial.c in Sources */, 98C821851C7A983800548839 /* binomial_tpe.c in Sources */, 988880EF20744F0200E10172 /* cauchy.c in Sources */, 98C8219C1C7A983800548839 /* infnan.c in Sources */, 982556BA1BA8EF930054CB3F /* eidos_functions.cpp in Sources */, 981DC37328E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */, 982556B91BA8EF8F0054CB3F /* eidos_interpreter.cpp in Sources */, 98EDB4F32E65389600CC8798 /* init.c in Sources */, 98C8218F1C7A983800548839 /* mt.c in Sources */, 98C821891C7A983800548839 /* gausszig.c in Sources */, 98C821821C7A983800548839 /* message.c in Sources */, 982556B81BA8EF8C0054CB3F /* eidos_property_signature.cpp in Sources */, 98C8217F1C7A983800548839 /* inline.c in Sources */, 985F3F0E24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */, 98729ADD2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 98C821AC1C7A9B1600548839 /* discrete.c in Sources */, 98EDB5072E65399600CC8798 /* permute.c in Sources */, 98EDB4E82E65366E00CC8798 /* daxpy.c in Sources */, 9807663924493E0B00F6CBB4 /* gzwrite.c in Sources */, 98CEFD252AAFABAA00D2C9B4 /* akima.c in Sources */, 98CEFD352AAFABAA00D2C9B4 /* spline.c in Sources */, 98C8218C1C7A983800548839 /* poisson.c in Sources */, 9893C7E61CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */, 98C821931C7A983800548839 /* exp.c in Sources */, 985F3EF524BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */, 982556B41BA8EF7F0054CB3F /* eidos_script.cpp in Sources */, 982556B51BA8EF830054CB3F /* eidos_symbol_table.cpp in Sources */, 981DC36328E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */, 98332AF31FDBC36400274FF0 /* submatrix.c in Sources */, 98C821A11C7A99B200548839 /* minmax.c in Sources */, 98CEFCF12AAFABAA00D2C9B4 /* spline2d.c in Sources */, 98C821A61C7A99F000548839 /* pow_int.c in Sources */, 987A2A0A24033856009A636F /* gaussinv.c in Sources */, 98CEFD832AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98C821951C7A983800548839 /* log.c in Sources */, 9876E6051ED55A7900FF9762 /* gamma_inc.c in Sources */, 985F3EFF24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */, 9890D1F027136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */, 98C821911C7A983800548839 /* taus.c in Sources */, 981DC35B28E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */, 982556B71BA8EF8A0054CB3F /* eidos_call_signature.cpp in Sources */, 982B50C92704048E006E91BC /* nbinomial.c in Sources */, 98CEFD8D2AAFB4F000D2C9B4 /* view.c in Sources */, 985F3EF024BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */, 98C8218A1C7A983800548839 /* lognormal.c in Sources */, 98332ACA1FDBA74A00274FF0 /* vector.c in Sources */, 98C8219B1C7A983800548839 /* fdiv.c in Sources */, 98332AFF1FDBC4BC00274FF0 /* matrix.c in Sources */, 98332B0D1FDBD03200274FF0 /* init.c in Sources */, 98C8218D1C7A983800548839 /* weibull.c in Sources */, 98332AB71FDBA1E600274FF0 /* blas.c in Sources */, 98235685252FDCF50096A745 /* lodepng.cpp in Sources */, 984824F7210B9F32002402A5 /* ddot.c in Sources */, 985F3EFA24BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */, 985F3F0924BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */, 9807663824493E0B00F6CBB4 /* gzlib.c in Sources */, 98CEFD692AAFABAA00D2C9B4 /* accel.c in Sources */, 98332ABC1FDBA32500274FF0 /* dtrmv.c in Sources */, 9807663324493E0B00F6CBB4 /* crc32.c in Sources */, 98EDB4D42E652F4A00CC8798 /* lu.c in Sources */, 980566E525A7C5B9008D3C7F /* fdist.c in Sources */, 9807663524493E0B00F6CBB4 /* trees.c in Sources */, 98C821941C7A983800548839 /* gamma.c in Sources */, 98C821841C7A983800548839 /* beta.c in Sources */, 98332B1B1FDBD16600274FF0 /* copy.c in Sources */, 98332AEF1FDBC29500274FF0 /* rowcol.c in Sources */, 9807663424493E0B00F6CBB4 /* zutil.c in Sources */, 98C821971C7A983800548839 /* psi.c in Sources */, 982556AC1BA8E77C0054CB3F /* main.cpp in Sources */, 982556BB1BA8EF960054CB3F /* eidos_class_TestElement.cpp in Sources */, 9876E6151ED55C7400FF9762 /* beta.c in Sources */, 98C821961C7A983800548839 /* pow_int.c in Sources */, 98C821921C7A983800548839 /* elementary.c in Sources */, 9893C7EC1CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */, 98332B061FDBCFF900274FF0 /* init.c in Sources */, 98C821831C7A983800548839 /* stream.c in Sources */, 98C821901C7A983800548839 /* rng.c in Sources */, 984824F4210B9F2D002402A5 /* dtrsv.c in Sources */, 98CEFD1D2AAFABAA00D2C9B4 /* bicubic.c in Sources */, 9876E5F81ED5573800FF9762 /* tdist.c in Sources */, 98332ADF1FDBC0D800274FF0 /* dgemv.c in Sources */, 98C821871C7A983800548839 /* gamma.c in Sources */, 982556B11BA8EF760054CB3F /* eidos_globals.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 9826634C1A3BABD300A0CBBF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 98CEFD502AAFABAA00D2C9B4 /* linear.c in Sources */, 98EFE62F1ADB611100CBEC78 /* eidos_symbol_table.cpp in Sources */, 9893C7E91CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */, 986D73E81B07E89E007FBB70 /* eidos_call_signature.cpp in Sources */, 98C6344A2EF9F632003F12A3 /* dirichlet.c in Sources */, 98CEFD102AAFABAA00D2C9B4 /* interp.c in Sources */, 98332ADC1FDBC0D000274FF0 /* dgemv.c in Sources */, 98332B111FDBD09800274FF0 /* init.c in Sources */, 98C8212C1C7A980000548839 /* gamma.c in Sources */, 98729AD82A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 98C8212D1C7A980000548839 /* gauss.c in Sources */, 985F3F0124BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */, 985F3F0B24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */, 98C8213B1C7A980000548839 /* log.c in Sources */, 981DC35428E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */, 98CEFD302AAFABAA00D2C9B4 /* spline.c in Sources */, 98EDB4E32E65366E00CC8798 /* daxpy.c in Sources */, 98800DC41B7EDCB50046F5F9 /* slim_test.cpp in Sources */, 9876E60D1ED55C0400FF9762 /* expint.c in Sources */, 98C821381C7A980000548839 /* elementary.c in Sources */, 98332A9E1FDB98ED00274FF0 /* mvgauss.c in Sources */, 98332B181FDBD13D00274FF0 /* copy.c in Sources */, 98235689252FE61A0096A745 /* eidos_class_Image.cpp in Sources */, 98C821B21C7A9B9F00548839 /* shuffle.c in Sources */, 9854D2652278B9F8001D43BC /* stats.c in Sources */, 981DC36428E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */, 981DC37028E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */, 98E9A6901A3CD4CF000AD4FC /* mutation_type.cpp in Sources */, 98DEB47E2AA632AA00ABE60F /* spatial_map.cpp in Sources */, 98E9A68A1A3CCFD0000AD4FC /* mutation.cpp in Sources */, 98C821401C7A980000548839 /* coerce.c in Sources */, 98332AB91FDBA32200274FF0 /* dtrmv.c in Sources */, 9876E6011ED55A2500FF9762 /* gamma_inc.c in Sources */, 9807C0F824BA21E3008CC658 /* slim_test_other.cpp in Sources */, 98332AC71FDBA6B600274FF0 /* vector.c in Sources */, 981BAC6A1ACC6E8B0005BE94 /* eidos_script.cpp in Sources */, 987A2A0724033856009A636F /* gaussinv.c in Sources */, 9876E6081ED55B4700FF9762 /* erfc.c in Sources */, 98C8212E1C7A980000548839 /* gausszig.c in Sources */, 9836868127CD72E900683639 /* community_eidos.cpp in Sources */, 98EDB4CF2E652F4A00CC8798 /* lu.c in Sources */, 984824EE210B9E8F002402A5 /* ddot.c in Sources */, 9878A93F1A4E57E70007B9D6 /* species.cpp in Sources */, 9893C7DD1CDA2D650029AC94 /* eidos_beep.cpp in Sources */, 980566E225A7C5B9008D3C7F /* fdist.c in Sources */, 98332AFC1FDBC4B200274FF0 /* matrix.c in Sources */, 98332AEA1FDBC1D900274FF0 /* submatrix.c in Sources */, 98C8213D1C7A980000548839 /* psi.c in Sources */, 98C8212A1C7A980000548839 /* binomial_tpe.c in Sources */, 985F3EF724BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */, 98E9A6A81A3CD5A0000AD4FC /* haplosome.cpp in Sources */, 98CEFD202AAFABAA00D2C9B4 /* akima.c in Sources */, 981DC35028E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */, 98CEFD882AAFB4F000D2C9B4 /* view.c in Sources */, 9807661C244934A800F6CBB4 /* zutil.c in Sources */, 98CEFD482AAFABAA00D2C9B4 /* interp2d.c in Sources */, 9854D2672278B9F8001D43BC /* trees.c in Sources */, 98332B031FDBCFC300274FF0 /* init.c in Sources */, 98C821281C7A980000548839 /* stream.c in Sources */, 9807C0F524BA21B7008CC658 /* slim_test_core.cpp in Sources */, 98A513541B66B6CA005A753D /* eidos_ast_node.cpp in Sources */, 98E9A6991A3CD52A000AD4FC /* chromosome.cpp in Sources */, 985F3EF224BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */, 98C821261C7A980000548839 /* error.c in Sources */, 9850D8E32063098E006BFD2E /* tables.c in Sources */, 98AB597B1B2531F10077CB4A /* slim_eidos_block.cpp in Sources */, 9854D2632278B9F8001D43BC /* convert.c in Sources */, 985F3EED24BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */, 985F3EFC24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */, 98EDB5022E65399600CC8798 /* permute.c in Sources */, 98076617244934A800F6CBB4 /* deflate.c in Sources */, 98EDB4F82E6538F200CC8798 /* permutation.c in Sources */, 98C8213A1C7A980000548839 /* gamma.c in Sources */, 982B50C62704048E006E91BC /* nbinomial.c in Sources */, 98C821311C7A980000548839 /* poisson.c in Sources */, 985D1D8B2808B84F00461CFA /* sparse_vector.cpp in Sources */, 98E9A6AE1A3CD5D3000AD4FC /* population.cpp in Sources */, 9876E6121ED55C6B00FF9762 /* beta.c in Sources */, 98332AE81FDBC1D900274FF0 /* rowcol.c in Sources */, 98C821361C7A980000548839 /* taus.c in Sources */, 98E9A6B81A3CE35E000AD4FC /* eidos_rng.cpp in Sources */, 98C821AD1C7A9B1600548839 /* geometric.c in Sources */, 98CEFD182AAFABAA00D2C9B4 /* bicubic.c in Sources */, 98A5134F1B66B69E005A753D /* eidos_token.cpp in Sources */, 9854D25F2278B9F8001D43BC /* core.c in Sources */, 98C8219E1C7A99B200548839 /* minmax.c in Sources */, 98076622244934A800F6CBB4 /* trees.c in Sources */, 981DC36C28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */, 98332AC21FDBA53F00274FF0 /* xerbla.c in Sources */, 98D7D6652AB24CBC002AFE34 /* chisq.c in Sources */, 985724D51AD481070047C223 /* eidos_test.cpp in Sources */, 98CEFD752AAFB12F00D2C9B4 /* cspline.c in Sources */, 98EDB5112E65410C00CC8798 /* copy.c in Sources */, 986070EA2AACECD600FD6143 /* spatial_kernel.cpp in Sources */, 98332AB41FDBA1E100274FF0 /* blas.c in Sources */, 9809DFA02550F32500C4E82D /* log_file.cpp in Sources */, 989A5BE92525304100E7192D /* eidos_class_Dictionary.cpp in Sources */, 98C92AED1D0B07A6001C82BC /* individual.cpp in Sources */, 98DC9841289986B300160DD8 /* GitSHA1_Xcode.cpp in Sources */, 98ACDC9D253522B80038703F /* eidos_class_Object.cpp in Sources */, 98235682252FDCF50096A745 /* lodepng.cpp in Sources */, 98606AEE1DED0DCD00821CFF /* mutation_run.cpp in Sources */, 98C821241C7A980000548839 /* inline.c in Sources */, 98CEFD6C2AAFABAA00D2C9B4 /* inline.c in Sources */, 98DE4C131B6F9657004FDF5F /* eidos_property_signature.cpp in Sources */, 98076614244934A800F6CBB4 /* compress.c in Sources */, 98E9A6961A3CD51A000AD4FC /* genomic_element_type.cpp in Sources */, 98CEFD642AAFABAA00D2C9B4 /* accel.c in Sources */, 98C821A91C7A9B1600548839 /* discrete.c in Sources */, 98C8212B1C7A980000548839 /* exponential.c in Sources */, 985724A51AD435310047C223 /* eidos_value.cpp in Sources */, 9893C7E31CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */, 985724A11AD34B740047C223 /* eidos_functions.cpp in Sources */, 981DC36028E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */, 98C8213C1C7A980000548839 /* pow_int.c in Sources */, 98C821321C7A980000548839 /* weibull.c in Sources */, 98C821411C7A980000548839 /* fdiv.c in Sources */, 98CEFCEC2AAFABAA00D2C9B4 /* spline2d.c in Sources */, 98332AD71FDBBD1600274FF0 /* cholesky.c in Sources */, 987D19A5209A53850030D28D /* kastore.c in Sources */, 9876E5F51ED5572C00FF9762 /* tdist.c in Sources */, 98E9A69C1A3CD542000AD4FC /* polymorphism.cpp in Sources */, 989790DA1AF3D0E100C6B14C /* eidos_class_TestElement.cpp in Sources */, 987A700F2AE8032100A049E2 /* laplace.c in Sources */, 982663541A3BABD300A0CBBF /* main.cpp in Sources */, 981DC36828E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */, 98D7D65C2AB24C40002AFE34 /* tdist.c in Sources */, 98DDAED6221765480038C133 /* slim_functions.cpp in Sources */, 9876E5FC1ED5599F00FF9762 /* gauss.c in Sources */, 98EDB4EE2E65389600CC8798 /* init.c in Sources */, 988880EC20744EE900E10172 /* cauchy.c in Sources */, 98E9A69F1A3CD551000AD4FC /* substitution.cpp in Sources */, 9854D2612278B9F8001D43BC /* genotypes.c in Sources */, 981DC35828E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */, 98C821391C7A980000548839 /* exp.c in Sources */, 98076620244934A800F6CBB4 /* gzwrite.c in Sources */, 98CEFD7E2AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98076626244934A800F6CBB4 /* adler32.c in Sources */, 98C8213E1C7A980000548839 /* trig.c in Sources */, 98321F941B406B67007337A3 /* eidos_globals.cpp in Sources */, 985F3EEA24BA27EC00E712E0 /* slim_test_genetics.cpp in Sources */, 98C821301C7A980000548839 /* multinomial.c in Sources */, 98332B0A1FDBD00800274FF0 /* init.c in Sources */, 98C821251C7A980000548839 /* math.c in Sources */, 98E9A6931A3CD4EF000AD4FC /* genomic_element.cpp in Sources */, 9898172F1A59750300F7417C /* slim_globals.cpp in Sources */, 98DB3D6F1E6122AE00E2C200 /* interaction_type.cpp in Sources */, 98E9A6AB1A3CD5BB000AD4FC /* subpopulation.cpp in Sources */, 98332AF71FDBC3F100274FF0 /* swap.c in Sources */, 9807661E244934A800F6CBB4 /* gzlib.c in Sources */, 98AC617A24BA34ED0001914C /* species_eidos.cpp in Sources */, 98C821341C7A980000548839 /* mt.c in Sources */, 9857249C1AD08A810047C223 /* eidos_interpreter.cpp in Sources */, 98C821331C7A980000548839 /* inline.c in Sources */, 98C821271C7A980000548839 /* message.c in Sources */, 984824F1210B9F23002402A5 /* dtrsv.c in Sources */, 98C821421C7A980000548839 /* infnan.c in Sources */, 9807662924493A8F00F6CBB4 /* crc32.c in Sources */, 98C8213F1C7A980000548839 /* zeta.c in Sources */, 9890D1ED27136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */, 98C821351C7A980000548839 /* rng.c in Sources */, 98332ACE1FDBA81A00274FF0 /* oper.c in Sources */, 98C821291C7A980000548839 /* beta.c in Sources */, D0A758F720A4CC9800132D2F /* text_input.c in Sources */, 98C821A31C7A99F000548839 /* pow_int.c in Sources */, 985F3F0624BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */, 98C8212F1C7A980000548839 /* lognormal.c in Sources */, 981DC35C28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */, 98CEFCF42AAFABAA00D2C9B4 /* bilinear.c in Sources */, 9836867427CD40CF00683639 /* community.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 984D5FA41E3AF0D200473719 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 986152112B167AED0083E68F /* polymorphism.cpp in Sources */, 986152632B167B4E0083E68F /* beta.c in Sources */, 9861526C2B167B4E0083E68F /* multinomial.c in Sources */, 986152712B167B4E0083E68F /* dtrsv.c in Sources */, 98C634462EF9F632003F12A3 /* dirichlet.c in Sources */, 9861525E2B167B4E0083E68F /* laplace.c in Sources */, 986151FD2B167A6A0083E68F /* eidos_rng.cpp in Sources */, 986152732B167B4E0083E68F /* binomial_tpe.c in Sources */, 986152672B167B4E0083E68F /* spline2d.c in Sources */, 986152612B167B4E0083E68F /* dgemv.c in Sources */, 986152482B167B4E0083E68F /* fdist.c in Sources */, 9861525A2B167B4E0083E68F /* interp.c in Sources */, 9861521F2B167AED0083E68F /* haplosome.cpp in Sources */, 986152572B167B4E0083E68F /* mt.c in Sources */, 986152182B167AED0083E68F /* slim_eidos_block.cpp in Sources */, 9861520D2B167AED0083E68F /* slim_globals.cpp in Sources */, 98EDB4E92E65366E00CC8798 /* daxpy.c in Sources */, 986152462B167B4E0083E68F /* infnan.c in Sources */, 986151F12B167A430083E68F /* eidos_functions.cpp in Sources */, 9861521D2B167AED0083E68F /* genomic_element_type.cpp in Sources */, 986152212B167AED0083E68F /* slim_test_core.cpp in Sources */, 986152762B167C620083E68F /* kastore.c in Sources */, 986152782B167D0B0083E68F /* eidos_sorting.cpp in Sources */, 9861525C2B167B4E0083E68F /* gaussinv.c in Sources */, 986151E22B167A130083E68F /* eidos_test_operators_comparison.cpp in Sources */, 9861520B2B167A940083E68F /* text_input.c in Sources */, 986152502B167B4E0083E68F /* gamma_inc.c in Sources */, 9861526E2B167B4E0083E68F /* dtrmv.c in Sources */, 9861521A2B167AED0083E68F /* individual.cpp in Sources */, 986152042B167A7A0083E68F /* crc32.c in Sources */, 986152192B167AED0083E68F /* slim_functions.cpp in Sources */, 986151F02B167A400083E68F /* eidos_functions_math.cpp in Sources */, 9861520A2B167A940083E68F /* stats.c in Sources */, 986151E52B167A1C0083E68F /* eidos_test_functions_statistics.cpp in Sources */, 986151E82B167A280083E68F /* eidos_functions_other.cpp in Sources */, 9861526B2B167B4E0083E68F /* submatrix.c in Sources */, 986152052B167A7A0083E68F /* compress.c in Sources */, 986152702B167B4E0083E68F /* nbinomial.c in Sources */, 986152252B167AED0083E68F /* sparse_vector.cpp in Sources */, 9861524F2B167B4E0083E68F /* message.c in Sources */, 98EDB4D52E652F4A00CC8798 /* lu.c in Sources */, 9861522B2B167B4E0083E68F /* interp2d.c in Sources */, 986152692B167B4E0083E68F /* minmax.c in Sources */, 986152552B167B4E0083E68F /* inline.c in Sources */, 986152582B167B4E0083E68F /* exp.c in Sources */, 9861523E2B167B4E0083E68F /* oper.c in Sources */, 986151FC2B167A670083E68F /* eidos_globals.cpp in Sources */, 9861522A2B167B4E0083E68F /* math.c in Sources */, 986152122B167AED0083E68F /* spatial_map.cpp in Sources */, 9861524B2B167B4E0083E68F /* error.c in Sources */, 986152222B167AED0083E68F /* subpopulation.cpp in Sources */, 9861522F2B167B4E0083E68F /* linear.c in Sources */, 986151EE2B167A3A0083E68F /* eidos_functions_distributions.cpp in Sources */, 986152382B167B4E0083E68F /* tridiag.c in Sources */, 986152372B167B4E0083E68F /* weibull.c in Sources */, 986152752B167B4E0083E68F /* view.c in Sources */, 986152302B167B4E0083E68F /* init.c in Sources */, 9861525F2B167B4E0083E68F /* beta.c in Sources */, 986151F42B167A4F0083E68F /* eidos_interpreter.cpp in Sources */, 98DB3D711E6122AE00E2C200 /* interaction_type.cpp in Sources */, 9861523C2B167B4E0083E68F /* pow_int.c in Sources */, 986151EB2B167A320083E68F /* eidos_functions_matrices.cpp in Sources */, 986152012B167A7A0083E68F /* trees.c in Sources */, 986152442B167B4E0083E68F /* stream.c in Sources */, 986151F92B167A5E0083E68F /* eidos_script.cpp in Sources */, 986152522B167B4E0083E68F /* akima.c in Sources */, 986151E72B167A210083E68F /* eidos_test_functions_other.cpp in Sources */, 9861521E2B167AED0083E68F /* substitution.cpp in Sources */, 9861522D2B167B4E0083E68F /* fdiv.c in Sources */, 98EDB5082E65399600CC8798 /* permute.c in Sources */, 986152652B167B4E0083E68F /* shuffle.c in Sources */, 98EDB4FE2E6538F200CC8798 /* permutation.c in Sources */, 9861526F2B167B4E0083E68F /* rowcol.c in Sources */, 98ACDCA1253522B80038703F /* eidos_class_Object.cpp in Sources */, 986151F22B167A490083E68F /* eidos_type_interpreter.cpp in Sources */, 986151E62B167A1E0083E68F /* eidos_test_functions_vector.cpp in Sources */, 986151E02B167A080083E68F /* eidos_class_TestElement.cpp in Sources */, 9861524C2B167B4E0083E68F /* expint.c in Sources */, 9861526A2B167B4E0083E68F /* matrix.c in Sources */, 986152152B167AED0083E68F /* slim_test_other.cpp in Sources */, 986152022B167A7A0083E68F /* gzlib.c in Sources */, 986151F32B167A4D0083E68F /* eidos_type_table.cpp in Sources */, 986151E42B167A190083E68F /* eidos_test_functions_math.cpp in Sources */, 984D5FB51E3AF1F000473719 /* SLiMTests.mm in Sources */, 986152772B167CBE0083E68F /* eidos_beep.cpp in Sources */, 986151EF2B167A3D0083E68F /* eidos_functions_stats.cpp in Sources */, 986152142B167AED0083E68F /* mutation_run.cpp in Sources */, 986152172B167AED0083E68F /* population.cpp in Sources */, 9861524E2B167B4E0083E68F /* bilinear.c in Sources */, 986151F82B167A5B0083E68F /* eidos_symbol_table.cpp in Sources */, 986152332B167B4E0083E68F /* tdist.c in Sources */, 9861525D2B167B4E0083E68F /* gauss.c in Sources */, 98EDB5172E65410C00CC8798 /* copy.c in Sources */, 98235686252FDCF50096A745 /* lodepng.cpp in Sources */, 986152292B167B4E0083E68F /* vector.c in Sources */, 986152342B167B4E0083E68F /* bicubic.c in Sources */, 986152202B167AED0083E68F /* slim_test_genetics.cpp in Sources */, 986152362B167B4E0083E68F /* zeta.c in Sources */, 986151EA2B167A2E0083E68F /* eidos_functions_files.cpp in Sources */, 989A5BED2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */, 986152642B167B4E0083E68F /* gamma.c in Sources */, 9861521B2B167AED0083E68F /* species.cpp in Sources */, 986152162B167AED0083E68F /* spatial_kernel.cpp in Sources */, 986152562B167B4E0083E68F /* coerce.c in Sources */, 986152592B167B4E0083E68F /* init.c in Sources */, 986152662B167B4E0083E68F /* init.c in Sources */, 9861524A2B167B4E0083E68F /* ddot.c in Sources */, 986152602B167B4E0083E68F /* lognormal.c in Sources */, 9861523D2B167B4E0083E68F /* exponential.c in Sources */, 986151E12B167A0F0083E68F /* eidos_test_operators_arithmetic.cpp in Sources */, 986152232B167AED0083E68F /* chromosome.cpp in Sources */, 986152032B167A7A0083E68F /* zutil.c in Sources */, 9861520C2B167A940083E68F /* trees.c in Sources */, 986151F52B167A520083E68F /* eidos_property_signature.cpp in Sources */, 986151DE2B1679C20083E68F /* slim_test.cpp in Sources */, 986152102B167AED0083E68F /* genomic_element.cpp in Sources */, 986152512B167B4E0083E68F /* geometric.c in Sources */, 986151FB2B167A640083E68F /* eidos_token.cpp in Sources */, 986152682B167B4E0083E68F /* cholesky.c in Sources */, 986152002B167A7A0083E68F /* gzwrite.c in Sources */, 986152272B167B4E0083E68F /* taus.c in Sources */, 986152092B167A940083E68F /* convert.c in Sources */, 986152262B167AED0083E68F /* mutation_type.cpp in Sources */, 986152312B167B4E0083E68F /* gamma.c in Sources */, 9861522C2B167B4E0083E68F /* discrete.c in Sources */, 986152412B167B4E0083E68F /* poisson.c in Sources */, 9861522E2B167B4E0083E68F /* pow_int.c in Sources */, 986152082B167A940083E68F /* core.c in Sources */, 986151F62B167A550083E68F /* eidos_call_signature.cpp in Sources */, 98EDB4F42E65389600CC8798 /* init.c in Sources */, 986152132B167AED0083E68F /* species_eidos.cpp in Sources */, 986152242B167AED0083E68F /* log_file.cpp in Sources */, 9861520F2B167AED0083E68F /* mutation.cpp in Sources */, 986152542B167B4E0083E68F /* erfc.c in Sources */, 9861525B2B167B4E0083E68F /* swap.c in Sources */, 986152352B167B4E0083E68F /* trig.c in Sources */, 986152722B167B4E0083E68F /* copy.c in Sources */, 986152492B167B4E0083E68F /* cspline.c in Sources */, 986152072B167A940083E68F /* tables.c in Sources */, 986152432B167B4E0083E68F /* tdist.c in Sources */, 986152452B167B4E0083E68F /* gausszig.c in Sources */, 986151E32B167A160083E68F /* eidos_test_operators_other.cpp in Sources */, 986151DF2B1679EC0083E68F /* eidos_test.cpp in Sources */, 986151E92B167A2C0083E68F /* eidos_functions_colors.cpp in Sources */, 986151EC2B167A350083E68F /* eidos_functions_strings.cpp in Sources */, 984D5FB31E3AF18C00473719 /* EidosTests.mm in Sources */, 986152472B167B4E0083E68F /* elementary.c in Sources */, 986151F72B167A580083E68F /* eidos_value.cpp in Sources */, 9861521C2B167AED0083E68F /* community.cpp in Sources */, 986152322B167B4E0083E68F /* inline.c in Sources */, 986151FA2B167A610083E68F /* eidos_ast_node.cpp in Sources */, 986152402B167B4E0083E68F /* accel.c in Sources */, 986152742B167B4E0083E68F /* gauss.c in Sources */, 986151FF2B167A7A0083E68F /* deflate.c in Sources */, 9861526D2B167B4E0083E68F /* mvgauss.c in Sources */, 986152622B167B4E0083E68F /* blas.c in Sources */, 986152392B167B4E0083E68F /* log.c in Sources */, 9823568D252FE61A0096A745 /* eidos_class_Image.cpp in Sources */, 986151ED2B167A380083E68F /* eidos_functions_values.cpp in Sources */, 9861523F2B167B4E0083E68F /* inline.c in Sources */, 986152282B167B4E0083E68F /* rng.c in Sources */, 9890D2002713741C001EAE98 /* eidos_class_DataFrame.cpp in Sources */, 9861520E2B167AED0083E68F /* community_eidos.cpp in Sources */, 9861524D2B167B4E0083E68F /* psi.c in Sources */, 9861523B2B167B4E0083E68F /* spline.c in Sources */, 986152532B167B4E0083E68F /* xerbla.c in Sources */, 986152062B167A940083E68F /* genotypes.c in Sources */, 986151FE2B167A710083E68F /* adler32.c in Sources */, 986152422B167B4E0083E68F /* cauchy.c in Sources */, 9861523A2B167B4E0083E68F /* chisq.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 985724AB1AD478630047C223 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 98CEFD812AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98332AF91FDBC42700274FF0 /* swap.c in Sources */, 98C821A01C7A99B200548839 /* minmax.c in Sources */, 981DC35E28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */, 985724D21AD479010047C223 /* eidos_functions.cpp in Sources */, 98C821621C7A983800548839 /* math.c in Sources */, 9876E60F1ED55C0C00FF9762 /* expint.c in Sources */, 98C8217C1C7A983800548839 /* coerce.c in Sources */, 98C821651C7A983800548839 /* stream.c in Sources */, 98321F961B406B67007337A3 /* eidos_globals.cpp in Sources */, 985724CF1AD479010047C223 /* eidos_script.cpp in Sources */, 9807663024493E0B00F6CBB4 /* gzlib.c in Sources */, 98EDB5052E65399600CC8798 /* permute.c in Sources */, 98C821741C7A983800548839 /* elementary.c in Sources */, 98EDB4FB2E6538F200CC8798 /* permutation.c in Sources */, 98332ABB1FDBA32500274FF0 /* dtrmv.c in Sources */, 98C8216E1C7A983800548839 /* poisson.c in Sources */, 9876E6041ED55A7800FF9762 /* gamma_inc.c in Sources */, 9807663224493E0B00F6CBB4 /* adler32.c in Sources */, 98CEFD1B2AAFABAA00D2C9B4 /* bicubic.c in Sources */, 981DC37228E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */, 98C8216D1C7A983800548839 /* multinomial.c in Sources */, 98332AB61FDBA1E500274FF0 /* blas.c in Sources */, 98C8216F1C7A983800548839 /* weibull.c in Sources */, 986D73EA1B07E89E007FBB70 /* eidos_call_signature.cpp in Sources */, 981DC36E28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */, 98A513511B66B69E005A753D /* eidos_token.cpp in Sources */, 9876E5FE1ED559A600FF9762 /* gauss.c in Sources */, 98332ADE1FDBC0D700274FF0 /* dgemv.c in Sources */, 98C821611C7A983800548839 /* inline.c in Sources */, 9876E6141ED55C7400FF9762 /* beta.c in Sources */, 981DC36A28E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */, 981DC35A28E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */, 981DC35228E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */, 985F3F0824BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */, 9825565C1BA32EE80054CB3F /* EidosCocoaExtra.mm in Sources */, 98321F911B406417007337A3 /* eidos_rng.cpp in Sources */, 98C821671C7A983800548839 /* binomial_tpe.c in Sources */, 98C821AB1C7A9B1600548839 /* discrete.c in Sources */, 98EDB4E62E65366E00CC8798 /* daxpy.c in Sources */, 9893C7E51CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */, 98ACDC9F253522B80038703F /* eidos_class_Object.cpp in Sources */, 98CEFD782AAFB12F00D2C9B4 /* cspline.c in Sources */, 9807662F24493E0B00F6CBB4 /* compress.c in Sources */, 98C8216B1C7A983800548839 /* gausszig.c in Sources */, 98C821AF1C7A9B1600548839 /* geometric.c in Sources */, 98C821631C7A983800548839 /* error.c in Sources */, 98EFE6311ADB611100CBEC78 /* eidos_symbol_table.cpp in Sources */, 985F3EF924BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */, 985F3EF424BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */, 98332AC41FDBA54600274FF0 /* xerbla.c in Sources */, 98C821731C7A983800548839 /* taus.c in Sources */, 9807662D24493E0B00F6CBB4 /* trees.c in Sources */, 98332B0C1FDBD03200274FF0 /* init.c in Sources */, 98A513561B66B6CA005A753D /* eidos_ast_node.cpp in Sources */, 98C821701C7A983800548839 /* inline.c in Sources */, 98C821B41C7A9B9F00548839 /* shuffle.c in Sources */, 98332B1A1FDBD16500274FF0 /* copy.c in Sources */, 98CEFD532AAFABAA00D2C9B4 /* linear.c in Sources */, 98332B131FDBD0F500274FF0 /* init.c in Sources */, 98332AD01FDBA87D00274FF0 /* oper.c in Sources */, 985724B71AD478630047C223 /* main.m in Sources */, 981DC36228E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */, 98CEFCF72AAFABAA00D2C9B4 /* bilinear.c in Sources */, 98090FA61B1B978900791DBF /* EidosValueWrapper.mm in Sources */, 98D7D65F2AB24C40002AFE34 /* tdist.c in Sources */, 98CEFD6F2AAFABAA00D2C9B4 /* inline.c in Sources */, 98C8217B1C7A983800548839 /* zeta.c in Sources */, 9807662E24493E0B00F6CBB4 /* deflate.c in Sources */, 98C821721C7A983800548839 /* rng.c in Sources */, 98332AD91FDBBE3600274FF0 /* cholesky.c in Sources */, 98C8216C1C7A983800548839 /* lognormal.c in Sources */, 98D218C61B2E213200156FC3 /* EidosTextView.mm in Sources */, 98C821751C7A983800548839 /* exp.c in Sources */, 98C821771C7A983800548839 /* log.c in Sources */, 98C821781C7A983800548839 /* pow_int.c in Sources */, 98332AF21FDBC36300274FF0 /* submatrix.c in Sources */, 98CEFD332AAFABAA00D2C9B4 /* spline.c in Sources */, 9807662C24493E0B00F6CBB4 /* zutil.c in Sources */, 98332AFE1FDBC4BB00274FF0 /* matrix.c in Sources */, 98235684252FDCF50096A745 /* lodepng.cpp in Sources */, 98C8217A1C7A983800548839 /* trig.c in Sources */, 98CEFD232AAFABAA00D2C9B4 /* akima.c in Sources */, 98332AC91FDBA74900274FF0 /* vector.c in Sources */, 98CEFD672AAFABAA00D2C9B4 /* accel.c in Sources */, 98EDB5142E65410C00CC8798 /* copy.c in Sources */, 988880EE20744F0100E10172 /* cauchy.c in Sources */, 98C8217D1C7A983800548839 /* fdiv.c in Sources */, 985724D01AD479010047C223 /* eidos_value.cpp in Sources */, 987A2A0924033856009A636F /* gaussinv.c in Sources */, 981DC35628E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */, 989A5BEB2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */, 982556661BA450980054CB3F /* EidosHelpController.mm in Sources */, 98332AEE1FDBC29500274FF0 /* rowcol.c in Sources */, 9890D1EF27136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */, 987AD8871B2CC0C10035D6C8 /* EidosVariableBrowserController.mm in Sources */, 98EDB4F12E65389600CC8798 /* init.c in Sources */, 987A70122AE8032100A049E2 /* laplace.c in Sources */, 98729ADB2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 9807662B24493E0B00F6CBB4 /* crc32.c in Sources */, 98CEFD132AAFABAA00D2C9B4 /* interp.c in Sources */, 98CEFCEF2AAFABAA00D2C9B4 /* spline2d.c in Sources */, 98C821661C7A983800548839 /* beta.c in Sources */, 985F3EFE24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */, 98C634452EF9F632003F12A3 /* dirichlet.c in Sources */, 98C821791C7A983800548839 /* psi.c in Sources */, 98CEFD8B2AAFB4F000D2C9B4 /* view.c in Sources */, 985724D31AD479010047C223 /* eidos_test.cpp in Sources */, 98EDB4D22E652F4A00CC8798 /* lu.c in Sources */, 98C8217E1C7A983800548839 /* infnan.c in Sources */, 98DE4C151B6F9657004FDF5F /* eidos_property_signature.cpp in Sources */, 98CEFD4B2AAFABAA00D2C9B4 /* interp2d.c in Sources */, 985724B51AD478630047C223 /* EidosAppDelegate.mm in Sources */, 9876E5F71ED5573700FF9762 /* tdist.c in Sources */, 9807663124493E0B00F6CBB4 /* gzwrite.c in Sources */, 9893C7E01CDA2FC20029AC94 /* eidos_beep.cpp in Sources */, 98C821711C7A983800548839 /* mt.c in Sources */, 980566E425A7C5B9008D3C7F /* fdist.c in Sources */, 98C821761C7A983800548839 /* gamma.c in Sources */, 984824F6210B9F32002402A5 /* ddot.c in Sources */, 985724E71AD622880047C223 /* EidosConsoleTextView.mm in Sources */, 989790DC1AF3D0E100C6B14C /* eidos_class_TestElement.cpp in Sources */, 9876E60A1ED55B4F00FF9762 /* erfc.c in Sources */, 985724D11AD479010047C223 /* eidos_interpreter.cpp in Sources */, 98D7D6682AB24CBC002AFE34 /* chisq.c in Sources */, 98C8216A1C7A983800548839 /* gauss.c in Sources */, 98C821681C7A983800548839 /* exponential.c in Sources */, 984824F3210B9F2D002402A5 /* dtrsv.c in Sources */, 985F3F0D24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */, 9893C7EB1CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */, 982B50C82704048E006E91BC /* nbinomial.c in Sources */, 9823568B252FE61A0096A745 /* eidos_class_Image.cpp in Sources */, 98C821A51C7A99F000548839 /* pow_int.c in Sources */, 987AD88B1B2CDBB80035D6C8 /* EidosConsoleWindowController.mm in Sources */, 985F3EEF24BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */, 98332B051FDBCFF900274FF0 /* init.c in Sources */, 98332AA01FDB990500274FF0 /* mvgauss.c in Sources */, 98C821641C7A983800548839 /* message.c in Sources */, 98D5246A1F2EB4DD005AD9A6 /* EidosPrettyprinter.mm in Sources */, 985F3F0324BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */, 981DC36628E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */, 98C821691C7A983800548839 /* gamma.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98CF51F4294A3FC900557BBA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 98CF51F5294A3FC900557BBA /* nbinomial.c in Sources */, 98CF51F6294A3FC900557BBA /* eidos_globals.cpp in Sources */, 98CF51F7294A3FC900557BBA /* eidos_class_TestElement.cpp in Sources */, 98CF51F8294A3FC900557BBA /* eidos_functions_other.cpp in Sources */, 98CF51F9294A3FC900557BBA /* pow_int.c in Sources */, 98CF51FA294A3FC900557BBA /* slim_eidos_block.cpp in Sources */, 98CF51FB294A3FC900557BBA /* eidos_functions_stats.cpp in Sources */, 98CF51FC294A3FC900557BBA /* geometric.c in Sources */, 98CF51FD294A3FC900557BBA /* eidos_script.cpp in Sources */, 98CF51FE294A3FC900557BBA /* beta.c in Sources */, 98CF51FF294A3FC900557BBA /* ddot.c in Sources */, 98CF5200294A3FC900557BBA /* main.m in Sources */, 98CF5201294A3FC900557BBA /* EidosValueWrapper.mm in Sources */, 98CF5202294A3FC900557BBA /* inline.c in Sources */, 98CEFD122AAFABAA00D2C9B4 /* interp.c in Sources */, 98CF5203294A3FC900557BBA /* error.c in Sources */, 98CF5204294A3FC900557BBA /* eidos_property_signature.cpp in Sources */, 98CF5205294A3FC900557BBA /* oper.c in Sources */, 98CF5206294A3FC900557BBA /* trees.c in Sources */, 98CF5207294A3FC900557BBA /* eidos_rng.cpp in Sources */, 98CEFCF62AAFABAA00D2C9B4 /* bilinear.c in Sources */, 98CF5208294A3FC900557BBA /* eidos_beep.cpp in Sources */, 98CF5209294A3FC900557BBA /* GraphView.mm in Sources */, 98CF520A294A3FC900557BBA /* stream.c in Sources */, 98CEFD662AAFABAA00D2C9B4 /* accel.c in Sources */, 98CF520B294A3FC900557BBA /* eidos_symbol_table.cpp in Sources */, 98CF520C294A3FC900557BBA /* init.c in Sources */, 98CF520D294A3FC900557BBA /* interaction_type.cpp in Sources */, 98CF520E294A3FC900557BBA /* subpopulation.cpp in Sources */, 98CF520F294A3FC900557BBA /* community.cpp in Sources */, 98CF5210294A3FC900557BBA /* eidos_test_operators_comparison.cpp in Sources */, 98CF5211294A3FC900557BBA /* EidosTextView.mm in Sources */, 98CF5212294A3FC900557BBA /* swap.c in Sources */, 98CF5213294A3FC900557BBA /* eidos_test.cpp in Sources */, 98CF5214294A3FC900557BBA /* dgemv.c in Sources */, 98CF5215294A3FC900557BBA /* binomial_tpe.c in Sources */, 98CF5216294A3FC900557BBA /* tables.c in Sources */, 98CF5217294A3FC900557BBA /* species_eidos.cpp in Sources */, 98729ADA2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 98CF5218294A3FC900557BBA /* slim_functions.cpp in Sources */, 98CF5219294A3FC900557BBA /* deflate.c in Sources */, 98CF521A294A3FC900557BBA /* FindRecipeController.mm in Sources */, 98CF521B294A3FC900557BBA /* eidos_functions_colors.cpp in Sources */, 98CF521C294A3FC900557BBA /* mutation_run.cpp in Sources */, 987A70112AE8032100A049E2 /* laplace.c in Sources */, 98CF521D294A3FC900557BBA /* eidos_test_functions_math.cpp in Sources */, 98CF521E294A3FC900557BBA /* copy.c in Sources */, 98CF521F294A3FC900557BBA /* tdist.c in Sources */, 98CF5220294A3FC900557BBA /* message.c in Sources */, 98CF5221294A3FC900557BBA /* multinomial.c in Sources */, 98CF5222294A3FC900557BBA /* rng.c in Sources */, 98CF5223294A3FC900557BBA /* taus.c in Sources */, 98CEFD6E2AAFABAA00D2C9B4 /* inline.c in Sources */, 98CF5224294A3FC900557BBA /* gaussinv.c in Sources */, 98CF5225294A3FC900557BBA /* slim_test_genetics.cpp in Sources */, 98CF5226294A3FC900557BBA /* blas.c in Sources */, 98CF5227294A3FC900557BBA /* mt.c in Sources */, 98CF5228294A3FC900557BBA /* genomic_element.cpp in Sources */, 98CF5229294A3FC900557BBA /* pow_int.c in Sources */, 98CF522A294A3FC900557BBA /* substitution.cpp in Sources */, 98CF522B294A3FC900557BBA /* core.c in Sources */, 98CF522C294A3FC900557BBA /* beta.c in Sources */, 98EDB5132E65410C00CC8798 /* copy.c in Sources */, 98CF522D294A3FC900557BBA /* exponential.c in Sources */, 98CF522E294A3FC900557BBA /* poisson.c in Sources */, 98CF522F294A3FC900557BBA /* eidos_functions_matrices.cpp in Sources */, 98CF5230294A3FC900557BBA /* gauss.c in Sources */, 98CF5231294A3FC900557BBA /* EidosHelpController.mm in Sources */, 98CF5232294A3FC900557BBA /* community_eidos.cpp in Sources */, 98EDB4FA2E6538F200CC8798 /* permutation.c in Sources */, 98CF5233294A3FC900557BBA /* lodepng.cpp in Sources */, 98CF5234294A3FC900557BBA /* slim_test_other.cpp in Sources */, 98CF5235294A3FC900557BBA /* eidos_value.cpp in Sources */, 98CF5236294A3FC900557BBA /* fdiv.c in Sources */, 98CF5237294A3FC900557BBA /* GraphView_MutationLossTimeHistogram.mm in Sources */, 98CF5238294A3FC900557BBA /* trig.c in Sources */, 98CEFD222AAFABAA00D2C9B4 /* akima.c in Sources */, 98CF5239294A3FC900557BBA /* exp.c in Sources */, 98CF523A294A3FC900557BBA /* individual.cpp in Sources */, 98CF523B294A3FC900557BBA /* eidos_functions_distributions.cpp in Sources */, 98CF523C294A3FC900557BBA /* weibull.c in Sources */, 98CF523D294A3FC900557BBA /* eidos_type_table.cpp in Sources */, 98CF523E294A3FC900557BBA /* lognormal.c in Sources */, 98CF523F294A3FC900557BBA /* gamma_inc.c in Sources */, 98CF5240294A3FC900557BBA /* trees.c in Sources */, 98CF5241294A3FC900557BBA /* CocoaExtra.mm in Sources */, 98CF5242294A3FC900557BBA /* init.c in Sources */, 98CF5243294A3FC900557BBA /* slim_test_core.cpp in Sources */, 98CF5244294A3FC900557BBA /* EidosCocoaExtra.mm in Sources */, 98CEFCEE2AAFABAA00D2C9B4 /* spline2d.c in Sources */, 98EDB4F02E65389600CC8798 /* init.c in Sources */, 98CF5245294A3FC900557BBA /* gamma.c in Sources */, 98CF5246294A3FC900557BBA /* dtrmv.c in Sources */, 98CF5247294A3FC900557BBA /* crc32.c in Sources */, 98CF5248294A3FC900557BBA /* eidos_test_functions_statistics.cpp in Sources */, 98DEB4802AA632AA00ABE60F /* spatial_map.cpp in Sources */, 98CF5249294A3FC900557BBA /* eidos_test_functions_other.cpp in Sources */, 98CF524A294A3FC900557BBA /* SLiMPDFDocument.mm in Sources */, 98CF524B294A3FC900557BBA /* eidos_functions.cpp in Sources */, 98CEFD8A2AAFB4F000D2C9B4 /* view.c in Sources */, 98CF524C294A3FC900557BBA /* log_file.cpp in Sources */, 98CEFD522AAFABAA00D2C9B4 /* linear.c in Sources */, 98CF524D294A3FC900557BBA /* zutil.c in Sources */, 98CF524E294A3FC900557BBA /* gauss.c in Sources */, 98CF524F294A3FC900557BBA /* population.cpp in Sources */, 98CF5250294A3FC900557BBA /* eidos_test_functions_vector.cpp in Sources */, 98CF5251294A3FC900557BBA /* eidos_class_Dictionary.cpp in Sources */, 98C634472EF9F632003F12A3 /* dirichlet.c in Sources */, 98CF5252294A3FC900557BBA /* elementary.c in Sources */, 986070EC2AACECD600FD6143 /* spatial_kernel.cpp in Sources */, 98CF5254294A3FC900557BBA /* GraphView_FitnessOverTime.mm in Sources */, 98CF5255294A3FC900557BBA /* SLiMWindowController.mm in Sources */, 98CF5256294A3FC900557BBA /* PopulationView.mm in Sources */, 98CF5257294A3FC900557BBA /* init.c in Sources */, 98CF5258294A3FC900557BBA /* GraphView_PopulationVisualization.mm in Sources */, 98CF5259294A3FC900557BBA /* EidosPrettyprinter.mm in Sources */, 98CF525A294A3FC900557BBA /* coerce.c in Sources */, 98CF525B294A3FC900557BBA /* genomic_element_type.cpp in Sources */, 98CF525C294A3FC900557BBA /* AppDelegate.mm in Sources */, 98CF525D294A3FC900557BBA /* shuffle.c in Sources */, 98CF525E294A3FC900557BBA /* gamma.c in Sources */, 98CF525F294A3FC900557BBA /* slim_test.cpp in Sources */, 98CF5260294A3FC900557BBA /* sparse_vector.cpp in Sources */, 98CF5261294A3FC900557BBA /* expint.c in Sources */, 98CF5262294A3FC900557BBA /* mutation_type.cpp in Sources */, 98CF5263294A3FC900557BBA /* convert.c in Sources */, 98CEFD4A2AAFABAA00D2C9B4 /* interp2d.c in Sources */, 98CF5264294A3FC900557BBA /* gzlib.c in Sources */, 98CF5265294A3FC900557BBA /* erfc.c in Sources */, 98CF5266294A3FC900557BBA /* polymorphism.cpp in Sources */, 98EDB4E52E65366E00CC8798 /* daxpy.c in Sources */, 98CF5267294A3FC900557BBA /* eidos_class_Object.cpp in Sources */, 98CEFD322AAFABAA00D2C9B4 /* spline.c in Sources */, 98CF5268294A3FC900557BBA /* species.cpp in Sources */, 98CF526A294A3FC900557BBA /* zeta.c in Sources */, 98CF526B294A3FC900557BBA /* text_input.c in Sources */, 98CF526C294A3FC900557BBA /* log.c in Sources */, 98CF526D294A3FC900557BBA /* cauchy.c in Sources */, 98CF526E294A3FC900557BBA /* eidos_call_signature.cpp in Sources */, 98CF526F294A3FC900557BBA /* fdist.c in Sources */, 98CF5270294A3FC900557BBA /* ChromosomeView.mm in Sources */, 98CF5272294A3FC900557BBA /* EidosConsoleTextView.mm in Sources */, 98CF5273294A3FC900557BBA /* eidos_class_Image.cpp in Sources */, 98CF5274294A3FC900557BBA /* vector.c in Sources */, 98CEFD772AAFB12F00D2C9B4 /* cspline.c in Sources */, 98CF5275294A3FC900557BBA /* math.c in Sources */, 98CF5276294A3FC900557BBA /* SLiMPDFWindowController.mm in Sources */, 98CF5277294A3FC900557BBA /* matrix.c in Sources */, 98CF5278294A3FC900557BBA /* GraphView_MutationFrequencyTrajectory.mm in Sources */, 98CF5279294A3FC900557BBA /* compress.c in Sources */, 98CF527A294A3FC900557BBA /* eidos_test_operators_other.cpp in Sources */, 98CF527B294A3FC900557BBA /* SLiMDocument.mm in Sources */, 98CF527C294A3FC900557BBA /* EidosConsoleWindowController.mm in Sources */, 98CF527D294A3FC900557BBA /* SLiMDocumentController.mm in Sources */, 98CF527E294A3FC900557BBA /* mvgauss.c in Sources */, 98CF527F294A3FC900557BBA /* GraphView_MutationFixationTimeHistogram.mm in Sources */, 98CF5280294A3FC900557BBA /* eidos_functions_files.cpp in Sources */, 98CF5281294A3FC900557BBA /* rowcol.c in Sources */, 98CF5282294A3FC900557BBA /* gausszig.c in Sources */, 98CF5283294A3FC900557BBA /* eidos_type_interpreter.cpp in Sources */, 98D7D6672AB24CBC002AFE34 /* chisq.c in Sources */, 98CF5284294A3FC900557BBA /* kastore.c in Sources */, 98CF5285294A3FC900557BBA /* gzwrite.c in Sources */, 98CF5286294A3FC900557BBA /* chromosome.cpp in Sources */, 98CF5287294A3FC900557BBA /* eidos_functions_values.cpp in Sources */, 98CF5288294A3FC900557BBA /* psi.c in Sources */, 98CF5289294A3FC900557BBA /* eidos_functions_strings.cpp in Sources */, 98E684412B694F09000B3B65 /* plot.mm in Sources */, 98CF528A294A3FC900557BBA /* GraphView_MutationFrequencySpectra.mm in Sources */, 98CF528B294A3FC900557BBA /* genotypes.c in Sources */, 98CF528C294A3FC900557BBA /* SLiMPDFView.mm in Sources */, 98CF528D294A3FC900557BBA /* eidos_class_DataFrame.cpp in Sources */, 98CEFD802AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98CF528E294A3FC900557BBA /* haplosome.cpp in Sources */, 98CF528F294A3FC900557BBA /* stats.c in Sources */, 98CF5290294A3FC900557BBA /* adler32.c in Sources */, 98CF5291294A3FC900557BBA /* EidosVariableBrowserController.mm in Sources */, 98EDB4D12E652F4A00CC8798 /* lu.c in Sources */, 98CF5292294A3FC900557BBA /* eidos_functions_math.cpp in Sources */, 98CF5293294A3FC900557BBA /* slim_gui.mm in Sources */, 98CF5294294A3FC900557BBA /* mutation.cpp in Sources */, 98CF5295294A3FC900557BBA /* slim_globals.cpp in Sources */, 98D7D65E2AB24C40002AFE34 /* tdist.c in Sources */, 98CF5296294A3FC900557BBA /* eidos_test_operators_arithmetic.cpp in Sources */, 98EDB5042E65399600CC8798 /* permute.c in Sources */, 98CF5297294A3FC900557BBA /* eidos_ast_node.cpp in Sources */, 98CF5298294A3FC900557BBA /* submatrix.c in Sources */, 98CF5299294A3FC900557BBA /* eidos_token.cpp in Sources */, 98CF529A294A3FC900557BBA /* dtrsv.c in Sources */, 98CF529B294A3FC900557BBA /* infnan.c in Sources */, 98CEFD1A2AAFABAA00D2C9B4 /* bicubic.c in Sources */, 98CF529C294A3FC900557BBA /* cholesky.c in Sources */, 98CF529D294A3FC900557BBA /* xerbla.c in Sources */, 98CF529E294A3FC900557BBA /* discrete.c in Sources */, 98CF529F294A3FC900557BBA /* minmax.c in Sources */, 98CF52A0294A3FC900557BBA /* inline.c in Sources */, 98CF52A1294A3FC900557BBA /* eidos_interpreter.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98CF5398294A714200557BBA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 98CEFD822AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98CF5399294A714200557BBA /* swap.c in Sources */, 98CF539A294A714200557BBA /* minmax.c in Sources */, 98CF539B294A714200557BBA /* eidos_functions_matrices.cpp in Sources */, 98CF539C294A714200557BBA /* eidos_functions.cpp in Sources */, 98CF539D294A714200557BBA /* math.c in Sources */, 98CF539E294A714200557BBA /* expint.c in Sources */, 98CF539F294A714200557BBA /* coerce.c in Sources */, 98CF53A0294A714200557BBA /* stream.c in Sources */, 98CF53A1294A714200557BBA /* eidos_globals.cpp in Sources */, 98CF53A2294A714200557BBA /* eidos_script.cpp in Sources */, 98CF53A3294A714200557BBA /* gzlib.c in Sources */, 98EDB5062E65399600CC8798 /* permute.c in Sources */, 98CF53A4294A714200557BBA /* elementary.c in Sources */, 98EDB4FC2E6538F200CC8798 /* permutation.c in Sources */, 98CF53A5294A714200557BBA /* dtrmv.c in Sources */, 98CF53A6294A714200557BBA /* poisson.c in Sources */, 98CF53A7294A714200557BBA /* gamma_inc.c in Sources */, 98CF53A8294A714200557BBA /* adler32.c in Sources */, 98CEFD1C2AAFABAA00D2C9B4 /* bicubic.c in Sources */, 98CF53A9294A714200557BBA /* eidos_functions_stats.cpp in Sources */, 98CF53AA294A714200557BBA /* multinomial.c in Sources */, 98CF53AB294A714200557BBA /* blas.c in Sources */, 98CF53AC294A714200557BBA /* weibull.c in Sources */, 98CF53AD294A714200557BBA /* eidos_call_signature.cpp in Sources */, 98CF53AE294A714200557BBA /* eidos_functions_other.cpp in Sources */, 98CF53AF294A714200557BBA /* eidos_token.cpp in Sources */, 98CF53B0294A714200557BBA /* gauss.c in Sources */, 98CF53B1294A714200557BBA /* dgemv.c in Sources */, 98CF53B2294A714200557BBA /* inline.c in Sources */, 98CF53B3294A714200557BBA /* beta.c in Sources */, 98CF53B4294A714200557BBA /* eidos_functions_strings.cpp in Sources */, 98CF53B5294A714200557BBA /* eidos_functions_colors.cpp in Sources */, 98CF53B6294A714200557BBA /* eidos_functions_files.cpp in Sources */, 98CF53B7294A714200557BBA /* eidos_test_operators_comparison.cpp in Sources */, 98CF53B8294A714200557BBA /* EidosCocoaExtra.mm in Sources */, 98CF53B9294A714200557BBA /* eidos_rng.cpp in Sources */, 98CF53BA294A714200557BBA /* binomial_tpe.c in Sources */, 98CF53BB294A714200557BBA /* discrete.c in Sources */, 98EDB4E72E65366E00CC8798 /* daxpy.c in Sources */, 98CF53BC294A714200557BBA /* eidos_type_table.cpp in Sources */, 98CF53BD294A714200557BBA /* eidos_class_Object.cpp in Sources */, 98CEFD792AAFB12F00D2C9B4 /* cspline.c in Sources */, 98CF53BE294A714200557BBA /* compress.c in Sources */, 98CF53BF294A714200557BBA /* gausszig.c in Sources */, 98CF53C0294A714200557BBA /* geometric.c in Sources */, 98CF53C1294A714200557BBA /* error.c in Sources */, 98CF53C2294A714200557BBA /* eidos_symbol_table.cpp in Sources */, 98CF53C3294A714200557BBA /* eidos_test_functions_math.cpp in Sources */, 98CF53C4294A714200557BBA /* eidos_test_functions_other.cpp in Sources */, 98CF53C5294A714200557BBA /* xerbla.c in Sources */, 98CF53C6294A714200557BBA /* taus.c in Sources */, 98CF53C7294A714200557BBA /* trees.c in Sources */, 98CF53C8294A714200557BBA /* init.c in Sources */, 98CF53C9294A714200557BBA /* eidos_ast_node.cpp in Sources */, 98CF53CA294A714200557BBA /* inline.c in Sources */, 98CF53CB294A714200557BBA /* shuffle.c in Sources */, 98CF53CC294A714200557BBA /* copy.c in Sources */, 98CEFD542AAFABAA00D2C9B4 /* linear.c in Sources */, 98CF53CD294A714200557BBA /* init.c in Sources */, 98CF53CE294A714200557BBA /* oper.c in Sources */, 98CF53CF294A714200557BBA /* main.m in Sources */, 98CF53D0294A714200557BBA /* eidos_functions_values.cpp in Sources */, 98CEFCF82AAFABAA00D2C9B4 /* bilinear.c in Sources */, 98CF53D1294A714200557BBA /* EidosValueWrapper.mm in Sources */, 98D7D6602AB24C40002AFE34 /* tdist.c in Sources */, 98CEFD702AAFABAA00D2C9B4 /* inline.c in Sources */, 98CF53D2294A714200557BBA /* zeta.c in Sources */, 98CF53D3294A714200557BBA /* deflate.c in Sources */, 98CF53D4294A714200557BBA /* rng.c in Sources */, 98CF53D5294A714200557BBA /* cholesky.c in Sources */, 98CF53D6294A714200557BBA /* lognormal.c in Sources */, 98CF53D7294A714200557BBA /* EidosTextView.mm in Sources */, 98CF53D8294A714200557BBA /* exp.c in Sources */, 98CF53D9294A714200557BBA /* log.c in Sources */, 98CF53DA294A714200557BBA /* pow_int.c in Sources */, 98CF53DB294A714200557BBA /* submatrix.c in Sources */, 98CEFD342AAFABAA00D2C9B4 /* spline.c in Sources */, 98CF53DC294A714200557BBA /* zutil.c in Sources */, 98CF53DD294A714200557BBA /* matrix.c in Sources */, 98CF53DE294A714200557BBA /* lodepng.cpp in Sources */, 98CF53DF294A714200557BBA /* trig.c in Sources */, 98CEFD242AAFABAA00D2C9B4 /* akima.c in Sources */, 98CF53E0294A714200557BBA /* vector.c in Sources */, 98CEFD682AAFABAA00D2C9B4 /* accel.c in Sources */, 98EDB5152E65410C00CC8798 /* copy.c in Sources */, 98CF53E1294A714200557BBA /* cauchy.c in Sources */, 98CF53E2294A714200557BBA /* fdiv.c in Sources */, 98CF53E3294A714200557BBA /* eidos_value.cpp in Sources */, 98CF53E4294A714200557BBA /* gaussinv.c in Sources */, 98CF53E5294A714200557BBA /* eidos_functions_math.cpp in Sources */, 98CF53E6294A714200557BBA /* eidos_class_Dictionary.cpp in Sources */, 98CF53E7294A714200557BBA /* EidosHelpController.mm in Sources */, 98CF53E8294A714200557BBA /* rowcol.c in Sources */, 98CF53E9294A714200557BBA /* eidos_class_DataFrame.cpp in Sources */, 98CF53EA294A714200557BBA /* EidosVariableBrowserController.mm in Sources */, 98EDB4F22E65389600CC8798 /* init.c in Sources */, 987A70132AE8032100A049E2 /* laplace.c in Sources */, 98729ADC2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 98CF53EB294A714200557BBA /* crc32.c in Sources */, 98CEFD142AAFABAA00D2C9B4 /* interp.c in Sources */, 98CEFCF02AAFABAA00D2C9B4 /* spline2d.c in Sources */, 98CF53EC294A714200557BBA /* beta.c in Sources */, 98CF53ED294A714200557BBA /* eidos_test_functions_vector.cpp in Sources */, 98C634482EF9F632003F12A3 /* dirichlet.c in Sources */, 98CF53EE294A714200557BBA /* psi.c in Sources */, 98CEFD8C2AAFB4F000D2C9B4 /* view.c in Sources */, 98CF53EF294A714200557BBA /* eidos_test.cpp in Sources */, 98EDB4D32E652F4A00CC8798 /* lu.c in Sources */, 98CF53F0294A714200557BBA /* infnan.c in Sources */, 98CF53F1294A714200557BBA /* eidos_property_signature.cpp in Sources */, 98CEFD4C2AAFABAA00D2C9B4 /* interp2d.c in Sources */, 98CF53F2294A714200557BBA /* EidosAppDelegate.mm in Sources */, 98CF53F3294A714200557BBA /* tdist.c in Sources */, 98CF53F4294A714200557BBA /* gzwrite.c in Sources */, 98CF53F5294A714200557BBA /* eidos_beep.cpp in Sources */, 98CF53F6294A714200557BBA /* mt.c in Sources */, 98CF53F7294A714200557BBA /* fdist.c in Sources */, 98CF53F8294A714200557BBA /* gamma.c in Sources */, 98CF53F9294A714200557BBA /* ddot.c in Sources */, 98CF53FA294A714200557BBA /* EidosConsoleTextView.mm in Sources */, 98CF53FB294A714200557BBA /* eidos_class_TestElement.cpp in Sources */, 98CF53FC294A714200557BBA /* erfc.c in Sources */, 98CF53FD294A714200557BBA /* eidos_interpreter.cpp in Sources */, 98D7D6692AB24CBC002AFE34 /* chisq.c in Sources */, 98CF53FE294A714200557BBA /* gauss.c in Sources */, 98CF53FF294A714200557BBA /* exponential.c in Sources */, 98CF5400294A714200557BBA /* dtrsv.c in Sources */, 98CF5401294A714200557BBA /* eidos_test_functions_statistics.cpp in Sources */, 98CF5402294A714200557BBA /* eidos_type_interpreter.cpp in Sources */, 98CF5403294A714200557BBA /* nbinomial.c in Sources */, 98CF5404294A714200557BBA /* eidos_class_Image.cpp in Sources */, 98CF5405294A714200557BBA /* pow_int.c in Sources */, 98CF5406294A714200557BBA /* EidosConsoleWindowController.mm in Sources */, 98CF5407294A714200557BBA /* eidos_test_operators_other.cpp in Sources */, 98CF5408294A714200557BBA /* init.c in Sources */, 98CF5409294A714200557BBA /* mvgauss.c in Sources */, 98CF540A294A714200557BBA /* message.c in Sources */, 98CF540B294A714200557BBA /* EidosPrettyprinter.mm in Sources */, 98CF540C294A714200557BBA /* eidos_test_operators_arithmetic.cpp in Sources */, 98CF540D294A714200557BBA /* eidos_functions_distributions.cpp in Sources */, 98CF540E294A714200557BBA /* gamma.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98D4C1AF1A6F537B00FFB083 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 982B50C72704048E006E91BC /* nbinomial.c in Sources */, 98321F951B406B67007337A3 /* eidos_globals.cpp in Sources */, 989790DB1AF3D0E100C6B14C /* eidos_class_TestElement.cpp in Sources */, 981DC36D28E26F8B000ABE91 /* eidos_functions_other.cpp in Sources */, 98C8215A1C7A983700548839 /* pow_int.c in Sources */, 98AB597C1B2531F10077CB4A /* slim_eidos_block.cpp in Sources */, 981DC37128E26F8B000ABE91 /* eidos_functions_stats.cpp in Sources */, 98C821AE1C7A9B1600548839 /* geometric.c in Sources */, 981BAC6B1ACC6E8B0005BE94 /* eidos_script.cpp in Sources */, 98C821481C7A983700548839 /* beta.c in Sources */, 984824F5210B9F31002402A5 /* ddot.c in Sources */, 98D4C1BB1A6F537B00FFB083 /* main.m in Sources */, 987AD8831B2CBF9A0035D6C8 /* EidosValueWrapper.mm in Sources */, 98C821521C7A983700548839 /* inline.c in Sources */, 98CEFD112AAFABAA00D2C9B4 /* interp.c in Sources */, 98C821451C7A983700548839 /* error.c in Sources */, 98DE4C141B6F9657004FDF5F /* eidos_property_signature.cpp in Sources */, 98332ACF1FDBA87D00274FF0 /* oper.c in Sources */, 9854D2682278B9F8001D43BC /* trees.c in Sources */, 98D4C1D61A6F541F00FFB083 /* eidos_rng.cpp in Sources */, 98CEFCF52AAFABAA00D2C9B4 /* bilinear.c in Sources */, 9893C7DF1CDA2FC10029AC94 /* eidos_beep.cpp in Sources */, 986926D91AA140550000E138 /* GraphView.mm in Sources */, 98C821471C7A983700548839 /* stream.c in Sources */, 98CEFD652AAFABAA00D2C9B4 /* accel.c in Sources */, 98EFE6301ADB611100CBEC78 /* eidos_symbol_table.cpp in Sources */, 98332B0B1FDBD03100274FF0 /* init.c in Sources */, 98DB3D701E6122AE00E2C200 /* interaction_type.cpp in Sources */, 98D4C1E21A6F544800FFB083 /* subpopulation.cpp in Sources */, 9836867527CD40CF00683639 /* community.cpp in Sources */, 985F3F0724BA310100E712E0 /* eidos_test_operators_comparison.cpp in Sources */, 98D218C51B2E213200156FC3 /* EidosTextView.mm in Sources */, 98332AF81FDBC42600274FF0 /* swap.c in Sources */, 985724AA1AD4551C0047C223 /* eidos_test.cpp in Sources */, 98332ADD1FDBC0D700274FF0 /* dgemv.c in Sources */, 98C821491C7A983700548839 /* binomial_tpe.c in Sources */, 9850D8E9206309A0006BFD2E /* tables.c in Sources */, 98AC617B24BA34ED0001914C /* species_eidos.cpp in Sources */, 98729AD92A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 98DDAED7221765480038C133 /* slim_functions.cpp in Sources */, 98076618244934A800F6CBB4 /* deflate.c in Sources */, 984252C3216FA9930019696A /* FindRecipeController.mm in Sources */, 981DC35928E26F8B000ABE91 /* eidos_functions_colors.cpp in Sources */, 98606AEF1DED0DCD00821CFF /* mutation_run.cpp in Sources */, 987A70102AE8032100A049E2 /* laplace.c in Sources */, 985F3EF824BA2DD300E712E0 /* eidos_test_functions_math.cpp in Sources */, 98332B191FDBD16500274FF0 /* copy.c in Sources */, 9876E5F61ED5573700FF9762 /* tdist.c in Sources */, 98C821461C7A983700548839 /* message.c in Sources */, 98C8214F1C7A983700548839 /* multinomial.c in Sources */, 98C821541C7A983700548839 /* rng.c in Sources */, 98C821551C7A983700548839 /* taus.c in Sources */, 98CEFD6D2AAFABAA00D2C9B4 /* inline.c in Sources */, 987A2A0824033856009A636F /* gaussinv.c in Sources */, 985F3EEB24BA27EC00E712E0 /* slim_test_genetics.cpp in Sources */, 98332AB51FDBA1E400274FF0 /* blas.c in Sources */, 98C821531C7A983700548839 /* mt.c in Sources */, 98D4C1DB1A6F542F00FFB083 /* genomic_element.cpp in Sources */, 98C821A41C7A99F000548839 /* pow_int.c in Sources */, 98D4C1DF1A6F543C00FFB083 /* substitution.cpp in Sources */, 9854D2602278B9F8001D43BC /* core.c in Sources */, 9876E6131ED55C7300FF9762 /* beta.c in Sources */, 98EDB5122E65410C00CC8798 /* copy.c in Sources */, 98C8214A1C7A983700548839 /* exponential.c in Sources */, 98C821501C7A983700548839 /* poisson.c in Sources */, 981DC35D28E26F8B000ABE91 /* eidos_functions_matrices.cpp in Sources */, 9876E5FD1ED559A600FF9762 /* gauss.c in Sources */, 982556651BA450980054CB3F /* EidosHelpController.mm in Sources */, 9836868227CD72E900683639 /* community_eidos.cpp in Sources */, 98EDB4F92E6538F200CC8798 /* permutation.c in Sources */, 98235683252FDCF50096A745 /* lodepng.cpp in Sources */, 9807C0F924BA21E3008CC658 /* slim_test_other.cpp in Sources */, 985724A61AD435310047C223 /* eidos_value.cpp in Sources */, 98C8215F1C7A983700548839 /* fdiv.c in Sources */, 986926E21AA3DD6C0000E138 /* GraphView_MutationLossTimeHistogram.mm in Sources */, 98C8215C1C7A983700548839 /* trig.c in Sources */, 98CEFD212AAFABAA00D2C9B4 /* akima.c in Sources */, 98C821571C7A983700548839 /* exp.c in Sources */, 98C92AEE1D0B07A6001C82BC /* individual.cpp in Sources */, 981DC36528E26F8B000ABE91 /* eidos_functions_distributions.cpp in Sources */, 98C821511C7A983700548839 /* weibull.c in Sources */, 9893C7E41CDFCF0D0029AC94 /* eidos_type_table.cpp in Sources */, 98C8214E1C7A983700548839 /* lognormal.c in Sources */, 9876E6031ED55A7800FF9762 /* gamma_inc.c in Sources */, 98076623244934A800F6CBB4 /* trees.c in Sources */, 98D4C21C1A718EFD00FFB083 /* CocoaExtra.mm in Sources */, 98332B041FDBCFF900274FF0 /* init.c in Sources */, 9807C0F624BA21B7008CC658 /* slim_test_core.cpp in Sources */, 9825565B1BA32EE80054CB3F /* EidosCocoaExtra.mm in Sources */, 98CEFCED2AAFABAA00D2C9B4 /* spline2d.c in Sources */, 98EDB4EF2E65389600CC8798 /* init.c in Sources */, 98C821581C7A983700548839 /* gamma.c in Sources */, 98332ABA1FDBA32500274FF0 /* dtrmv.c in Sources */, 9807662A24493A8F00F6CBB4 /* crc32.c in Sources */, 985F3F0C24BA31D900E712E0 /* eidos_test_functions_statistics.cpp in Sources */, 98DEB47F2AA632AA00ABE60F /* spatial_map.cpp in Sources */, 985F3EF324BA2A8C00E712E0 /* eidos_test_functions_other.cpp in Sources */, 9887946B1EA8804900AE0C8D /* SLiMPDFDocument.mm in Sources */, 985724A21AD34B740047C223 /* eidos_functions.cpp in Sources */, 98CEFD892AAFB4F000D2C9B4 /* view.c in Sources */, 9809DFA12550F32500C4E82D /* log_file.cpp in Sources */, 98CEFD512AAFABAA00D2C9B4 /* linear.c in Sources */, 9807661D244934A800F6CBB4 /* zutil.c in Sources */, 98C8214C1C7A983700548839 /* gauss.c in Sources */, 98D4C1E31A6F544B00FFB083 /* population.cpp in Sources */, 985F3EFD24BA2F1500E712E0 /* eidos_test_functions_vector.cpp in Sources */, 989A5BEA2525304100E7192D /* eidos_class_Dictionary.cpp in Sources */, 98C6344C2EF9F632003F12A3 /* dirichlet.c in Sources */, 98C821561C7A983700548839 /* elementary.c in Sources */, 986070EB2AACECD600FD6143 /* spatial_kernel.cpp in Sources */, 986926E81AA40AFF0000E138 /* GraphView_FitnessOverTime.mm in Sources */, 98D4C2041A701D5A00FFB083 /* SLiMWindowController.mm in Sources */, 98D4C2071A704EA700FFB083 /* PopulationView.mm in Sources */, 98332B121FDBD0F500274FF0 /* init.c in Sources */, 986926EB1AA6B7480000E138 /* GraphView_PopulationVisualization.mm in Sources */, 98D524691F2EB4DD005AD9A6 /* EidosPrettyprinter.mm in Sources */, 98C8215E1C7A983700548839 /* coerce.c in Sources */, 98D4C1DC1A6F543200FFB083 /* genomic_element_type.cpp in Sources */, 98D4C1B91A6F537B00FFB083 /* AppDelegate.mm in Sources */, 98C821B31C7A9B9F00548839 /* shuffle.c in Sources */, 98C8214B1C7A983700548839 /* gamma.c in Sources */, 98800DC51B82B6C60046F5F9 /* slim_test.cpp in Sources */, 985D1D8C2808B84F00461CFA /* sparse_vector.cpp in Sources */, 9876E60E1ED55C0B00FF9762 /* expint.c in Sources */, 98D4C1D91A6F542900FFB083 /* mutation_type.cpp in Sources */, 9854D2642278B9F8001D43BC /* convert.c in Sources */, 98CEFD492AAFABAA00D2C9B4 /* interp2d.c in Sources */, 9807661F244934A800F6CBB4 /* gzlib.c in Sources */, 9876E6091ED55B4F00FF9762 /* erfc.c in Sources */, 98D4C1DE1A6F543900FFB083 /* polymorphism.cpp in Sources */, 98EDB4E42E65366E00CC8798 /* daxpy.c in Sources */, 98ACDC9E253522B80038703F /* eidos_class_Object.cpp in Sources */, 98CEFD312AAFABAA00D2C9B4 /* spline.c in Sources */, 98D4C1D41A6F541700FFB083 /* species.cpp in Sources */, 98C8215D1C7A983700548839 /* zeta.c in Sources */, D0A758F920A4CCCF00132D2F /* text_input.c in Sources */, 98C821591C7A983700548839 /* log.c in Sources */, 988880ED20744F0100E10172 /* cauchy.c in Sources */, 986D73E91B07E89E007FBB70 /* eidos_call_signature.cpp in Sources */, 980566E325A7C5B9008D3C7F /* fdist.c in Sources */, 98D4C20A1A7086FF00FFB083 /* ChromosomeView.mm in Sources */, 987AD8821B2CBF960035D6C8 /* EidosConsoleTextView.mm in Sources */, 9823568A252FE61A0096A745 /* eidos_class_Image.cpp in Sources */, 98332AC81FDBA74900274FF0 /* vector.c in Sources */, 98CEFD762AAFB12F00D2C9B4 /* cspline.c in Sources */, 98C821441C7A983700548839 /* math.c in Sources */, 9887946F1EA8808000AE0C8D /* SLiMPDFWindowController.mm in Sources */, 98332AFD1FDBC4BB00274FF0 /* matrix.c in Sources */, 980DD51D1AB0B01F00D5B7B8 /* GraphView_MutationFrequencyTrajectory.mm in Sources */, 98076615244934A800F6CBB4 /* compress.c in Sources */, 985F3EEE24BA2A5D00E712E0 /* eidos_test_operators_other.cpp in Sources */, 989524A91E40AE74007E62FA /* SLiMDocument.mm in Sources */, 987AD88A1B2CDBB80035D6C8 /* EidosConsoleWindowController.mm in Sources */, 98CF26521E4353FE00E392D8 /* SLiMDocumentController.mm in Sources */, 98332A9F1FDB990400274FF0 /* mvgauss.c in Sources */, 986926E51AA3FF000000E138 /* GraphView_MutationFixationTimeHistogram.mm in Sources */, 981DC35128E26F8B000ABE91 /* eidos_functions_files.cpp in Sources */, 98332AED1FDBC29400274FF0 /* rowcol.c in Sources */, 98C8214D1C7A983700548839 /* gausszig.c in Sources */, 9893C7EA1CDFE9870029AC94 /* eidos_type_interpreter.cpp in Sources */, 98D7D6662AB24CBC002AFE34 /* chisq.c in Sources */, 987D19A6209A53850030D28D /* kastore.c in Sources */, 98076621244934A800F6CBB4 /* gzwrite.c in Sources */, 98D4C1DD1A6F543600FFB083 /* chromosome.cpp in Sources */, 981DC36128E26F8B000ABE91 /* eidos_functions_values.cpp in Sources */, 98C8215B1C7A983700548839 /* psi.c in Sources */, 981DC36928E26F8B000ABE91 /* eidos_functions_strings.cpp in Sources */, 98E684402B694F09000B3B65 /* plot.mm in Sources */, 986926DF1AA14CF10000E138 /* GraphView_MutationFrequencySpectra.mm in Sources */, 9854D2622278B9F8001D43BC /* genotypes.c in Sources */, 988794731EA8C42200AE0C8D /* SLiMPDFView.mm in Sources */, 9890D1EE27136BB7001EAE98 /* eidos_class_DataFrame.cpp in Sources */, 98CEFD7F2AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98D4C1E11A6F544500FFB083 /* haplosome.cpp in Sources */, 9854D2662278B9F8001D43BC /* stats.c in Sources */, 98076627244934A800F6CBB4 /* adler32.c in Sources */, 987AD8861B2CC0C10035D6C8 /* EidosVariableBrowserController.mm in Sources */, 98EDB4D02E652F4A00CC8798 /* lu.c in Sources */, 981DC35528E26F8B000ABE91 /* eidos_functions_math.cpp in Sources */, 98EBCD1421F3CFC600B385CF /* slim_gui.mm in Sources */, 98D4C1D81A6F542600FFB083 /* mutation.cpp in Sources */, 98D4C1D31A6F541200FFB083 /* slim_globals.cpp in Sources */, 98D7D65D2AB24C40002AFE34 /* tdist.c in Sources */, 985F3F0224BA307200E712E0 /* eidos_test_operators_arithmetic.cpp in Sources */, 98EDB5032E65399600CC8798 /* permute.c in Sources */, 98A513551B66B6CA005A753D /* eidos_ast_node.cpp in Sources */, 98332AF11FDBC36300274FF0 /* submatrix.c in Sources */, 98A513501B66B69E005A753D /* eidos_token.cpp in Sources */, 984824F2210B9F2C002402A5 /* dtrsv.c in Sources */, 98C821601C7A983700548839 /* infnan.c in Sources */, 98CEFD192AAFABAA00D2C9B4 /* bicubic.c in Sources */, 98332AD81FDBBE3500274FF0 /* cholesky.c in Sources */, 98332AC31FDBA54600274FF0 /* xerbla.c in Sources */, 98C821AA1C7A9B1600548839 /* discrete.c in Sources */, 98C8219F1C7A99B200548839 /* minmax.c in Sources */, 98C821431C7A983700548839 /* inline.c in Sources */, 9857249D1AD08A810047C223 /* eidos_interpreter.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98D7EB8428CE557C00DEAAC4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 98D7EB8528CE557C00DEAAC4 /* coerce.c in Sources */, 98D7EB8628CE557C00DEAAC4 /* compress.c in Sources */, 987A70152AE8032100A049E2 /* laplace.c in Sources */, 98D7EB8728CE557C00DEAAC4 /* eidos_value.cpp in Sources */, 98CEFD722AAFABAA00D2C9B4 /* inline.c in Sources */, 98D7EB8828CE557C00DEAAC4 /* math.c in Sources */, 98D7EB8928CE557C00DEAAC4 /* eidos_class_Object.cpp in Sources */, 98D7EB8A28CE557C00DEAAC4 /* eidos_token.cpp in Sources */, 98D7EB8B28CE557C00DEAAC4 /* cholesky.c in Sources */, 98D7EB8C28CE557C00DEAAC4 /* mvgauss.c in Sources */, 98D7EB8D28CE557C00DEAAC4 /* xerbla.c in Sources */, 98D7EB8E28CE557C00DEAAC4 /* init.c in Sources */, 98D7EB8F28CE557C00DEAAC4 /* gauss.c in Sources */, 98D7EB9028CE557C00DEAAC4 /* shuffle.c in Sources */, 98D7EB9128CE557C00DEAAC4 /* exponential.c in Sources */, 98D7EB9228CE557C00DEAAC4 /* erfc.c in Sources */, 98D7EB9328CE557C00DEAAC4 /* expint.c in Sources */, 98D7EB9428CE557C00DEAAC4 /* trig.c in Sources */, 98D7EB9528CE557C00DEAAC4 /* eidos_test.cpp in Sources */, 98CEFCFA2AAFABAA00D2C9B4 /* bilinear.c in Sources */, 98D7EB9628CE557C00DEAAC4 /* oper.c in Sources */, 98D7D6622AB24C40002AFE34 /* tdist.c in Sources */, 98D7EB9728CE557C00DEAAC4 /* eidos_class_Image.cpp in Sources */, 98D7EB9828CE557C00DEAAC4 /* swap.c in Sources */, 98C6344B2EF9F632003F12A3 /* dirichlet.c in Sources */, 98D7EB9928CE557C00DEAAC4 /* eidos_class_Dictionary.cpp in Sources */, 98D7EB9A28CE557C00DEAAC4 /* deflate.c in Sources */, 98D7EB9B28CE557C00DEAAC4 /* error.c in Sources */, 98D7EB9C28CE557C00DEAAC4 /* eidos_test_operators_arithmetic.cpp in Sources */, 98D7EB9D28CE557C00DEAAC4 /* eidos_rng.cpp in Sources */, 98D7EB9E28CE557C00DEAAC4 /* inline.c in Sources */, 98D7EB9F28CE557C00DEAAC4 /* geometric.c in Sources */, 98D7EBA028CE557C00DEAAC4 /* eidos_ast_node.cpp in Sources */, 98CEFD7B2AAFB12F00D2C9B4 /* cspline.c in Sources */, 98D7EBA128CE557C00DEAAC4 /* eidos_beep.cpp in Sources */, 98EDB5182E65410C00CC8798 /* copy.c in Sources */, 98D7EBA228CE557C00DEAAC4 /* adler32.c in Sources */, 98CEFD4E2AAFABAA00D2C9B4 /* interp2d.c in Sources */, 98CEFD562AAFABAA00D2C9B4 /* linear.c in Sources */, 98D7EBA328CE557C00DEAAC4 /* gauss.c in Sources */, 98EDB4FF2E6538F200CC8798 /* permutation.c in Sources */, 98D7EBA428CE557C00DEAAC4 /* zeta.c in Sources */, 98D7EBA528CE557C00DEAAC4 /* multinomial.c in Sources */, 98CEFD162AAFABAA00D2C9B4 /* interp.c in Sources */, 98D7EBA628CE557C00DEAAC4 /* binomial_tpe.c in Sources */, 98D7EBA728CE557C00DEAAC4 /* cauchy.c in Sources */, 98D7EBA828CE557C00DEAAC4 /* infnan.c in Sources */, 98D7D66B2AB24CBC002AFE34 /* chisq.c in Sources */, 98D7EBA928CE557C00DEAAC4 /* eidos_functions.cpp in Sources */, 98D7EBAA28CE557C00DEAAC4 /* eidos_interpreter.cpp in Sources */, 98D7EBAB28CE557C00DEAAC4 /* mt.c in Sources */, 981DC37428E27300000ABE91 /* eidos_functions_other.cpp in Sources */, 98D7EBAC28CE557C00DEAAC4 /* gausszig.c in Sources */, 98D7EBAD28CE557C00DEAAC4 /* message.c in Sources */, 98D7EBAE28CE557C00DEAAC4 /* eidos_property_signature.cpp in Sources */, 98D7EBAF28CE557C00DEAAC4 /* inline.c in Sources */, 98D7EBB028CE557C00DEAAC4 /* eidos_test_functions_statistics.cpp in Sources */, 98EDB4F52E65389600CC8798 /* init.c in Sources */, 981DC37C28E27300000ABE91 /* eidos_functions_files.cpp in Sources */, 98D7EBB128CE557C00DEAAC4 /* discrete.c in Sources */, 98D7EBB228CE557C00DEAAC4 /* gzwrite.c in Sources */, 98D7EBB328CE557C00DEAAC4 /* poisson.c in Sources */, 98D7EBB428CE557C00DEAAC4 /* eidos_type_table.cpp in Sources */, 98D7EBB528CE557C00DEAAC4 /* exp.c in Sources */, 98729ADE2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 98D7EBB628CE557C00DEAAC4 /* eidos_test_functions_other.cpp in Sources */, 98EDB5092E65399600CC8798 /* permute.c in Sources */, 98EDB4EA2E65366E00CC8798 /* daxpy.c in Sources */, 98D7EBB728CE557C00DEAAC4 /* eidos_script.cpp in Sources */, 98CEFD262AAFABAA00D2C9B4 /* akima.c in Sources */, 98CEFD362AAFABAA00D2C9B4 /* spline.c in Sources */, 981DC37B28E27300000ABE91 /* eidos_functions_distributions.cpp in Sources */, 98D7EBB828CE557C00DEAAC4 /* eidos_symbol_table.cpp in Sources */, 98D7EBB928CE557C00DEAAC4 /* submatrix.c in Sources */, 981DC37728E27300000ABE91 /* eidos_functions_math.cpp in Sources */, 98D7EBBA28CE557C00DEAAC4 /* minmax.c in Sources */, 981DC37828E27300000ABE91 /* eidos_functions_matrices.cpp in Sources */, 98D7EBBB28CE557C00DEAAC4 /* pow_int.c in Sources */, 98D7EBBC28CE557C00DEAAC4 /* gaussinv.c in Sources */, 98D7EBBD28CE557C00DEAAC4 /* log.c in Sources */, 98CEFCF22AAFABAA00D2C9B4 /* spline2d.c in Sources */, 981DC37928E27300000ABE91 /* eidos_functions_values.cpp in Sources */, 98D7EBBE28CE557C00DEAAC4 /* gamma_inc.c in Sources */, 98CEFD842AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98D7EBBF28CE557C00DEAAC4 /* eidos_test_functions_vector.cpp in Sources */, 98D7EBC028CE557C00DEAAC4 /* eidos_class_DataFrame.cpp in Sources */, 981DC37628E27300000ABE91 /* eidos_functions_strings.cpp in Sources */, 98D7EBC128CE557C00DEAAC4 /* taus.c in Sources */, 98D7EBC228CE557C00DEAAC4 /* eidos_call_signature.cpp in Sources */, 98D7EBC328CE557C00DEAAC4 /* nbinomial.c in Sources */, 98D7EBC428CE557C00DEAAC4 /* eidos_test_operators_other.cpp in Sources */, 98D7EBC528CE557C00DEAAC4 /* lognormal.c in Sources */, 98CEFD8E2AAFB4F000D2C9B4 /* view.c in Sources */, 98D7EBC628CE557C00DEAAC4 /* vector.c in Sources */, 98D7EBC728CE557C00DEAAC4 /* fdiv.c in Sources */, 981DC37A28E27300000ABE91 /* eidos_functions_colors.cpp in Sources */, 98D7EBC828CE557C00DEAAC4 /* matrix.c in Sources */, 98D7EBC928CE557C00DEAAC4 /* init.c in Sources */, 98D7EBCA28CE557C00DEAAC4 /* weibull.c in Sources */, 98D7EBCB28CE557C00DEAAC4 /* blas.c in Sources */, 98D7EBCC28CE557C00DEAAC4 /* lodepng.cpp in Sources */, 98D7EBCD28CE557C00DEAAC4 /* ddot.c in Sources */, 98D7EBCE28CE557C00DEAAC4 /* eidos_test_functions_math.cpp in Sources */, 98D7EBCF28CE557C00DEAAC4 /* eidos_test_operators_comparison.cpp in Sources */, 98D7EBD028CE557C00DEAAC4 /* gzlib.c in Sources */, 98D7EBD128CE557C00DEAAC4 /* dtrmv.c in Sources */, 98CEFD6A2AAFABAA00D2C9B4 /* accel.c in Sources */, 98D7EBD228CE557C00DEAAC4 /* crc32.c in Sources */, 98D7EBD328CE557C00DEAAC4 /* fdist.c in Sources */, 98EDB4D62E652F4A00CC8798 /* lu.c in Sources */, 981DC37528E27300000ABE91 /* eidos_functions_stats.cpp in Sources */, 98D7EBD428CE557C00DEAAC4 /* trees.c in Sources */, 98D7EBD528CE557C00DEAAC4 /* gamma.c in Sources */, 98D7EBD628CE557C00DEAAC4 /* beta.c in Sources */, 98D7EBD728CE557C00DEAAC4 /* copy.c in Sources */, 98D7EBD828CE557C00DEAAC4 /* rowcol.c in Sources */, 98D7EBD928CE557C00DEAAC4 /* zutil.c in Sources */, 98D7EBDA28CE557C00DEAAC4 /* psi.c in Sources */, 98D7EBDB28CE557C00DEAAC4 /* main.cpp in Sources */, 98D7EBDC28CE557C00DEAAC4 /* eidos_class_TestElement.cpp in Sources */, 98D7EBDD28CE557C00DEAAC4 /* beta.c in Sources */, 98D7EBDE28CE557C00DEAAC4 /* pow_int.c in Sources */, 98D7EBDF28CE557C00DEAAC4 /* elementary.c in Sources */, 98D7EBE028CE557C00DEAAC4 /* eidos_type_interpreter.cpp in Sources */, 98D7EBE128CE557C00DEAAC4 /* init.c in Sources */, 98D7EBE228CE557C00DEAAC4 /* stream.c in Sources */, 98D7EBE328CE557C00DEAAC4 /* rng.c in Sources */, 98D7EBE428CE557C00DEAAC4 /* dtrsv.c in Sources */, 98CEFD1E2AAFABAA00D2C9B4 /* bicubic.c in Sources */, 98D7EBE528CE557C00DEAAC4 /* tdist.c in Sources */, 98D7EBE628CE557C00DEAAC4 /* dgemv.c in Sources */, 98D7EBE728CE557C00DEAAC4 /* gamma.c in Sources */, 98D7EBE828CE557C00DEAAC4 /* eidos_globals.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 98D7ECA028CE58FC00DEAAC4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 98CEFD572AAFABAA00D2C9B4 /* linear.c in Sources */, 98D7ECA128CE58FC00DEAAC4 /* eidos_symbol_table.cpp in Sources */, 98D7ECA228CE58FC00DEAAC4 /* eidos_type_interpreter.cpp in Sources */, 98D7ECA328CE58FC00DEAAC4 /* eidos_call_signature.cpp in Sources */, 98C634442EF9F632003F12A3 /* dirichlet.c in Sources */, 98CEFD172AAFABAA00D2C9B4 /* interp.c in Sources */, 98D7ECA428CE58FC00DEAAC4 /* dgemv.c in Sources */, 98D7ECA528CE58FC00DEAAC4 /* init.c in Sources */, 98D7ECA628CE58FC00DEAAC4 /* gamma.c in Sources */, 98729ADF2A87DFBE00E81662 /* eidos_sorting.cpp in Sources */, 98D7ECA728CE58FC00DEAAC4 /* gauss.c in Sources */, 98D7ECA828CE58FC00DEAAC4 /* eidos_test_operators_arithmetic.cpp in Sources */, 981DC38528E27301000ABE91 /* eidos_functions_files.cpp in Sources */, 98D7ECA928CE58FC00DEAAC4 /* eidos_test_functions_statistics.cpp in Sources */, 98D7ECAA28CE58FC00DEAAC4 /* log.c in Sources */, 98CEFD372AAFABAA00D2C9B4 /* spline.c in Sources */, 98EDB4EB2E65366E00CC8798 /* daxpy.c in Sources */, 98D7ECAB28CE58FC00DEAAC4 /* slim_test.cpp in Sources */, 98D7ECAC28CE58FC00DEAAC4 /* expint.c in Sources */, 981DC38028E27301000ABE91 /* eidos_functions_math.cpp in Sources */, 98D7ECAD28CE58FC00DEAAC4 /* elementary.c in Sources */, 98D7ECAE28CE58FC00DEAAC4 /* mvgauss.c in Sources */, 98D7ECAF28CE58FC00DEAAC4 /* copy.c in Sources */, 98D7ECB028CE58FC00DEAAC4 /* eidos_class_Image.cpp in Sources */, 98D7ECB128CE58FC00DEAAC4 /* shuffle.c in Sources */, 98D7ECB228CE58FC00DEAAC4 /* stats.c in Sources */, 98D7ECB328CE58FC00DEAAC4 /* mutation_type.cpp in Sources */, 98D7ECB428CE58FC00DEAAC4 /* mutation.cpp in Sources */, 98DEB4812AA632AA00ABE60F /* spatial_map.cpp in Sources */, 98D7ECB528CE58FC00DEAAC4 /* coerce.c in Sources */, 98D7ECB628CE58FC00DEAAC4 /* dtrmv.c in Sources */, 98D7ECB728CE58FC00DEAAC4 /* gamma_inc.c in Sources */, 98D7ECB828CE58FC00DEAAC4 /* slim_test_other.cpp in Sources */, 981DC38428E27301000ABE91 /* eidos_functions_distributions.cpp in Sources */, 98D7ECB928CE58FC00DEAAC4 /* vector.c in Sources */, 98D7ECBA28CE58FC00DEAAC4 /* eidos_script.cpp in Sources */, 98D7ECBB28CE58FC00DEAAC4 /* gaussinv.c in Sources */, 98D7ECBC28CE58FC00DEAAC4 /* erfc.c in Sources */, 98D7ECBD28CE58FC00DEAAC4 /* gausszig.c in Sources */, 98D7ECBE28CE58FC00DEAAC4 /* community_eidos.cpp in Sources */, 98EDB4D72E652F4A00CC8798 /* lu.c in Sources */, 98D7ECBF28CE58FC00DEAAC4 /* ddot.c in Sources */, 98D7ECC028CE58FC00DEAAC4 /* species.cpp in Sources */, 98D7ECC128CE58FC00DEAAC4 /* eidos_beep.cpp in Sources */, 98D7ECC228CE58FC00DEAAC4 /* fdist.c in Sources */, 98D7ECC328CE58FC00DEAAC4 /* matrix.c in Sources */, 98D7ECC428CE58FC00DEAAC4 /* submatrix.c in Sources */, 981DC38228E27301000ABE91 /* eidos_functions_values.cpp in Sources */, 98D7ECC528CE58FC00DEAAC4 /* psi.c in Sources */, 98D7ECC628CE58FC00DEAAC4 /* binomial_tpe.c in Sources */, 98D7ECC728CE58FC00DEAAC4 /* eidos_test_functions_math.cpp in Sources */, 98CEFD272AAFABAA00D2C9B4 /* akima.c in Sources */, 98D7ECC828CE58FC00DEAAC4 /* haplosome.cpp in Sources */, 98CEFD8F2AAFB4F000D2C9B4 /* view.c in Sources */, 98D7ECC928CE58FC00DEAAC4 /* zutil.c in Sources */, 98CEFD4F2AAFABAA00D2C9B4 /* interp2d.c in Sources */, 98D7ECCA28CE58FC00DEAAC4 /* trees.c in Sources */, 98D7ECCB28CE58FC00DEAAC4 /* init.c in Sources */, 98D7ECCC28CE58FC00DEAAC4 /* stream.c in Sources */, 98D7ECCD28CE58FC00DEAAC4 /* slim_test_core.cpp in Sources */, 98D7ECCE28CE58FC00DEAAC4 /* eidos_ast_node.cpp in Sources */, 98D7ECCF28CE58FC00DEAAC4 /* chromosome.cpp in Sources */, 98D7ECD028CE58FC00DEAAC4 /* eidos_test_functions_other.cpp in Sources */, 98D7ECD128CE58FC00DEAAC4 /* error.c in Sources */, 98D7ECD228CE58FC00DEAAC4 /* tables.c in Sources */, 98D7ECD328CE58FC00DEAAC4 /* slim_eidos_block.cpp in Sources */, 98D7ECD428CE58FC00DEAAC4 /* convert.c in Sources */, 98D7ECD528CE58FC00DEAAC4 /* eidos_test_operators_other.cpp in Sources */, 98D7ECD628CE58FC00DEAAC4 /* eidos_test_functions_vector.cpp in Sources */, 98EDB50A2E65399600CC8798 /* permute.c in Sources */, 98D7ECD728CE58FC00DEAAC4 /* deflate.c in Sources */, 98EDB5002E6538F200CC8798 /* permutation.c in Sources */, 981DC38128E27301000ABE91 /* eidos_functions_matrices.cpp in Sources */, 98D7ECD828CE58FC00DEAAC4 /* gamma.c in Sources */, 98D7ECD928CE58FC00DEAAC4 /* nbinomial.c in Sources */, 98D7ECDA28CE58FC00DEAAC4 /* poisson.c in Sources */, 98D7ECDB28CE58FC00DEAAC4 /* sparse_vector.cpp in Sources */, 98D7ECDC28CE58FC00DEAAC4 /* population.cpp in Sources */, 98D7ECDD28CE58FC00DEAAC4 /* beta.c in Sources */, 981DC37E28E27301000ABE91 /* eidos_functions_stats.cpp in Sources */, 98D7ECDE28CE58FC00DEAAC4 /* rowcol.c in Sources */, 98D7ECDF28CE58FC00DEAAC4 /* taus.c in Sources */, 98CEFD1F2AAFABAA00D2C9B4 /* bicubic.c in Sources */, 981DC37D28E27301000ABE91 /* eidos_functions_other.cpp in Sources */, 981DC37F28E27301000ABE91 /* eidos_functions_strings.cpp in Sources */, 98D7ECE028CE58FC00DEAAC4 /* eidos_rng.cpp in Sources */, 98D7ECE128CE58FC00DEAAC4 /* geometric.c in Sources */, 98D7ECE228CE58FC00DEAAC4 /* eidos_token.cpp in Sources */, 98D7ECE328CE58FC00DEAAC4 /* core.c in Sources */, 98D7D66C2AB24CBC002AFE34 /* chisq.c in Sources */, 98D7ECE428CE58FC00DEAAC4 /* minmax.c in Sources */, 98CEFD7C2AAFB12F00D2C9B4 /* cspline.c in Sources */, 98EDB5192E65410C00CC8798 /* copy.c in Sources */, 986070ED2AACECD600FD6143 /* spatial_kernel.cpp in Sources */, 98D7ECE528CE58FC00DEAAC4 /* trees.c in Sources */, 98D7ECE628CE58FC00DEAAC4 /* xerbla.c in Sources */, 98D7ECE728CE58FC00DEAAC4 /* eidos_test.cpp in Sources */, 98D7ECE828CE58FC00DEAAC4 /* blas.c in Sources */, 98D7ECE928CE58FC00DEAAC4 /* log_file.cpp in Sources */, 98D7ECEA28CE58FC00DEAAC4 /* eidos_class_Dictionary.cpp in Sources */, 98D7ECEB28CE58FC00DEAAC4 /* individual.cpp in Sources */, 98D7ECEC28CE58FC00DEAAC4 /* GitSHA1_Xcode.cpp in Sources */, 98D7ECED28CE58FC00DEAAC4 /* eidos_class_Object.cpp in Sources */, 98CEFD732AAFABAA00D2C9B4 /* inline.c in Sources */, 98D7ECEE28CE58FC00DEAAC4 /* lodepng.cpp in Sources */, 98D7ECEF28CE58FC00DEAAC4 /* mutation_run.cpp in Sources */, 98D7ECF028CE58FC00DEAAC4 /* inline.c in Sources */, 98CEFD6B2AAFABAA00D2C9B4 /* accel.c in Sources */, 98D7ECF128CE58FC00DEAAC4 /* eidos_property_signature.cpp in Sources */, 98D7ECF228CE58FC00DEAAC4 /* compress.c in Sources */, 98D7ECF328CE58FC00DEAAC4 /* genomic_element_type.cpp in Sources */, 98D7ECF428CE58FC00DEAAC4 /* discrete.c in Sources */, 98D7ECF528CE58FC00DEAAC4 /* exponential.c in Sources */, 98D7ECF628CE58FC00DEAAC4 /* eidos_value.cpp in Sources */, 98D7ECF728CE58FC00DEAAC4 /* eidos_type_table.cpp in Sources */, 98D7ECF828CE58FC00DEAAC4 /* eidos_functions.cpp in Sources */, 98D7ECF928CE58FC00DEAAC4 /* pow_int.c in Sources */, 98CEFCF32AAFABAA00D2C9B4 /* spline2d.c in Sources */, 98D7ECFA28CE58FC00DEAAC4 /* weibull.c in Sources */, 98D7ECFB28CE58FC00DEAAC4 /* fdiv.c in Sources */, 98D7ECFC28CE58FC00DEAAC4 /* cholesky.c in Sources */, 98D7ECFD28CE58FC00DEAAC4 /* kastore.c in Sources */, 98D7ECFE28CE58FC00DEAAC4 /* tdist.c in Sources */, 987A70162AE8032100A049E2 /* laplace.c in Sources */, 98D7ECFF28CE58FC00DEAAC4 /* polymorphism.cpp in Sources */, 98D7ED0028CE58FC00DEAAC4 /* eidos_class_TestElement.cpp in Sources */, 98D7D6632AB24C40002AFE34 /* tdist.c in Sources */, 98D7ED0128CE58FC00DEAAC4 /* main.cpp in Sources */, 98D7ED0228CE58FC00DEAAC4 /* slim_functions.cpp in Sources */, 98EDB4F62E65389600CC8798 /* init.c in Sources */, 98D7ED0328CE58FC00DEAAC4 /* gauss.c in Sources */, 98D7ED0428CE58FC00DEAAC4 /* cauchy.c in Sources */, 98D7ED0528CE58FC00DEAAC4 /* substitution.cpp in Sources */, 98D7ED0628CE58FC00DEAAC4 /* genotypes.c in Sources */, 981DC38328E27301000ABE91 /* eidos_functions_colors.cpp in Sources */, 98D7ED0728CE58FC00DEAAC4 /* exp.c in Sources */, 98CEFD852AAFB23E00D2C9B4 /* tridiag.c in Sources */, 98D7ED0828CE58FC00DEAAC4 /* gzwrite.c in Sources */, 98D7ED0928CE58FC00DEAAC4 /* adler32.c in Sources */, 98D7ED0A28CE58FC00DEAAC4 /* trig.c in Sources */, 98D7ED0B28CE58FC00DEAAC4 /* eidos_globals.cpp in Sources */, 98D7ED0C28CE58FC00DEAAC4 /* slim_test_genetics.cpp in Sources */, 98D7ED0D28CE58FC00DEAAC4 /* multinomial.c in Sources */, 98D7ED0E28CE58FC00DEAAC4 /* init.c in Sources */, 98D7ED0F28CE58FC00DEAAC4 /* math.c in Sources */, 98D7ED1028CE58FC00DEAAC4 /* genomic_element.cpp in Sources */, 98D7ED1128CE58FC00DEAAC4 /* slim_globals.cpp in Sources */, 98D7ED1228CE58FC00DEAAC4 /* interaction_type.cpp in Sources */, 98D7ED1328CE58FC00DEAAC4 /* subpopulation.cpp in Sources */, 98D7ED1428CE58FC00DEAAC4 /* swap.c in Sources */, 98D7ED1528CE58FC00DEAAC4 /* gzlib.c in Sources */, 98D7ED1628CE58FC00DEAAC4 /* species_eidos.cpp in Sources */, 98D7ED1728CE58FC00DEAAC4 /* mt.c in Sources */, 98D7ED1828CE58FC00DEAAC4 /* eidos_interpreter.cpp in Sources */, 98D7ED1928CE58FC00DEAAC4 /* inline.c in Sources */, 98D7ED1A28CE58FC00DEAAC4 /* message.c in Sources */, 98D7ED1B28CE58FC00DEAAC4 /* dtrsv.c in Sources */, 98D7ED1C28CE58FC00DEAAC4 /* infnan.c in Sources */, 98D7ED1D28CE58FC00DEAAC4 /* crc32.c in Sources */, 98D7ED1E28CE58FC00DEAAC4 /* zeta.c in Sources */, 98D7ED1F28CE58FC00DEAAC4 /* eidos_class_DataFrame.cpp in Sources */, 98D7ED2028CE58FC00DEAAC4 /* rng.c in Sources */, 98D7ED2128CE58FC00DEAAC4 /* oper.c in Sources */, 98D7ED2228CE58FC00DEAAC4 /* beta.c in Sources */, 98D7ED2328CE58FC00DEAAC4 /* text_input.c in Sources */, 98D7ED2428CE58FC00DEAAC4 /* pow_int.c in Sources */, 98D7ED2528CE58FC00DEAAC4 /* eidos_test_operators_comparison.cpp in Sources */, 98D7ED2628CE58FC00DEAAC4 /* lognormal.c in Sources */, 98CEFCFB2AAFABAA00D2C9B4 /* bilinear.c in Sources */, 98D7ED2728CE58FC00DEAAC4 /* community.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 984D5FAE1E3AF0D200473719 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 98D4C1B21A6F537B00FFB083 /* SLiMguiLegacy */; targetProxy = 984D5FAD1E3AF0D200473719 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 98024740215D85880025D29C /* FindRecipePanel.xib */ = { isa = PBXVariantGroup; children = ( 98024741215D85880025D29C /* Base */, ); name = FindRecipePanel.xib; sourceTree = ""; }; 980DD5181AAE42F900D5B7B8 /* GraphAxisRescaleSheet.xib */ = { isa = PBXVariantGroup; children = ( 980DD5191AAE42F900D5B7B8 /* Base */, ); name = GraphAxisRescaleSheet.xib; sourceTree = ""; }; 982556671BA451D00054CB3F /* EidosHelpWindow.xib */ = { isa = PBXVariantGroup; children = ( 982556681BA451D00054CB3F /* Base */, ); name = EidosHelpWindow.xib; sourceTree = ""; }; 982A9DDC1FCA9FF0007BA3DF /* GraphBarRescaleSheet.xib */ = { isa = PBXVariantGroup; children = ( 982A9DDD1FCA9FF0007BA3DF /* Base */, ); name = GraphBarRescaleSheet.xib; sourceTree = ""; }; 985724BA1AD478630047C223 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 985724BB1AD478630047C223 /* Base */, ); name = MainMenu.xib; sourceTree = ""; }; 986926DA1AA1429D0000E138 /* GraphWindow.xib */ = { isa = PBXVariantGroup; children = ( 986926DB1AA1429D0000E138 /* Base */, ); name = GraphWindow.xib; sourceTree = ""; }; 98A4EC921B67C1CD00CD92FD /* EidosConsoleWindow.xib */ = { isa = PBXVariantGroup; children = ( 98A4EC931B67C1CD00CD92FD /* Base */, ); name = EidosConsoleWindow.xib; sourceTree = ""; }; 98D4C1BE1A6F537B00FFB083 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 98D4C1BF1A6F537B00FFB083 /* Base */, ); name = MainMenu.xib; sourceTree = ""; }; 98D4C1FF1A70192A00FFB083 /* SLiMWindow.xib */ = { isa = PBXVariantGroup; children = ( 98D4C2001A70192A00FFB083 /* Base */, ); name = SLiMWindow.xib; sourceTree = ""; }; 98D4C20B1A715F6100FFB083 /* AboutWindow.xib */ = { isa = PBXVariantGroup; children = ( 98D4C20C1A715F6100FFB083 /* Base */, ); name = AboutWindow.xib; sourceTree = ""; }; 98D4C2121A71838400FFB083 /* HelpWindow.xib */ = { isa = PBXVariantGroup; children = ( 98D4C2131A71838400FFB083 /* Base */, ); name = HelpWindow.xib; sourceTree = ""; }; 98EA965A1ECC2541006BA35B /* ProfileReport.xib */ = { isa = PBXVariantGroup; children = ( 98EA965B1ECC2541006BA35B /* Base */, ); name = ProfileReport.xib; sourceTree = ""; }; 98EFE6321ADD92BC00CBEC78 /* EidosAboutWindow.xib */ = { isa = PBXVariantGroup; children = ( 98EFE6331ADD92BC00CBEC78 /* Base */, ); name = EidosAboutWindow.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 982556AE1BA8E77C0054CB3F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 982556AF1BA8E77C0054CB3F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; 982663551A3BABD300A0CBBF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CXX0X_EXTENSIONS = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_SHADOW = YES; GCC_WARN_SIGN_COMPARE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ""; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-fno-math-errno", ); PER_ARCH_CFLAGS_arm64 = "-DEIDOS_HAS_NEON=1"; PER_ARCH_CFLAGS_x86_64 = "-mavx2 -mfma -DEIDOS_HAS_AVX2=1 -DEIDOS_HAS_FMA=1"; SDKROOT = macosx; STRIP_STYLE = debugging; WARNING_CFLAGS = "-Wimplicit-fallthrough"; }; name = Debug; }; 982663561A3BABD300A0CBBF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CXX0X_EXTENSIONS = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 3; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_SHADOW = YES; GCC_WARN_SIGN_COMPARE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = ""; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-fno-math-errno", ); PER_ARCH_CFLAGS_arm64 = "-DEIDOS_HAS_NEON=1"; PER_ARCH_CFLAGS_x86_64 = "-mavx2 -mfma -DEIDOS_HAS_AVX2=1 -DEIDOS_HAS_FMA=1"; SDKROOT = macosx; STRIP_STYLE = debugging; WARNING_CFLAGS = "-Wimplicit-fallthrough"; }; name = Release; }; 982663581A3BABD300A0CBBF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", TSK_TRACE_ERRORS, "$(inherited)", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; PRODUCT_NAME = slim; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/"; }; name = Debug; }; 982663591A3BABD300A0CBBF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "SLIMPROFILING=0"; LIBRARY_SEARCH_PATHS = "$(inherited)"; PRODUCT_NAME = slim; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/"; }; name = Release; }; 984D5FAF1E3AF0D200473719 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = C5P6M43RZ7; GCC_C_LANGUAGE_STANDARD = gnu99; INFOPLIST_FILE = EidosSLiMTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stick.EidosSLiMTests; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/"; }; name = Debug; }; 984D5FB01E3AF0D200473719 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEVELOPMENT_TEAM = C5P6M43RZ7; GCC_C_LANGUAGE_STANDARD = gnu99; INFOPLIST_FILE = EidosSLiMTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stick.EidosSLiMTests; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/"; }; name = Release; }; 985724CA1AD478640047C223 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( EIDOS_GUI, "$(inherited)", ); GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "EidosScribe/EidosScribe-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 985724CB1AD478640047C223 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = EIDOS_GUI; GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "EidosScribe/EidosScribe-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; 98CF52FF294A3FC900557BBA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = SLiMgui/SLiMguiLegacy_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "SLIMGUI=1", EIDOS_GUI, ); GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "SLiMgui/SLiMguiLegacy_multi-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = /usr/local/lib/; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", "-Xclang", "-fopenmp", ); PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/ /usr/local/include/"; }; name = Debug; }; 98CF5300294A3FC900557BBA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = SLiMgui/SLiMguiLegacy_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "SLIMGUI=1", EIDOS_GUI, "SLIMPROFILING=1", ); GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "SLiMgui/SLiMguiLegacy_multi-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = /usr/local/lib/; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", "-Xclang", "-fopenmp", ); PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/ /usr/local/include/"; }; name = Release; }; 98CF5432294A714200557BBA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = EidosScribe/EidosScribe_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( EIDOS_GUI, "$(inherited)", ); GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "EidosScribe/EidosScribe_multi-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = /usr/local/lib/; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", "-Xclang", "-fopenmp", ); PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = /usr/local/include/; }; name = Debug; }; 98CF5433294A714200557BBA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = EidosScribe/EidosScribe_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = EIDOS_GUI; GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "EidosScribe/EidosScribe_multi-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = /usr/local/lib/; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", "-Xclang", "-fopenmp", ); PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = /usr/local/include/; }; name = Release; }; 98D4C1CE1A6F537C00FFB083 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "SLIMGUI=1", EIDOS_GUI, TSK_TRACE_ERRORS, ); GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "SLiMgui/SLiMguiLegacy-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/"; }; name = Debug; }; 98D4C1CF1A6F537C00FFB083 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "SLIMGUI=1", EIDOS_GUI, "SLIMPROFILING=1", ); GCC_WARN_UNUSED_PARAMETER = NO; GCC_WARN_UNUSED_VALUE = NO; INFOPLIST_FILE = "SLiMgui/SLiMguiLegacy-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "edu.MesserLab.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/"; }; name = Release; }; 98D7EBEC28CE557C00DEAAC4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = eidos_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMPILER_INDEX_STORE_ENABLE = NO; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; LIBRARY_SEARCH_PATHS = /usr/local/lib/; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", ); PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = /usr/local/include/; }; name = Debug; }; 98D7EBED28CE557C00DEAAC4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = eidos_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMPILER_INDEX_STORE_ENABLE = NO; COPY_PHASE_STRIP = NO; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; LIBRARY_SEARCH_PATHS = /usr/local/lib/; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", ); PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = /usr/local/include/; }; name = Release; }; 98D7ED2B28CE58FC00DEAAC4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = slim_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMPILER_INDEX_STORE_ENABLE = NO; ENABLE_HARDENED_RUNTIME = YES; LIBRARY_SEARCH_PATHS = /usr/local/lib/; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", "-Xclang", "-fopenmp", ); PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/ /usr/local/include/"; }; name = Debug; }; 98D7ED2C28CE58FC00DEAAC4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = slim_multi.entitlements; CODE_SIGN_IDENTITY = "-"; COMPILER_INDEX_STORE_ENABLE = NO; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "SLIMPROFILING=0"; LIBRARY_SEARCH_PATHS = /usr/local/lib/; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-Xclang", "-fopenmp", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "$(OTHER_CPLUSPLUSFLAGS)", "-Xclang", "-fopenmp", ); PRODUCT_NAME = "$(TARGET_NAME)"; SYSTEM_HEADER_SEARCH_PATHS = "\"${SRCROOT}\"/treerec/ \"${SRCROOT}\"/treerec/tskit/kastore/ /usr/local/include/"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 982556AD1BA8E77C0054CB3F /* Build configuration list for PBXNativeTarget "eidos" */ = { isa = XCConfigurationList; buildConfigurations = ( 982556AE1BA8E77C0054CB3F /* Debug */, 982556AF1BA8E77C0054CB3F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 9826634B1A3BABD300A0CBBF /* Build configuration list for PBXProject "SLiM" */ = { isa = XCConfigurationList; buildConfigurations = ( 982663551A3BABD300A0CBBF /* Debug */, 982663561A3BABD300A0CBBF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 982663571A3BABD300A0CBBF /* Build configuration list for PBXNativeTarget "SLiM" */ = { isa = XCConfigurationList; buildConfigurations = ( 982663581A3BABD300A0CBBF /* Debug */, 982663591A3BABD300A0CBBF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 984D5FB11E3AF0D200473719 /* Build configuration list for PBXNativeTarget "EidosSLiMTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 984D5FAF1E3AF0D200473719 /* Debug */, 984D5FB01E3AF0D200473719 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 985724C91AD478640047C223 /* Build configuration list for PBXNativeTarget "EidosScribe" */ = { isa = XCConfigurationList; buildConfigurations = ( 985724CA1AD478640047C223 /* Debug */, 985724CB1AD478640047C223 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 98CF52FE294A3FC900557BBA /* Build configuration list for PBXNativeTarget "SLiMguiLegacy_multi" */ = { isa = XCConfigurationList; buildConfigurations = ( 98CF52FF294A3FC900557BBA /* Debug */, 98CF5300294A3FC900557BBA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 98CF5431294A714200557BBA /* Build configuration list for PBXNativeTarget "EidosScribe_multi" */ = { isa = XCConfigurationList; buildConfigurations = ( 98CF5432294A714200557BBA /* Debug */, 98CF5433294A714200557BBA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 98D4C1CD1A6F537C00FFB083 /* Build configuration list for PBXNativeTarget "SLiMguiLegacy" */ = { isa = XCConfigurationList; buildConfigurations = ( 98D4C1CE1A6F537C00FFB083 /* Debug */, 98D4C1CF1A6F537C00FFB083 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 98D7EBEB28CE557C00DEAAC4 /* Build configuration list for PBXNativeTarget "eidos_multi" */ = { isa = XCConfigurationList; buildConfigurations = ( 98D7EBEC28CE557C00DEAAC4 /* Debug */, 98D7EBED28CE557C00DEAAC4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 98D7ED2A28CE58FC00DEAAC4 /* Build configuration list for PBXNativeTarget "slim_multi" */ = { isa = XCConfigurationList; buildConfigurations = ( 98D7ED2B28CE58FC00DEAAC4 /* Debug */, 98D7ED2C28CE58FC00DEAAC4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 982663481A3BABD300A0CBBF /* Project object */; } ================================================ FILE: SLiM.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: SLiM.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: SLiM.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/EidosScribe.xcscheme ================================================ ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/EidosScribe_multi.xcscheme ================================================ ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/SLiM.xcscheme ================================================ ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/SLiM_multi.xcscheme ================================================ ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/SLiMguiLegacy.xcscheme ================================================ ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/SLiMguiLegacy_multi.xcscheme ================================================ ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/eidos.xcscheme ================================================ ================================================ FILE: SLiM.xcodeproj/xcshareddata/xcschemes/eidos_multi.xcscheme ================================================ ================================================ FILE: SLiMgui/AppDelegate.h ================================================ // // AppDelegate.h // SLiMgui // // Created by Ben Haller on 1/20/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #import #import #include @class SLiMWindowController; @class EidosTextView; // User defaults keys extern NSString *defaultsLaunchActionKey; extern NSString *defaultsDisplayFontSizeKey; extern NSString *defaultsSyntaxHighlightScriptKey; extern NSString *defaultsSyntaxHighlightOutputKey; extern NSString *defaultsPlaySoundParseSuccessKey; extern NSString *defaultsPlaySoundParseFailureKey; @interface AppDelegate : NSObject { std::string app_cwd_; // the app's current working directory, set up by applicationWillFinishLaunching: bool launchedFromShell_; // TRUE if launched from shell, FALSE if launched from Finder/other } @property (nonatomic, retain) IBOutlet NSMenu *openRecipesMenu; - (std::string &)SLiMguiCurrentWorkingDirectory; - (bool)launchedFromShell; - (IBAction)resetSuppressionFlags:(id)sender; - (IBAction)showAboutWindow:(id)sender; - (IBAction)showHelp:(id)sender; - (IBAction)sendFeedback:(id)sender; - (IBAction)slimWorkshops:(id)sender; - (IBAction)mailingListAnnounce:(id)sender; - (IBAction)mailingListDiscuss:(id)sender; - (IBAction)showMesserLab:(id)sender; - (IBAction)showBenHaller:(id)sender; - (IBAction)showStickSoftware:(id)sender; @end ================================================ FILE: SLiMgui/AppDelegate.mm ================================================ // // AppDelegate.m // SLiMgui // // Created by Ben Haller on 1/20/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "AppDelegate.h" #import "SLiMWindowController.h" #import "SLiMDocument.h" #import "SLiMDocumentController.h" #import "EidosHelpController.h" #import "CocoaExtra.h" #import "EidosCocoaExtra.h" #import "eidos_beep.h" #import "slim_gui.h" #import "plot.h" #import #include #include // User defaults keys NSString *defaultsLaunchActionKey = @"LaunchAction"; NSString *defaultsDisplayFontSizeKey = @"DisplayFontSize"; NSString *defaultsSyntaxHighlightScriptKey = @"SyntaxHighlightScript"; NSString *defaultsSyntaxHighlightOutputKey = @"SyntaxHighlightOutput"; NSString *defaultsPlaySoundParseSuccessKey = @"PlaySoundParseSuccess"; NSString *defaultsPlaySoundParseFailureKey = @"PlaySoundParseFailure"; typedef enum SLiMLaunchAction { kLaunchDoNothing = 0, kLaunchNewScriptWindow, kLaunchRunOpenPanel } SLiMLaunchAction; @interface AppDelegate () { // About window cruft IBOutlet NSWindow *aboutWindow; IBOutlet NSTextField *aboutVersionTextField; IBOutlet NSTextField *messerLabLineTextField; IBOutlet NSTextField *benHallerLineTextField; IBOutlet NSTextField *licenseTextField; // Help window cruft; kept for possible resurrection, see -showHelp: //IBOutlet NSWindow *helpWindow; //IBOutlet PDFView *helpPDFView; } @end @implementation AppDelegate @synthesize openRecipesMenu; + (void)initialize { [[NSUserDefaults standardUserDefaults] registerDefaults:@{ defaultsLaunchActionKey : @(kLaunchNewScriptWindow), defaultsDisplayFontSizeKey : @11, defaultsSyntaxHighlightScriptKey : @YES, defaultsSyntaxHighlightOutputKey : @YES, defaultsPlaySoundParseSuccessKey : @YES, defaultsPlaySoundParseFailureKey : @YES }]; } - (void)dealloc { [self setOpenRecipesMenu:nil]; [super dealloc]; } - (void)setUpRecipesMenu { [openRecipesMenu removeAllItems]; // Add the Find Recipe... menu item and separator { NSMenuItem *menuItem = [openRecipesMenu addItemWithTitle:@"Find Recipe..." action:@selector(findRecipe:) keyEquivalent:@"O"]; [menuItem setTarget:[NSDocumentController sharedDocumentController]]; [openRecipesMenu addItem:[NSMenuItem separatorItem]]; } // Find recipes in our bundle NSURL *urlForRecipesFolder = [[NSBundle mainBundle] URLForResource:@"Recipes" withExtension:@""]; NSFileManager *fm = [NSFileManager defaultManager]; NSDirectoryEnumerator *dirEnum = [fm enumeratorAtURL:urlForRecipesFolder includingPropertiesForKeys:@[NSURLNameKey, NSURLIsDirectoryKey] options:(NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsSubdirectoryDescendants) errorHandler:nil]; NSMutableArray *recipeNames = [NSMutableArray array]; for (NSURL *fileURL in dirEnum) { NSNumber *isDirectory = nil; [fileURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; if (![isDirectory boolValue]) { NSString *name = nil; [fileURL getResourceValue:&name forKey:NSURLNameKey error:nil]; if ([name hasPrefix:@"Recipe "]) { if ([name hasSuffix:@".txt"]) { // Remove the .txt extension for SLiM models NSString *trimmedName = [name substringWithRange:NSMakeRange(7, [name length] - 11)]; [recipeNames addObject:trimmedName]; } else if ([name hasSuffix:@".py"]) { // Leave the .py extension for Python models NSString *trimmedName = [name substringWithRange:NSMakeRange(7, [name length] - 7)]; [recipeNames addObject:[trimmedName stringByAppendingString:@" 🐍"]]; } } } } [recipeNames sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { return [(NSString *)obj1 compare:(NSString *)obj2 options:NSNumericSearch]; }]; NSString *previousItemChapter = nil; NSMenu *chapterSubmenu = nil; for (NSString *recipeName in recipeNames) { NSUInteger firstDotIndex = [recipeName rangeOfString:@"."].location; NSString *recipeChapter = (firstDotIndex != NSNotFound) ? [recipeName substringToIndex:firstDotIndex] : nil; // Create a submenu for each chapter if (recipeChapter && ![recipeChapter isEqualToString:previousItemChapter]) { int recipeChapterValue = [recipeChapter intValue]; NSString *chapterName = nil; switch (recipeChapterValue) { case 4: chapterName = @"Getting started: Neutral evolution in a panmictic population"; break; case 5: chapterName = @"Demography and population structure"; break; case 6: chapterName = @"Mutation types, genomic elements, and chromosome structure"; break; case 7: chapterName = @"SLiMgui visualizations for polymorphism patterns"; break; case 8: chapterName = @"Reproduction, meiosis, and multiple chromosomes"; break; case 9: chapterName = @"Selective sweeps"; break; case 10:chapterName = @"Context-dependent selection using mutationEffect() callbacks"; break; case 11:chapterName = @"Complex mating schemes using mateChoice() callbacks"; break; case 12:chapterName = @"Direct child modifications using modifyChild() callbacks"; break; case 13:chapterName = @"Phenotypes, fitness functions, quantitative traits, and QTLs"; break; case 14:chapterName = @"Advanced WF models"; break; case 15:chapterName = @"Going beyond Wright-Fisher models: nonWF model recipes"; break; case 16:chapterName = @"Advanced nonWF techniques for managing reproduction"; break; case 17:chapterName = @"Continuous-space models, interactions, and spatial maps"; break; case 18:chapterName = @"Tree-sequence recording: tracking population history"; break; case 19:chapterName = @"Modeling explicit nucleotides"; break; case 20:chapterName = @"Multispecies modeling"; break; case 23:chapterName = @"Parallel SLiM: Running SLiM multithreaded"; break; default: break; } if (chapterName) { NSString *fullChapterName = [NSString stringWithFormat:@"%d – %@", recipeChapterValue, chapterName]; NSMenuItem *mainItem = [openRecipesMenu addItemWithTitle:fullChapterName action:NULL keyEquivalent:@""]; chapterSubmenu = [[[NSMenu alloc] init] autorelease]; [mainItem setSubmenu:chapterSubmenu]; } else { NSLog(@"unrecognized chapter value %d", recipeChapterValue); NSBeep(); break; } } // Move on to the current chapter previousItemChapter = recipeChapter; // And now add the menu item for the recipe NSMenuItem *menuItem = [chapterSubmenu addItemWithTitle:recipeName action:@selector(openRecipe:) keyEquivalent:@""]; [menuItem setTarget:[NSDocumentController sharedDocumentController]]; } } - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { // Determine whether we were launched from a shell or from something else (Finder, Xcode, etc.) launchedFromShell_ = (isatty(fileno(stdin)) == 1) && !SLiM_AmIBeingDebugged(); //NSLog(@"launched from %@", launchedFromShell_ ? @"shell" : @"Finder"); // Require light appearance, at least for now; supporting dark mode would require custom art etc. if ([NSApp respondsToSelector:@selector(setAppearance:)]) [NSApp setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameAqua]]; // Install our custom beep handler Eidos_Beep = &Eidos_Beep_MACOS; // Warm up our back ends before anything else happens, including our own class objects #ifdef _OPENMP // Multithreading in SLiMguiLegacy is not for end user use; this is for testing/debugging only. // We always use 4 threads; we don't want to hog the whole machine, just run with a couple threads. // We pass false for active_threads to let the worker threads sleep, otherwise the CPU is pegged // the whole time SLiMgui is running, even when sitting idle. Eidos_WarmUpOpenMP(&std::cout, true, 4, false, /* default per-task thread counts */ ""); #endif SLiM_ConfigureContext(); Eidos_WarmUp(); SLiM_WarmUp(); gSLiM_Plot_Class = new Plot_Class(gStr_Plot, gEidosDictionaryUnretained_Class); gSLiM_Plot_Class->CacheDispatchTables(); gSLiM_SLiMgui_Class = new SLiMgui_Class(gStr_SLiMgui, gEidosDictionaryUnretained_Class); gSLiM_SLiMgui_Class->CacheDispatchTables(); // QtSLiM frees the RNG that Eidos_WarmUp() just made, here. We could do that too; it would // do no harm. But with the initialization order in SLiMguiLegacy, it will be freed by // startNewSimulationFromScript anyway, before any problems arise. // Remember our current working directory, to return to whenever we are not inside SLiM/Eidos app_cwd_ = Eidos_CurrentDirectory(); //NSLog(@"current directory == %s", app_cwd_.c_str()); // Create the Open Recipes menu [self setUpRecipesMenu]; } - (std::string &)SLiMguiCurrentWorkingDirectory { return app_cwd_; } - (bool)launchedFromShell { return launchedFromShell_; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // check for required fonts, to prevent problems downstream NSFont *times = [NSFont fontWithName:@"Times New Roman" size:13.0]; NSFont *menlo = [NSFont fontWithName:@"Menlo" size:13.0]; NSFont *optima = [NSFont fontWithName:@"Optima" size:13.0]; NSFont *optima_b = [NSFont fontWithName:@"Optima-Bold" size:13.0]; NSFont *optima_i = [NSFont fontWithName:@"Optima-Italic" size:13.0]; NSString *fontName = nil; if (!times) fontName = @"Times New Roman"; else if (!menlo) fontName = @"Menlo"; else if (!optima) fontName = @"Optima"; else if (!optima_b) fontName = @"Optima-Bold"; else if (!optima_i) fontName = @"Optima-Italic"; if (fontName) { NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleCritical]; [alert setMessageText:@"Missing font"]; [alert setInformativeText:[NSString stringWithFormat:@"The standard system font %@ is required for SLiMgui to run, but appears to be missing. Please check your macOS installation.\n\nSLiMgui will now exit.", fontName]]; [alert addButtonWithTitle:@"OK"]; [alert runModal]; [[NSApplication sharedApplication] terminate:nil]; return; } // perform the launch action SLiMLaunchAction launchAction = (SLiMLaunchAction)[[NSUserDefaults standardUserDefaults] integerForKey:defaultsLaunchActionKey]; switch (launchAction) { case kLaunchDoNothing: break; case kLaunchNewScriptWindow: [self performSelector:@selector(openNewDocumentIfNeeded:) withObject:nil afterDelay:0.01]; break; case kLaunchRunOpenPanel: [[NSDocumentController sharedDocumentController] openDocument:nil]; break; } // The tips window has been removed so I don't have to maintain it, but the sources remain in the repository. BCH 3/3/2022 //[TipsWindowController showTipsWindowOnLaunch]; } - (void)openNewDocumentIfNeeded:(id)sender { NSUInteger documentCount = [[[NSDocumentController sharedDocumentController] documents] count]; // Open an untitled document if there is no document (restored, opened) if (documentCount == 0) { SLiMDocument *doc = [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:YES error:NULL]; [doc setTransient:YES]; [[doc slimWindowController] displayStartupMessage]; } } - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender { // Prevent AppKit from opening a new untitled window on launch. We do this because we do this ourselves // instead; and we do it ourselves instead because Apple seems to have screwed it up. (I.e., sometimes // this method does not get called even when no other document is being restored.) return NO; } - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag { // Prevent a new untitled window from opening if we are already running, have no open windows, and are activated. // This is non-standard behavior, but I really hate the standard behavior, so. return NO; } - (void)applicationWillTerminate:(NSNotification *)aNotification { } - (IBAction)resetSuppressionFlags:(id)sender { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults removeObjectForKey:EidosDefaultsSuppressScriptCheckSuccessPanelKey]; // The tips window has been removed so I don't have to maintain it, but the sources remain in the repository. BCH 3/3/2022 //[defaults removeObjectForKey:SLiMDefaultsShowTipsPanelKey]; //[defaults removeObjectForKey:SLiMDefaultsTipsIndexKey]; } // // About window management // - (IBAction)showAboutWindow:(id)sender { [[NSBundle mainBundle] loadNibNamed:@"AboutWindow" owner:self topLevelObjects:NULL]; // The window is the top-level object in this nib. It will release itself when closed, so we will retain it on its behalf here. // Note that the aboutWindow and aboutWebView outlets do not get zeroed out when the about window closes; but we only use them here. [aboutWindow retain]; // // And we need to make our WebView load our README.html file // NSString *readmeURL = [[[NSBundle mainBundle] URLForResource:@"README" withExtension:@"html"] absoluteString]; // // [aboutWebView setShouldCloseWithWindow:YES]; // [aboutWebView setMainFrameURL:readmeURL]; // Set our version number string NSString *bundleVersionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; NSString *versionString = [NSString stringWithFormat:@"version %@", bundleVersionString]; [aboutVersionTextField setStringValue:versionString]; // Fix up hyperlinks [messerLabLineTextField eidosSetHyperlink:[NSURL URLWithString:@"http://messerlab.org/slim/"] onText:@"http://messerlab.org/slim/"]; [benHallerLineTextField eidosSetHyperlink:[NSURL URLWithString:@"http://benhaller.com/"] onText:@"http://benhaller.com/"]; [licenseTextField eidosSetHyperlink:[NSURL URLWithString:@"http://www.gnu.org/licenses/"] onText:@"http://www.gnu.org/licenses/"]; // Now that everything is set up, show the window [aboutWindow makeKeyAndOrderFront:nil]; } - (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id )listener { if ([actionInformation objectForKey:WebActionElementKey]) { [listener ignore]; [[NSWorkspace sharedWorkspace] openURL:[request URL]]; } else { [listener use]; } } // // Help // - (IBAction)showHelp:(id)sender { /* if (!helpWindow) { [[NSBundle mainBundle] loadNibNamed:@"HelpWindow" owner:self topLevelObjects:NULL]; // The window is the top-level object in this nib. It will not release itself when closed, so it will stay around forever // (to improve reopening speed). We retain it here on its behalf. [helpWindow retain]; // And we need to make our WebView load our README.html file NSURL *manualURL = [[NSBundle mainBundle] URLForResource:@"SLiM_manual" withExtension:@"pdf"]; [helpPDFView setDocument:[[[PDFDocument alloc] initWithURL:manualURL] autorelease]]; } // Now that everything is set up, show the window [helpWindow makeKeyAndOrderFront:nil]; */ // We used to show the SLiM 1.8 manual as a PDF in a separate window. That code is commented out above. // I've left HelpWindow.xib in the project, in case we want to go back to doing that sort of thing; but // for now, we show the script help window. [[EidosHelpController sharedController] showWindow]; } - (IBAction)sendFeedback:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"mailto:philipp.messer@gmail.com?subject=SLiM%20Feedback"]]; } - (IBAction)slimWorkshops:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://benhaller.com/workshops/workshops.html"]]; } - (IBAction)mailingListAnnounce:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://groups.google.com/d/forum/slim-announce"]]; } - (IBAction)mailingListDiscuss:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://groups.google.com/d/forum/slim-discuss"]]; } - (IBAction)showSlimHomePage:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://messerlab.org/slim/"]]; } - (IBAction)showSlimExtrasPage:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/MesserLab/SLiM-Extras"]]; } - (IBAction)showSlimPublication:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://doi.org/10.1093/molbev/msw211"]]; } - (IBAction)showMesserLab:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://messerlab.org/"]]; } - (IBAction)showBenHaller:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.benhaller.com/"]]; } - (IBAction)showStickSoftware:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sticksoftware.com/"]]; } // Dummy actions; see validateMenuItem: - (IBAction)play:(id)sender {} - (IBAction)profile:(id)sender {} - (IBAction)toggleConsoleVisibility:(id)sender {} - (IBAction)toggleBrowserVisibility:(id)sender {} - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; // Handle validation for menu items that really belong to SLiMWindowController. This provides a default validation // for these menu items when no SLiMWindowController is receiving. The point is to reset the titles of these menu // items back to their base state, not just to disable them (which would happen anyway). if (sel == @selector(play:)) { [menuItem setTitle:@"Play"]; return NO; } if (sel == @selector(profile:)) { [menuItem setTitle:@"Profile"]; return NO; } if (sel == @selector(toggleConsoleVisibility:)) { [menuItem setTitle:@"Show Eidos Console"]; return NO; } if (sel == @selector(toggleBrowserVisibility:)) { [menuItem setTitle:@"Show Variable Browser"]; return NO; } return YES; } @end ================================================ FILE: SLiMgui/Base.lproj/AboutWindow.xib ================================================ SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with SLiM. If not, see http://www.gnu.org/licenses/. Please see the SLiM manual for credits and license information for code that has been incorporated into SLiM from other authors. ================================================ FILE: SLiMgui/Base.lproj/FindRecipePanel.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/GraphAxisRescaleSheet.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/GraphBarRescaleSheet.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/GraphWindow.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/HelpWindow.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/MainMenu.xib ================================================ DQ DQ ================================================ FILE: SLiMgui/Base.lproj/ProfileReport.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/SLiMWindow.xib ================================================ NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean NSNegateBoolean ================================================ FILE: SLiMgui/Base.lproj/ScriptModSheet.xib ================================================ This action cannot be executed while a simulation is running; a recycle will be necessary for the action to take effect. ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_AddGenomicElement.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_AddGenomicElementType.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_AddMutationType.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_AddRecombinationRate.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_AddSexConfiguration.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_AddSubpop.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_ChangeCloning.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_ChangeMigration.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_ChangeSelfing.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_ChangeSexRatio.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_ChangeSubpopSize.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_OutputFixedMutations.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_OutputFullPopulation.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_OutputSubpopSample.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_RemoveSubpop.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/ScriptMod_SplitSubpop.xib ================================================ ================================================ FILE: SLiMgui/Base.lproj/TipsWindow.xib ================================================ ================================================ FILE: SLiMgui/ChromosomeView.h ================================================ // // ChromosomeView.h // SLiM // // Created by Ben Haller on 1/21/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #import "CocoaExtra.h" #include "slim_globals.h" #include class Chromosome; extern NSString *SLiMChromosomeSelectionChangedNotification; @interface ChromosomeView : NSView { @public // Display options std::vector display_muttypes_; // if empty, display all mutation types; otherwise, display only the muttypes chosen } @property (nonatomic) BOOL enabled; @property (nonatomic) BOOL overview; @property (nonatomic) BOOL shouldDrawGenomicElements; @property (nonatomic) BOOL shouldDrawRateMaps; @property (nonatomic) BOOL shouldDrawMutations; @property (nonatomic) BOOL shouldDrawFixedSubstitutions; - (NSRange)displayedRangeForChromosome:(Chromosome *)chromosome; // the full chromosome range - (void)setNeedsDisplayInInterior; // set to need display only within the interior; avoid redrawing ticks unnecessarily @end ================================================ FILE: SLiMgui/ChromosomeView.mm ================================================ // // ChromosomeView.m // SLiM // // Created by Ben Haller on 1/21/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "ChromosomeView.h" #import "SLiMWindowController.h" #import "CocoaExtra.h" #include "community.h" NSString *SLiMChromosomeSelectionChangedNotification = @"SLiMChromosomeSelectionChangedNotification"; static NSDictionary *tickAttrs = nil; static const int numberOfTicksPlusOne = 4; static const int tickLength = 5; static const int heightForTicks = 16; static const int spaceBetweenChromosomes = 5; @implementation ChromosomeView @synthesize shouldDrawGenomicElements, shouldDrawFixedSubstitutions, shouldDrawMutations, shouldDrawRateMaps; + (void)initialize { if (!tickAttrs) tickAttrs = [@{NSForegroundColorAttributeName : [NSColor blackColor], NSFontAttributeName : [NSFont systemFontOfSize:9.0]} retain]; [self exposeBinding:@"enabled"]; } - (id)initWithFrame:(NSRect)frameRect { if (self = [super initWithFrame:frameRect]) { // this is not called for objects in the nib (I imagine initWithCoder: is instead); put stuff in awakeFromNib } return self; } - (void)setEnabled:(BOOL)enabled { if (_enabled != enabled) { _enabled = enabled; [self setNeedsDisplayAll]; } } - (void)setOverview:(BOOL)overview { if (_overview != overview) { _overview = overview; [self setNeedsDisplayAll]; } } - (void)awakeFromNib { display_muttypes_.clear(); [self bind:@"enabled" toObject:[[self window] windowController] withKeyPath:@"invalidSimulation" options:@{NSValueTransformerNameBindingOption : NSNegateBooleanTransformerName}]; } - (void)dealloc { [self unbind:@"enabled"]; [super dealloc]; } - (NSRange)displayedRangeForChromosome:(Chromosome *)chromosome { slim_position_t chromosomeLastPosition = chromosome->last_position_; return NSMakeRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed } - (NSRect)rectEncompassingBase:(slim_position_t)startBase toBase:(slim_position_t)endBase interiorRect:(NSRect)interiorRect displayedRange:(NSRange)displayedRange { double startFraction = (startBase - (slim_position_t)displayedRange.location) / (double)(displayedRange.length); double leftEdgeDouble = interiorRect.origin.x + startFraction * interiorRect.size.width; double endFraction = (endBase + 1 - (slim_position_t)displayedRange.location) / (double)(displayedRange.length); double rightEdgeDouble = interiorRect.origin.x + endFraction * interiorRect.size.width; int leftEdge, rightEdge; if (rightEdgeDouble - leftEdgeDouble > 1.0) { // If the range spans a width of more than one pixel, then use the maximal pixel range leftEdge = (int)floor(leftEdgeDouble); rightEdge = (int)ceil(rightEdgeDouble); } else { // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the left-right positions span a pixel boundary leftEdge = (int)floor(leftEdgeDouble); rightEdge = leftEdge + 1; } return NSMakeRect(leftEdge, interiorRect.origin.y, rightEdge - leftEdge, interiorRect.size.height); } - (NSRect)contentRect { NSRect bounds = [self bounds]; // Two things are going on here. The width gets inset by two pixels on each side because our frame is outset that much from our apparent frame, to // make room for the selection knobs to spill over a bit. The height gets adjusted because our "content rect" does not include our ticks. if ([self overview]) return NSMakeRect(bounds.origin.x + 2, bounds.origin.y, bounds.size.width - 4, bounds.size.height); else return NSMakeRect(bounds.origin.x + 2, bounds.origin.y + heightForTicks, bounds.size.width - 4, bounds.size.height - heightForTicks); } - (NSRect)interiorRect { return NSInsetRect([self contentRect], 1, 1); } - (void)setNeedsDisplayAll { [self setNeedsDisplay:YES]; } - (void)setNeedsDisplayInInterior { [self setNeedsDisplayInRect:[self interiorRect]]; } // This is a fast macro for when all we need is the offset of a base from the left edge of interiorRect; interiorRect.origin.x is not added here! // This is based on the same math as rectEncompassingBase:toBase:interiorRect:displayedRange: above, and must be kept in synch with that method. #define LEFT_OFFSET_OF_BASE(startBase, interiorRect, displayedRange) ((int)floor(((startBase - (slim_position_t)displayedRange.location) / (double)(displayedRange.length)) * interiorRect.size.width)) - (slim_position_t)baseForPosition:(double)position interiorRect:(NSRect)interiorRect displayedRange:(NSRange)displayedRange { double fraction = (position - interiorRect.origin.x) / interiorRect.size.width; slim_position_t base = (slim_position_t)floor(fraction * (displayedRange.length + 1) + displayedRange.location); return base; } - (void)drawTicksInContentRect:(NSRect)contentRect withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange { NSRect interiorRect = NSInsetRect(contentRect, 1, 1); if (displayedRange.length == 0) { // Handle the "no genetics" case separately if (![self overview]) { NSAttributedString *tickAttrLabel = [[NSAttributedString alloc] initWithString:@"no genetics" attributes:tickAttrs]; NSSize tickLabelSize = [tickAttrLabel size]; int tickLabelX = (int)floor(interiorRect.origin.x + (interiorRect.size.width - tickLabelSize.width) / 2.0); [tickAttrLabel drawAtPoint:NSMakePoint(tickLabelX, contentRect.origin.y - 14)]; [tickAttrLabel release]; } return; } int64_t lastTickIndex = numberOfTicksPlusOne; // Display fewer ticks when we are displaying a very small number of positions lastTickIndex = MIN(lastTickIndex, ((int64_t)displayedRange.length + 1) / 3); double tickIndexDivisor = ((lastTickIndex == 0) ? 1.0 : (double)lastTickIndex); // avoid a divide by zero when we are displaying a single site // BCH 12/25/2024: Start by measuring the tick labels and figuring out who fits. FIXME we could be even smarter // and switch to scientific notation if things get too crowded, but I'll leave that for another day. double widthAllLabels = 0.0, widthLeftRightLabels = 0.0, widthRightLabelOnly = 0.0; for (int tickIndex = 0; tickIndex <= lastTickIndex; ++tickIndex) { slim_position_t tickBase = (slim_position_t)displayedRange.location + (slim_position_t)ceil((displayedRange.length - 1) * (tickIndex / tickIndexDivisor)); // -1 because we are choosing an in-between-base position that falls, at most, to the left of the last base NSString *tickLabel; if (tickBase >= 1e10) tickLabel = [NSString stringWithFormat:@"%.6e", (double)tickBase]; else tickLabel = [NSString stringWithFormat:@"%lld", (long long int)tickBase]; NSAttributedString *tickAttrLabel = [[NSAttributedString alloc] initWithString:tickLabel attributes:tickAttrs]; NSSize tickLabelSize = [tickAttrLabel size]; int tickLabelWidth = 5 + (int)tickLabelSize.width; [tickAttrLabel release]; widthAllLabels += tickLabelWidth; if (tickIndex == 0) { widthLeftRightLabels += tickLabelWidth; } if (tickIndex == lastTickIndex) { widthLeftRightLabels += tickLabelWidth; widthRightLabelOnly += tickLabelWidth; } } for (int tickIndex = 0; tickIndex <= lastTickIndex; ++tickIndex) { // BCH 12/25/2024: If we're not going to draw the middle tick labels, skip their tick marks also; // and if we're not going to draw any tick labels at all, then skip drawing all the tick marks int interiorWidth = (int)interiorRect.size.width; if ((widthRightLabelOnly > interiorWidth) || ((widthAllLabels > interiorWidth) && (tickIndex != 0) && (tickIndex != lastTickIndex))) continue; slim_position_t tickBase = (slim_position_t)displayedRange.location + (slim_position_t)ceil((displayedRange.length - 1) * (tickIndex / tickIndexDivisor)); // -1 because we are choosing an in-between-base position that falls, at most, to the left of the last base NSRect tickRect = [self rectEncompassingBase:tickBase toBase:tickBase interiorRect:interiorRect displayedRange:displayedRange]; tickRect.origin.y = contentRect.origin.y - tickLength; tickRect.size.height = tickLength; // if we are displaying a single site or two sites, make a tick mark one pixel wide, rather than a very wide one, which looks weird if (displayedRange.length <= 2) { tickRect.origin.x = (int)floor(tickRect.origin.x + tickRect.size.width / 2.0 - 0.5); tickRect.size.width = 1.0; } [[NSColor colorWithCalibratedWhite:0.5 alpha:1.0] set]; NSRectFill(tickRect); // BCH 12/25/2024: Using the measurements taken above, decide whether to draw this tick label or not if (widthAllLabels > interiorWidth) { if ((tickIndex != 0) && (tickIndex != lastTickIndex)) continue; if (widthLeftRightLabels > interiorWidth) { if (tickIndex != lastTickIndex) continue; if (widthRightLabelOnly > interiorWidth) continue; } } // BCH 15 May 2018: display in scientific notation for positions at or above 1e10, as it gets a bit ridiculous... NSString *tickLabel; if (tickBase >= 1e10) tickLabel = [NSString stringWithFormat:@"%.6e", (double)tickBase]; else tickLabel = [NSString stringWithFormat:@"%lld", (long long int)tickBase]; NSAttributedString *tickAttrLabel = [[NSAttributedString alloc] initWithString:tickLabel attributes:tickAttrs]; NSSize tickLabelSize = [tickAttrLabel size]; int tickLabelX = (int)floor(tickRect.origin.x + tickRect.size.width / 2.0); BOOL forceCenteredLabel = (displayedRange.length <= 101); // a selected subrange is never <=101 length, so this is safe even with large chromosomes if ((tickIndex == lastTickIndex) && !forceCenteredLabel) tickLabelX -= (int)round(tickLabelSize.width - 2); else if ((tickIndex > 0) || forceCenteredLabel) tickLabelX -= (int)round(tickLabelSize.width / 2.0); [tickAttrLabel drawAtPoint:NSMakePoint(tickLabelX, contentRect.origin.y - (tickLength + 12))]; [tickAttrLabel release]; } } - (void)drawGenomicElementsInInteriorRect:(NSRect)interiorRect chromosome:(Chromosome *)chromosome withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange { CGFloat previousIntervalLeftEdge = -10000; for (GenomicElement *genomicElement : chromosome->GenomicElements()) { slim_position_t startPosition = genomicElement->start_position_; slim_position_t endPosition = genomicElement->end_position_; NSRect elementRect = [self rectEncompassingBase:startPosition toBase:endPosition interiorRect:interiorRect displayedRange:displayedRange]; BOOL widthOne = (elementRect.size.width == 1); // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. if (!widthOne && (elementRect.origin.x == previousIntervalLeftEdge)) { elementRect.origin.x++; elementRect.size.width--; } // draw only the visible part, if any elementRect = NSIntersectionRect(elementRect, interiorRect); if (!NSIsEmptyRect(elementRect)) { GenomicElementType *geType = genomicElement->genomic_element_type_ptr_; NSColor *elementColor = [controller colorForGenomicElementType:geType withID:geType->genomic_element_type_id_]; [elementColor set]; NSRectFill(elementRect); // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location if (widthOne) previousIntervalLeftEdge = elementRect.origin.x; else previousIntervalLeftEdge = -10000; } } } - (void)_drawRateMapIntervalsInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange ends:(std::vector &)ends rates:(std::vector &)rates { int recombinationIntervalCount = (int)ends.size(); slim_position_t intervalStartPosition = 0; CGFloat previousIntervalLeftEdge = -10000; for (int interval = 0; interval < recombinationIntervalCount; ++interval) { slim_position_t intervalEndPosition = ends[interval]; double intervalRate = rates[interval]; NSRect intervalRect = [self rectEncompassingBase:intervalStartPosition toBase:intervalEndPosition interiorRect:interiorRect displayedRange:displayedRange]; BOOL widthOne = (intervalRect.size.width == 1); // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. if (!widthOne && (intervalRect.origin.x == previousIntervalLeftEdge)) { intervalRect.origin.x++; intervalRect.size.width--; } // draw only the visible part, if any intervalRect = NSIntersectionRect(intervalRect, interiorRect); if (!NSIsEmptyRect(intervalRect)) { // color according to how "hot" the region is double brightness = 0.0; double saturation = 1.0; if (intervalRate > 0.0) { if (intervalRate > 1.0e-8) { if (intervalRate < 5.0e-8) { brightness = 0.5 + 0.5 * ((intervalRate - 1.0e-8) / 4.0e-8); } else { brightness = 1.0; if (intervalRate < 1.0e-7) saturation = 1.0 - ((intervalRate - 5.0e-8) * 2.0e7); else saturation = 0.0; } } else { brightness = 0.5; } } NSColor *intervalColor = [NSColor colorWithCalibratedHue:0.65 saturation:saturation brightness:brightness alpha:1.0]; [intervalColor set]; NSRectFill(intervalRect); // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location if (widthOne) previousIntervalLeftEdge = intervalRect.origin.x; else previousIntervalLeftEdge = -10000; } // the next interval starts at the next base after this one ended intervalStartPosition = intervalEndPosition + 1; } } - (void)drawRecombinationIntervalsInInteriorRect:(NSRect)interiorRect chromosome:(Chromosome *)chromosome withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange { if (chromosome->single_recombination_map_) { [self _drawRateMapIntervalsInInteriorRect:interiorRect withController:controller displayedRange:displayedRange ends:chromosome->recombination_end_positions_H_ rates:chromosome->recombination_rates_H_]; } else { NSRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; CGFloat halfHeight = ceil(interiorRect.size.height / 2.0); CGFloat remainingHeight = interiorRect.size.height - halfHeight; topInteriorRect.size.height = halfHeight; topInteriorRect.origin.y += remainingHeight; bottomInteriorRect.size.height = remainingHeight; [self _drawRateMapIntervalsInInteriorRect:topInteriorRect withController:controller displayedRange:displayedRange ends:chromosome->recombination_end_positions_M_ rates:chromosome->recombination_rates_M_]; [self _drawRateMapIntervalsInInteriorRect:bottomInteriorRect withController:controller displayedRange:displayedRange ends:chromosome->recombination_end_positions_F_ rates:chromosome->recombination_rates_F_]; } } - (void)drawMutationIntervalsInInteriorRect:(NSRect)interiorRect chromosome:(Chromosome *)chromosome withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange { if (chromosome->single_mutation_map_) { [self _drawRateMapIntervalsInInteriorRect:interiorRect withController:controller displayedRange:displayedRange ends:chromosome->mutation_end_positions_H_ rates:chromosome->mutation_rates_H_]; } else { NSRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; CGFloat halfHeight = ceil(interiorRect.size.height / 2.0); CGFloat remainingHeight = interiorRect.size.height - halfHeight; topInteriorRect.size.height = halfHeight; topInteriorRect.origin.y += remainingHeight; bottomInteriorRect.size.height = remainingHeight; [self _drawRateMapIntervalsInInteriorRect:topInteriorRect withController:controller displayedRange:displayedRange ends:chromosome->mutation_end_positions_M_ rates:chromosome->mutation_rates_M_]; [self _drawRateMapIntervalsInInteriorRect:bottomInteriorRect withController:controller displayedRange:displayedRange ends:chromosome->mutation_end_positions_F_ rates:chromosome->mutation_rates_F_]; } } - (void)drawRateMapsInInteriorRect:(NSRect)interiorRect chromosome:(Chromosome *)chromosome withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange { BOOL recombinationWorthShowing = NO; BOOL mutationWorthShowing = NO; if (chromosome->single_mutation_map_) mutationWorthShowing = (chromosome->mutation_end_positions_H_.size() > 1); else mutationWorthShowing = ((chromosome->mutation_end_positions_M_.size() > 1) || (chromosome->mutation_end_positions_F_.size() > 1)); if (chromosome->single_recombination_map_) recombinationWorthShowing = (chromosome->recombination_end_positions_H_.size() > 1); else recombinationWorthShowing = ((chromosome->recombination_end_positions_M_.size() > 1) || (chromosome->recombination_end_positions_F_.size() > 1)); // If neither map is worth showing, we show just the recombination map, to mirror the behavior of 2.4 and earlier if ((!mutationWorthShowing && !recombinationWorthShowing) || (!mutationWorthShowing && recombinationWorthShowing)) { [self drawRecombinationIntervalsInInteriorRect:interiorRect chromosome:chromosome withController:controller displayedRange:displayedRange]; } else if (mutationWorthShowing && !recombinationWorthShowing) { [self drawMutationIntervalsInInteriorRect:interiorRect chromosome:chromosome withController:controller displayedRange:displayedRange]; } else // mutationWorthShowing && recombinationWorthShowing { NSRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; CGFloat halfHeight = ceil(interiorRect.size.height / 2.0); CGFloat remainingHeight = interiorRect.size.height - halfHeight; topInteriorRect.size.height = halfHeight; topInteriorRect.origin.y += remainingHeight; bottomInteriorRect.size.height = remainingHeight; [self drawRecombinationIntervalsInInteriorRect:topInteriorRect chromosome:chromosome withController:controller displayedRange:displayedRange]; [self drawMutationIntervalsInInteriorRect:bottomInteriorRect chromosome:chromosome withController:controller displayedRange:displayedRange]; } } - (void)drawFixedSubstitutionsInInteriorRect:(NSRect)interiorRect chromosome:(Chromosome *)chromosome withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange { double scalingFactor = 0.8; // used to be controller->selectionColorScale; Species *displaySpecies = [controller focalDisplaySpecies]; Population &pop = displaySpecies->population_; bool chromosomeHasDefaultColor = !chromosome->color_sub_.empty(); float colorRed = 0.2f, colorGreen = 0.2f, colorBlue = 1.0f; if (chromosomeHasDefaultColor) { colorRed = chromosome->color_sub_red_; colorGreen = chromosome->color_sub_green_; colorBlue = chromosome->color_sub_blue_; } [[NSColor colorWithCalibratedRed:colorRed green:colorGreen blue:colorBlue alpha:1.0] set]; std::vector &substitutions = pop.substitutions_; if ((substitutions.size() < 1000) || (displayedRange.length < interiorRect.size.width)) { // This is the simple version of the display code, avoiding the memory allocations and such for (const Substitution *substitution : substitutions) { if (substitution->mutation_type_ptr_->mutation_type_displayed_) { slim_position_t substitutionPosition = substitution->position_; NSRect substitutionTickRect = [self rectEncompassingBase:substitutionPosition toBase:substitutionPosition interiorRect:interiorRect displayedRange:displayedRange]; if (!shouldDrawMutations || !chromosomeHasDefaultColor) { // If we're drawing mutations as well, then substitutions just get colored blue, to contrast // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations const MutationType *mutType = substitution->mutation_type_ptr_; if (!mutType->color_sub_.empty()) { [[NSColor colorWithCalibratedRed:mutType->color_sub_red_ green:mutType->color_sub_green_ blue:mutType->color_sub_blue_ alpha:1.0] set]; } else { RGBForSelectionCoeff(substitution->selection_coeff_, &colorRed, &colorGreen, &colorBlue, scalingFactor); [[NSColor colorWithCalibratedRed:colorRed green:colorGreen blue:colorBlue alpha:1.0] set]; } } NSRectFill(substitutionTickRect); } } } else { // We have a lot of substitutions, so do a radix sort, as we do in drawMutationsInInteriorRect: below. int displayPixelWidth = (int)interiorRect.size.width; const Substitution **subBuffer = (const Substitution **)calloc(displayPixelWidth, sizeof(Substitution *)); for (const Substitution *substitution : substitutions) { if (substitution->mutation_type_ptr_->mutation_type_displayed_) { slim_position_t substitutionPosition = substitution->position_; double startFraction = (substitutionPosition - (slim_position_t)displayedRange.location) / (double)(displayedRange.length); int xPos = (int)floor(startFraction * interiorRect.size.width); if ((xPos >= 0) && (xPos < displayPixelWidth)) subBuffer[xPos] = substitution; } } if (shouldDrawMutations && chromosomeHasDefaultColor) { // If we're drawing mutations as well, then substitutions just get colored blue, to contrast NSRect mutationTickRect = NSMakeRect(interiorRect.origin.x, interiorRect.origin.y, 1, interiorRect.size.height); for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { const Substitution *substitution = subBuffer[binIndex]; if (substitution) { mutationTickRect.origin.x = interiorRect.origin.x + binIndex; mutationTickRect.size.width = 1; // consolidate adjacent lines together, since they are all the same color while ((binIndex + 1 < displayPixelWidth) && subBuffer[binIndex + 1]) { mutationTickRect.size.width++; binIndex++; } NSRectFill(mutationTickRect); } } } else { // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations NSRect mutationTickRect = NSMakeRect(interiorRect.origin.x, interiorRect.origin.y, 1, interiorRect.size.height); for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { const Substitution *substitution = subBuffer[binIndex]; if (substitution) { const MutationType *mutType = substitution->mutation_type_ptr_; if (!mutType->color_sub_.empty()) { [[NSColor colorWithCalibratedRed:mutType->color_sub_red_ green:mutType->color_sub_green_ blue:mutType->color_sub_blue_ alpha:1.0] set]; } else { RGBForSelectionCoeff(substitution->selection_coeff_, &colorRed, &colorGreen, &colorBlue, scalingFactor); [[NSColor colorWithCalibratedRed:colorRed green:colorGreen blue:colorBlue alpha:1.0] set]; } mutationTickRect.origin.x = interiorRect.origin.x + binIndex; NSRectFill(mutationTickRect); } } } free(subBuffer); } } - (void)updateDisplayedMutationTypes { // We use a flag in MutationType to indicate whether we're drawing that type or not; we update those flags here, // before every drawing of mutations, from the vector of mutation type IDs that we keep internally SLiMWindowController *controller = (SLiMWindowController *)[[self window] windowController]; if (controller) { Species *displaySpecies = [controller focalDisplaySpecies]; if (displaySpecies) { std::map &muttypes = displaySpecies->mutation_types_; for (auto muttype_iter : muttypes) { MutationType *muttype = muttype_iter.second; if (display_muttypes_.size()) { slim_objectid_t muttype_id = muttype->mutation_type_id_; muttype->mutation_type_displayed_ = (std::find(display_muttypes_.begin(), display_muttypes_.end(), muttype_id) != display_muttypes_.end()); } else { muttype->mutation_type_displayed_ = true; } } } } } - (void)drawMutationsInInteriorRect:(NSRect)interiorRect chromosome:(Chromosome *)chromosome withController:(SLiMWindowController *)controller displayedRange:(NSRange)displayedRange { double scalingFactor = 0.8; // used to be controller->selectionColorScale; Species *displaySpecies = [controller focalDisplaySpecies]; slim_chromosome_index_t chromosome_index = chromosome->Index(); Population &pop = displaySpecies->population_; double totalHaplosomeCount = chromosome->gui_total_haplosome_count_; // this includes only haplosomes in the selected subpopulations int registry_size; const MutationIndex *registry = pop.MutationRegistry(®istry_size); Mutation *mut_block_ptr = gSLiM_Mutation_Block; if ((registry_size < 1000) || (displayedRange.length < interiorRect.size.width)) { // This is the simple version of the display code, avoiding the memory allocations and such for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; if (mutation->chromosome_index_ == chromosome_index) // display only mutations in the displayed chromosome { const MutationType *mutType = mutation->mutation_type_ptr_; if (mutType->mutation_type_displayed_) { slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; if (!mutType->color_.empty()) { [[NSColor colorWithCalibratedRed:mutType->color_red_ green:mutType->color_green_ blue:mutType->color_blue_ alpha:1.0] set]; } else { float colorRed = 0.0, colorGreen = 0.0, colorBlue = 0.0; RGBForSelectionCoeff(mutation->selection_coeff_, &colorRed, &colorGreen, &colorBlue, scalingFactor); [[NSColor colorWithCalibratedRed:colorRed green:colorGreen blue:colorBlue alpha:1.0] set]; } mutationTickRect.size.height = (int)ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.size.height); NSRectFill(mutationTickRect); } } } } else { // We have a lot of mutations, so let's try to be smarter. It's hard to be smarter. The overhead from allocating the NSColors and such // is pretty negligible; practially all the time is spent in NSRectFill(). Unfortunately, NSRectFillListWithColors() provides basically // no speedup; Apple doesn't appear to have optimized it. So, here's what I came up with. For each mutation type that uses a fixed DFE, // and thus a fixed color, we can do a radix sort of mutations into bins corresponding to each pixel in our displayed image. Then we // can draw each bin just once, making one bar for the highest bar in that bin. Mutations from non-fixed DFEs, and mutations which have // had their selection coefficient changed, will be drawn at the end in the usual (slow) way. float colorRed = 0.0, colorGreen = 0.0, colorBlue = 0.0; int displayPixelWidth = (int)interiorRect.size.width; int16_t *heightBuffer = (int16_t *)malloc(displayPixelWidth * sizeof(int16_t)); bool *mutationsPlotted = (bool *)calloc(registry_size, sizeof(bool)); // faster than using gui_scratch_reference_count_ because of cache locality int64_t remainingMutations = registry_size; // First zero out the scratch refcount, which we use to track which mutations we have drawn already //for (int mutIndex = 0; mutIndex < mutationCount; ++mutIndex) // mutations[mutIndex]->gui_scratch_reference_count_ = 0; // Then loop through the declared mutation types for (auto mutationTypeIter : displaySpecies->mutation_types_) { MutationType *mut_type = mutationTypeIter.second; if (mut_type->mutation_type_displayed_) { bool mut_type_fixed_color = !mut_type->color_.empty(); // We optimize fixed-DFE mutation types only, and those using a fixed color set by the user if ((mut_type->dfe_type_ == DFEType::kFixed) || mut_type_fixed_color) { slim_selcoeff_t mut_type_selcoeff = (mut_type_fixed_color ? 0.0 : (slim_selcoeff_t)mut_type->dfe_parameters_[0]); EIDOS_BZERO(heightBuffer, displayPixelWidth * sizeof(int16_t)); // Scan through the mutation list for mutations of this type with the right selcoeff for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; if ((mutation->mutation_type_ptr_ == mut_type) && (mut_type_fixed_color || (mutation->selection_coeff_ == mut_type_selcoeff))) { if (mutation->chromosome_index_ == chromosome_index) { slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // includes only refs from the selected subpopulations slim_position_t mutationPosition = mutation->position_; //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); int16_t height = (int16_t)ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.size.height); if ((xPos >= 0) && (xPos < displayPixelWidth)) if (height > heightBuffer[xPos]) heightBuffer[xPos] = height; } // tally this mutation as handled //mutation->gui_scratch_reference_count_ = 1; mutationsPlotted[registry_index] = true; --remainingMutations; } } // Now draw all of the mutations we found, by looping through our radix bins if (mut_type_fixed_color) { [[NSColor colorWithCalibratedRed:mut_type->color_red_ green:mut_type->color_green_ blue:mut_type->color_blue_ alpha:1.0] set]; } else { RGBForSelectionCoeff(mut_type_selcoeff, &colorRed, &colorGreen, &colorBlue, scalingFactor); [[NSColor colorWithCalibratedRed:colorRed green:colorGreen blue:colorBlue alpha:1.0] set]; } for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { int height = heightBuffer[binIndex]; if (height) { NSRect mutationTickRect = NSMakeRect(interiorRect.origin.x + binIndex, interiorRect.origin.y, 1, height); NSRectFill(mutationTickRect); } } } } else { // We're not displaying this mutation type, so we need to mark off all the mutations belonging to it as handled for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; if (mutation->mutation_type_ptr_ == mut_type) { // tally this mutation as handled //mutation->gui_scratch_reference_count_ = 1; mutationsPlotted[registry_index] = true; --remainingMutations; } } } } // Draw any undrawn mutations on top; these are guaranteed not to use a fixed color set by the user, since those are all handled above if (remainingMutations) { if (remainingMutations < 1000) { // Plot the remainder by brute force, since there are not that many for (int registry_index = 0; registry_index < registry_size; ++registry_index) { //if (mutation->gui_scratch_reference_count_ == 0) if (!mutationsPlotted[registry_index]) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; if (mutation->chromosome_index_ == chromosome_index) { slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; mutationTickRect.size.height = (int)ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.size.height); RGBForSelectionCoeff(mutation->selection_coeff_, &colorRed, &colorGreen, &colorBlue, scalingFactor); [[NSColor colorWithCalibratedRed:colorRed green:colorGreen blue:colorBlue alpha:1.0] set]; NSRectFill(mutationTickRect); } } } } else { // OK, we have a lot of mutations left to draw. Here we will again use the radix sort trick, to keep track of only the tallest bar in each column MutationIndex *mutationBuffer = (MutationIndex *)calloc(displayPixelWidth, sizeof(MutationIndex)); EIDOS_BZERO(heightBuffer, displayPixelWidth * sizeof(int16_t)); // Find the tallest bar in each column for (int registry_index = 0; registry_index < registry_size; ++registry_index) { //if (mutation->gui_scratch_reference_count_ == 0) if (!mutationsPlotted[registry_index]) { MutationIndex mutationBlockIndex = registry[registry_index]; const Mutation *mutation = mut_block_ptr + mutationBlockIndex; if (mutation->chromosome_index_ == chromosome_index) { slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations slim_position_t mutationPosition = mutation->position_; //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); int16_t height = (int16_t)ceil((mutationRefCount / totalHaplosomeCount) * interiorRect.size.height); if ((xPos >= 0) && (xPos < displayPixelWidth)) { if (height > heightBuffer[xPos]) { heightBuffer[xPos] = height; mutationBuffer[xPos] = mutationBlockIndex; } } } } } // Now plot the bars for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) { int height = heightBuffer[binIndex]; if (height) { NSRect mutationTickRect = NSMakeRect(interiorRect.origin.x + binIndex, interiorRect.origin.y, 1, height); const Mutation *mutation = mut_block_ptr + mutationBuffer[binIndex]; RGBForSelectionCoeff(mutation->selection_coeff_, &colorRed, &colorGreen, &colorBlue, scalingFactor); [[NSColor colorWithCalibratedRed:colorRed green:colorGreen blue:colorBlue alpha:1.0] set]; NSRectFill(mutationTickRect); } } free(mutationBuffer); } } free(heightBuffer); free(mutationsPlotted); } } - (void)drawRect:(NSRect)dirtyRect { SLiMWindowController *controller = (SLiMWindowController *)[[self window] windowController]; bool ready = ([self enabled] && ![controller invalidSimulation]); NSRect contentRect = [self contentRect]; NSRect interiorRect = [self interiorRect]; // if the simulation is at tick 0, it is not ready if (ready) if (controller->community->Tick() == 0) ready = NO; if (ready) { Species *displaySpecies = [controller focalDisplaySpecies]; const std::vector &chromosomes = displaySpecies->Chromosomes(); int chromosomeCount = (int)chromosomes.size(); int64_t availableWidth = (int64_t)(contentRect.size.width - (chromosomeCount * 2) - (chromosomeCount - 1) * spaceBetweenChromosomes); int64_t totalLength = 0; for (Chromosome *chrom : chromosomes) { slim_position_t chromLength = (chrom->last_position_ + 1); totalLength += chromLength; } int64_t remainingLength = totalLength; int leftPosition = (int)contentRect.origin.x; for (Chromosome *chrom : chromosomes) { double scale = (double)availableWidth / remainingLength; slim_position_t chromLength = (chrom->last_position_ + 1); int width = (int)round(chromLength * scale); int paddedWidth = 2 + width; NSRect chromContentRect = NSMakeRect(leftPosition, contentRect.origin.y, paddedWidth, contentRect.size.height); NSRect chromInteriorRect = NSInsetRect(chromContentRect, 1, 1); // erase the content area itself [[NSColor colorWithCalibratedWhite:0.0 alpha:1.0] set]; NSRectFill(chromInteriorRect); NSRange displayedRange = [self displayedRangeForChromosome:chrom]; BOOL splitHeight = (shouldDrawRateMaps && shouldDrawGenomicElements); NSRect topInteriorRect = chromInteriorRect, bottomInteriorRect = chromInteriorRect; CGFloat halfHeight = ceil(chromInteriorRect.size.height / 2.0); CGFloat remainingHeight = chromInteriorRect.size.height - halfHeight; topInteriorRect.size.height = halfHeight; topInteriorRect.origin.y += remainingHeight; bottomInteriorRect.size.height = remainingHeight; // draw ticks at bottom of content rect if (![self overview] && !NSContainsRect(chromInteriorRect, dirtyRect)) [self drawTicksInContentRect:chromContentRect withController:controller displayedRange:displayedRange]; // draw recombination intervals in interior if (shouldDrawRateMaps) [self drawRateMapsInInteriorRect:(splitHeight ? topInteriorRect : chromInteriorRect) chromosome:chrom withController:controller displayedRange:displayedRange]; // draw genomic elements in interior if (shouldDrawGenomicElements) [self drawGenomicElementsInInteriorRect:(splitHeight ? bottomInteriorRect : chromInteriorRect) chromosome:chrom withController:controller displayedRange:displayedRange]; // figure out which mutation types we're displaying if (shouldDrawFixedSubstitutions || shouldDrawMutations) [self updateDisplayedMutationTypes]; // draw fixed substitutions in interior if (shouldDrawFixedSubstitutions) [self drawFixedSubstitutionsInInteriorRect:chromInteriorRect chromosome:chrom withController:controller displayedRange:displayedRange]; // draw mutations in interior if (shouldDrawMutations) [self drawMutationsInInteriorRect:chromInteriorRect chromosome:chrom withController:controller displayedRange:displayedRange]; // frame near the end, so that any roundoff errors that caused overdrawing by a pixel get cleaned up [[NSColor colorWithCalibratedWhite:0.6 alpha:1.0] set]; NSFrameRect(chromContentRect); leftPosition += (paddedWidth + spaceBetweenChromosomes); availableWidth -= width; remainingLength -= chromLength; } } else { // erase the content area itself [[NSColor colorWithCalibratedWhite:0.88 alpha:1.0] set]; NSRectFill(interiorRect); // frame [[NSColor colorWithCalibratedWhite:0.6 alpha:1.0] set]; NSFrameRect(contentRect); } } - (IBAction)filterMutations:(id)sender { NSMenuItem *senderMenuItem = (NSMenuItem *)sender; slim_objectid_t muttype_id = (int)[senderMenuItem tag]; if (muttype_id == -1) { display_muttypes_.clear(); } else { auto present_iter = std::find(display_muttypes_.begin(), display_muttypes_.end(), muttype_id); if (present_iter == display_muttypes_.end()) { // this mut-type is not being displayed, so add it to our display list display_muttypes_.emplace_back(muttype_id); } else { // this mut-type is being displayed, so remove it from our display list display_muttypes_.erase(present_iter); } } [self setNeedsDisplayAll]; } - (IBAction)filterNonNeutral:(id)sender { SLiMWindowController *controller = (SLiMWindowController *)[[self window] windowController]; Species *displaySpecies = [controller focalDisplaySpecies]; if (displaySpecies) { display_muttypes_.clear(); std::map &muttypes = displaySpecies->mutation_types_; for (auto muttype_iter : muttypes) { MutationType *muttype = muttype_iter.second; slim_objectid_t muttype_id = muttype->mutation_type_id_; if ((muttype->dfe_type_ != DFEType::kFixed) || (muttype->dfe_parameters_[0] != 0.0)) display_muttypes_.emplace_back(muttype_id); } [self setNeedsDisplayAll]; } } - (NSMenu *)menuForEvent:(NSEvent *)theEvent { SLiMWindowController *controller = (SLiMWindowController *)[[self window] windowController]; if (![controller invalidSimulation] && ![[controller window] attachedSheet] && ![self overview] && [self enabled]) { Species *displaySpecies = [controller focalDisplaySpecies]; if (displaySpecies) { std::map &muttypes = displaySpecies->mutation_types_; if (muttypes.size() > 0) { NSMenu *menu = [[NSMenu alloc] initWithTitle:@"chromosome_menu"]; NSMenuItem *menuItem; menuItem = [menu addItemWithTitle:@"Display All Mutations" action:@selector(filterMutations:) keyEquivalent:@""]; [menuItem setTag:-1]; [menuItem setTarget:self]; if (display_muttypes_.size() == 0) [menuItem setState:NSOnState]; // Make a sorted list of all mutation types we know – those that exist, and those that used to exist that we are displaying std::vector all_muttypes; for (auto muttype_iter : muttypes) { MutationType *muttype = muttype_iter.second; slim_objectid_t muttype_id = muttype->mutation_type_id_; all_muttypes.emplace_back(muttype_id); } all_muttypes.insert(all_muttypes.end(), display_muttypes_.begin(), display_muttypes_.end()); // Avoid building a huge menu, which will hang the app if (all_muttypes.size() <= 500) { std::sort(all_muttypes.begin(), all_muttypes.end()); all_muttypes.resize(std::distance(all_muttypes.begin(), std::unique(all_muttypes.begin(), all_muttypes.end()))); // Then add menu items for each of those muttypes for (slim_objectid_t muttype_id : all_muttypes) { menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"Display m%d", (int)muttype_id] action:@selector(filterMutations:) keyEquivalent:@""]; [menuItem setTag:muttype_id]; [menuItem setTarget:self]; if (std::find(display_muttypes_.begin(), display_muttypes_.end(), muttype_id) != display_muttypes_.end()) [menuItem setState:NSOnState]; } } [menu addItem:[NSMenuItem separatorItem]]; menuItem = [menu addItemWithTitle:@"Select Non-Neutral MutationTypes" action:@selector(filterNonNeutral:) keyEquivalent:@""]; [menuItem setTarget:self]; return [menu autorelease]; } } } return nil; } @end ================================================ FILE: SLiMgui/CocoaExtra.h ================================================ // // CocoaExtra.h // SLiM // // Created by Ben Haller on 1/22/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import @class SLiMWindowController; class MutationType; class InteractionType; // Returns true if we are running under the debugger bool SLiM_AmIBeingDebugged(void); // An NSTableView subclass that avoids becoming first responder; annoying that this is necessary, sigh... @interface SLiMTableView : NSTableView @end // A button that runs a pop-up menu when clicked @interface SLiMMenuButton : NSButton @property (nonatomic, retain) NSMenu *slimMenu; @end // A cell that draws a color swatch, used in the genomic element type tableview; note it is a subclass of NSTextFieldCell only for IB convenience... @interface SLiMColorCell : NSTextFieldCell @end void RGBForFitness(double fitness, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor); void RGBForSelectionCoeff(double selectionCoeff, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor); // Classes to show a selection index marker when dragging out a selection in a ChromosomeView @interface SLiMSelectionView : NSView @end @interface SLiMSelectionMarker : NSPanel @property (nonatomic, retain) NSString *label; @property (nonatomic) NSPoint tipPoint; @property (nonatomic) BOOL isLeftMarker; + (instancetype)new; // makes a new marker, not shown; set it up with a label and tip point and then call orderFront: @end // Classes to show a custom tooltip view; this code is derived from SLiMSelectionView / SLiMSelectionMarker // This is not a general-purpose class really; it is specifically for labeling the Play Speed slider @interface SLiMPlaySliderToolTipView : NSView @end @interface SLiMPlaySliderToolTipWindow : NSPanel @property (nonatomic, retain) NSString *label; @property (nonatomic) NSPoint tipPoint; + (instancetype)new; // makes a new marker, not shown; set it up with a label and tip point and then call orderFront: @end // Classes to show a custom tooltip view displaying a graph of a mutation type's DFE in the muttype table view // Now also used for similarly displaying an interaction type's IF in the interaction type table view @interface SLiMFunctionGraphToolTipView : NSView @end @interface SLiMFunctionGraphToolTipWindow : NSPanel @property (nonatomic, assign) MutationType *mutType; @property (nonatomic, assign) InteractionType *interactionType; @property (nonatomic) NSPoint tipPoint; + (instancetype)new; // makes a new marker, not shown; set it up with a mutType/intType and tip point and then call orderFront: @end // A category to help us position windows visibly @interface NSScreen (SLiMWindowFrames) + (BOOL)visibleCandidateWindowFrame:(NSRect)candidateFrame; @end // A category to add sorting of menus @interface NSPopUpButton (SLiMSorting) - (void)slimSortMenuItemsByTag; @end // A category to add tinting of NSPopUpButton, used in the ScriptMod panels for validation @interface NSButton (SLiMTinting) - (void)slimSetTintColor:(NSColor *)tintColor; @end // This is a vestigial tail left over from the old ScriptMod class of SLiMguiLegacy; I ripped out that class completely, // but a few other places in the code used its validation logic for their own purposes, so I've moved that to CocoaExtra. @interface ScriptMod : NSObject + (BOOL)validIntValueInTextField:(NSTextField *)textfield withMin:(int64_t)minValue max:(int64_t)maxValue; + (BOOL)validFloatValueInTextField:(NSTextField *)textfield withMin:(double)minValue max:(double)maxValue; + (NSColor *)backgroundColorForValidationState:(BOOL)valid; @end // A subclass to make an NSTextView that selects its content when clicked, for the tick textfield @interface SLiMAutoselectTextField : NSTextField @end // A subclass for a view that forces its (single) subview to match its own bounds, except that a half-pixel // alignment in this view will be corrected in the subview; this makes OpenGL views play nice with Retina. // This code is not general-purpose at the moment, it works only specifically with SLiMgui's view setup. @interface SLiMLayoutRoundoffView : NSView @end @interface NSString (SLiMBytes) + (NSString *)stringForByteCount:(int64_t)bytes; @end @interface NSColor (SLiMHeatColors) + (NSColor *)slimColorForFraction:(double)fraction; @end @interface NSAttributedString (SLiMBytes) + (NSAttributedString *)attributedStringForByteCount:(int64_t)bytes total:(double)total attributes:(NSDictionary *)attrs; @end // Create a path for a temporary file; see https://stackoverflow.com/a/8307013/2752221 // Code is originally from https://developer.apple.com/library/archive/samplecode/SimpleURLConnections/Introduction/Intro.html#//apple_ref/doc/uid/DTS40009245 @interface NSString (SLiMTempFiles) + (NSString *)slimPathForTemporaryFileWithPrefix:(NSString *)prefix; @end ================================================ FILE: SLiMgui/CocoaExtra.mm ================================================ // // CocoaExtra.m // SLiM // // Created by Ben Haller on 1/22/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "CocoaExtra.h" #import "AppDelegate.h" #include "eidos_rng.h" #include "mutation_type.h" #include "interaction_type.h" // for SLiM_AmIBeingDebugged() #include #include #include #include #include #include // From Apple tech note #1361, https://developer.apple.com/library/archive/qa/qa1361/_index.html // see https://stackoverflow.com/a/2200786/2752221 bool SLiM_AmIBeingDebugged(void) // Returns true if the current process is being debugged (either // running under the debugger or has a debugger attached post facto). { int junk; int mib[4]; struct kinfo_proc info; size_t size; // Initialize the flags so that, if sysctl fails for some bizarre // reason, we get a predictable result. info.kp_proc.p_flag = 0; // Initialize mib, which tells sysctl the info we want, in this case // we're looking for information about a specific process ID. mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; mib[3] = getpid(); // Call sysctl. size = sizeof(info); junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); assert(junk == 0); // We're being debugged if the P_TRACED flag is set. return ( (info.kp_proc.p_flag & P_TRACED) != 0 ); } @implementation SLiMTableView - (BOOL)acceptsFirstResponder { return NO; } @end const float greenBrightness = 0.8f; void RGBForFitness(double value, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor) { // apply the scaling factor value = (value - 1.0) * scalingFactor + 1.0; if (value <= 0.5) { // value <= 0.5 is a shade of red, going down to black *colorRed = (float)(value * 2.0); *colorGreen = 0.0; *colorBlue = 0.0; } else if (value >= 2.0) { // value >= 2.0 is a shade of green, going up to white *colorRed = (float)((value - 2.0) * greenBrightness / value); *colorGreen = greenBrightness; *colorBlue = (float)((value - 2.0) * greenBrightness / value); } else if (value <= 1.0) { // value <= 1.0 (but > 0.5) goes from red (unfit) to yellow (neutral) *colorRed = 1.0; *colorGreen = (float)((value - 0.5) * 2.0); *colorBlue = 0.0; } else // 1.0 < value < 2.0 { // value > 1.0 (but < 2.0) goes from yellow (neutral) to green (fit) *colorRed = (float)(2.0 - value); *colorGreen = (float)(greenBrightness + (1.0 - greenBrightness) * (2.0 - value)); *colorBlue = 0.0; } } void RGBForSelectionCoeff(double value, float *colorRed, float *colorGreen, float *colorBlue, double scalingFactor) { // apply a scaling factor; this could be user-adjustible since different models have different relevant fitness ranges value *= scalingFactor; // and add 1, just so we can re-use the same code as in RGBForFitness() value += 1.0; if (value <= 0.0) { // value <= 0.0 is the darkest shade of red we use *colorRed = 0.5; *colorGreen = 0.0; *colorBlue = 0.0; } else if (value <= 0.5) { // value <= 0.5 is a shade of red, going down toward black *colorRed = (float)(value + 0.5); *colorGreen = 0.0; *colorBlue = 0.0; } else if (value < 1.0) { // value <= 1.0 (but > 0.5) goes from red (very unfit) to orange (nearly neutral) *colorRed = 1.0; *colorGreen = (float)((value - 0.5) * 1.0); *colorBlue = 0.0; } else if (value == 1.0) { // exactly neutral mutations are yellow *colorRed = 1.0; *colorGreen = 1.0; *colorBlue = 0.0; } else if (value <= 1.5) { // value > 1.0 (but < 1.5) goes from green (nearly neutral) to cyan (fit) *colorRed = 0.0; *colorGreen = greenBrightness; *colorBlue = (float)((value - 1.0) * 2.0); } else if (value <= 2.0) { // value > 1.5 (but < 2.0) goes from cyan (fit) to blue (very fit) *colorRed = 0.0; *colorGreen = (float)(greenBrightness * ((2.0 - value) * 2.0)); *colorBlue = 1.0; } else // (value > 2.0) { // value > 2.0 is a shade of blue, going up toward white *colorRed = (float)((value - 2.0) * 0.75 / value); *colorGreen = (float)((value - 2.0) * 0.75 / value); *colorBlue = 1.0; } } @implementation SLiMMenuButton - (void)dealloc { [self setSlimMenu:nil]; [super dealloc]; } - (void)fixMenu { NSMenu *menu = [self slimMenu]; NSDictionary *itemAttrs = @{NSFontAttributeName : [NSFont systemFontOfSize:12.0]}; for (int i = 0; i < [menu numberOfItems]; ++i) { NSMenuItem *menuItem = [menu itemAtIndex:i]; NSString *title = [menuItem title]; NSAttributedString *attrTitle = [[NSAttributedString alloc] initWithString:title attributes:itemAttrs]; [menuItem setAttributedTitle:attrTitle]; [attrTitle release]; } } - (void)mouseDown:(NSEvent *)theEvent { // We do not call super; we do mouse tracking entirely ourselves //[super mouseDown:theEvent]; if ([self isEnabled]) { NSRect bounds = [self bounds]; [self highlight:YES]; [self fixMenu]; [[self slimMenu] popUpMenuPositioningItem:nil atLocation:NSMakePoint(bounds.size.width * 0.80 - 1, bounds.size.height * 0.80 + 1) inView:self]; [self highlight:NO]; } } - (void)mouseDragged:(NSEvent *)theEvent { // We do not call super; we do mouse tracking entirely ourselves //[super mouseDragged:theEvent]; } - (void)mouseUp:(NSEvent *)theEvent { // We do not call super; we do mouse tracking entirely ourselves //[super mouseDown:theEvent]; } @end @implementation SLiMColorCell - (void)setObjectValue:(id)objectValue { // Ensure that only NSColor objects are set as our object value if ([objectValue isKindOfClass:[NSColor class]]) [super setObjectValue:objectValue]; } - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { NSRect swatchFrame = NSInsetRect(cellFrame, 1, 1); // Make our swatch be square, if we have enough room to do it if (swatchFrame.size.width > swatchFrame.size.height) { swatchFrame.origin.x = (int)round(swatchFrame.origin.x + (swatchFrame.size.width - swatchFrame.size.height) / 2.0); swatchFrame.size.width = swatchFrame.size.height; } [[NSColor blackColor] set]; NSFrameRect(swatchFrame); [(NSColor *)[self objectValue] set]; NSRectFill(NSInsetRect(swatchFrame, 1, 1)); } @end @implementation SLiMSelectionView - (void)drawRect:(NSRect)dirtyRect { static NSDictionary *labelAttrs = nil; if (!labelAttrs) labelAttrs = [@{NSFontAttributeName : [NSFont fontWithName:@"Times New Roman" size:10], NSForegroundColorAttributeName : [NSColor blackColor]} retain]; NSRect bounds = [self bounds]; SLiMSelectionMarker *marker = (SLiMSelectionMarker *)[self window]; NSAttributedString *attrLabel = [[NSAttributedString alloc] initWithString:[marker label] attributes:labelAttrs]; NSSize labelStringSize = [attrLabel size]; NSSize labelSize = NSMakeSize(round(labelStringSize.width + 8.0), round(labelStringSize.height + 1.0)); NSRect labelRect = NSMakeRect(bounds.origin.x + round(bounds.size.width / 2.0), bounds.origin.y + bounds.size.height - labelSize.height, labelSize.width, labelSize.height); if ([marker isLeftMarker]) labelRect.origin.x -= round(labelSize.width - 1.0); // Frame our whole bounds, for debugging; note that we draw in only a portion of our bounds, and the rest is transparent //[[NSColor blackColor] set]; //NSFrameRect(bounds); #if 0 // Debugging code: frame and fill our label rect without using NSBezierPath [[NSColor colorWithCalibratedHue:0.15 saturation:0.2 brightness:1.0 alpha:1.0] set]; NSRectFill(labelRect); [[NSColor colorWithCalibratedHue:0.15 saturation:0.2 brightness:0.3 alpha:1.0] set]; NSFrameRect(labelRect); #else // Production code: use NSBezierPath to get a label that has a tag off of it NSBezierPath *bez = [NSBezierPath bezierPath]; NSRect ilr = NSInsetRect(labelRect, 0.5, 0.5); // inset by 0.5 to place us mid-pixel, so stroke looks good const double tagHeight = 5.0; if ([marker isLeftMarker]) { // label rect with a diagonal tag down from the right edge [bez moveToPoint:NSMakePoint(ilr.origin.x, ilr.origin.y)]; [bez relativeLineToPoint:NSMakePoint(ilr.size.width - tagHeight, 0)]; [bez relativeLineToPoint:NSMakePoint(tagHeight, -tagHeight)]; [bez relativeLineToPoint:NSMakePoint(0, ilr.size.height + tagHeight)]; [bez relativeLineToPoint:NSMakePoint(-ilr.size.width, 0)]; [bez closePath]; } else { // label rect with a diagonal tag down from the right edge [bez moveToPoint:NSMakePoint(ilr.origin.x + ilr.size.width, ilr.origin.y)]; [bez relativeLineToPoint:NSMakePoint(- (ilr.size.width - tagHeight), 0)]; [bez relativeLineToPoint:NSMakePoint(-tagHeight, -tagHeight)]; [bez relativeLineToPoint:NSMakePoint(0, ilr.size.height + tagHeight)]; [bez relativeLineToPoint:NSMakePoint(ilr.size.width, 0)]; [bez closePath]; } [[NSColor colorWithCalibratedHue:0.15 saturation:0.2 brightness:1.0 alpha:1.0] set]; [bez fill]; [[NSColor blackColor] set]; [bez setLineWidth:1.0]; [bez stroke]; #endif [attrLabel drawAtPoint:NSMakePoint(labelRect.origin.x + 4, labelRect.origin.y + 1)]; [attrLabel release]; } - (BOOL)isOpaque { return NO; } @end @implementation SLiMSelectionMarker // makes a new marker with no label and no tip point, not shown + (instancetype)new { return [[[self class] alloc] initWithContentRect:NSMakeRect(0, 0, 150, 20) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES]; // 150x20 should suffice, unless we change our font size... } - (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag { if (self = [super initWithContentRect:contentRect styleMask:aStyle backing:bufferingType defer:flag]) { [self setFloatingPanel:YES]; [self setBecomesKeyOnlyIfNeeded:YES]; [self setHasShadow:NO]; [self setOpaque:NO]; [self setBackgroundColor:[NSColor clearColor]]; SLiMSelectionView *view = [[SLiMSelectionView alloc] initWithFrame:contentRect]; [self setContentView:view]; [view release]; _tipPoint = NSMakePoint(round(contentRect.origin.x + contentRect.size.width / 2.0), contentRect.origin.y); _label = [@"1000000000" retain]; } return self; } - (void)dealloc { [self setLabel:nil]; [super dealloc]; } - (void)setLabel:(NSString *)label { if (![_label isEqualToString:label]) { [label retain]; [_label release]; _label = label; [[self contentView] setNeedsDisplay:YES]; } } - (void)setTipPoint:(NSPoint)tipPoint { if (!NSEqualPoints(_tipPoint, tipPoint)) { NSPoint origin = [self frame].origin; origin.x += (tipPoint.x - _tipPoint.x); origin.y += (tipPoint.y - _tipPoint.y); _tipPoint = tipPoint; [self setFrameOrigin:origin]; } } @end @implementation SLiMPlaySliderToolTipView - (void)drawRect:(NSRect)dirtyRect { static NSDictionary *labelAttrs = nil; if (!labelAttrs) labelAttrs = [@{NSFontAttributeName : [NSFont fontWithName:@"Times New Roman" size:10], NSForegroundColorAttributeName : [NSColor blackColor]} retain]; NSRect bounds = [self bounds]; SLiMPlaySliderToolTipWindow *tooltipWindow = (SLiMPlaySliderToolTipWindow *)[self window]; NSAttributedString *attrLabel = [[NSAttributedString alloc] initWithString:[tooltipWindow label] attributes:labelAttrs]; NSSize labelStringSize = [attrLabel size]; NSSize labelSize = NSMakeSize(round(labelStringSize.width + 8.0), round(labelStringSize.height + 1.0)); NSRect labelRect = NSMakeRect(bounds.origin.x, bounds.origin.y + bounds.size.height - labelSize.height, labelSize.width, labelSize.height); // Frame our whole bounds, for debugging; note that we draw in only a portion of our bounds, and the rest is transparent //[[NSColor blackColor] set]; //NSFrameRect(bounds); // Frame and fill our label rect [[NSColor colorWithCalibratedHue:0.15 saturation:0.2 brightness:1.0 alpha:1.0] set]; NSRectFill(labelRect); [[NSColor colorWithCalibratedHue:0.15 saturation:0.2 brightness:0.3 alpha:1.0] set]; NSFrameRect(labelRect); [attrLabel drawAtPoint:NSMakePoint(labelRect.origin.x + 4, labelRect.origin.y + 1)]; [attrLabel release]; } - (BOOL)isOpaque { return NO; } @end @implementation SLiMPlaySliderToolTipWindow // makes a new marker with no label and no tip point, not shown + (instancetype)new { return [[[self class] alloc] initWithContentRect:NSMakeRect(0, 0, 50, 20) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES]; // 50x20 should suffice, unless we change our font size... } - (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag { if (self = [super initWithContentRect:contentRect styleMask:aStyle backing:bufferingType defer:flag]) { [self setFloatingPanel:YES]; [self setBecomesKeyOnlyIfNeeded:YES]; [self setHasShadow:NO]; [self setOpaque:NO]; [self setBackgroundColor:[NSColor clearColor]]; SLiMPlaySliderToolTipView *view = [[SLiMPlaySliderToolTipView alloc] initWithFrame:contentRect]; [self setContentView:view]; [view release]; _tipPoint = NSMakePoint(contentRect.origin.x, contentRect.origin.y); _label = [@"1000000000" retain]; } return self; } - (void)dealloc { [self setLabel:nil]; [super dealloc]; } - (void)setLabel:(NSString *)label { if (![_label isEqualToString:label]) { [label retain]; [_label release]; _label = label; [[self contentView] setNeedsDisplay:YES]; } } - (void)setTipPoint:(NSPoint)tipPoint { if (!NSEqualPoints(_tipPoint, tipPoint)) { NSPoint origin = [self frame].origin; origin.x += (tipPoint.x - _tipPoint.x); origin.y += (tipPoint.y - _tipPoint.y); _tipPoint = tipPoint; [self setFrameOrigin:origin]; } } @end @implementation SLiMFunctionGraphToolTipView - (void)drawRect:(NSRect)dirtyRect { static NSDictionary *labelAttrs = nil; static NSDictionary *questionMarkAttrs = nil; if (!labelAttrs) { labelAttrs = [@{NSFontAttributeName : [NSFont fontWithName:@"Times New Roman" size:9], NSForegroundColorAttributeName : [NSColor blackColor]} retain]; questionMarkAttrs = [@{NSFontAttributeName : [NSFont fontWithName:@"Times New Roman" size:18], NSForegroundColorAttributeName : [NSColor blackColor]} retain]; } NSRect bounds = [self bounds]; SLiMFunctionGraphToolTipWindow *tooltipWindow = (SLiMFunctionGraphToolTipWindow *)[self window]; // Frame and fill our label rect [[NSColor colorWithCalibratedHue:0.0 saturation:0.0 brightness:0.95 alpha:1.0] set]; NSRectFill(bounds); [[NSColor colorWithCalibratedHue:0.15 saturation:0.0 brightness:0.75 alpha:1.0] set]; NSFrameRect(bounds); // Plan our plotting MutationType *mut_type = [tooltipWindow mutType]; InteractionType *interaction_type = [tooltipWindow interactionType]; if ((!mut_type && !interaction_type) || (mut_type && interaction_type)) return; int sample_size; std::vector draws; bool draw_positive = false, draw_negative = false; bool heights_negative = false; double axis_min, axis_max; bool draw_axis_midpoint = true, custom_axis_max = false; if (mut_type) { // Generate draws for a mutation type; this case is stochastic, based upon a large number of DFE samples. // Draw all the values we will plot; we need our own private RNG so we don't screw up the simulation's. // Drawing selection coefficients could raise, if they are type "s" and there is an error in the script, // so we run the sampling inside a try/catch block; if we get a raise, we just show a "?" in the plot. static bool rng_initialized = false; static Eidos_RNG_State local_rng; sample_size = (mut_type->dfe_type_ == DFEType::kScript) ? 100000 : 1000000; // large enough to make curves pretty smooth, small enough to be reasonably fast draws.reserve(sample_size); if (!rng_initialized) { _Eidos_InitializeOneRNG(local_rng); rng_initialized = true; } _Eidos_SetOneRNGSeed(local_rng, 10); // arbitrary seed, but the same seed every time Eidos_RNG_State *slim_rng_state = EIDOS_STATE_RNG(omp_get_thread_num()); std::swap(local_rng, *slim_rng_state); // swap in our local RNG for DrawSelectionCoefficient() //std::clock_t start = std::clock(); try { for (int sample_count = 0; sample_count < sample_size; ++sample_count) { double draw = mut_type->DrawSelectionCoefficient(); draws.emplace_back(draw); if (draw < 0.0) draw_negative = true; else if (draw > 0.0) draw_positive = true; } } catch (...) { draws.clear(); draw_negative = true; draw_positive = true; } //NSLog(@"Draws took %f seconds", (std::clock() - start) / (double)CLOCKS_PER_SEC); std::swap(local_rng, *slim_rng_state); // swap out our local RNG; restore the standard RNG // figure out axis limits if (draw_negative && !draw_positive) { axis_min = -1.0; axis_max = 0.0; } else if (draw_positive && !draw_negative) { axis_min = 0.0; axis_max = 1.0; } else { axis_min = -1.0; axis_max = 1.0; } } else // if (interaction_type) { // Since interaction types are deterministic, we don't need draws; we will just calculate our // bin heights directly below. sample_size = 0; draw_negative = false; draw_positive = true; axis_min = 0.0; if ((interaction_type->max_distance_ < 1.0) || std::isinf(interaction_type->max_distance_)) { axis_max = 1.0; } else { axis_max = interaction_type->max_distance_; draw_axis_midpoint = false; custom_axis_max = true; } heights_negative = (interaction_type->if_param1_ < 0.0); // this is a negative-strength interaction, if T } // Draw the graph axes and ticks NSRect graphRect = NSMakeRect(bounds.origin.x + 6, bounds.origin.y + (heights_negative ? 5 : 14), bounds.size.width - 12, bounds.size.height - 20); CGFloat axis_y = (heights_negative ? graphRect.origin.y + graphRect.size.height - 1 : graphRect.origin.y); CGFloat tickoff3 = (heights_negative ? 1 : -3); CGFloat tickoff1 = (heights_negative ? 1 : -1); [[NSColor colorWithCalibratedHue:0.15 saturation:0.0 brightness:0.2 alpha:1.0] set]; NSRectFill(NSMakeRect(graphRect.origin.x, axis_y, graphRect.size.width, 1)); NSRectFill(NSMakeRect(graphRect.origin.x, axis_y + tickoff3, 1, 3)); NSRectFill(NSMakeRect(graphRect.origin.x + (graphRect.size.width - 1) * 0.125, axis_y + tickoff1, 1, 1)); NSRectFill(NSMakeRect(graphRect.origin.x + (graphRect.size.width - 1) * 0.25, axis_y + tickoff1, 1, 1)); NSRectFill(NSMakeRect(graphRect.origin.x + (graphRect.size.width - 1) * 0.375, axis_y + tickoff1, 1, 1)); NSRectFill(NSMakeRect(graphRect.origin.x + (graphRect.size.width - 1) * 0.5, axis_y + tickoff3, 1, 3)); NSRectFill(NSMakeRect(graphRect.origin.x + (graphRect.size.width - 1) * 0.625, axis_y + tickoff1, 1, 1)); NSRectFill(NSMakeRect(graphRect.origin.x + (graphRect.size.width - 1) * 0.75, axis_y + tickoff1, 1, 1)); NSRectFill(NSMakeRect(graphRect.origin.x + (graphRect.size.width - 1) * 0.875, axis_y + tickoff1, 1, 1)); NSRectFill(NSMakeRect(graphRect.origin.x + graphRect.size.width - 1, axis_y + tickoff3, 1, 3)); // Draw the axis labels std::ostringstream ss; ss << axis_max; std::string ss_str = ss.str(); NSString *axis_max_pretty_string = [NSString stringWithUTF8String:ss_str.c_str()]; NSString *axis_min_label = (axis_min == 0.0 ? @"0" : @"−1"); NSString *axis_half_label = (axis_min == 0.0 ? @"0.5" : (axis_max == 0.0 ? @"−0.5" : @"0")); NSString *axis_max_label = (custom_axis_max ? axis_max_pretty_string : (axis_max == 0.0 ? @"0" : @"1")); NSSize min_label_size = [axis_min_label sizeWithAttributes:labelAttrs]; NSSize half_label_size = [axis_half_label sizeWithAttributes:labelAttrs]; NSSize max_label_size = [axis_max_label sizeWithAttributes:labelAttrs]; double min_label_halfwidth = round(min_label_size.width / 2.0); double half_label_halfwidth = round(half_label_size.width / 2.0); double max_label_halfwidth = round(max_label_size.width / 2.0); CGFloat label_y = (heights_negative ? bounds.origin.y + bounds.size.height - 12 : bounds.origin.y + 1); [axis_min_label drawAtPoint:NSMakePoint(bounds.origin.x + 7 - min_label_halfwidth, label_y) withAttributes:labelAttrs]; if (draw_axis_midpoint) [axis_half_label drawAtPoint:NSMakePoint(bounds.origin.x + 38.5 - half_label_halfwidth, label_y) withAttributes:labelAttrs]; if (custom_axis_max) [axis_max_label drawAtPoint:NSMakePoint(bounds.origin.x + 72 - round(max_label_size.width), label_y) withAttributes:labelAttrs]; else [axis_max_label drawAtPoint:NSMakePoint(bounds.origin.x + 70 - max_label_halfwidth, label_y) withAttributes:labelAttrs]; // If we had an exception while drawing values, just show a question mark and return if (mut_type && !draws.size()) { NSString *questionMark = @"?"; NSSize q_size = [questionMark sizeWithAttributes:questionMarkAttrs]; double q_halfwidth = round(q_size.width / 2.0); [questionMark drawAtPoint:NSMakePoint(bounds.origin.x + bounds.size.width / 2.0 - q_halfwidth, bounds.origin.y + 22) withAttributes:questionMarkAttrs]; return; } NSRect interiorRect = NSMakeRect(graphRect.origin.x, graphRect.origin.y + (heights_negative ? 0 : 2), graphRect.size.width, graphRect.size.height - 2); // Tabulate the distribution from the samples we took; the math here is a bit subtle, because when we are doing a -1 to +1 axis // we want those values to fall at bin centers, but when we're doing 0 to +1 or -1 to 0 we want 0 to fall at the bin edge. int half_bin_count = (int)round(interiorRect.size.width); int bin_count = half_bin_count * 2; // 2x bins to look nice on Retina displays double *bins = (double *)calloc(bin_count, sizeof(double)); if (sample_size) { // sample-based tabulation into a histogram; mutation types only, right now for (int sample_count = 0; sample_count < sample_size; ++sample_count) { double sel_coeff = draws[sample_count]; int bin_index; if ((axis_min == -1.0) && (axis_max == 1.0)) bin_index = (int)floor(((sel_coeff + 1.0) / 2.0) * (bin_count - 1) + 0.5); else if ((axis_min == -1.0) && (axis_max == 0.0)) bin_index = (int)ceil((sel_coeff + 1.0) * (bin_count - 1 - 0.5) + 0.5); // 0.0 maps to bin_count - 1, -1.0 maps to the center of bin 0 else // if ((axis_min == 0.0) && (axis_max == 1.0)) bin_index = (int)floor(sel_coeff * (bin_count - 1 + 0.5)); // 0.0 maps to 0, 1.0 maps to the center of bin_count - 1 if ((bin_index >= 0) && (bin_index < bin_count)) bins[bin_index]++; } } else { // non-sample-based construction of a function by evaluation; interaction types only, right now double max_x = interaction_type->max_distance_; for (int bin_index = 0; bin_index < bin_count; ++bin_index) { double bin_left = (bin_index / (double)bin_count) * axis_max; double bin_right = ((bin_index + 1) / (double)bin_count) * axis_max; double total_value = 0.0; for (int evaluate_index = 0; evaluate_index <= 999; ++evaluate_index) { double evaluate_x = bin_left + (bin_right - bin_left) / 999; if (evaluate_x < max_x) total_value += interaction_type->CalculateStrengthNoCallbacks(evaluate_x); } bins[bin_index] = total_value / 1000.0; } } // If we only have samples equal to zero, replicate the center column for symmetry if (!draw_positive && !draw_negative) { double zero_count = std::max(bins[half_bin_count - 1], bins[half_bin_count]); // whichever way it rounds... bins[half_bin_count - 1] = zero_count; bins[half_bin_count] = zero_count; } // Find the maximum-magnitude bin count double max_bin = 0; if (heights_negative) { for (int bin_index = 0; bin_index < bin_count; ++bin_index) max_bin = std::min(max_bin, bins[bin_index]); } else { for (int bin_index = 0; bin_index < bin_count; ++bin_index) max_bin = std::max(max_bin, bins[bin_index]); } // Plot the bins [[NSColor colorWithCalibratedHue:0.15 saturation:0.0 brightness:0.0 alpha:1.0] set]; if (heights_negative) { for (int bin_index = 0; bin_index < bin_count; ++bin_index) { if (bins[bin_index] < 0) { double height = interiorRect.size.height * (bins[bin_index] / max_bin); NSRectFill(NSMakeRect(interiorRect.origin.x + bin_index * 0.5, interiorRect.origin.y + interiorRect.size.height - height, 0.5, height)); } } } else { for (int bin_index = 0; bin_index < bin_count; ++bin_index) { if (bins[bin_index] > 0) NSRectFill(NSMakeRect(interiorRect.origin.x + bin_index * 0.5, interiorRect.origin.y, 0.5, interiorRect.size.height * (bins[bin_index] / max_bin))); } } free(bins); } - (BOOL)isOpaque { return YES; } @end @implementation SLiMFunctionGraphToolTipWindow // makes a new marker with no label and no tip point, not shown + (instancetype)new { return [[[self class] alloc] initWithContentRect:NSMakeRect(0, 0, 77, 50) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES]; } - (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag { if (self = [super initWithContentRect:contentRect styleMask:aStyle backing:bufferingType defer:flag]) { [self setFloatingPanel:YES]; [self setBecomesKeyOnlyIfNeeded:YES]; [self setHasShadow:NO]; [self setOpaque:NO]; [self setBackgroundColor:[NSColor clearColor]]; SLiMFunctionGraphToolTipView *view = [[SLiMFunctionGraphToolTipView alloc] initWithFrame:contentRect]; [self setContentView:view]; [view release]; _tipPoint = NSMakePoint(contentRect.origin.x, contentRect.origin.y); _mutType = nullptr; _interactionType = nullptr; } return self; } - (void)setMutType:(MutationType *)mutType { if (_mutType != mutType) { _mutType = mutType; [[self contentView] setNeedsDisplay:YES]; } } - (void)setInteractionType:(InteractionType *)interactionType { if (_interactionType != interactionType) { _interactionType = interactionType; [[self contentView] setNeedsDisplay:YES]; } } - (void)setTipPoint:(NSPoint)tipPoint { if (!NSEqualPoints(_tipPoint, tipPoint)) { NSPoint origin = [self frame].origin; origin.x += (tipPoint.x - _tipPoint.x); origin.y += (tipPoint.y - _tipPoint.y); _tipPoint = tipPoint; [self setFrameOrigin:origin]; } } @end @implementation NSScreen (SLiMWindowFrames) + (BOOL)visibleCandidateWindowFrame:(NSRect)candidateFrame { NSArray *screens = [NSScreen screens]; NSUInteger nScreens = [screens count]; for (NSUInteger i = 0; i < nScreens; ++i) { NSScreen *screen = [screens objectAtIndex:i]; NSRect screenFrame = [screen visibleFrame]; if (NSContainsRect(screenFrame, candidateFrame)) return YES; } return NO; } @end @implementation NSPopUpButton (SLiMSorting) - (void)slimSortMenuItemsByTag { NSMenu *menu = [self menu]; int nItems = (int)[menu numberOfItems]; // completely dumb bubble sort; not worth worrying about do { BOOL foundSwap = NO; for (int i = 0; i < nItems - 1; ++i) { NSMenuItem *firstItem = [menu itemAtIndex:i]; NSMenuItem *secondItem = [menu itemAtIndex:i + 1]; NSInteger firstTag = [firstItem tag]; NSInteger secondTag = [secondItem tag]; if (firstTag > secondTag) { [secondItem retain]; [menu removeItemAtIndex:i + 1]; [menu insertItem:secondItem atIndex:i]; [secondItem release]; foundSwap = YES; } } if (!foundSwap) break; } while (YES); } @end @implementation NSButton (SLiMTinting) - (void)slimSetTintColor:(NSColor *)tintColor { [self setContentFilters:[NSArray array]]; if (tintColor) { if (!self.layer) [self setWantsLayer:YES]; CIFilter *tintFilter = [CIFilter filterWithName:@"CIColorMatrix"]; if (tintFilter) { CGFloat redComponent = [tintColor redComponent]; CGFloat greenComponent = [tintColor greenComponent]; CGFloat blueComponent = [tintColor blueComponent]; //NSLog(@"tintColor: redComponent == %f, greenComponent == %f, blueComponent == %f", redComponent, greenComponent, blueComponent); // The goal is to use CIColorMatrix to multiply color components so that white turns into tintColor; these vectors do that CIVector *rVector = [CIVector vectorWithX:redComponent Y:0.0 Z:0.0 W:0.0]; CIVector *gVector = [CIVector vectorWithX:0.0 Y:greenComponent Z:0.0 W:0.0]; CIVector *bVector = [CIVector vectorWithX:0.0 Y:0.0 Z:blueComponent W:0.0]; [tintFilter setDefaults]; [tintFilter setValue:rVector forKey:@"inputRVector"]; [tintFilter setValue:gVector forKey:@"inputGVector"]; [tintFilter setValue:bVector forKey:@"inputBVector"]; [self setContentFilters:@[tintFilter]]; } else { NSLog(@"could not create [CIFilter filterWithName:@\"CIColorMatrix\"]"); } } else { // BCH added 21 May 2017: revert to non-layer-backed when tinting is removed. This restores the previous behavior // of the view, which is important for making the play and profile buttons draw correctly where they overlap. The // doc does not explicitly say that setWantsLayer:NO removes the layer, but in practice it does seem to. if (self.layer) [self setWantsLayer:NO]; } [self setNeedsDisplay]; [self.layer setNeedsDisplay]; } @end // This is a vestigial tail left over from the old ScriptMod class of SLiMguiLegacy; I ripped out that class completely, // but a few other places in the code used its validation logic for their own purposes, so I've moved that to CocoaExtra. @implementation ScriptMod + (NSColor *)validationErrorColor { static NSColor *color = nil; if (!color) color = [[NSColor colorWithCalibratedHue:0.0 saturation:0.15 brightness:1.0 alpha:1.0] retain]; return color; } + (NSRegularExpression *)regexForInt { static NSRegularExpression *regex = nil; if (!regex) regex = [[NSRegularExpression alloc] initWithPattern:@"^[0-9]+$" options:0 error:NULL]; return regex; } + (NSRegularExpression *)regexForFloat { static NSRegularExpression *regex = nil; if (!regex) regex = [[NSRegularExpression alloc] initWithPattern:@"^\\-?[0-9]+(\\.[0-9]*)?$" options:0 error:NULL]; return regex; } + (BOOL)validIntValueInTextField:(NSTextField *)textfield withMin:(int64_t)minValue max:(int64_t)maxValue { NSString *stringValue = [textfield stringValue]; int64_t intValue = [[textfield stringValue] longLongValue]; if ([stringValue length] == 0) return NO; if ([[ScriptMod regexForInt] numberOfMatchesInString:stringValue options:0 range:NSMakeRange(0, [stringValue length])] == 0) return NO; if (intValue < minValue) return NO; if (intValue > maxValue) return NO; return YES; } + (BOOL)validFloatValueInTextField:(NSTextField *)textfield withMin:(double)minValue max:(double)maxValue { return [self validFloatValueInTextField:textfield withMin:minValue max:maxValue excludingMin:NO excludingMax:NO]; } + (BOOL)validFloatValueInTextField:(NSTextField *)textfield withMin:(double)minValue max:(double)maxValue excludingMin:(BOOL)excludeMin excludingMax:(BOOL)excludeMax { NSString *stringValue = [textfield stringValue]; double doubleValue = [textfield doubleValue]; if ([stringValue length] == 0) return NO; if ([[ScriptMod regexForFloat] numberOfMatchesInString:stringValue options:0 range:NSMakeRange(0, [stringValue length])] == 0) return NO; if (doubleValue < minValue) return NO; if (excludeMin && (doubleValue == minValue)) return NO; if (doubleValue > maxValue) return NO; if (excludeMax && (doubleValue == maxValue)) return NO; return YES; } + (NSColor *)backgroundColorForValidationState:(BOOL)valid { return (valid ? [NSColor whiteColor] : [ScriptMod validationErrorColor]); } @end @implementation SLiMAutoselectTextField - (void)mouseDown:(NSEvent *)theEvent { [[self currentEditor] selectAll:nil]; } @end @implementation SLiMLayoutRoundoffView - (void)layout { // We want to manually layout our (single) subview, to correct for subpixel positioning that screws up our drawing. // The documentation for -layout is abysmal, but it turns out what you want to do is remove all constraints from the // view that you want to manually lay out, so that the autolayout code is not fighting with you, and *then* use // -layout in the superview of that view to manually set the frame of the subview. We remove the relevant constraints // in -windowDidLoad in SLiMWindowController. Here we just check for a half-pixel position in our own bounds, and if // present, tweak our subview's frame to remove it. Seems to work; took hours upon hours to figure out. :-< NSArray *subviews = [self subviews]; if ([subviews count] == 1) { NSView *subview = [subviews objectAtIndex:0]; NSRect selfBounds = [self frame]; NSRect frame = selfBounds; if (selfBounds.size.height != round(selfBounds.size.height)) { frame.origin.y += 0.5; frame.size.height -= 0.5; } //NSLog(@"selfBounds: %@", NSStringFromRect(selfBounds)); //NSLog(@"frame: %@", NSStringFromRect(frame)); [subview setFrame:frame]; [subview setNeedsLayout:YES]; } [super layout]; } @end // Work around Apple's bug that they never fix that causes console logs on startup // Thanks to TyngJJ on https://forums.developer.apple.com/thread/49052 for this workaround // FIXME check whether they have fixed it yet, from time to time... @interface NSWindow (FirstResponding) -(void)_setFirstResponder:(NSResponder *)responder; @end @interface NSDrawerWindow : NSWindow @end @implementation NSDrawerWindow (FirstResponding) -(void)_setFirstResponder:(NSResponder *)responder { if (![responder isKindOfClass:NSView.class] || [(NSView *)responder window] == self) [super _setFirstResponder:responder]; } @end @implementation NSString (SLiMBytes) + (NSString *)stringForByteCount:(int64_t)bytes { if (bytes > 512LL * 1024L * 1024L * 1024L) return [NSString stringWithFormat:@"%0.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0)]; else if (bytes > 512L * 1024L * 1024L) return [NSString stringWithFormat:@"%0.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)]; else if (bytes > 512L * 1024L) return [NSString stringWithFormat:@"%0.2f MB", bytes / (1024.0 * 1024.0)]; else if (bytes > 512L) return [NSString stringWithFormat:@"%0.2f KB", bytes / 1024.0]; else return [NSString stringWithFormat:@"%lld bytes", (long long int)bytes]; } @end @implementation NSColor (SLiMHeatColors) #define SLIM_YELLOW_FRACTION 0.10 #define SLIM_SATURATION 0.75 + (NSColor *)slimColorForFraction:(double)fraction { if (fraction < SLIM_YELLOW_FRACTION) { // small fractions fall on a ramp from white (0.0) to yellow (SLIM_YELLOW_FRACTION) return [NSColor colorWithCalibratedHue:(1.0 / 6.0) saturation:(fraction / SLIM_YELLOW_FRACTION) * SLIM_SATURATION brightness:1.0 alpha:1.0]; } else { // larger fractions ramp from yellow (SLIM_YELLOW_FRACTION) to red (1.0) return [NSColor colorWithCalibratedHue:(1.0 / 6.0) * (1.0 - (fraction - SLIM_YELLOW_FRACTION) / (1.0 - SLIM_YELLOW_FRACTION)) saturation:SLIM_SATURATION brightness:1.0 alpha:1.0]; } } @end @implementation NSAttributedString (SLiMBytes) + (NSAttributedString *)attributedStringForByteCount:(int64_t)bytes total:(double)total attributes:(NSDictionary *)attrs { NSString *byteString = [NSString stringForByteCount:bytes]; double fraction = bytes / total; NSColor *fractionColor = [NSColor slimColorForFraction:fraction]; NSMutableDictionary *colorAttrs = [NSMutableDictionary dictionaryWithDictionary:attrs]; [colorAttrs setObject:fractionColor forKey:NSBackgroundColorAttributeName]; return [[[NSAttributedString alloc] initWithString:byteString attributes:colorAttrs] autorelease]; } @end // Create a path for a temporary file; see https://stackoverflow.com/a/8307013/2752221 // Code is originally from https://developer.apple.com/library/archive/samplecode/SimpleURLConnections/Introduction/Intro.html#//apple_ref/doc/uid/DTS40009245 @implementation NSString (SLiMTempFiles) + (NSString *)slimPathForTemporaryFileWithPrefix:(NSString *)prefix { NSString * result; CFUUIDRef uuid; CFStringRef uuidStr; uuid = CFUUIDCreate(NULL); assert(uuid != NULL); uuidStr = CFUUIDCreateString(NULL, uuid); assert(uuidStr != NULL); result = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@", prefix, uuidStr]]; assert(result != nil); CFRelease(uuidStr); CFRelease(uuid); return result; } @end ================================================ FILE: SLiMgui/FindRecipeController.h ================================================ // // FindRecipeController.h // SLiM // // Created by Ben Haller on 10/11/18. // Copyright (c) 2018-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #import "EidosTextView.h" @interface FindRecipeController : NSWindowController { NSArray *recipeFilenames; NSArray *recipeContents; NSArray *matchRecipeFilenames; IBOutlet NSTextField *keyword1TextField; IBOutlet NSTextField *keyword2TextField; IBOutlet NSTextField *keyword3TextField; IBOutlet NSButton *buttonOK; IBOutlet NSTableView *matchTableView; IBOutlet EidosTextView *scriptPreview; } + (void)runFindRecipesPanel; - (IBAction)okButtonPressed:(id)sender; - (IBAction)cancelButtonPressed:(id)sender; @end ================================================ FILE: SLiMgui/FindRecipeController.mm ================================================ // // FindRecipeController.m // SLiMgui // // Created by Ben Haller on 10/11/18. // Copyright (c) 2018-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "FindRecipeController.h" #import "SLiMDocumentController.h" #import "AppDelegate.h" @implementation FindRecipeController + (void)runFindRecipesPanel { FindRecipeController *controller = [[FindRecipeController alloc] initWithWindowNibName:@"FindRecipePanel"]; [[NSApplication sharedApplication] runModalForWindow:[controller window]]; [[controller window] close]; [controller release]; } - (void)loadRecipes { NSURL *urlForRecipesFolder = [[NSBundle mainBundle] URLForResource:@"Recipes" withExtension:@""]; NSFileManager *fm = [NSFileManager defaultManager]; NSDirectoryEnumerator *dirEnum = [fm enumeratorAtURL:urlForRecipesFolder includingPropertiesForKeys:@[NSURLNameKey, NSURLIsDirectoryKey] options:(NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsSubdirectoryDescendants) errorHandler:nil]; NSMutableArray *filenames = [NSMutableArray array]; for (NSURL *fileURL in dirEnum) { NSNumber *isDirectory = nil; [fileURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; if (![isDirectory boolValue]) { NSString *name = nil; [fileURL getResourceValue:&name forKey:NSURLNameKey error:nil]; if ([name hasPrefix:@"Recipe "]) [filenames addObject:name]; } } [filenames sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { return [(NSString *)obj1 compare:(NSString *)obj2 options:NSNumericSearch]; }]; recipeFilenames = [filenames retain]; matchRecipeFilenames = [recipeFilenames retain]; // Look up recipe file contents and cache them NSMutableArray *contents = [NSMutableArray array]; for (NSString *filename : recipeFilenames) { NSString *filePath = [[urlForRecipesFolder path] stringByAppendingPathComponent:filename]; NSString *fileContents = [NSString stringWithContentsOfFile:filePath usedEncoding:nil error:nil]; [contents addObject:fileContents]; } recipeContents = [contents retain]; } - (NSString *)displayStringForRecipeFilename:(NSString *)name { if ([name hasSuffix:@".txt"]) { // Remove the .txt extension for SLiM models name = [name substringWithRange:NSMakeRange(7, [name length] - 11)]; } else if ([name hasSuffix:@".py"]) { // Leave the .py extension for Python models name = [name substringWithRange:NSMakeRange(7, [name length] - 7)]; name = [name stringByAppendingString:@" 🐍"]; } return name; } - (BOOL)recipeIndex:(int)recipeIndex matchesKeyword:(NSString *)keyword { // an empty keyword matches all recipes if (!keyword || ([keyword length] == 0)) return YES; // look for a match in the filename NSString *filename = [recipeFilenames objectAtIndex:recipeIndex]; if ([filename rangeOfString:keyword options:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch].location != NSNotFound) return YES; // look for a match in the file contents NSString *contents = [recipeContents objectAtIndex:recipeIndex]; if ([contents rangeOfString:keyword options:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch].location != NSNotFound) return YES; return NO; } - (void)constructMatchList { NSMutableArray *matches = [NSMutableArray array]; NSString *keyword1 = [keyword1TextField stringValue]; NSString *keyword2 = [keyword2TextField stringValue]; NSString *keyword3 = [keyword3TextField stringValue]; for (int i = 0; i < (int)[recipeFilenames count]; ++i) { if ([self recipeIndex:i matchesKeyword:keyword1] && [self recipeIndex:i matchesKeyword:keyword2] && [self recipeIndex:i matchesKeyword:keyword3]) [matches addObject:[recipeFilenames objectAtIndex:i]]; } [matchRecipeFilenames release]; matchRecipeFilenames = [matches retain]; } - (void)dealloc { [recipeFilenames release]; recipeFilenames = nil; [recipeContents release]; recipeContents = nil; [matchRecipeFilenames release]; matchRecipeFilenames = nil; [super dealloc]; } - (void)windowDidLoad { [super windowDidLoad]; [self loadRecipes]; [matchTableView reloadData]; [matchTableView setNeedsDisplay]; [self validateOK]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [scriptPreview setSyntaxColoring:[defaults boolForKey:defaultsSyntaxHighlightScriptKey] ? kEidosSyntaxColoringEidos : kEidosSyntaxColoringNone]; [scriptPreview setDisplayFontSize:10]; } - (void)validateOK { [buttonOK setEnabled:([matchTableView numberOfSelectedRows] > 0)]; } - (void)updatePreview { NSInteger selectedRowIndex = [matchTableView selectedRow]; if (selectedRowIndex == -1) { [scriptPreview setString:@""]; } else { NSString *filename = [matchRecipeFilenames objectAtIndex:selectedRowIndex]; NSBundle *bundle = [NSBundle mainBundle]; NSURL *urlForRecipe = [bundle URLForResource:[filename stringByDeletingPathExtension] withExtension:[filename pathExtension] subdirectory:@"Recipes"]; NSString *scriptString = [NSString stringWithContentsOfURL:urlForRecipe usedEncoding:NULL error:NULL]; [scriptPreview setString:scriptString]; [scriptPreview recolorAfterChanges]; [self highlightPreview]; } } - (void)highlightPreview { // Highlight matches in the selected recipe [scriptPreview clearHighlightMatches]; NSString *keyword1 = [keyword1TextField stringValue]; NSString *keyword2 = [keyword2TextField stringValue]; NSString *keyword3 = [keyword3TextField stringValue]; if ([keyword1 length]) [scriptPreview highlightMatchesForString:keyword1]; if ([keyword2 length]) [scriptPreview highlightMatchesForString:keyword2]; if ([keyword3 length]) [scriptPreview highlightMatchesForString:keyword3]; } - (void)keywordChanged:(id)sender { // Remember the title of the currently selected recipes NSInteger selectedRowIndex = [matchTableView selectedRow]; NSString *selectedRowText = nil; if (selectedRowIndex != -1) selectedRowText = [[matchRecipeFilenames objectAtIndex:selectedRowIndex] retain]; // Filter the recipe list by the keywords entered [self constructMatchList]; // Reload the tableview with recipes that match the new keywords [matchTableView reloadData]; // Restore the selection to the extent possible if (selectedRowText) { BOOL foundSelection = NO; for (NSInteger i = 0; i < [matchTableView numberOfRows]; ++i) { NSString *rowText = [matchRecipeFilenames objectAtIndex:i]; if ([rowText isEqualToString:selectedRowText]) { [matchTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:i] byExtendingSelection:NO]; foundSelection = YES; break; } } if (foundSelection) { [self highlightPreview]; } else { // tableViewSelectionDidChange: is not called when the selection is lost by reloadData [self validateOK]; [self updatePreview]; } [selectedRowText release]; } } - (IBAction)okButtonPressed:(id)sender { // Open the selected recipe(s) NSInteger selectedRowIndex = [matchTableView selectedRow]; if (selectedRowIndex != -1) [[NSDocumentController sharedDocumentController] openRecipeWithFilename:[matchRecipeFilenames objectAtIndex:selectedRowIndex]]; // Then terminate the modal run loop [[NSApplication sharedApplication] stopModal]; } - (IBAction)cancelButtonPressed:(id)sender { // Terminate the modal run loop [[NSApplication sharedApplication] abortModal]; } // NSTableViewDataSource #pragma mark - #pragma mark NSTableViewDataSource - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return [matchRecipeFilenames count]; } - (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { if ((row < 0) || (row >= (NSInteger)[matchRecipeFilenames count])) return nil; return [self displayStringForRecipeFilename:[matchRecipeFilenames objectAtIndex:row]]; } - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { // This is mostly Apple boilerplate code; I couldn't get a cell-based table to work, so I gave up and made it view-based // Get an existing cell with the MyView identifier if it exists NSTextField *result = [tableView makeViewWithIdentifier:@"MyView" owner:self]; // There is no existing cell to reuse so create a new one if (result == nil) { // Create the new NSTextField with a frame of the {0,0} with the width of the table. // Note that the height of the frame is not really relevant, because the row height will modify the height. result = [[NSTextField alloc] initWithFrame:NSZeroRect]; // The identifier of the NSTextField instance is set to MyView. // This allows the cell to be reused. result.identifier = @"MyView"; // BCH: configure the textfield view to look the way we want; could get it from the nib instead but this is easy [result setBordered:NO]; [result setDrawsBackground:NO]; [result setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; // the truncation of lines with ellipses works, but expansion tooltips only show when the row is selected, grr [[result cell] setLineBreakMode:NSLineBreakByTruncatingTail]; [[result cell] setTruncatesLastVisibleLine:YES]; [result setAllowsExpansionToolTips:YES]; // make the textfield uneditable [result setEditable:NO]; } return result; } // NSTableViewDelegate #pragma mark - #pragma mark NSTableViewDelegate - (void)tableViewSelectionDidChange:(NSNotification *)notification { [self validateOK]; [self updatePreview]; } // NSTextFieldDelegate #pragma mark - #pragma mark NSTextFieldDelegate - (void)controlTextDidChange:(NSNotification *)obj { [self keywordChanged:nil]; } @end ================================================ FILE: SLiMgui/GraphView.h ================================================ // // GraphView.h // SLiM // // Created by Ben Haller on 2/27/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #include "slim_globals.h" @class SLiMWindowController; // A quick and dirty macro to enable rounding of coordinates to the nearest pixel only when we are not generating PDF // FIXME this ought to be revisited in the light of Retina displays, printing, etc. #define SLIM_SCREEN_ROUND(x) (generatingPDF ? (x) : round(x)) @interface GraphView : NSView { std::string focalSpeciesName; // we keep the name of our focal species, since a pointer would be unsafe std::string focalSpeciesAvatar; // cached so we can display it even when the simulation is invalid // set to YES during a copy: operation, to allow customization BOOL generatingPDF; // caching for drawing speed is up to subclasses, if they want to do it, but we provide minimal support in GraphView to make it work smoothly // this flag is to prevent recursion in the drawing code, and to disable drawing of things that don't belong in a cache, such as the legend BOOL cachingNow; // GraphAxisRescaleSheet outlets IBOutlet NSWindow *rescaleSheet; IBOutlet NSTextField *rescaleSheetMinTextfield; IBOutlet NSTextField *rescaleSheetMaxTextfield; IBOutlet NSTextField *rescaleSheetMajorIntervalTextfield; IBOutlet NSTextField *rescaleSheetMinorModulusTextfield; IBOutlet NSTextField *rescaleSheetTickPrecisionTextfield; // GraphBarRescaleSheet outlets IBOutlet NSWindow *rescaleBarsSheet; IBOutlet NSTextField *rescaleBarsSheetCountTextfield; } @property (nonatomic, assign) SLiMWindowController *slimWindowController; @property (nonatomic) BOOL showXAxis; @property (nonatomic) BOOL allowXAxisUserRescale; @property (nonatomic) BOOL xAxisIsUserRescaled; @property (nonatomic) BOOL showXAxisTicks; @property (nonatomic) double xAxisMin, xAxisMax; @property (nonatomic) double xAxisMajorTickInterval, xAxisMinorTickInterval; @property (nonatomic) int xAxisMajorTickModulus, xAxisTickValuePrecision; @property (nonatomic, retain) NSAttributedString *xAxisLabel; @property (nonatomic) BOOL showYAxis; @property (nonatomic) BOOL allowYAxisUserRescale; @property (nonatomic) BOOL yAxisIsUserRescaled; @property (nonatomic) BOOL showYAxisTicks; @property (nonatomic) double yAxisMin, yAxisMax; @property (nonatomic) double yAxisMajorTickInterval, yAxisMinorTickInterval; @property (nonatomic) int yAxisMajorTickModulus, yAxisTickValuePrecision; @property (nonatomic, retain) NSAttributedString *yAxisLabel; @property (nonatomic) BOOL legendVisible; @property (nonatomic) BOOL showHorizontalGridLines; @property (nonatomic) BOOL showVerticalGridLines; @property (nonatomic) BOOL showFullBox; @property (nonatomic) BOOL tweakXAxisTickLabelAlignment; + (NSString *)labelFontName; + (NSDictionary *)attributesForAxisLabels; + (NSDictionary *)attributesForTickLabels; + (NSDictionary *)attributesForLegendLabels; + (NSColor *)gridLineColor; - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller; // designated initializer - (Species *)focalDisplaySpecies; - (void)updateSpeciesBadge; - (void)cleanup; - (void)setXAxisLabelString:(NSString *)labelString; - (void)setYAxisLabelString:(NSString *)labelString; - (NSRect)interiorRectForBounds:(NSRect)bounds; - (double)plotToDeviceX:(double)plotx withInteriorRect:(NSRect)interiorRect; - (double)plotToDeviceY:(double)ploty withInteriorRect:(NSRect)interiorRect; - (double)roundPlotToDeviceX:(double)plotx withInteriorRect:(NSRect)interiorRect; // rounded off to the nearest midpixel - (double)roundPlotToDeviceY:(double)ploty withInteriorRect:(NSRect)interiorRect; // rounded off to the nearest midpixel - (void)willDrawWithInteriorRect:(NSRect)interiorRect andController:(SLiMWindowController *)controller; // called prior to drawing, to allow dynamic axis rescaling and other adjustments - (void)drawXAxisTicksWithInteriorRect:(NSRect)interiorRect; - (void)drawXAxisWithInteriorRect:(NSRect)interiorRect; - (void)drawYAxisTicksWithInteriorRect:(NSRect)interiorRect; - (void)drawYAxisWithInteriorRect:(NSRect)interiorRect; - (void)drawVerticalGridLinesWithInteriorRect:(NSRect)interiorRect; - (void)drawHorizontalGridLinesWithInteriorRect:(NSRect)interiorRect; - (void)drawMessage:(NSString *)messageString inRect:(NSRect)rect; - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller; - (NSArray *)legendKey; // subclasses can provide an NSArray of key entries, each an NSArray of an NSString and an NSColor - (NSSize)legendSize; // unless overridden, this calls -legendKey and follows its intructions - (void)drawLegendInRect:(NSRect)legendRect; // unless overridden, this calls -legendKey and follows its intructions - (IBAction)rescaleSheetOK:(id)sender; - (IBAction)rescaleSheetCancel:(id)sender; - (IBAction)userRescaleXAxis:(id)sender; - (IBAction)userRescaleYAxis:(id)sender; - (IBAction)copy:(id)sender; - (IBAction)saveGraph:(id)sender; - (IBAction)copyData:(id)sender; - (IBAction)saveData:(id)sender; - (NSString *)dateline; - (NSMenu *)menuForEvent:(NSEvent *)theEvent; - (void)subclassAddItemsToMenu:(NSMenu *)menu forEvent:(NSEvent *)theEvent; // subclasses normally add their menu items here so they appear above the copy graph/data menu items - (void)invalidateDrawingCache; // GraphView does not keep a drawing cache, but it supports having one; this gets called in situations when such a cache would be invalidated - (void)graphWindowResized; // called by SLiMWindowController to let the GraphView do whatever recalculation, cache invalidation, etc. it might want to do - (void)controllerRecycled; // called by SLiMWindowController when the simulation is recycled, to let the GraphView do whatever re-initialization is needed - (void)controllerSelectionChanged; // called by SLiMWindowController when the selection changes, to let the GraphView respond - (void)controllerTickFinished; // called by SLiMWindowController when a simulation tick ends, to allow per-tick data gathering; redrawing should not be done here - (void)updateAfterTick; // by default, calls setNeedsDisplay:YES; can also perform other updating work // Additional properties that conceptually belong to PrefabAdditions below @property (nonatomic) int histogramBinCount; // provided for barplots @property (nonatomic) BOOL allowXAxisBinRescale; // if YES, the GraphView will provide a context menu item and run a panel to set the bar count @end @interface GraphView (PrefabAdditions) // A prefab method to set up a good x axis to span the tick range, whatever it might be - (void)setXAxisRangeFromTick; // a prefab legend that shows all of the mutation types, with color swatches and labels - (NSArray *)mutationTypeLegendKey; // a prefab method to draw simple barplots - (void)drawBarplotInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller buffer:(double *)buffer binCount:(int)binCount firstBinValue:(double)firstBinValue binWidth:(double)binWidth; // a prefab method to draw grouped barplots - (void)drawGroupedBarplotInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller buffer:(double *)buffer subBinCount:(int)subBinCount mainBinCount:(int)mainBinCount firstBinValue:(double)firstBinValue mainBinWidth:(double)mainBinWidth; @end @interface GraphView (OptionalSubclassMethods) - (NSString *)stringForDataWithController:(SLiMWindowController *)controller; @end ================================================ FILE: SLiMgui/GraphView.mm ================================================ // // GraphView.m // SLiM // // Created by Ben Haller on 2/27/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "GraphView.h" #import "SLiMWindowController.h" #import "species.h" #include "community.h" @implementation GraphView + (NSString *)labelFontName { return @"Times New Roman"; } + (NSDictionary *)attributesForAxisLabels { static NSDictionary *attrs = nil; if (!attrs) attrs = [@{NSFontAttributeName : [NSFont fontWithName:[self labelFontName] size:14]} retain]; return attrs; } + (NSDictionary *)attributesForAvatars { static NSDictionary *attrs = nil; if (!attrs) attrs = [@{NSFontAttributeName : [NSFont fontWithName:[self labelFontName] size:12]} retain]; return attrs; } + (NSDictionary *)attributesForTickLabels { static NSDictionary *attrs = nil; if (!attrs) attrs = [@{NSFontAttributeName : [NSFont fontWithName:[self labelFontName] size:10]} retain]; return attrs; } + (NSDictionary *)attributesForLegendLabels { static NSDictionary *attrs = nil; if (!attrs) attrs = [@{NSFontAttributeName : [NSFont fontWithName:[self labelFontName] size:10]} retain]; return attrs; } + (NSColor *)gridLineColor { static NSColor *gridColor = nil; if (!gridColor) gridColor = [[NSColor colorWithCalibratedWhite:0.85 alpha:1.0] retain]; return gridColor; } - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller { if (self = [super initWithFrame:frameRect]) { [self setSlimWindowController:controller]; focalSpeciesName = [controller focalDisplaySpecies]->name_; _showXAxis = TRUE; _allowXAxisUserRescale = TRUE; _showXAxisTicks = TRUE; _showYAxis = TRUE; _allowYAxisUserRescale = TRUE; _showYAxisTicks = TRUE; _xAxisMin = 0.0; _xAxisMax = 1.0; _xAxisMajorTickInterval = 0.5; _xAxisMinorTickInterval = 0.25; _xAxisMajorTickModulus = 2; _xAxisTickValuePrecision = 1; _yAxisMin = 0.0; _yAxisMax = 1.0; _yAxisMajorTickInterval = 0.5; _yAxisMinorTickInterval = 0.25; _yAxisMajorTickModulus = 2; _yAxisTickValuePrecision = 1; _xAxisLabel = [[NSAttributedString alloc] initWithString:@"This is the x-axis, yo" attributes:[[self class] attributesForAxisLabels]]; _yAxisLabel = [[NSAttributedString alloc] initWithString:@"This is the y-axis, yo" attributes:[[self class] attributesForAxisLabels]]; _legendVisible = YES; _showHorizontalGridLines = NO; _showVerticalGridLines = NO; _showFullBox = NO; } return self; } - (Species *)focalDisplaySpecies { // We look up our focal species object by name every time, since keeping a pointer to it would be unsafe // Before initialize() is done species have not been created, so we return nullptr in that case SLiMWindowController *controller = [self slimWindowController]; if (controller && controller->community && (controller->community->Tick() >= 1)) return controller->community->SpeciesWithName(focalSpeciesName); return nullptr; } - (void)updateSpeciesBadge { Species *graphSpecies = [self focalDisplaySpecies]; // Cache our graphSpecies avatar whenever we're in a valid state, because it could change, // and because we want to be able to display it even when the sim is in an invalid state if (graphSpecies) { if (graphSpecies->community_.all_species_.size() > 1) focalSpeciesAvatar = graphSpecies->avatar_; else focalSpeciesAvatar = ""; } } - (void)cleanup { //NSLog(@"[GraphView cleanup]"); [self invalidateDrawingCache]; [_xAxisLabel release]; _xAxisLabel = nil; [_yAxisLabel release]; _yAxisLabel = nil; _slimWindowController = nil; } - (void)dealloc { [self cleanup]; //NSLog(@"[GraphView dealloc]"); [super dealloc]; } - (void)setXAxisLabelString:(NSString *)labelString { [self setXAxisLabel:[[[NSAttributedString alloc] initWithString:labelString attributes:[[self class] attributesForAxisLabels]] autorelease]]; } - (void)setYAxisLabelString:(NSString *)labelString { [self setYAxisLabel:[[[NSAttributedString alloc] initWithString:labelString attributes:[[self class] attributesForAxisLabels]] autorelease]]; } - (NSRect)interiorRectForBounds:(NSRect)bounds { NSRect interiorRect = bounds; // For now, 10 pixels margin on a side if there is no axis, 40 pixels margin if there is an axis if ([self showXAxis]) { interiorRect.origin.x += 50; interiorRect.size.width -= 60; } else { interiorRect.origin.x += 10; interiorRect.size.width -= 20; } if ([self showYAxis]) { interiorRect.origin.y += 50; interiorRect.size.height -= 60; } else { interiorRect.origin.y += 10; interiorRect.size.height -= 20; } return interiorRect; } - (double)plotToDeviceX:(double)plotx withInteriorRect:(NSRect)interiorRect { double fractionAlongAxis = (plotx - _xAxisMin) / (_xAxisMax - _xAxisMin); // We go from the center of the first pixel to the center of the last pixel return (fractionAlongAxis * (interiorRect.size.width - 1.0) + interiorRect.origin.x) + 0.5; } - (double)plotToDeviceY:(double)ploty withInteriorRect:(NSRect)interiorRect { double fractionAlongAxis = (ploty - _yAxisMin) / (_yAxisMax - _yAxisMin); // We go from the center of the first pixel to the center of the last pixel return (fractionAlongAxis * (interiorRect.size.height - 1.0) + interiorRect.origin.y) + 0.5; } - (double)roundPlotToDeviceX:(double)plotx withInteriorRect:(NSRect)interiorRect { double fractionAlongAxis = (plotx - _xAxisMin) / (_xAxisMax - _xAxisMin); // We go from the center of the first pixel to the center of the last pixel, rounded off to pixel midpoints return SLIM_SCREEN_ROUND(fractionAlongAxis * (interiorRect.size.width - 1.0) + interiorRect.origin.x) + 0.5; } - (double)roundPlotToDeviceY:(double)ploty withInteriorRect:(NSRect)interiorRect { double fractionAlongAxis = (ploty - _yAxisMin) / (_yAxisMax - _yAxisMin); // We go from the center of the first pixel to the center of the last pixel, rounded off to pixel midpoints return SLIM_SCREEN_ROUND(fractionAlongAxis * (interiorRect.size.height - 1.0) + interiorRect.origin.y) + 0.5; } - (void)willDrawWithInteriorRect:(NSRect)interiorRect andController:(SLiMWindowController *)controller { } - (void)drawXAxisTicksWithInteriorRect:(NSRect)interiorRect { NSDictionary *tickAttributes = [[self class] attributesForTickLabels]; NSBezierPath *path = [NSBezierPath bezierPath]; double axisMin = [self xAxisMin]; double axisMax = [self xAxisMax]; double minorTickInterval = [self xAxisMinorTickInterval]; int majorTickModulus = [self xAxisMajorTickModulus]; int tickValuePrecision = [self xAxisTickValuePrecision]; double tickValue; int tickIndex; for (tickValue = axisMin, tickIndex = 0; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += minorTickInterval, tickIndex++) { BOOL isMajorTick = ((tickIndex % majorTickModulus) == 0); double xValueForTick = SLIM_SCREEN_ROUND(interiorRect.origin.x + (interiorRect.size.width - 1) * ((tickValue - axisMin) / (axisMax - axisMin))) + 0.5; //NSLog(@"tickValue == %f, isMajorTick == %@, xValueForTick == %f", tickValue, isMajorTick ? @"YES" : @"NO", xValueForTick); [path moveToPoint:NSMakePoint(xValueForTick, interiorRect.origin.y - (isMajorTick ? 6 : 3))]; [path lineToPoint:NSMakePoint(xValueForTick, interiorRect.origin.y - 0.5)]; if (isMajorTick) { double labelValueForTick; labelValueForTick = tickValue; NSString *labelText = [NSString stringWithFormat:@"%.*f", tickValuePrecision, labelValueForTick]; NSAttributedString *attributedLabel = [[NSMutableAttributedString alloc] initWithString:labelText attributes:tickAttributes]; NSSize labelSize = [attributedLabel size]; double labelY = interiorRect.origin.y - (labelSize.height + 4); if ([self tweakXAxisTickLabelAlignment]) { if (tickValue == axisMin) [attributedLabel drawAtPoint:NSMakePoint(xValueForTick - 2.0, labelY)]; else if (tickValue == axisMax) [attributedLabel drawAtPoint:NSMakePoint(xValueForTick - SLIM_SCREEN_ROUND(labelSize.width) + 2.0, labelY)]; else [attributedLabel drawAtPoint:NSMakePoint(xValueForTick - SLIM_SCREEN_ROUND(labelSize.width / 2.0), labelY)]; } else { [attributedLabel drawAtPoint:NSMakePoint(xValueForTick - SLIM_SCREEN_ROUND(labelSize.width / 2.0), labelY)]; } [attributedLabel release]; } } [path setLineWidth:1.0]; [[NSColor blackColor] set]; [path stroke]; } - (void)drawXAxisWithInteriorRect:(NSRect)interiorRect { int yAxisFudge = [self showYAxis] ? 1 : 0; NSRect axisRect = NSMakeRect(interiorRect.origin.x - yAxisFudge, interiorRect.origin.y - 1, interiorRect.size.width + yAxisFudge, 1); [[NSColor blackColor] set]; NSRectFill(axisRect); // show label NSAttributedString *label = [self xAxisLabel]; NSSize labelSize = [label size]; [label drawAtPoint:NSMakePoint(interiorRect.origin.x + (interiorRect.size.width - labelSize.width) / 2.0, interiorRect.origin.y - 45)]; } - (void)drawYAxisTicksWithInteriorRect:(NSRect)interiorRect { NSDictionary *tickAttributes = [[self class] attributesForTickLabels]; NSBezierPath *path = [NSBezierPath bezierPath]; double axisMin = [self yAxisMin]; double axisMax = [self yAxisMax]; double minorTickInterval = [self yAxisMinorTickInterval]; int majorTickModulus = [self yAxisMajorTickModulus]; int tickValuePrecision = [self yAxisTickValuePrecision]; double tickValue; int tickIndex; for (tickValue = axisMin, tickIndex = 0; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += minorTickInterval, tickIndex++) { BOOL isMajorTick = ((tickIndex % majorTickModulus) == 0); double yValueForTick = SLIM_SCREEN_ROUND(interiorRect.origin.y + (interiorRect.size.height - 1) * ((tickValue - axisMin) / (axisMax - axisMin))) + 0.5; //NSLog(@"tickValue == %f, isMajorTick == %@, yValueForTick == %f", tickValue, isMajorTick ? @"YES" : @"NO", yValueForTick); [path moveToPoint:NSMakePoint(interiorRect.origin.x - (isMajorTick ? 6 : 3), yValueForTick)]; [path lineToPoint:NSMakePoint(interiorRect.origin.x - 0.5, yValueForTick)]; if (isMajorTick) { double labelValueForTick; labelValueForTick = tickValue; NSString *labelText = [NSString stringWithFormat:@"%.*f", tickValuePrecision, labelValueForTick]; NSAttributedString *attributedLabel = [[NSMutableAttributedString alloc] initWithString:labelText attributes:tickAttributes]; NSSize labelSize = [attributedLabel size]; [attributedLabel drawAtPoint:NSMakePoint(interiorRect.origin.x - (labelSize.width + 8), yValueForTick - SLIM_SCREEN_ROUND(labelSize.height / 2.0) + 2)]; [attributedLabel release]; } } [path setLineWidth:1.0]; [[NSColor blackColor] set]; [path stroke]; } - (void)drawYAxisWithInteriorRect:(NSRect)interiorRect { int xAxisFudge = [self showXAxis] ? 1 : 0; NSRect axisRect = NSMakeRect(interiorRect.origin.x - 1, interiorRect.origin.y - xAxisFudge, 1, interiorRect.size.height + xAxisFudge); [[NSColor blackColor] set]; NSRectFill(axisRect); // show label, rotated NSAttributedString *label = [self yAxisLabel]; NSSize labelSize = [label size]; [NSGraphicsContext saveGraphicsState]; NSAffineTransform *transform = [NSAffineTransform transform]; [transform translateXBy:interiorRect.origin.x - 30 yBy:interiorRect.origin.y + SLIM_SCREEN_ROUND(interiorRect.size.height / 2.0)]; [transform rotateByDegrees:90]; [transform concat]; [label drawAtPoint:NSMakePoint(SLIM_SCREEN_ROUND(-labelSize.width / 2.0), 0)]; [NSGraphicsContext restoreGraphicsState]; } - (void)drawFullBoxWithInteriorRect:(NSRect)interiorRect { NSRect axisRect; // upper x axis int yAxisFudge = [self showYAxis] ? 1 : 0; axisRect = NSMakeRect(interiorRect.origin.x - yAxisFudge, interiorRect.origin.y + interiorRect.size.height, interiorRect.size.width + yAxisFudge + 1, 1); [[NSColor blackColor] set]; NSRectFill(axisRect); // right-hand y axis int xAxisFudge = [self showXAxis] ? 1 : 0; axisRect = NSMakeRect(interiorRect.origin.x + interiorRect.size.width, interiorRect.origin.y - xAxisFudge, 1, interiorRect.size.height + xAxisFudge + 1); [[NSColor blackColor] set]; NSRectFill(axisRect); } - (void)drawVerticalGridLinesWithInteriorRect:(NSRect)interiorRect { NSBezierPath *path = [NSBezierPath bezierPath]; double axisMin = [self xAxisMin]; double axisMax = [self xAxisMax]; double minorTickInterval = [self xAxisMinorTickInterval]; double tickValue; int tickIndex; for (tickValue = axisMin, tickIndex = 0; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += minorTickInterval, tickIndex++) { double xValueForTick = SLIM_SCREEN_ROUND(interiorRect.origin.x + (interiorRect.size.width - 1) * ((tickValue - axisMin) / (axisMax - axisMin))) + 0.5; if (fabs((xValueForTick - 0.5) - interiorRect.origin.x) < 0.001) continue; if ((fabs((xValueForTick - 0.5) - (interiorRect.origin.x + interiorRect.size.width - 1)) < 0.001) && [self showFullBox]) continue; [path moveToPoint:NSMakePoint(xValueForTick, interiorRect.origin.y - 0.01)]; [path lineToPoint:NSMakePoint(xValueForTick, interiorRect.origin.y + interiorRect.size.height + 0.01)]; } [path setLineWidth:1.0]; [[[self class] gridLineColor] set]; [path stroke]; } - (void)drawHorizontalGridLinesWithInteriorRect:(NSRect)interiorRect { NSBezierPath *path = [NSBezierPath bezierPath]; double axisMin = [self yAxisMin]; double axisMax = [self yAxisMax]; double minorTickInterval = [self yAxisMinorTickInterval]; double tickValue; int tickIndex; for (tickValue = axisMin, tickIndex = 0; tickValue <= (axisMax + minorTickInterval / 10.0); tickValue += minorTickInterval, tickIndex++) { double yValueForTick = SLIM_SCREEN_ROUND(interiorRect.origin.y + (interiorRect.size.height - 1) * ((tickValue - axisMin) / (axisMax - axisMin))) + 0.5; if (fabs((yValueForTick - 0.5) - interiorRect.origin.y) < 0.001) continue; if ((fabs((yValueForTick - 0.5) - (interiorRect.origin.y + interiorRect.size.height - 1)) < 0.001) && [self showFullBox]) continue; [path moveToPoint:NSMakePoint(interiorRect.origin.x - 0.01, yValueForTick)]; [path lineToPoint:NSMakePoint(interiorRect.origin.x + interiorRect.size.width + 0.01, yValueForTick)]; } [path setLineWidth:1.0]; [[[self class] gridLineColor] set]; [path stroke]; } - (void)drawMessage:(NSString *)messageString inRect:(NSRect)rect { static NSDictionary *attrs = nil; if (!attrs) attrs = [@{NSFontAttributeName : [NSFont fontWithName:[[self class] labelFontName] size:16], NSForegroundColorAttributeName : [NSColor colorWithCalibratedWhite:0.4 alpha:1.0]} retain]; NSAttributedString *attrMessage = [[NSAttributedString alloc] initWithString:messageString attributes:attrs]; NSPoint centerPoint = NSMakePoint(rect.origin.x + rect.size.width / 2, rect.origin.y + rect.size.height / 2); NSSize messageSize = [attrMessage size]; NSPoint drawPoint = NSMakePoint(SLIM_SCREEN_ROUND(centerPoint.x - messageSize.width / 2.0), SLIM_SCREEN_ROUND(centerPoint.y - messageSize.height / 2.0)); [attrMessage drawAtPoint:drawPoint]; [attrMessage release]; } - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { [[NSColor colorWithCalibratedHue:0.15 saturation:0.15 brightness:1.0 alpha:1.0] set]; NSRectFill(interiorRect); } - (NSArray *)legendKey { return nil; } - (NSSize)legendSize { NSArray *legendKey = [self legendKey]; NSInteger legendEntryCount = [legendKey count]; const int legendRowHeight = 15; NSDictionary *legendAttrs = [[self class] attributesForLegendLabels]; __block NSSize legendSize = NSMakeSize(0, legendRowHeight * legendEntryCount - 6); [legendKey enumerateObjectsUsingBlock:^(NSArray *legendEntry, NSUInteger idx, BOOL *stop) { NSString *labelString = [legendEntry objectAtIndex:0]; NSAttributedString *label = [[NSAttributedString alloc] initWithString:labelString attributes:legendAttrs]; NSSize labelSize = [label size]; legendSize.width = MAX(legendSize.width, 0 + (legendRowHeight - 6) + 5 + labelSize.width); [label release]; }]; return legendSize; } - (void)drawLegendInRect:(NSRect)legendRect { NSArray *legendKey = [self legendKey]; NSInteger legendEntryCount = [legendKey count]; const int legendRowHeight = 15; NSDictionary *legendAttrs = [[self class] attributesForLegendLabels]; [legendKey enumerateObjectsUsingBlock:^(NSArray *legendEntry, NSUInteger idx, BOOL *stop) { NSRect swatchRect = NSMakeRect(legendRect.origin.x, legendRect.origin.y + ((legendEntryCount - 1) * legendRowHeight - 3) - idx * legendRowHeight + 3, legendRowHeight - 6, legendRowHeight - 6); NSString *labelString = [legendEntry objectAtIndex:0]; NSAttributedString *label = [[NSAttributedString alloc] initWithString:labelString attributes:legendAttrs]; NSColor *entryColor = [legendEntry objectAtIndex:1]; [entryColor set]; NSRectFill(swatchRect); [[NSColor blackColor] set]; NSFrameRect(swatchRect); [label drawAtPoint:NSMakePoint(swatchRect.origin.x + swatchRect.size.width + 5, swatchRect.origin.y - 2)]; [label release]; }]; } - (void)drawLegendInInteriorRect:(NSRect)interiorRect { NSSize legendSize = [self legendSize]; legendSize.width = ceil(legendSize.width); legendSize.height = ceil(legendSize.height); if ((legendSize.width > 0.0) && (legendSize.height > 0.0)) { const int legendMargin = 10; NSRect legendRect = NSMakeRect(0, 0, legendSize.width + legendMargin + legendMargin, legendSize.height + legendMargin + legendMargin); // Position the legend in the upper right, for now; this should be configurable legendRect.origin.x = interiorRect.origin.x + interiorRect.size.width - legendRect.size.width; legendRect.origin.y = interiorRect.origin.y + interiorRect.size.height - legendRect.size.height; // Frame the legend and erase it with a slightly translucent wash [[NSColor colorWithCalibratedWhite:0.95 alpha:0.9] set]; NSRectFillUsingOperation(legendRect, NSCompositingOperationSourceOver); [[NSColor colorWithCalibratedWhite:0.3 alpha:1.0] set]; NSFrameRect(legendRect); // Inset and draw the legend content legendRect = NSInsetRect(legendRect, legendMargin, legendMargin); [self drawLegendInRect:legendRect]; } } - (void)drawSpeciesAvatar { SLiMWindowController *controller = [self slimWindowController]; NSRect bounds = [self bounds]; if (controller->community->all_species_.size() > 1) { Species *displaySpecies = [self focalDisplaySpecies]; if (displaySpecies) { NSDictionary *avatarAttrs = [[self class] attributesForAvatars]; NSString *avatarString = [NSString stringWithUTF8String:displaySpecies->avatar_.c_str()]; NSAttributedString *avatar = [[NSAttributedString alloc] initWithString:avatarString attributes:avatarAttrs]; [avatar drawAtPoint:NSMakePoint(bounds.origin.x + bounds.size.width - 23, bounds.origin.x + 7)]; [avatar release]; } } } - (void)drawRect:(NSRect)dirtyRect { NSRect bounds = [self bounds]; // Erase background [[NSColor whiteColor] set]; NSRectFill(dirtyRect); // Get our controller and test for validity, so subclasses don't have to worry about this SLiMWindowController *controller = [self slimWindowController]; if (!controller || [controller invalidSimulation]) { [self drawMessage:@" invalid\nsimulation" inRect:bounds]; } else if (controller->community->Tick() == 0) { [self drawMessage:@" no\ndata" inRect:bounds]; } else if (![self focalDisplaySpecies]) { // The species name no longer refers to a species in the community [self drawMessage:@"missing\nspecies" inRect:bounds]; } else { NSRect interiorRect = [self interiorRectForBounds:bounds]; [self willDrawWithInteriorRect:interiorRect andController:controller]; // Draw grid lines, if requested, and if tick marks are turned on for the corresponding axis if ([self showHorizontalGridLines] && [self showYAxis] && [self showYAxisTicks]) [self drawHorizontalGridLinesWithInteriorRect:interiorRect]; if ([self showVerticalGridLines] && [self showXAxis] && [self showXAxisTicks]) [self drawVerticalGridLinesWithInteriorRect:interiorRect]; // Draw the interior of the graph; this will be overridden by the subclass // We clip the interior drawing to the interior rect, so outliers get clipped out [NSGraphicsContext saveGraphicsState]; [[NSBezierPath bezierPathWithRect:NSInsetRect(interiorRect, -0.1, -0.1)] addClip]; [self drawGraphInInteriorRect:interiorRect withController:controller]; [NSGraphicsContext restoreGraphicsState]; // If we're caching, skip all overdrawing, since it cannot be cached (it would then appear under new drawing that supplements the cache) if (!cachingNow) { // Overdraw axes, ticks, and axis labels, if requested if ([self showXAxis] && [self showXAxisTicks]) [self drawXAxisTicksWithInteriorRect:interiorRect]; if ([self showYAxis] && [self showYAxisTicks]) [self drawYAxisTicksWithInteriorRect:interiorRect]; if ([self showXAxis]) [self drawXAxisWithInteriorRect:interiorRect]; if ([self showYAxis]) [self drawYAxisWithInteriorRect:interiorRect]; if ([self showFullBox]) [self drawFullBoxWithInteriorRect:interiorRect]; // Overdraw the legend if ([self legendVisible]) [self drawLegendInInteriorRect:interiorRect]; // Overdraw the species avatar [self drawSpeciesAvatar]; } } } - (IBAction)toggleLegend:(id)sender { [self setLegendVisible:![self legendVisible]]; [self setNeedsDisplay:YES]; } - (IBAction)toggleHorizontalGridLines:(id)sender { [self setShowHorizontalGridLines:![self showHorizontalGridLines]]; [self setNeedsDisplay:YES]; } - (IBAction)toggleVerticalGridLines:(id)sender { [self setShowVerticalGridLines:![self showVerticalGridLines]]; [self setNeedsDisplay:YES]; } - (IBAction)toggleFullBox:(id)sender { [self setShowFullBox:![self showFullBox]]; [self setNeedsDisplay:YES]; } - (IBAction)rescaleSheetOK:(id)sender { SLiMWindowController *controller = [self slimWindowController]; NSWindow *window = [controller window]; // Give our textfields a chance to validate if (![rescaleSheet makeFirstResponder:nil]) return; [window endSheet:rescaleSheet returnCode:NSAlertFirstButtonReturn]; } - (IBAction)rescaleSheetCancel:(id)sender { SLiMWindowController *controller = [self slimWindowController]; NSWindow *window = [controller window]; [window endSheet:rescaleSheet returnCode:NSAlertSecondButtonReturn]; } - (IBAction)userRescaleXAxis:(id)sender { if ([self allowXAxisUserRescale]) { SLiMWindowController *controller = [self slimWindowController]; NSWindow *window = [controller window]; [[NSBundle mainBundle] loadNibNamed:@"GraphAxisRescaleSheet" owner:self topLevelObjects:NULL]; [rescaleSheet retain]; [rescaleSheetMinTextfield setDoubleValue:[self xAxisMin]]; [rescaleSheetMaxTextfield setDoubleValue:[self xAxisMax]]; [rescaleSheetMajorIntervalTextfield setDoubleValue:[self xAxisMajorTickInterval]]; [rescaleSheetMinorModulusTextfield setIntValue:[self xAxisMajorTickModulus]]; [rescaleSheetTickPrecisionTextfield setIntValue:[self xAxisTickValuePrecision]]; [window beginSheet:rescaleSheet completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertFirstButtonReturn) { double newMin = [rescaleSheetMinTextfield doubleValue]; double newMax = [rescaleSheetMaxTextfield doubleValue]; double newMajorInterval = [rescaleSheetMajorIntervalTextfield doubleValue]; int newModulus = [rescaleSheetMinorModulusTextfield intValue]; int newPrecision = [rescaleSheetTickPrecisionTextfield intValue]; double newMinorInterval = newMajorInterval / newModulus; [self setXAxisMin:newMin]; [self setXAxisMax:newMax]; [self setXAxisMajorTickInterval:newMajorInterval]; [self setXAxisMajorTickModulus:newModulus]; [self setXAxisMinorTickInterval:newMinorInterval]; [self setXAxisTickValuePrecision:newPrecision]; [self setXAxisIsUserRescaled:YES]; [self invalidateDrawingCache]; [self setNeedsDisplay:YES]; } [rescaleSheet autorelease]; rescaleSheet = nil; }]; } } - (IBAction)userRescaleYAxis:(id)sender { if ([self allowYAxisUserRescale]) { SLiMWindowController *controller = [self slimWindowController]; NSWindow *window = [controller window]; [[NSBundle mainBundle] loadNibNamed:@"GraphAxisRescaleSheet" owner:self topLevelObjects:NULL]; [rescaleSheet retain]; [rescaleSheetMinTextfield setDoubleValue:[self yAxisMin]]; [rescaleSheetMaxTextfield setDoubleValue:[self yAxisMax]]; [rescaleSheetMajorIntervalTextfield setDoubleValue:[self yAxisMajorTickInterval]]; [rescaleSheetMinorModulusTextfield setIntValue:[self yAxisMajorTickModulus]]; [rescaleSheetTickPrecisionTextfield setIntValue:[self yAxisTickValuePrecision]]; [window beginSheet:rescaleSheet completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertFirstButtonReturn) { double newMin = [rescaleSheetMinTextfield doubleValue]; double newMax = [rescaleSheetMaxTextfield doubleValue]; double newMajorInterval = [rescaleSheetMajorIntervalTextfield doubleValue]; int newModulus = [rescaleSheetMinorModulusTextfield intValue]; int newPrecision = [rescaleSheetTickPrecisionTextfield intValue]; double newMinorInterval = newMajorInterval / newModulus; [self setYAxisMin:newMin]; [self setYAxisMax:newMax]; [self setYAxisMajorTickInterval:newMajorInterval]; [self setYAxisMajorTickModulus:newModulus]; [self setYAxisMinorTickInterval:newMinorInterval]; [self setYAxisTickValuePrecision:newPrecision]; [self setYAxisIsUserRescaled:YES]; [self invalidateDrawingCache]; [self setNeedsDisplay:YES]; } [rescaleSheet autorelease]; rescaleSheet = nil; }]; } } - (IBAction)rescaleBarSheetOK:(id)sender { SLiMWindowController *controller = [self slimWindowController]; NSWindow *window = [controller window]; // Give our textfields a chance to validate if (![rescaleBarsSheet makeFirstResponder:nil]) return; [window endSheet:rescaleBarsSheet returnCode:NSAlertFirstButtonReturn]; } - (IBAction)rescaleBarSheetCancel:(id)sender { SLiMWindowController *controller = [self slimWindowController]; NSWindow *window = [controller window]; [window endSheet:rescaleBarsSheet returnCode:NSAlertSecondButtonReturn]; } - (IBAction)userRescaleXBars:(id)sender { if ([self allowXAxisBinRescale]) { SLiMWindowController *controller = [self slimWindowController]; NSWindow *window = [controller window]; [[NSBundle mainBundle] loadNibNamed:@"GraphBarRescaleSheet" owner:self topLevelObjects:NULL]; [rescaleBarsSheet retain]; [rescaleBarsSheetCountTextfield setIntValue:[self histogramBinCount]]; [window beginSheet:rescaleBarsSheet completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertFirstButtonReturn) { int newBinCount = [rescaleBarsSheetCountTextfield intValue]; [self setHistogramBinCount:newBinCount]; [self invalidateDrawingCache]; [self setNeedsDisplay:YES]; } [rescaleBarsSheet autorelease]; rescaleBarsSheet = nil; }]; } } - (IBAction)copy:(id)sender { NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; [pasteboard declareTypes:@[NSPDFPboardType] owner:self]; // We set generatingPDF to allow customization for PDF generation, such as not rounding to pixel values generatingPDF = YES; [self writePDFInsideRect:[self bounds] toPasteboard:pasteboard]; generatingPDF = NO; } - (IBAction)saveGraph:(id)sender { // We set generatingPDF to allow customization for PDF generation, such as not rounding to pixel values generatingPDF = YES; NSData *pdfData = [self dataWithPDFInsideRect:[self bounds]]; generatingPDF = NO; SLiMWindowController *controller = [self slimWindowController]; NSSavePanel *sp = [[NSSavePanel savePanel] retain]; [sp setTitle:@"Export Graph"]; [sp setNameFieldLabel:@"Export As:"]; [sp setMessage:@"Export the graph to a file:"]; [sp setExtensionHidden:NO]; [sp setCanSelectHiddenExtension:NO]; [sp setAllowedFileTypes:@[@"pdf"]]; [sp beginSheetModalForWindow:[controller window] completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [pdfData writeToURL:[sp URL] atomically:YES]; [sp autorelease]; } }]; } - (NSString *)dateline { NSString *dateString = [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterMediumStyle]; return [NSString stringWithFormat:@"# %@", dateString]; } - (IBAction)copyData:(id)sender { if ([self respondsToSelector:@selector(stringForDataWithController:)]) { NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; [pasteboard declareTypes:@[NSStringPboardType] owner:self]; [pasteboard setString:[self stringForDataWithController:[self slimWindowController]] forType:NSStringPboardType]; } } - (IBAction)saveData:(id)sender { if ([self respondsToSelector:@selector(stringForDataWithController:)]) { SLiMWindowController *controller = [self slimWindowController]; NSString *dataString = [self stringForDataWithController:controller]; NSSavePanel *sp = [[NSSavePanel savePanel] retain]; [sp setTitle:@"Export Data"]; [sp setNameFieldLabel:@"Export As:"]; [sp setMessage:@"Export the graph data to a file:"]; [sp setExtensionHidden:NO]; [sp setCanSelectHiddenExtension:NO]; [sp setAllowedFileTypes:@[@"txt"]]; [sp beginSheetModalForWindow:[controller window] completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [dataString writeToURL:[sp URL] atomically:YES encoding:NSUTF8StringEncoding error:NULL]; [sp autorelease]; } }]; } } - (NSMenu *)menuForEvent:(NSEvent *)theEvent { SLiMWindowController *controller = [self slimWindowController]; if (![controller invalidSimulation] && ![[controller window] attachedSheet] && [self focalDisplaySpecies]) { BOOL addedItems = NO; NSMenu *menu = [[NSMenu alloc] initWithTitle:@"graph_menu"]; // Toggle legend visibility NSSize legendSize = [self legendSize]; if ((legendSize.width > 0.0) && (legendSize.height > 0.0)) { NSMenuItem *menuItem = [menu addItemWithTitle:([self legendVisible] ? @"Hide Legend" : @"Show Legend") action:@selector(toggleLegend:) keyEquivalent:@""]; [menuItem setTarget:self]; addedItems = YES; } // Toggle horizontal grid line visibility if ([self showYAxis] && [self showYAxisTicks]) { NSMenuItem *menuItem = [menu addItemWithTitle:([self showHorizontalGridLines] ? @"Hide Horizontal Grid" : @"Show Horizontal Grid") action:@selector(toggleHorizontalGridLines:) keyEquivalent:@""]; [menuItem setTarget:self]; addedItems = YES; } // Toggle vertical grid line visibility if ([self showXAxis] && [self showXAxisTicks]) { NSMenuItem *menuItem = [menu addItemWithTitle:([self showVerticalGridLines] ? @"Hide Vertical Grid" : @"Show Vertical Grid") action:@selector(toggleVerticalGridLines:) keyEquivalent:@""]; [menuItem setTarget:self]; addedItems = YES; } // Toggle box visibility if ([self showXAxis] && [self showYAxis]) { NSMenuItem *menuItem = [menu addItemWithTitle:([self showFullBox] ? @"Hide Full Box" : @"Show Full Box") action:@selector(toggleFullBox:) keyEquivalent:@""]; [menuItem setTarget:self]; addedItems = YES; } // Add a separator if we had any visibility-toggle menu items above if (addedItems) [menu addItem:[NSMenuItem separatorItem]]; addedItems = NO; // Rescale x axis if ([self showXAxis] && [self showXAxisTicks] && [self allowXAxisUserRescale]) { NSMenuItem *menuItem = [menu addItemWithTitle:@"Change X Axis Scale..." action:@selector(userRescaleXAxis:) keyEquivalent:@""]; [menuItem setTarget:self]; addedItems = YES; } if ([self histogramBinCount] && [self allowXAxisBinRescale]) { NSMenuItem *menuItem = [menu addItemWithTitle:@"Change X Bar Count..." action:@selector(userRescaleXBars:) keyEquivalent:@""]; [menuItem setTarget:self]; addedItems = YES; } // Rescale y axis if ([self showYAxis] && [self showYAxisTicks] && [self allowYAxisUserRescale]) { NSMenuItem *menuItem = [menu addItemWithTitle:@"Change Y Axis Scale..." action:@selector(userRescaleYAxis:) keyEquivalent:@""]; [menuItem setTarget:self]; addedItems = YES; } // Add a separator if we had any visibility-toggle menu items above if (addedItems) [menu addItem:[NSMenuItem separatorItem]]; addedItems = NO; (void)addedItems; // dead store above is deliberate // Allow a subclass to introduce menu items here, above the copy/export menu items, which belong at the bottom; we are responsible for adding a separator afterwards if needed NSInteger preSubclassItemCount = [menu numberOfItems]; [self subclassAddItemsToMenu:menu forEvent:theEvent]; if (preSubclassItemCount != [menu numberOfItems]) [menu addItem:[NSMenuItem separatorItem]]; // Copy/export the graph image { NSMenuItem *menuItem; menuItem = [menu addItemWithTitle:@"Copy Graph" action:@selector(copy:) keyEquivalent:@""]; [menuItem setTarget:self]; menuItem = [menu addItemWithTitle:@"Export Graph..." action:@selector(saveGraph:) keyEquivalent:@""]; [menuItem setTarget:self]; } // Copy/export the data to the clipboard if ([self respondsToSelector:@selector(stringForDataWithController:)]) { NSMenuItem *menuItem; [menu addItem:[NSMenuItem separatorItem]]; menuItem = [menu addItemWithTitle:@"Copy Data" action:@selector(copyData:) keyEquivalent:@""]; [menuItem setTarget:self]; menuItem = [menu addItemWithTitle:@"Export Data..." action:@selector(saveData:) keyEquivalent:@""]; [menuItem setTarget:self]; } return [menu autorelease]; } return nil; } - (void)subclassAddItemsToMenu:(NSMenu *)menu forEvent:(NSEvent *)theEvent { } - (void)invalidateDrawingCache { // GraphView has no drawing cache, but it supports the idea of one } - (void)graphWindowResized { [self invalidateDrawingCache]; } - (void)controllerRecycled { [self updateSpeciesBadge]; [self invalidateDrawingCache]; [self setNeedsDisplay:YES]; } - (void)controllerSelectionChanged { } - (void)controllerTickFinished { } - (void)updateAfterTick { [self updateSpeciesBadge]; [self setNeedsDisplay:YES]; } @end @implementation GraphView (PrefabAdditions) - (void)setXAxisRangeFromTick { SLiMWindowController *controller = [self slimWindowController]; Community &community = *controller->community; // We can't get the estimated last tick until tick ranges are known if (community.Tick() < 1) return; slim_tick_t lastTick = community.EstimatedLastTick(); // The last tick could be just about anything, so we need some smart axis setup code here – a problem we neglect elsewhere // since we use hard-coded axis setups in other places. The goal is to (1) have the axis max be >= lastTick, (2) have the axis // max be == lastTick if lastTick is a reasonably round number (a single-digit multiple of a power of 10, say), (3) have just a few // other major tick intervals drawn, so labels don't collide or look crowded, and (4) have a few minor tick intervals in between // the majors. Labels that are single-digit multiples of powers of 10 are to be strongly preferred. double lower10power = pow(10.0, floor(log10(lastTick))); // 8000 gives 1000, 1000 gives 1000, 10000 gives 10000 double lower5mult = lower10power / 2.0; // 8000 gives 500, 1000 gives 500, 10000 gives 5000 double axisMax = ceil(lastTick / lower5mult) * lower5mult; // 8000 gives 8000, 7500 gives 7500, 1100 gives 1500 double contained5mults = axisMax / lower5mult; // 8000 gives 16, 7500 gives 15, 1100 gives 3, 1000 gives 2 if (contained5mults <= 3) { // We have a max like 1500 that divides into 5mults well, so do that [self setXAxisMax:axisMax]; [self setXAxisMajorTickInterval:lower5mult]; [self setXAxisMinorTickInterval:lower5mult / 5]; [self setXAxisMajorTickModulus:5]; [self setXAxisTickValuePrecision:0]; } else { // We have a max like 7000 that does not divide into 5mults well; for simplicity, we just always divide these in two [self setXAxisMax:axisMax]; [self setXAxisMajorTickInterval:axisMax / 2]; [self setXAxisMinorTickInterval:axisMax / 4]; [self setXAxisMajorTickModulus:2]; [self setXAxisTickValuePrecision:0]; } } - (NSArray *)mutationTypeLegendKey { Species *displaySpecies = [self focalDisplaySpecies]; if (!displaySpecies) return nil; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); // if we only have one mutation type, do not show a legend if (mutationTypeCount < 2) return nil; NSMutableArray *legendKey = [NSMutableArray array]; // first we put in placeholders for (auto mutationTypeIter = displaySpecies->mutation_types_.begin(); mutationTypeIter != displaySpecies->mutation_types_.end(); ++mutationTypeIter) [legendKey addObject:@"placeholder"]; // then we replace the placeholders with lines, but we do it out of order, according to mutation_type_index_ values for (auto mutationTypeIter = displaySpecies->mutation_types_.begin(); mutationTypeIter != displaySpecies->mutation_types_.end(); ++mutationTypeIter) { MutationType *mutationType = (*mutationTypeIter).second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! NSString *labelString = [NSString stringWithFormat:@"m%lld", (long long int)mutationType->mutation_type_id_]; [legendKey replaceObjectAtIndex:mutationTypeIndex withObject:@[labelString, [SLiMWindowController blackContrastingColorForIndex:mutationTypeIndex]]]; } return legendKey; } - (void)drawGroupedBarplotInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller buffer:(double *)buffer subBinCount:(int)subBinCount mainBinCount:(int)mainBinCount firstBinValue:(double)firstBinValue mainBinWidth:(double)mainBinWidth { // Decide on a display style; if we have lots of width, then we draw bars with a fill color, spaced out nicely, // but if we are cramped for space then we draw solid black bars. Note the latter style does not really // work with sub-bins; not much we can do about that, since we don't have the room to draw... int interiorWidth = (int)interiorRect.size.width; int totalBarCount = subBinCount * mainBinCount; int drawStyle = 0; if (totalBarCount * 7 + 1 <= interiorWidth) // room for space, space, space, frame, fill, fill, frame... drawStyle = 0; else if (totalBarCount * 5 + 1 <= interiorWidth) // room for space, frame, fill, fill, frame... drawStyle = 1; else if (totalBarCount * 2 + 1 <= interiorWidth) // room for frame, fill, [frame]... drawStyle = 2; else { [[NSColor blackColor] set]; drawStyle = 3; } for (int mainBinIndex = 0; mainBinIndex < mainBinCount; ++mainBinIndex) { double binMinValue = mainBinIndex * mainBinWidth + firstBinValue; double binMaxValue = (mainBinIndex + 1) * mainBinWidth + firstBinValue; double barLeft = [self roundPlotToDeviceX:binMinValue withInteriorRect:interiorRect]; double barRight = [self roundPlotToDeviceX:binMaxValue withInteriorRect:interiorRect]; switch (drawStyle) { case 0: barLeft += 1.5; barRight -= 1.5; break; case 1: barLeft += 0.5; barRight -= 0.5; break; case 2: barLeft -= 0.5; barRight += 0.5; break; case 3: barLeft -= 0.5; barRight += 0.5; break; } for (int subBinIndex = 0; subBinIndex < subBinCount; ++subBinIndex) { int actualBinIndex = subBinIndex + mainBinIndex * subBinCount; double binValue = buffer[actualBinIndex]; double barBottom = interiorRect.origin.y - 1; double barTop = (binValue == 0 ? interiorRect.origin.y - 1 : [self roundPlotToDeviceY:binValue withInteriorRect:interiorRect] + 0.5); NSRect barRect = NSMakeRect(barLeft, barBottom, barRight - barLeft, barTop - barBottom); if (barRect.size.height > 0.0) { // subdivide into sub-bars for each mutation type, if there is more than one if (subBinCount > 1) { double barWidth = barRect.size.width; double subBarWidth = (barWidth - 1) / subBinCount; double subbarLeft = SLIM_SCREEN_ROUND(barRect.origin.x + subBinIndex * subBarWidth); double subbarRight = SLIM_SCREEN_ROUND(barRect.origin.x + (subBinIndex + 1) * subBarWidth) + 1; barRect.origin.x = subbarLeft; barRect.size.width = subbarRight - subbarLeft; } // fill and frame if (drawStyle == 3) { NSRectFill(barRect); NSFrameRect(barRect); } else { [[SLiMWindowController blackContrastingColorForIndex:subBinIndex] set]; NSRectFill(barRect); [[NSColor blackColor] set]; NSFrameRect(barRect); } } } } } - (void)drawBarplotInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller buffer:(double *)buffer binCount:(int)binCount firstBinValue:(double)firstBinValue binWidth:(double)binWidth { [self drawGroupedBarplotInInteriorRect:interiorRect withController:controller buffer:buffer subBinCount:1 mainBinCount:binCount firstBinValue:firstBinValue mainBinWidth:binWidth]; } @end ================================================ FILE: SLiMgui/GraphView_FitnessOverTime.h ================================================ // // GraphView_FitnessOverTime.h // SLiM // // Created by Ben Haller on 3/1/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* This graph plots a histogram of the distribution of times to loss of mutations. The data is gathered by SLiM, so it is quite simple. */ #import "GraphView.h" @interface GraphView_FitnessOverTime : GraphView { NSImage *drawingCache; slim_tick_t drawingCacheTick; } @property (nonatomic) BOOL showSubpopulations; @property (nonatomic) BOOL drawLines; @end ================================================ FILE: SLiMgui/GraphView_FitnessOverTime.mm ================================================ // // GraphView_FitnessOverTime.m // SLiM // // Created by Ben Haller on 3/1/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "GraphView_FitnessOverTime.h" #import "SLiMWindowController.h" #include "community.h" @implementation GraphView_FitnessOverTime - (void)invalidateDrawingCache { [drawingCache release]; drawingCache = nil; drawingCacheTick = 0; } - (void)setDefaultYAxisRange { [self setYAxisMin:0.9]; [self setYAxisMax:1.1]; // dynamic [self setYAxisMajorTickInterval:0.1]; [self setYAxisMinorTickInterval:0.02]; [self setYAxisMajorTickModulus:5]; [self setYAxisTickValuePrecision:1]; } - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller { if (self = [super initWithFrame:frameRect withController:controller]) { //[self setXAxisRangeFromTick]; // the end tick is not yet known [self setDefaultYAxisRange]; [self setXAxisLabelString:@"Tick"]; [self setYAxisLabelString:@"Fitness (rescaled)"]; [self setAllowXAxisUserRescale:YES]; [self setAllowYAxisUserRescale:YES]; [self setShowHorizontalGridLines:YES]; [self setTweakXAxisTickLabelAlignment:YES]; [self setShowSubpopulations:YES]; } return self; } - (void)controllerRecycled { SLiMWindowController *controller = [self slimWindowController]; if (![controller invalidSimulation]) { if (![self yAxisIsUserRescaled]) [self setDefaultYAxisRange]; //if (![self xAxisIsUserRescaled]) // the end tick is not yet known // [self setXAxisRangeFromTick]; [self setNeedsDisplay:YES]; } [super controllerRecycled]; } - (void)controllerSelectionChanged { [self invalidateDrawingCache]; [self setNeedsDisplay:YES]; [super controllerSelectionChanged]; } - (void)dealloc { [super dealloc]; } - (void)updateAfterTick { Species *displaySpecies = [self focalDisplaySpecies]; // BCH 3/20/2024: We set the x axis range each tick, because the end tick is now invalid until after initialize() callbacks if (displaySpecies && ![self xAxisIsUserRescaled]) [self setXAxisRangeFromTick]; if (displaySpecies && ![self yAxisIsUserRescaled]) { Population &pop = displaySpecies->population_; double minHistory = INFINITY; double maxHistory = -INFINITY; BOOL showSubpops = [self showSubpopulations] && (pop.fitness_histories_.size() > 2); for (auto history_record_iter : pop.fitness_histories_) { if (showSubpops || (history_record_iter.first == -1)) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; // find the min and max history value for (int i = 0; i < historyLength; ++i) { double historyEntry = history[i]; if (!isnan(historyEntry)) { if (historyEntry > maxHistory) maxHistory = historyEntry; if (historyEntry < minHistory) minHistory = historyEntry; } } } } // set axis range to encompass the data if (!isinf(minHistory) && !isinf(maxHistory)) { if ((minHistory < 0.9) || (maxHistory > 1.1)) // if we're outside our original axis range... { double axisMin = (minHistory < 0.5 ? 0.0 : 0.5); // either 0.0 or 0.5 double axisMax = ceil(maxHistory * 2.0) / 2.0; // 1.5, 2.0, 2.5, ... if (axisMax < 1.5) axisMax = 1.5; if ((axisMin != [self yAxisMin]) || (axisMax != [self yAxisMax])) { [self setYAxisMin:axisMin]; [self setYAxisMax:axisMax]; [self setYAxisMajorTickInterval:0.5]; [self setYAxisMinorTickInterval:0.25]; [self setYAxisMajorTickModulus:2]; [self setYAxisTickValuePrecision:1]; [self invalidateDrawingCache]; } } } } [super updateAfterTick]; } - (void)drawPointGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; slim_tick_t completedTicks = controller->community->Tick() - 1; // The tick counter can get set backwards, in which case our drawing cache is invalid – it contains drawing of things in the // future that may no longer happen. So we need to detect that case and invalidate our cache. if (!cachingNow && drawingCache && (drawingCacheTick > completedTicks)) { //NSLog(@"backward tick change detected, invalidating drawing cache"); [self invalidateDrawingCache]; } // If we're not caching, then: if our cache is invalid OR we have crossed a 1000-tick boundary since we last cached, cache an image if (!cachingNow && (!drawingCache || ((completedTicks / 1000) > (drawingCacheTick / 1000)))) { [self invalidateDrawingCache]; NSBitmapImageRep *bitmap = [self bitmapImageRepForCachingDisplayInRect:interiorRect]; cachingNow = YES; //NSLog(@"recaching! completedTicks == %d", completedTicks); [bitmap setSize:interiorRect.size]; [self cacheDisplayInRect:interiorRect toBitmapImageRep:bitmap]; drawingCache = [[NSImage alloc] initWithSize:interiorRect.size]; [drawingCache addRepresentation:bitmap]; drawingCacheTick = completedTicks; cachingNow = NO; } // Now draw our cache, if we have one if (drawingCache) [drawingCache drawInRect:interiorRect]; // Draw fixation events std::vector &substitutions = pop.substitutions_; for (const Substitution *substitution : substitutions) { slim_tick_t fixation_tick = substitution->fixation_tick_; // If we are caching, draw all events; if we are not, draw only those that are not already in the cache if (!cachingNow && (fixation_tick < drawingCacheTick)) continue; double substitutionX = [self plotToDeviceX:fixation_tick withInteriorRect:interiorRect]; NSRect substitutionRect = NSMakeRect(substitutionX - 0.5, interiorRect.origin.x, 1.0, interiorRect.size.height); [[NSColor colorWithCalibratedRed:0.2 green:0.2 blue:1.0 alpha:0.2] set]; NSRectFillUsingOperation(substitutionRect, NSCompositingOperationSourceOver); } // Draw the fitness history as a scatter plot; better suited to caching of the image BOOL showSubpops = [self showSubpopulations] && (pop.fitness_histories_.size() > 2); BOOL drawSubpopsGray = (showSubpops && (pop.fitness_histories_.size() > 8)); // 7 subpops + pop // First draw subpops if (showSubpops) { for (auto history_record_iter : pop.fitness_histories_) { if (history_record_iter.first != -1) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; if (drawSubpopsGray) [[NSColor colorWithCalibratedWhite:0.5 alpha:1.0] set]; else [[SLiMWindowController whiteContrastingColorForIndex:history_record_iter.first] set]; // If we're caching now, draw all points; otherwise, if we have a cache, draw only additional points slim_tick_t firstHistoryEntryToDraw = (cachingNow ? 0 : (drawingCache ? drawingCacheTick : 0)); for (slim_tick_t i = firstHistoryEntryToDraw; (i < historyLength) && (i < completedTicks); ++i) { double historyEntry = history[i]; if (!isnan(historyEntry)) { NSPoint historyPoint = NSMakePoint([self plotToDeviceX:i withInteriorRect:interiorRect], [self plotToDeviceY:historyEntry withInteriorRect:interiorRect]); NSRectFill(NSMakeRect(historyPoint.x - 0.5, historyPoint.y - 0.5, 1.0, 1.0)); } } } } } // Then draw the mean population fitness for (auto history_record_iter : pop.fitness_histories_) { if (history_record_iter.first == -1) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; [[NSColor blackColor] set]; // If we're caching now, draw all points; otherwise, if we have a cache, draw only additional points slim_tick_t firstHistoryEntryToDraw = (cachingNow ? 0 : (drawingCache ? drawingCacheTick : 0)); for (slim_tick_t i = firstHistoryEntryToDraw; (i < historyLength) && (i < completedTicks); ++i) { double historyEntry = history[i]; if (!isnan(historyEntry)) { NSPoint historyPoint = NSMakePoint([self plotToDeviceX:i withInteriorRect:interiorRect], [self plotToDeviceY:historyEntry withInteriorRect:interiorRect]); NSRectFill(NSMakeRect(historyPoint.x - 0.5, historyPoint.y - 0.5, 1.0, 1.0)); } } } } } - (void)drawLineGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; slim_tick_t completedTicks = controller->community->Tick() - 1; // Draw fixation events std::vector &substitutions = pop.substitutions_; for (const Substitution *substitution : substitutions) { slim_tick_t fixation_tick = substitution->fixation_tick_; double substitutionX = [self plotToDeviceX:fixation_tick withInteriorRect:interiorRect]; NSRect substitutionRect = NSMakeRect(substitutionX - 0.5, interiorRect.origin.x, 1.0, interiorRect.size.height); [[NSColor colorWithCalibratedRed:0.2 green:0.2 blue:1.0 alpha:0.2] set]; NSRectFillUsingOperation(substitutionRect, NSCompositingOperationSourceOver); } // Draw the fitness history as a scatter plot; better suited to caching of the image BOOL showSubpops = [self showSubpopulations] && (pop.fitness_histories_.size() > 2); BOOL drawSubpopsGray = (showSubpops && (pop.fitness_histories_.size() > 8)); // 7 subpops + pop // First draw subpops if (showSubpops) { for (auto history_record_iter : pop.fitness_histories_) { if (history_record_iter.first != -1) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; NSBezierPath *linePath = [NSBezierPath bezierPath]; BOOL startedLine = NO; for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) { double historyEntry = history[i]; if (isnan(historyEntry)) { startedLine = NO; } else { NSPoint historyPoint = NSMakePoint([self plotToDeviceX:i withInteriorRect:interiorRect], [self plotToDeviceY:historyEntry withInteriorRect:interiorRect]); if (startedLine) [linePath lineToPoint:historyPoint]; else [linePath moveToPoint:historyPoint]; startedLine = YES; } } if (drawSubpopsGray) [[NSColor colorWithCalibratedWhite:0.5 alpha:1.0] set]; else [[SLiMWindowController whiteContrastingColorForIndex:history_record_iter.first] set]; [linePath setLineWidth:1.0]; [linePath stroke]; } } } // Then draw the mean population fitness for (auto history_record_iter : pop.fitness_histories_) { if (history_record_iter.first == -1) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; NSBezierPath *linePath = [NSBezierPath bezierPath]; BOOL startedLine = NO; for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) { double historyEntry = history[i]; if (isnan(historyEntry)) { startedLine = NO; } else { NSPoint historyPoint = NSMakePoint([self plotToDeviceX:i withInteriorRect:interiorRect], [self plotToDeviceY:historyEntry withInteriorRect:interiorRect]); if (startedLine) [linePath lineToPoint:historyPoint]; else [linePath moveToPoint:historyPoint]; startedLine = YES; } } [[NSColor blackColor] set]; [linePath setLineWidth:1.5]; [linePath stroke]; } } } - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { if ([self drawLines]) [self drawLineGraphInInteriorRect:interiorRect withController:controller]; else [self drawPointGraphInInteriorRect:interiorRect withController:controller]; } - (NSString *)stringForDataWithController:(SLiMWindowController *)controller { NSMutableString *string = [NSMutableString stringWithString:@"# Graph data: fitness ~ tick\n"]; Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; slim_tick_t completedTicks = controller->community->Tick() - 1; [string appendString:[self dateline]]; // Fixation events [string appendString:@"\n\n# Fixation ticks:\n"]; std::vector &substitutions = pop.substitutions_; for (const Substitution *substitution : substitutions) { slim_tick_t fixation_tick = substitution->fixation_tick_; [string appendFormat:@"%lld, ", (long long int)fixation_tick]; } // Fitness history [string appendString:@"\n\n# Fitness history:\n"]; for (auto history_record_iter : pop.fitness_histories_) { if (history_record_iter.first == -1) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) [string appendFormat:@"%.4f, ", history[i]]; [string appendString:@"\n"]; } } // Subpopulation fitness histories BOOL showSubpops = [self showSubpopulations] && (pop.fitness_histories_.size() > 2); if (showSubpops) { for (auto history_record_iter : pop.fitness_histories_) { if (history_record_iter.first != -1) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; slim_tick_t historyLength = history_record.history_length_; [string appendFormat:@"\n\n# Fitness history (subpopulation p%d):\n", history_record_iter.first]; for (slim_tick_t i = 0; (i < historyLength) && (i < completedTicks); ++i) [string appendFormat:@"%.4f, ", history[i]]; [string appendString:@"\n"]; } } } // Get rid of extra commas [string replaceOccurrencesOfString:@", \n" withString:@"\n" options:0 range:NSMakeRange(0, [string length])]; return string; } - (NSArray *)legendKey { Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; BOOL showSubpops = [self showSubpopulations] && (pop.fitness_histories_.size() > 2); BOOL drawSubpopsGray = (showSubpops && (pop.fitness_histories_.size() > 8)); // 7 subpops + pop if (!showSubpops) return nil; NSMutableArray *legendKey = [NSMutableArray array]; [legendKey addObject:@[@"All", [NSColor blackColor]]]; if (drawSubpopsGray) { [legendKey addObject:@[@"pX", [NSColor colorWithCalibratedWhite:0.5 alpha:1.0]]]; } else { for (auto history_record_iter : pop.fitness_histories_) { if (history_record_iter.first != -1) { NSString *labelString = [NSString stringWithFormat:@"p%lld", (long long int)history_record_iter.first]; [legendKey addObject:@[labelString, [SLiMWindowController whiteContrastingColorForIndex:history_record_iter.first]]]; } } } return legendKey; } - (IBAction)toggleShowSubpopulations:(id)sender { [self setShowSubpopulations:![self showSubpopulations]]; [self invalidateDrawingCache]; [self setNeedsDisplay:YES]; } - (IBAction)toggleDrawLines:(id)sender { [self setDrawLines:![self drawLines]]; [self invalidateDrawingCache]; [self setNeedsDisplay:YES]; } - (void)subclassAddItemsToMenu:(NSMenu *)menu forEvent:(NSEvent *)theEvent { if (menu) { NSMenuItem *menuItem; menuItem = [menu addItemWithTitle:([self showSubpopulations] ? @"Hide Subpopulations" : @"Show Subpopulations") action:@selector(toggleShowSubpopulations:) keyEquivalent:@""]; [menuItem setTarget:self]; menuItem = [menu addItemWithTitle:([self drawLines] ? @"Draw Points (Faster)" : @"Draw Lines (Slower)") action:@selector(toggleDrawLines:) keyEquivalent:@""]; [menuItem setTarget:self]; } } @end ================================================ FILE: SLiMgui/GraphView_MutationFixationTimeHistogram.h ================================================ // // GraphView_MutationFixationTimeHistogram.h // SLiM // // Created by Ben Haller on 3/1/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* This graph plots a histogram of the distribution of times to loss of mutations. The data is gathered by SLiM, so it is quite simple. */ #import "GraphView.h" @interface GraphView_MutationFixationTimeHistogram : GraphView { } @end ================================================ FILE: SLiMgui/GraphView_MutationFixationTimeHistogram.mm ================================================ // // GraphView_MutationFixationTimeHistogram.m // SLiM // // Created by Ben Haller on 3/1/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "GraphView_MutationFixationTimeHistogram.h" #import "SLiMWindowController.h" @implementation GraphView_MutationFixationTimeHistogram - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller { if (self = [super initWithFrame:frameRect withController:controller]) { [self setHistogramBinCount:10]; //[self setAllowXAxisBinRescale:YES]; // not supported yet [self setXAxisMax:1000]; [self setXAxisMajorTickInterval:200]; [self setXAxisMinorTickInterval:100]; [self setXAxisMajorTickModulus:2]; [self setXAxisTickValuePrecision:0]; [self setXAxisLabelString:@"Mutation fixation time"]; [self setYAxisLabelString:@"Proportion of fixed mutations"]; [self setAllowXAxisUserRescale:NO]; [self setAllowYAxisUserRescale:YES]; [self setShowHorizontalGridLines:YES]; } return self; } - (void)dealloc { [super dealloc]; } - (double *)fixationTimeDataWithController:(SLiMWindowController *)controller { Species *displaySpecies = [self focalDisplaySpecies]; int binCount = [self histogramBinCount]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); slim_tick_t *histogram = displaySpecies->population_.mutation_fixation_times_; int64_t histogramBins = (int64_t)displaySpecies->population_.mutation_fixation_tick_slots_; // fewer than binCount * mutationTypeCount may exist static double *rebin = NULL; static uint32_t rebinBins = 0; uint32_t usedRebinBins = binCount * mutationTypeCount; // re-bin for display; SLiM bins every 10 ticks, but right now we want to plot every 100 ticks as a bin if (!rebin || (rebinBins < usedRebinBins)) { rebinBins = usedRebinBins; rebin = (double *)realloc(rebin, rebinBins * sizeof(double)); } for (unsigned int i = 0; i < usedRebinBins; ++i) rebin[i] = 0.0; for (int i = 0; i < binCount * 10; ++i) { for (int j = 0; j < mutationTypeCount; ++j) { int histIndex = j + i * mutationTypeCount; if (histIndex < histogramBins) rebin[j + (i / 10) * mutationTypeCount] += histogram[histIndex]; } } // normalize within each mutation type for (int mutationTypeIndex = 0; mutationTypeIndex < mutationTypeCount; ++mutationTypeIndex) { uint32_t total = 0; for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; total += (int64_t)rebin[binIndex]; } for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; rebin[binIndex] /= (double)total; } } return rebin; } - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { Species *displaySpecies = [self focalDisplaySpecies]; double *plotData = [self fixationTimeDataWithController:controller]; int binCount = [self histogramBinCount]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); // plot our histogram bars [self drawGroupedBarplotInInteriorRect:interiorRect withController:controller buffer:plotData subBinCount:mutationTypeCount mainBinCount:binCount firstBinValue:0.0 mainBinWidth:100.0]; } - (NSArray *)legendKey { return [self mutationTypeLegendKey]; // we use the prefab mutation type legend } - (NSString *)stringForDataWithController:(SLiMWindowController *)controller { NSMutableString *string = [NSMutableString stringWithString:@"# Graph data: Mutation fixation time histogram\n"]; [string appendString:[self dateline]]; [string appendString:@"\n\n"]; Species *displaySpecies = [self focalDisplaySpecies]; double *plotData = [self fixationTimeDataWithController:controller]; int binCount = [self histogramBinCount]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); for (auto mutationTypeIter = displaySpecies->mutation_types_.begin(); mutationTypeIter != displaySpecies->mutation_types_.end(); ++mutationTypeIter) { MutationType *mutationType = (*mutationTypeIter).second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! [string appendFormat:@"\"m%lld\", ", (long long int)mutationType->mutation_type_id_]; for (int i = 0; i < binCount; ++i) { int histIndex = mutationTypeIndex + i * mutationTypeCount; [string appendFormat:@"%.4f, ", plotData[histIndex]]; } [string appendString:@"\n"]; } // Get rid of extra commas [string replaceOccurrencesOfString:@", \n" withString:@"\n" options:0 range:NSMakeRange(0, [string length])]; return string; } @end ================================================ FILE: SLiMgui/GraphView_MutationFrequencySpectra.h ================================================ // // GraphView_MutationFrequencySpectra.h // SLiM // // Created by Ben Haller on 2/27/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* This graph plots a histogram of the frequencies of the mutations present in the whole population. In other words, it loops through all of the mutations, determines their frequencies in the population, and bins those frequences to show as a histogram. Determining the frequences of mutations is actually done automatically by SLiM, stored in reference_count_, so this is pretty strightforward. */ #import "GraphView.h" @interface GraphView_MutationFrequencySpectra : GraphView { } @end ================================================ FILE: SLiMgui/GraphView_MutationFrequencySpectra.mm ================================================ // // GraphView_MutationFrequencySpectra.m // SLiM // // Created by Ben Haller on 2/27/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "GraphView_MutationFrequencySpectra.h" #import "SLiMWindowController.h" @implementation GraphView_MutationFrequencySpectra - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller { if (self = [super initWithFrame:frameRect withController:controller]) { [self setHistogramBinCount:10]; [self setAllowXAxisBinRescale:YES]; [self setXAxisMajorTickInterval:0.2]; [self setXAxisMinorTickInterval:0.1]; [self setXAxisMajorTickModulus:2]; [self setXAxisTickValuePrecision:1]; [self setXAxisLabelString:@"Mutation frequency"]; [self setYAxisLabelString:@"Proportion of mutations"]; [self setAllowXAxisUserRescale:NO]; [self setAllowYAxisUserRescale:YES]; [self setShowHorizontalGridLines:YES]; } return self; } - (void)dealloc { [super dealloc]; } - (double *)mutationFrequencySpectrumWithController:(SLiMWindowController *)controller mutationTypeCount:(int)mutationTypeCount { static uint32_t *spectrum = NULL; // used for tallying static double *doubleSpectrum = NULL; // not used for tallying, to avoid precision issues static uint32_t spectrumBins = 0; int binCount = [self histogramBinCount]; uint32_t usedSpectrumBins = binCount * mutationTypeCount; // allocate our bins if (!spectrum || (spectrumBins < usedSpectrumBins)) { spectrumBins = usedSpectrumBins; spectrum = (uint32_t *)realloc(spectrum, spectrumBins * sizeof(uint32_t)); doubleSpectrum = (double *)realloc(doubleSpectrum, spectrumBins * sizeof(double)); } // clear our bins for (unsigned int i = 0; i < usedSpectrumBins; ++i) spectrum[i] = 0; // tally into our bins Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; pop.TallyMutationReferencesAcrossPopulation(/* p_clock_for_mutrun_experiments */ false); // update tallies; usually this will just use the cache set up by Population::MaintainMutationRegistry() Mutation *mut_block_ptr = gSLiM_Mutation_Block; slim_refcount_t *refcount_block_ptr = gSLiM_Mutation_Refcounts; int registry_size; const MutationIndex *registry = pop.MutationRegistry(®istry_size); for (int registry_index = 0; registry_index < registry_size; ++registry_index) { const Mutation *mutation = mut_block_ptr + registry[registry_index]; Chromosome *mut_chromosome = displaySpecies->Chromosomes()[mutation->chromosome_index_]; slim_refcount_t mutationRefCount = *(refcount_block_ptr + mutation->BlockIndex()); double mutationFrequency = mutationRefCount / mut_chromosome->total_haplosome_count_; int mutationBin = (int)floor(mutationFrequency * binCount); int mutationTypeIndex = mutation->mutation_type_ptr_->mutation_type_index_; if (mutationBin == binCount) mutationBin = binCount - 1; (spectrum[mutationTypeIndex + mutationBin * mutationTypeCount])++; // bins in sequence for each mutation type within one frequency bin, then again for the next frequency bin, etc. } // normalize within each mutation type for (int mutationTypeIndex = 0; mutationTypeIndex < mutationTypeCount; ++mutationTypeIndex) { uint32_t total = 0; for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; total += spectrum[binIndex]; } for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; doubleSpectrum[binIndex] = spectrum[binIndex] / (double)total; } } // return the final tally; note this is a pointer in to our static ivar, and must not be freed! return doubleSpectrum; } - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { int binCount = [self histogramBinCount]; Species *displaySpecies = [self focalDisplaySpecies]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); double *spectrum = [self mutationFrequencySpectrumWithController:controller mutationTypeCount:mutationTypeCount]; // plot our histogram bars [self drawGroupedBarplotInInteriorRect:interiorRect withController:controller buffer:spectrum subBinCount:mutationTypeCount mainBinCount:binCount firstBinValue:0.0 mainBinWidth:(1.0 / binCount)]; } - (NSArray *)legendKey { return [self mutationTypeLegendKey]; // we use the prefab mutation type legend } - (void)controllerSelectionChanged { [self setNeedsDisplay:YES]; } - (NSString *)stringForDataWithController:(SLiMWindowController *)controller { NSMutableString *string = [NSMutableString stringWithString:@"# Graph data: Mutation frequency spectrum\n"]; [string appendString:[self dateline]]; [string appendString:@"\n\n"]; int binCount = [self histogramBinCount]; Species *displaySpecies = [self focalDisplaySpecies]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); double *plotData = [self mutationFrequencySpectrumWithController:controller mutationTypeCount:mutationTypeCount]; for (auto mutationTypeIter = displaySpecies->mutation_types_.begin(); mutationTypeIter != displaySpecies->mutation_types_.end(); ++mutationTypeIter) { MutationType *mutationType = (*mutationTypeIter).second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! [string appendFormat:@"\"m%lld\", ", (long long int)mutationType->mutation_type_id_]; for (int i = 0; i < binCount; ++i) { int histIndex = mutationTypeIndex + i * mutationTypeCount; [string appendFormat:@"%.4f, ", plotData[histIndex]]; } [string appendString:@"\n"]; } // Get rid of extra commas [string replaceOccurrencesOfString:@", \n" withString:@"\n" options:0 range:NSMakeRange(0, [string length])]; return string; } @end ================================================ FILE: SLiMgui/GraphView_MutationFrequencyTrajectory.h ================================================ // // GraphView_MutationFrequencyTrajectory.h // SLiM // // Created by Ben Haller on 3/11/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* This graph plots frequency trajectories over time of all of the individual mutations within a mutation type. */ #import "GraphView.h" #include "mutation.h" #include "mutation_type.h" @class MutationFrequencyHistory; @interface GraphView_MutationFrequencyTrajectory : GraphView { NSMutableDictionary *frequencyHistoryDict; // dictionary of MutationFrequencyHistory objects with NSNumber (long long) keys NSMutableArray *frequencyHistoryColdStorageLost; // array of MutationFrequencyHistory objects that have been lost NSMutableArray *frequencyHistoryColdStorageFixed; // array of MutationFrequencyHistory objects that have been fixed NSPopUpButton *subpopulationButton; NSPopUpButton *mutationTypeButton; slim_tick_t lastTick; // the last tick data was gathered for; used to detect a backward move in time } // The subpop and mutation type selected; -1 indicates no current selection (which will be fixed as soon as the menu is populated) @property (nonatomic) slim_objectid_t selectedSubpopulationID; @property (nonatomic) int selectedMutationTypeIndex; @property (nonatomic) BOOL plotLostMutations; @property (nonatomic) BOOL plotFixedMutations; @property (nonatomic) BOOL plotActiveMutations; @property (nonatomic) BOOL useColorsForPlotting; - (NSPopUpButton *)addPopUpWithAction:(SEL)action; - (BOOL)addSubpopulationsToMenu; - (BOOL)addMutationTypesToMenu; - (void)setConstraintsForPopups; - (IBAction)subpopulationPopupChanged:(id)sender; - (IBAction)mutationTypePopupChanged:(id)sender; @end // We want to keep a history of frequency values for each mutation of the chosen mutation type in the // chosen subpopulation. The history of a mutation should persist after it has vanished, and if a // new mutation object gets allocated at the same memory location, it should be treated as a distinct // mutation; so we can't use pointers to identify mutations. Instead, we keep data on them using a // unique 64-bit ID generated only when SLiM is running under SLiMgui. At the end of a tick, // we loop through all mutations in the registry, and add an entry for that mutation in our data store. // This is probably O(n^2), but so it goes. It should only be used for mutation types that generate // few mutations; if somebody tries to plot every mutation in a common mutation-type, they will suffer. @interface MutationFrequencyHistory : NSObject { @public // The 64-bit mutation ID is how we keep track of the mutation we reference; its pointer might go stale and be reused. slim_mutationid_t mutationID; // Mostly we are just a malloced array of uint16s. The data we're storing is doubles, conceptually, but to minimize our memory footprint // (which might be very large!) we convert the doubles, which are guaranteed to be in the range [0.0, 1.0], to uint16s in the range // [0, UINT16_MAX] (65535). The buffer size is the number of entries allocated, the entry count is the number used so far, and the // base tick is the first tick recorded; the assumption is that entries are then sequential without gaps. int bufferSize, entryCount; slim_tick_t baseTick; uint16_t *entries; // Remember our mutation type so we can set our line color, etc., if we wish const MutationType *mutationType; // Finally, we keep a flag that we use to figure out if our mutation is dead; if it is, we can be moved into cold storage. BOOL updated; } - (instancetype)initWithEntry:(uint16_t)value forMutation:(const Mutation *)mutation atBaseTick:(slim_tick_t)tick; - (void)addEntry:(uint16_t)value; @end ================================================ FILE: SLiMgui/GraphView_MutationFrequencyTrajectory.mm ================================================ // // GraphView_MutationFrequencyTrajectory.m // SLiM // // Created by Ben Haller on 3/11/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "GraphView_MutationFrequencyTrajectory.h" #import "SLiMWindowController.h" #include "community.h" @implementation GraphView_MutationFrequencyTrajectory - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller { if (self = [super initWithFrame:frameRect withController:controller]) { //[self setXAxisRangeFromTick]; // the end tick is not yet known [self setXAxisLabelString:@"Tick"]; [self setYAxisLabelString:@"Frequency"]; [self setAllowXAxisUserRescale:YES]; [self setAllowYAxisUserRescale:YES]; [self setShowHorizontalGridLines:YES]; [self setTweakXAxisTickLabelAlignment:YES]; // Start with no selected subpop or mutation-type; these will be set to the first menu item added when menus are constructed _selectedSubpopulationID = -1; _selectedMutationTypeIndex = -1; // Start plotting lost mutations, by default _plotLostMutations = YES; _plotFixedMutations = YES; _plotActiveMutations = YES; _useColorsForPlotting = YES; // Make our pop-up menu buttons subpopulationButton = [self addPopUpWithAction:@selector(subpopulationPopupChanged:)]; mutationTypeButton = [self addPopUpWithAction:@selector(mutationTypePopupChanged:)]; [self addSubpopulationsToMenu]; [self addMutationTypesToMenu]; [self setConstraintsForPopups]; } return self; } - (void)dealloc { [self invalidateCachedData]; [super dealloc]; } - (NSPopUpButton *)addPopUpWithAction:(SEL)action { NSPopUpButton *popupButton = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(10, 10, 100, 47) pullsDown:NO]; [popupButton setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeMini]]]; [popupButton setControlSize:NSControlSizeMini]; [popupButton setAutoenablesItems:NO]; [popupButton setTarget:self]; [popupButton setAction:action]; [popupButton addItemWithTitle:@"foo"]; // Resize the popup using its intrinsic size [popupButton sizeToFit]; NSSize intrinsicSize = [popupButton intrinsicContentSize]; [popupButton setFrame:NSMakeRect(10, 10, intrinsicSize.width, intrinsicSize.height)]; [popupButton setTranslatesAutoresizingMaskIntoConstraints:NO]; // Add the popup as a subview [self addSubview:[popupButton autorelease]]; return popupButton; } - (void)setConstraintsForPopups { NSDictionary *viewsDictionary = @{@"subpopulationButton" : subpopulationButton, @"mutationTypeButton" : mutationTypeButton}; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-6-[subpopulationButton]-6-[mutationTypeButton]" options:0 metrics:nil views:viewsDictionary]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[subpopulationButton]-6-|" options:0 metrics:nil views:viewsDictionary]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[mutationTypeButton]-6-|" options:0 metrics:nil views:viewsDictionary]]; } - (BOOL)addSubpopulationsToMenu { Species *displaySpecies = [self focalDisplaySpecies]; NSMenuItem *lastItem; slim_objectid_t firstTag = -1; // Depopulate and populate the menu [subpopulationButton removeAllItems]; if (displaySpecies) { Population &population = displaySpecies->population_; for (auto popIter = population.subpops_.begin(); popIter != population.subpops_.end(); ++popIter) { slim_objectid_t subpopID = popIter->first; //Subpopulation *subpop = popIter->second; NSString *subpopString = [NSString stringWithFormat:@"p%lld", (long long int)subpopID]; [subpopulationButton addItemWithTitle:subpopString]; lastItem = [subpopulationButton lastItem]; [lastItem setTag:subpopID]; // Remember the first item we add; we will use this item's tag to make a selection if needed if (firstTag == -1) firstTag = subpopID; } } [subpopulationButton slimSortMenuItemsByTag]; // If it is empty, disable it BOOL hasItems = ([subpopulationButton numberOfItems] >= 1); [subpopulationButton setEnabled:hasItems]; // Fix the selection and then select the chosen subpopulation if (hasItems) { NSInteger indexOfTag = [subpopulationButton indexOfItemWithTag:[self selectedSubpopulationID]]; if (indexOfTag == -1) [self setSelectedSubpopulationID:-1]; if ([self selectedSubpopulationID] == -1) [self setSelectedSubpopulationID:firstTag]; [subpopulationButton selectItemWithTag:[self selectedSubpopulationID]]; [subpopulationButton synchronizeTitleAndSelectedItem]; } return hasItems; // YES if we found at least one subpop to add to the menu, NO otherwise } - (BOOL)addMutationTypesToMenu { Species *displaySpecies = [self focalDisplaySpecies]; NSMenuItem *lastItem; int firstTag = -1; // Depopulate and populate the menu [mutationTypeButton removeAllItems]; if (displaySpecies) { std::map &mutationTypes = displaySpecies->mutation_types_; for (auto mutTypeIter = mutationTypes.begin(); mutTypeIter != mutationTypes.end(); ++mutTypeIter) { MutationType *mutationType = mutTypeIter->second; slim_objectid_t mutationTypeID = mutationType->mutation_type_id_; int mutationTypeIndex = mutationType->mutation_type_index_; NSString *mutationTypeString = [NSString stringWithFormat:@"m%lld", (long long int)mutationTypeID]; [mutationTypeButton addItemWithTitle:mutationTypeString]; lastItem = [mutationTypeButton lastItem]; [lastItem setTag:mutationTypeIndex]; // Remember the first item we add; we will use this item's tag to make a selection if needed if (firstTag == -1) firstTag = mutationTypeIndex; } } [mutationTypeButton slimSortMenuItemsByTag]; // If it is empty, disable it BOOL hasItems = ([mutationTypeButton numberOfItems] >= 1); [mutationTypeButton setEnabled:hasItems]; // Fix the selection and then select the chosen mutation type if (hasItems) { NSInteger indexOfTag = [mutationTypeButton indexOfItemWithTag:[self selectedMutationTypeIndex]]; if (indexOfTag == -1) [self setSelectedMutationTypeIndex:-1]; if ([self selectedMutationTypeIndex] == -1) [self setSelectedMutationTypeIndex:firstTag]; [mutationTypeButton selectItemWithTag:[self selectedMutationTypeIndex]]; [mutationTypeButton synchronizeTitleAndSelectedItem]; } return hasItems; // YES if we found at least one muttype to add to the menu, NO otherwise } - (void)invalidateCachedData { // SLiMWindowController *controller = [self slimWindowController]; // Species &species = controller->community->single_species_; // slim_tick_t tick = community->Tick(); // // NSLog(@"-invalidateCachedData called at tick %d", tick); if (frequencyHistoryDict) { [frequencyHistoryDict release]; frequencyHistoryDict = nil; } if (frequencyHistoryColdStorageLost) { [frequencyHistoryColdStorageLost release]; frequencyHistoryColdStorageLost = nil; } if (frequencyHistoryColdStorageFixed) { [frequencyHistoryColdStorageFixed release]; frequencyHistoryColdStorageFixed = nil; } } - (void)fetchDataForFinishedTick { SLiMWindowController *controller = [self slimWindowController]; Community &community = *controller->community; Species *displaySpecies = [self focalDisplaySpecies]; if (!displaySpecies) return; Population &population = displaySpecies->population_; int registry_size; const MutationIndex *registry = population.MutationRegistry(®istry_size); static BOOL alreadyHere = NO; // Check that the subpop we're supposed to be surveying exists; if not, bail. BOOL foundSelectedSubpop = NO; BOOL foundSelectedMutType = NO; for (const std::pair &subpop_pair : population.subpops_) if (subpop_pair.first == _selectedSubpopulationID) // find our chosen subpop foundSelectedSubpop = YES; for (const std::pair &subpop_pair : displaySpecies->mutation_types_) if (subpop_pair.second->mutation_type_index_ == _selectedMutationTypeIndex) // find our chosen muttype foundSelectedMutType = YES; // Make sure we have a selected subpop if possible. Our menu might not have been loaded, or our chosen subpop might have vanished. if ((_selectedSubpopulationID == -1) || !foundSelectedSubpop) { // Call -addSubpopulationsToMenu to reload our subpop menu and choose a subpop. This will call us, so we use a static flag to prevent re-entrancy. alreadyHere = YES; foundSelectedSubpop = [self addSubpopulationsToMenu]; alreadyHere = NO; } // Make sure we have a selected muttype if possible. Our menu might not have been loaded, or our chosen muttype might have vanished. if ((_selectedMutationTypeIndex == -1) || !foundSelectedMutType) { // Call -addMutationTypesToMenu to reload our muttype menu and choose a muttype. This will call us, so we use a static flag to prevent re-entrancy. alreadyHere = YES; foundSelectedMutType = [self addMutationTypesToMenu]; alreadyHere = NO; } if (!foundSelectedSubpop || !foundSelectedMutType) return; if (!frequencyHistoryDict) frequencyHistoryDict = [[NSMutableDictionary alloc] init]; if (!frequencyHistoryColdStorageLost) frequencyHistoryColdStorageLost = [[NSMutableArray alloc] init]; if (!frequencyHistoryColdStorageFixed) frequencyHistoryColdStorageFixed = [[NSMutableArray alloc] init]; // Start by zeroing out the "updated" flags; this is how we find dead mutations [frequencyHistoryDict enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, MutationFrequencyHistory *history, BOOL *stop) { history->updated = NO; }]; // // this code is a slightly modified clone of the code in Population::TallyMutationReferences; here we scan only the // subpopulation that is being displayed in this graph, and tally into gui_scratch_reference_count only // BCH 4/21/2023: This could use mutrun use counts to run faster... // int haplosome_count_per_individual = displaySpecies->HaplosomeCountPerIndividual(); int subpop_total_haplosome_count = 0; Mutation *mut_block_ptr = gSLiM_Mutation_Block; const MutationIndex *registry_iter = registry; const MutationIndex *registry_iter_end = registry + registry_size; for (; registry_iter != registry_iter_end; ++registry_iter) (mut_block_ptr + *registry_iter)->gui_scratch_reference_count_ = 0; for (const std::pair &subpop_pair : population.subpops_) { if (subpop_pair.first == _selectedSubpopulationID) // tally only within our chosen subpop { Subpopulation *subpop = subpop_pair.second; for (Individual *ind : subpop->parent_individuals_) { Haplosome **haplosomes = ind->haplosomes_; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; if (!haplosome->IsNull()) { int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; const MutationIndex *haplosome_iter = mutrun->begin_pointer_const(); const MutationIndex *haplosome_end_iter = mutrun->end_pointer_const(); for (; haplosome_iter != haplosome_end_iter; ++haplosome_iter) { const Mutation *mutation = mut_block_ptr + *haplosome_iter; if (mutation->mutation_type_ptr_->mutation_type_index_ == _selectedMutationTypeIndex) (mutation->gui_scratch_reference_count_)++; } } subpop_total_haplosome_count++; } } } } } // Now we can run through the mutations and use the tallies in gui_scratch_reference_count to update our histories for (registry_iter = registry; registry_iter != registry_iter_end; ++registry_iter) { const Mutation *mutation = mut_block_ptr + *registry_iter; slim_refcount_t refcount = mutation->gui_scratch_reference_count_; if (refcount) { uint16_t value = (uint16_t)((refcount * (unsigned long long)UINT16_MAX) / subpop_total_haplosome_count); // FIXME static analyzer says potential divide by zero here NSNumber *mutationIDNumber = [[NSNumber alloc] initWithLongLong:mutation->mutation_id_]; MutationFrequencyHistory *history = [frequencyHistoryDict objectForKey:mutationIDNumber]; //NSLog(@"mutation refcount %d has uint16_t value %d, found history %p for id %lld", refcount, value, history, (long long int)(mutation->mutation_id_)); if (history) { // We have a history for this mutation, so we just need to add an entry; this sets the updated flag [history addEntry:value]; } else { // No history, so we make one starting at this tick; this also sets the updated flag // Note we use Tick() - 1, because the tick counter has already been advanced to the next tick history = [[MutationFrequencyHistory alloc] initWithEntry:value forMutation:mutation atBaseTick:community.Tick() - 1]; [frequencyHistoryDict setObject:history forKey:mutationIDNumber]; [history release]; } [mutationIDNumber release]; } } // OK, now every mutation that has frequency >0 in our subpop has got a current entry. But what about mutations that used to circulate, // but don't any more? These could still be active in a different subpop, or they might be gone – lost or fixed. For the former case, // we need to add an entry with frequency zero. For the latter case, we need to put their history into "cold storage" for efficiency. __block NSMutableDictionary *historiesToAddToColdStorage = nil; [frequencyHistoryDict enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, MutationFrequencyHistory *history, BOOL *stop) { if (!history->updated) { slim_mutationid_t historyID = history->mutationID; const MutationIndex *mutation_iter = registry; const MutationIndex *mutation_iter_end = registry + registry_size; BOOL mutationStillExists = NO; for ( ; mutation_iter != mutation_iter_end; ++mutation_iter) { const Mutation *mutation = mut_block_ptr + *mutation_iter; slim_mutationid_t mutationID = mutation->mutation_id_; if (historyID == mutationID) { mutationStillExists = YES; break; } } if (mutationStillExists) { // The mutation is still around, so just add a zero entry for it [history addEntry:0]; } else { // The mutation is gone, so we need to put its history into cold storage, but we can't modify // our dictionary since we are enumerating it, so we just make a record and do it below if (historiesToAddToColdStorage) [historiesToAddToColdStorage setObject:history forKey:key]; else historiesToAddToColdStorage = [[NSMutableDictionary alloc] initWithObjectsAndKeys:history, key, nil]; } } }]; // Now, if historiesToAddToColdStorage is non-nil, we have histories to put into cold storage; do it now if (historiesToAddToColdStorage) { [historiesToAddToColdStorage enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, MutationFrequencyHistory *history, BOOL *stop) { // The remaining tricky bit is that we have to figure out whether the vanished mutation was fixed or lost; we do this by // scanning through all our Substitution objects, which use the same unique IDs as Mutations use. We need to know this // for two reasons: to add the final entry for the mutation, and to put it into the correct cold storage array. slim_mutationid_t mutationID = history->mutationID; BOOL wasFixed = NO; std::vector &substitutions = population.substitutions_; for (const Substitution *substitution : substitutions) { if (substitution->mutation_id_ == mutationID) { wasFixed = YES; break; } } if (wasFixed) { [history addEntry:UINT16_MAX]; [frequencyHistoryColdStorageFixed addObject:history]; } else { [history addEntry:0]; [frequencyHistoryColdStorageLost addObject:history]; } [frequencyHistoryDict removeObjectForKey:key]; }]; [historiesToAddToColdStorage release]; } //NSLog(@"frequencyHistoryDict has %lld entries, frequencyHistoryColdStorageLost has %lld entries, frequencyHistoryColdStorageFixed has %lld entries", (long long int)[frequencyHistoryDict count], (long long int)[frequencyHistoryColdStorageLost count], (long long int)[frequencyHistoryColdStorageFixed count]); lastTick = community.Tick(); } - (void)setSelectedSubpopulationID:(slim_objectid_t)newID { if (_selectedSubpopulationID != newID) { _selectedSubpopulationID = newID; [self invalidateCachedData]; [self fetchDataForFinishedTick]; [self setNeedsDisplay:YES]; } } - (void)setSelectedMutationTypeIndex:(int)newIndex { if (_selectedMutationTypeIndex != newIndex) { _selectedMutationTypeIndex = newIndex; [self invalidateCachedData]; [self fetchDataForFinishedTick]; [self setNeedsDisplay:YES]; } } - (IBAction)subpopulationPopupChanged:(id)sender { [self setSelectedSubpopulationID:SLiMClampToObjectidType([subpopulationButton selectedTag])]; } - (IBAction)mutationTypePopupChanged:(id)sender { [self setSelectedMutationTypeIndex:(int)[mutationTypeButton selectedTag]]; } - (void)controllerRecycled { SLiMWindowController *controller = [self slimWindowController]; if (![controller invalidSimulation]) { //if (![self xAxisIsUserRescaled]) // [self setXAxisRangeFromTick]; // the end tick is not yet known [self setNeedsDisplay:YES]; } // Remake our popups, whether or not the controller is valid [self invalidateCachedData]; [self addSubpopulationsToMenu]; [self addMutationTypesToMenu]; [super controllerRecycled]; } - (void)controllerTickFinished { [super controllerTickFinished]; // Check for an unexpected change in tick, in which case we invalidate all our histories and start over SLiMWindowController *controller = [self slimWindowController]; Community *community = controller->community; if (lastTick != community->Tick() - 1) { [self invalidateCachedData]; [self setNeedsDisplay:YES]; } // Fetch and store the frequencies for all mutations of the selected mutation type(s), within the subpopulation selected [self fetchDataForFinishedTick]; } - (void)updateAfterTick { // BCH 3/20/2024: We set the x axis range each tick, because the end tick is now invalid until after initialize() callbacks if (![self xAxisIsUserRescaled]) [self setXAxisRangeFromTick]; // Rebuild the subpop and muttype menus; this has the side effect of checking and fixing our selections, and that, // in turn, will have the side effect of invaliding our cache and fetching new data if needed [self addSubpopulationsToMenu]; [self addMutationTypesToMenu]; [super updateAfterTick]; } - (void)drawHistory:(MutationFrequencyHistory *)history inInteriorRect:(NSRect)interiorRect { int entryCount = history->entryCount; if (entryCount > 1) // one entry just generates a moveto { uint16_t *entries = history->entries; NSBezierPath *linePath = [NSBezierPath bezierPath]; uint16_t firstValue = *entries; double firstFrequency = ((double)firstValue) / UINT16_MAX; slim_tick_t tick = history->baseTick; NSPoint firstPoint = NSMakePoint([self plotToDeviceX:tick withInteriorRect:interiorRect], [self plotToDeviceY:firstFrequency withInteriorRect:interiorRect]); [linePath moveToPoint:firstPoint]; for (int entryIndex = 1; entryIndex < entryCount; ++entryIndex) { uint16_t value = entries[entryIndex]; double frequency = ((double)value) / UINT16_MAX; NSPoint nextPoint = NSMakePoint([self plotToDeviceX:++tick withInteriorRect:interiorRect], [self plotToDeviceY:frequency withInteriorRect:interiorRect]); [linePath lineToPoint:nextPoint]; } [linePath setLineWidth:1.0]; [linePath stroke]; } } - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { BOOL plotInColor = [self useColorsForPlotting]; if (!plotInColor) [[NSColor blackColor] set]; // Go through all our history entries and draw a line for each. First we draw the ones in cold storage, then the active ones. if ([self plotLostMutations]) { if (plotInColor) [[NSColor redColor] set]; [frequencyHistoryColdStorageLost enumerateObjectsUsingBlock:^(MutationFrequencyHistory *history, NSUInteger idx, BOOL *stop) { [self drawHistory:history inInteriorRect:interiorRect]; }]; } if ([self plotFixedMutations]) { if (plotInColor) [[NSColor colorWithCalibratedRed:0.4 green:0.4 blue:1.0 alpha:1.0] set]; [frequencyHistoryColdStorageFixed enumerateObjectsUsingBlock:^(MutationFrequencyHistory *history, NSUInteger idx, BOOL *stop) { [self drawHistory:history inInteriorRect:interiorRect]; }]; } if ([self plotActiveMutations]) { if (plotInColor) [[NSColor blackColor] set]; [frequencyHistoryDict enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, MutationFrequencyHistory *history, BOOL *stop) { [self drawHistory:history inInteriorRect:interiorRect]; }]; } } - (IBAction)toggleShowLostMutations:(id)sender { [self setPlotLostMutations:![self plotLostMutations]]; [self setNeedsDisplay:YES]; } - (IBAction)toggleShowFixedMutations:(id)sender { [self setPlotFixedMutations:![self plotFixedMutations]]; [self setNeedsDisplay:YES]; } - (IBAction)toggleShowActiveMutations:(id)sender { [self setPlotActiveMutations:![self plotActiveMutations]]; [self setNeedsDisplay:YES]; } - (IBAction)toggleUseColorsForPlotting:(id)sender { [self setUseColorsForPlotting:![self useColorsForPlotting]]; [self setNeedsDisplay:YES]; } - (IBAction)copy:(id)sender { // Hide our popups around the call to super, so they don't appear in the snapshot [subpopulationButton setHidden:YES]; [mutationTypeButton setHidden:YES]; [super copy:sender]; [subpopulationButton setHidden:NO]; [mutationTypeButton setHidden:NO]; } - (IBAction)saveGraph:(id)sender { // Hide our popups around the call to super, so they don't appear in the snapshot [subpopulationButton setHidden:YES]; [mutationTypeButton setHidden:YES]; [super saveGraph:sender]; [subpopulationButton setHidden:NO]; [mutationTypeButton setHidden:NO]; } - (void)subclassAddItemsToMenu:(NSMenu *)menu forEvent:(NSEvent *)theEvent { if (menu) { { NSMenuItem *menuItem = [menu addItemWithTitle:([self plotLostMutations] ? @"Hide Lost Mutations" : @"Show Lost Mutations") action:@selector(toggleShowLostMutations:) keyEquivalent:@""]; [menuItem setTarget:self]; } { NSMenuItem *menuItem = [menu addItemWithTitle:([self plotFixedMutations] ? @"Hide Fixed Mutations" : @"Show Fixed Mutations") action:@selector(toggleShowFixedMutations:) keyEquivalent:@""]; [menuItem setTarget:self]; } { NSMenuItem *menuItem = [menu addItemWithTitle:([self plotActiveMutations] ? @"Hide Active Mutations" : @"Show Active Mutations") action:@selector(toggleShowActiveMutations:) keyEquivalent:@""]; [menuItem setTarget:self]; } [menu addItem:[NSMenuItem separatorItem]]; { NSMenuItem *menuItem = [menu addItemWithTitle:([self useColorsForPlotting] ? @"Black Plot Lines" : @"Colored Plot Lines") action:@selector(toggleUseColorsForPlotting:) keyEquivalent:@""]; [menuItem setTarget:self]; } } } - (NSArray *)legendKey { if ([self useColorsForPlotting]) { NSMutableArray *legendKey = [NSMutableArray array]; if ([self plotLostMutations]) [legendKey addObject:@[@"lost", [NSColor redColor]]]; if ([self plotFixedMutations]) [legendKey addObject:@[@"fixed", [NSColor colorWithCalibratedRed:0.4 green:0.4 blue:1.0 alpha:1.0]]]; if ([self plotActiveMutations]) [legendKey addObject:@[@"active", [NSColor blackColor]]]; return legendKey; } return nil; } - (void)appendEntriesFromArray:(NSArray *)array toString:(NSMutableString *)string completedTicks:(slim_tick_t)completedTicks { [array enumerateObjectsUsingBlock:^(MutationFrequencyHistory *history, NSUInteger idx, BOOL *stop) { int entryCount = history->entryCount; slim_tick_t baseTick = history->baseTick; for (slim_tick_t tick = 1; tick <= completedTicks; ++tick) { if (tick < baseTick) [string appendString:@"NA, "]; else if (tick - baseTick < entryCount) [string appendFormat:@"%.4f, ", ((double)history->entries[tick - baseTick]) / UINT16_MAX]; else [string appendString:@"NA, "]; } [string appendString:@"\n"]; }]; } - (NSString *)stringForDataWithController:(SLiMWindowController *)controller { NSMutableString *string = [NSMutableString stringWithString:@"# Graph data: fitness trajectories\n"]; slim_tick_t completedTicks = controller->community->Tick() - 1; [string appendString:[self dateline]]; if ([self plotLostMutations]) { [string appendString:@"\n\n# Lost mutations:\n"]; [self appendEntriesFromArray:frequencyHistoryColdStorageLost toString:string completedTicks:completedTicks]; } if ([self plotFixedMutations]) { [string appendString:@"\n\n# Fixed mutations:\n"]; [self appendEntriesFromArray:frequencyHistoryColdStorageFixed toString:string completedTicks:completedTicks]; } if ([self plotActiveMutations]) { [string appendString:@"\n\n# Active mutations:\n"]; [self appendEntriesFromArray:[frequencyHistoryDict allValues] toString:string completedTicks:completedTicks]; } // Get rid of extra commas [string replaceOccurrencesOfString:@", \n" withString:@"\n" options:0 range:NSMakeRange(0, [string length])]; return string; } @end @implementation MutationFrequencyHistory - (instancetype)initWithEntry:(uint16_t)value forMutation:(const Mutation *)mutation atBaseTick:(slim_tick_t)tick { if (self = [super init]) { mutationID = mutation->mutation_id_; mutationType = mutation->mutation_type_ptr_; baseTick = tick; bufferSize = 0; entryCount = 0; [self addEntry:value]; } return self; } - (void)dealloc { if (entries) { free(entries); entries = NULL; bufferSize = 0; entryCount = 0; } [super dealloc]; } - (void)addEntry:(uint16_t)value { if (entryCount == bufferSize) { // We need to expand bufferSize += 1024; entries = (uint16_t *)realloc(entries, bufferSize * sizeof(uint16_t)); } entries[entryCount++] = value; updated = YES; } @end ================================================ FILE: SLiMgui/GraphView_MutationLossTimeHistogram.h ================================================ // // GraphView_MutationLossTimeHistogram.h // SLiM // // Created by Ben Haller on 3/1/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* This graph plots a histogram of the distribution of times to loss of mutations. The data is gathered by SLiM, so it is quite simple. */ #import "GraphView.h" @interface GraphView_MutationLossTimeHistogram : GraphView { } @end ================================================ FILE: SLiMgui/GraphView_MutationLossTimeHistogram.mm ================================================ // // GraphView_MutationLossTimeHistogram.m // SLiM // // Created by Ben Haller on 3/1/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "GraphView_MutationLossTimeHistogram.h" #import "SLiMWindowController.h" @implementation GraphView_MutationLossTimeHistogram - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller { if (self = [super initWithFrame:frameRect withController:controller]) { [self setHistogramBinCount:10]; //[self setAllowXAxisBinRescale:YES]; // not supported yet [self setXAxisMax:100]; [self setXAxisMajorTickInterval:20]; [self setXAxisMinorTickInterval:10]; [self setXAxisMajorTickModulus:2]; [self setXAxisTickValuePrecision:0]; [self setXAxisLabelString:@"Mutation loss time"]; [self setYAxisLabelString:@"Proportion of lost mutations"]; [self setAllowXAxisUserRescale:NO]; [self setAllowYAxisUserRescale:YES]; [self setShowHorizontalGridLines:YES]; } return self; } - (void)dealloc { [super dealloc]; } - (double *)lossTimeDataWithController:(SLiMWindowController *)controller { int binCount = [self histogramBinCount]; Species *displaySpecies = [self focalDisplaySpecies]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); slim_tick_t *histogram = displaySpecies->population_.mutation_loss_times_; int64_t histogramBins = (int64_t)displaySpecies->population_.mutation_loss_tick_slots_; // fewer than binCount * mutationTypeCount may exist static double *rebin = NULL; static uint32_t rebinBins = 0; uint32_t usedRebinBins = binCount * mutationTypeCount; // re-bin for display; use double, normalize, etc. if (!rebin || (rebinBins < usedRebinBins)) { rebinBins = usedRebinBins; rebin = (double *)realloc(rebin, rebinBins * sizeof(double)); } for (unsigned int i = 0; i < usedRebinBins; ++i) rebin[i] = 0.0; for (int i = 0; i < binCount; ++i) { for (int j = 0; j < mutationTypeCount; ++j) { int histIndex = j + i * mutationTypeCount; if (histIndex < histogramBins) rebin[histIndex] += histogram[histIndex]; } } // normalize within each mutation type for (int mutationTypeIndex = 0; mutationTypeIndex < mutationTypeCount; ++mutationTypeIndex) { uint32_t total = 0; for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; total += (int64_t)rebin[binIndex]; } for (int bin = 0; bin < binCount; ++bin) { int binIndex = mutationTypeIndex + bin * mutationTypeCount; rebin[binIndex] /= (double)total; } } return rebin; } - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { Species *displaySpecies = [self focalDisplaySpecies]; double *plotData = [self lossTimeDataWithController:controller]; int binCount = [self histogramBinCount]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); // plot our histogram bars [self drawGroupedBarplotInInteriorRect:interiorRect withController:controller buffer:plotData subBinCount:mutationTypeCount mainBinCount:binCount firstBinValue:0.0 mainBinWidth:10.0]; } - (NSArray *)legendKey { return [self mutationTypeLegendKey]; // we use the prefab mutation type legend } - (NSString *)stringForDataWithController:(SLiMWindowController *)controller { NSMutableString *string = [NSMutableString stringWithString:@"# Graph data: Mutation loss time histogram\n"]; [string appendString:[self dateline]]; [string appendString:@"\n\n"]; Species *displaySpecies = [self focalDisplaySpecies]; double *plotData = [self lossTimeDataWithController:controller]; int binCount = [self histogramBinCount]; int mutationTypeCount = (int)displaySpecies->mutation_types_.size(); for (auto mutationTypeIter = displaySpecies->mutation_types_.begin(); mutationTypeIter != displaySpecies->mutation_types_.end(); ++mutationTypeIter) { MutationType *mutationType = (*mutationTypeIter).second; int mutationTypeIndex = mutationType->mutation_type_index_; // look up the index used for this mutation type in the history info; not necessarily sequential! [string appendFormat:@"\"m%lld\", ", (long long int)mutationType->mutation_type_id_]; for (int i = 0; i < binCount; ++i) { int histIndex = mutationTypeIndex + i * mutationTypeCount; [string appendFormat:@"%.4f, ", plotData[histIndex]]; } [string appendString:@"\n"]; } // Get rid of extra commas [string replaceOccurrencesOfString:@", \n" withString:@"\n" options:0 range:NSMakeRange(0, [string length])]; return string; } @end ================================================ FILE: SLiMgui/GraphView_PopulationVisualization.h ================================================ // // GraphView_PopulationVisualization.h // SLiM // // Created by Ben Haller on 3/3/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* This graph plots a visualization of all of the populations, their size, their average fitness, and the migration between them */ #import "GraphView.h" @interface GraphView_PopulationVisualization : GraphView { } @property (nonatomic) BOOL optimizePositions; @end ================================================ FILE: SLiMgui/GraphView_PopulationVisualization.mm ================================================ // // GraphView_PopulationVisualization.m // SLiM // // Created by Ben Haller on 3/3/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "GraphView_PopulationVisualization.h" #import "SLiMWindowController.h" #include "community.h" @implementation GraphView_PopulationVisualization - (id)initWithFrame:(NSRect)frameRect withController:(SLiMWindowController *)controller { if (self = [super initWithFrame:frameRect withController:controller]) { [self setShowXAxis:NO]; [self setShowYAxis:NO]; } return self; } - (void)dealloc { [super dealloc]; } - (NSRect)rectForSubpop:(Subpopulation *)subpop centeredAt:(NSPoint)center { // figure out the right radius, clamped to reasonable limits slim_popsize_t subpopSize = subpop->parent_subpop_size_; slim_popsize_t clampedSubpopSize = subpopSize; if (clampedSubpopSize < 200) clampedSubpopSize = 200; if (clampedSubpopSize > 10000) clampedSubpopSize = 10000; double subpopRadius = sqrt(clampedSubpopSize) / 500; // size 10,000 has radius 0.2 if (subpop->gui_radius_scaling_from_user_) subpopRadius *= subpop->gui_radius_scaling_; // draw the circle NSRect subpopRect = NSMakeRect(center.x - subpopRadius, center.y - subpopRadius, 2.0 * subpopRadius, 2.0 * subpopRadius); return subpopRect; } - (void)drawSubpop:(Subpopulation *)subpop withID:(slim_objectid_t)subpopID centeredAt:(NSPoint)center controller:(SLiMWindowController *)controller { static NSDictionary *blackLabelAttrs = nil, *whiteLabelAttrs = nil; if (!blackLabelAttrs) blackLabelAttrs = [@{NSFontAttributeName : [NSFont fontWithName:[GraphView labelFontName] size:0.04], NSForegroundColorAttributeName : [NSColor blackColor]} retain]; if (!whiteLabelAttrs) whiteLabelAttrs = [@{NSFontAttributeName : [NSFont fontWithName:[GraphView labelFontName] size:0.04], NSForegroundColorAttributeName : [NSColor whiteColor]} retain]; // figure out the right radius, clamped to reasonable limits slim_popsize_t subpopSize = subpop->parent_subpop_size_; slim_popsize_t clampedSubpopSize = subpopSize; if (clampedSubpopSize < 200) clampedSubpopSize = 200; if (clampedSubpopSize > 10000) clampedSubpopSize = 10000; double subpopRadius = sqrt(clampedSubpopSize) / 500; // size 10,000 has radius 0.2 if (subpop->gui_radius_scaling_from_user_) subpopRadius *= subpop->gui_radius_scaling_; subpop->gui_radius_ = subpopRadius; // determine the color float colorRed = 0.0, colorGreen = 0.0, colorBlue = 0.0; if (subpop->gui_color_from_user_) { colorRed = subpop->gui_color_red_; colorGreen = subpop->gui_color_green_; colorBlue = subpop->gui_color_blue_; } else { // calculate the color from the mean fitness of the population // we normalize fitness values with subpopFitnessScaling so individual fitness, unscaled by subpopulation fitness, is used for coloring const double fitnessScalingFactor = 0.8; // used to be controller->fitnessColorScale; double fitness = ((subpopSize == 0) ? -10000.0 : subpop->parental_mean_unscaled_fitness_); RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, fitnessScalingFactor); } NSColor *color = [NSColor colorWithDeviceRed:colorRed green:colorGreen blue:colorBlue alpha:1.0]; // device, to match OpenGL // draw the circle NSRect subpopRect = NSMakeRect(center.x - subpopRadius, center.y - subpopRadius, 2.0 * subpopRadius, 2.0 * subpopRadius); NSBezierPath *subpopCircle = [NSBezierPath bezierPathWithOvalInRect:subpopRect]; [color set]; [subpopCircle fill]; [[NSColor blackColor] set]; [subpopCircle setLineWidth:0.002]; [subpopCircle stroke]; // label it with the subpopulation ID NSString *popString = [NSString stringWithFormat:@"p%lld", (long long int)subpopID]; double brightness = 0.299 * colorRed + 0.587 * colorGreen + 0.114 * colorBlue; NSDictionary *labelAttrs; double scaling = 1.0; if (subpop->gui_radius_scaling_from_user_ && (subpop->gui_radius_scaling_ != 1.0)) { scaling = subpop->gui_radius_scaling_; labelAttrs = @{NSFontAttributeName : [NSFont fontWithName:[GraphView labelFontName] size:(0.04 * scaling)], NSForegroundColorAttributeName : ((brightness > 0.5) ? [NSColor blackColor] : [NSColor whiteColor])}; } else { labelAttrs = ((brightness > 0.5) ? blackLabelAttrs : whiteLabelAttrs); } NSAttributedString *popLabel = [[NSAttributedString alloc] initWithString:popString attributes:labelAttrs]; NSSize labelSize = [popLabel size]; // Note that labelSize.height seems to be clamped to a minimum of 1.0, although labelSize.width is not, which is odd; the math here seems to work... NSPoint drawPoint = NSMakePoint(center.x - labelSize.width / 2, center.y - labelSize.height - 0.008 * scaling); //NSLog(@"center = %@", NSStringFromPoint(center)); //NSLog(@"labelSize = %@", NSStringFromSize(labelSize)); //NSLog(@"drawPoint = %@", NSStringFromPoint(drawPoint)); [popLabel drawAtPoint:drawPoint]; [popLabel release]; } - (void)drawArrowFromSubpop:(Subpopulation *)sourceSubpop toSubpop:(Subpopulation *)destSubpop migrantFraction:(double)migrantFraction { double destCenterX = destSubpop->gui_center_x_; double destCenterY = destSubpop->gui_center_y_; double sourceCenterX = sourceSubpop->gui_center_x_; double sourceCenterY = sourceSubpop->gui_center_y_; // we want to draw an arrow connecting these two subpops; first, we need to figure out the endpoints // they start and end a small fixed distance outside of the source/dest subpop circles double vectorDX = (destCenterX - sourceCenterX); double vectorDY = (destCenterY - sourceCenterY); double vectorLength = sqrt(vectorDX * vectorDX + vectorDY * vectorDY); double sourceEndWeight = (0.01 + sourceSubpop->gui_radius_) / vectorLength; double sourceEndX = sourceCenterX + (destCenterX - sourceCenterX) * sourceEndWeight; double sourceEndY = sourceCenterY + (destCenterY - sourceCenterY) * sourceEndWeight; double destEndWeight = (0.01 + destSubpop->gui_radius_) / vectorLength; double destEndX = destCenterX + (sourceCenterX - destCenterX) * destEndWeight; double destEndY = destCenterY + (sourceCenterY - destCenterY) * destEndWeight; // now, using those endpoints, we have a "partial vector" that goes from just outside the source subpop circle to // just outside the dest subpop circle; this partial vector will be the basis for the bezier that we draw, but we // need to calculate control points to make the bezier curve outward, using a perpendicular vector double partVecDX = destEndX - sourceEndX; // dx/dy for the partial vector from source to dest that we have just calculated double partVecDY = destEndY - sourceEndY; double partVecLength = sqrt(partVecDX * partVecDX + partVecDY * partVecDY); // the length of that partial vector double perpendicularFromSourceDX = partVecDY; // a vector perpendicular to that partial vector, by clockwise rotation double perpendicularFromSourceDY = -partVecDX; double controlPoint1X = sourceEndX + partVecDX * 0.3 + perpendicularFromSourceDX * 0.1; double controlPoint1Y = sourceEndY + partVecDY * 0.3 + perpendicularFromSourceDY * 0.1; double controlPoint2X = destEndX - partVecDX * 0.3 + perpendicularFromSourceDX * 0.1; double controlPoint2Y = destEndY - partVecDY * 0.3 + perpendicularFromSourceDY * 0.1; // now we figure out our line width, and we calculate a spatial translation of the bezier to shift in slightly off of // the midline, based on the line width, so that incoming and outgoing vectors do not overlap at the start/end points double lineWidth = 0.001 * (sqrt(migrantFraction) / 0.03); // non-linear line width scale double finalShiftMagnitude = MAX(lineWidth * 0.75, 0.010); double finalShiftX = perpendicularFromSourceDX * finalShiftMagnitude / partVecLength; double finalShiftY = perpendicularFromSourceDY * finalShiftMagnitude / partVecLength; double arrowheadSize = MAX(lineWidth * 1.5, 0.008); // we have to use a clipping path to cut back the destination end of the vector, to make room for the arrowhead [NSGraphicsContext saveGraphicsState]; double clipRadius = vectorLength - (destSubpop->gui_radius_ + arrowheadSize + 0.01); NSRect clipCircle = NSMakeRect(sourceCenterX - clipRadius, sourceCenterY - clipRadius, clipRadius * 2, clipRadius * 2); NSBezierPath *clipBezier = [NSBezierPath bezierPathWithOvalInRect:clipCircle]; // [[NSColor redColor] set]; // [clipBezier setLineWidth:0.001]; // [clipBezier stroke]; [clipBezier addClip]; // now draw the curved line connecting the subpops NSBezierPath *bezierLines = [NSBezierPath bezierPath]; double shiftedSourceEndX = sourceEndX + finalShiftX, shiftedSourceEndY = sourceEndY + finalShiftY; double shiftedDestEndX = destEndX + finalShiftX, shiftedDestEndY = destEndY + finalShiftY; double shiftedControl1X = controlPoint1X + finalShiftX, shiftedControl1Y = controlPoint1Y + finalShiftY; double shiftedControl2X = controlPoint2X + finalShiftX, shiftedControl2Y = controlPoint2Y + finalShiftY; [bezierLines moveToPoint:NSMakePoint(shiftedSourceEndX, shiftedSourceEndY)]; //[bezierLines lineToPoint:NSMakePoint(shiftedDestEndX, shiftedDestEndY)]; [bezierLines curveToPoint:NSMakePoint(shiftedDestEndX, shiftedDestEndY) controlPoint1:NSMakePoint(shiftedControl1X, shiftedControl1Y) controlPoint2:NSMakePoint(shiftedControl2X, shiftedControl2Y)]; [[NSColor blackColor] set]; [bezierLines setLineWidth:lineWidth]; [bezierLines stroke]; // restore the clipping path [NSGraphicsContext restoreGraphicsState]; // draw the arrowhead; this is oriented along the line from (shiftedDestEndX, shiftedDestEndY) to (shiftedControl2X, shiftedControl2Y), // of length partVecLength, and is calculated using a perpendicular off of that vector NSBezierPath *bezierArrowheads = [NSBezierPath bezierPath]; double angleCorrectionFactor = (arrowheadSize / partVecLength) * 0.5; // large arrowheads need to be tilted closer to the source-dest pop line //NSLog(@"angleCorrectionFactor == %f (arrowheadSize == %f, partVecLength == %f)", angleCorrectionFactor, arrowheadSize, partVecLength); double headInsideX = shiftedControl2X * (1 - angleCorrectionFactor) + shiftedSourceEndX * angleCorrectionFactor; double headInsideY = shiftedControl2Y * (1 - angleCorrectionFactor) + shiftedSourceEndY * angleCorrectionFactor; double headMidlineDX = headInsideX - shiftedDestEndX, headMidlineDY = headInsideY - shiftedDestEndY; double headMidlineLength = sqrt(headMidlineDX * headMidlineDX + headMidlineDY * headMidlineDY); double headMidlineNormDX = (headMidlineDX / headMidlineLength) * arrowheadSize; // normalized to have length arrowheadSize double headMidlineNormDY = (headMidlineDY / headMidlineLength) * arrowheadSize; double headPerpendicular1DX = headMidlineNormDY, headPerpendicular1DY = -headMidlineNormDX; // perpendicular to the normalized midline double headPerpendicular2DX = -headMidlineNormDY, headPerpendicular2DY = headMidlineNormDX; // just the negation of perpendicular 1 [bezierArrowheads moveToPoint:NSMakePoint(shiftedDestEndX, shiftedDestEndY)]; [bezierArrowheads lineToPoint:NSMakePoint(shiftedDestEndX + headMidlineNormDX * 1.75 + headPerpendicular1DX * 0.9, shiftedDestEndY + headMidlineNormDY * 1.75 + headPerpendicular1DY * 0.9)]; [bezierArrowheads lineToPoint:NSMakePoint(shiftedDestEndX + headMidlineNormDX * 1.2, shiftedDestEndY + headMidlineNormDY * 1.2)]; [bezierArrowheads lineToPoint:NSMakePoint(shiftedDestEndX + headMidlineNormDX * 1.75 + headPerpendicular2DX * 0.9, shiftedDestEndY + headMidlineNormDY * 1.75 + headPerpendicular2DY * 0.9)]; [bezierArrowheads closePath]; [[NSColor blackColor] set]; [bezierArrowheads fill]; } BOOL is_line_intersection(double p0_x, double p0_y, double p1_x, double p1_y, double p2_x, double p2_y, double p3_x, double p3_y); BOOL is_line_intersection(double p0_x, double p0_y, double p1_x, double p1_y, double p2_x, double p2_y, double p3_x, double p3_y) { double s02_x, s02_y, s10_x, s10_y, s32_x, s32_y, s_numer, t_numer, denom; s10_x = p1_x - p0_x; s10_y = p1_y - p0_y; s32_x = p3_x - p2_x; s32_y = p3_y - p2_y; denom = s10_x * s32_y - s32_x * s10_y; if (denom == 0) return FALSE; // Collinear bool denomPositive = denom > 0; s02_x = p0_x - p2_x; s02_y = p0_y - p2_y; s_numer = s10_x * s02_y - s10_y * s02_x; if ((s_numer < 0) == denomPositive) return FALSE; // No collision t_numer = s32_x * s02_y - s32_y * s02_x; if ((t_numer < 0) == denomPositive) return FALSE; // No collision if (((s_numer > denom) == denomPositive) || ((t_numer > denom) == denomPositive)) return FALSE; // No collision return TRUE; } - (double)scorePositionsWithX:(double *)center_x y:(double *)center_y connected:(BOOL *)connected count:(int)subpopCount { double score = 0.0; double meanEdge = 0.0; int edgeCount = 0; double minx = INFINITY, maxy = -INFINITY; // First we calculate the mean edge length; we will consider this the optimum length for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { double x = center_x[subpopIndex]; double y = center_y[subpopIndex]; // If any node has a NaN value, that is an immediate disqualifier; I'm not sure how it happens, but it occasionally does if (isnan(x) || isnan(y)) return -100000000; if (x < minx) minx = x; if (y > maxy) maxy = y; for (int sourceIndex = subpopIndex + 1; sourceIndex < subpopCount; ++sourceIndex) { if (connected[subpopIndex * subpopCount + sourceIndex]) { double dx = x - center_x[sourceIndex]; double dy = y - center_y[sourceIndex]; double distanceSquared = dx * dx + dy * dy; double distance = sqrt(distanceSquared); meanEdge += distance; edgeCount++; } } } meanEdge /= edgeCount; // Add a little score if the first subpop is near the upper left if ((fabs(center_x[0] - minx) < 0.05) && (fabs(center_y[0] - maxy) < 0.05)) { score += 0.01; // Add a little more score if the second subpop is to its right in roughly the same row if ((center_x[1] - center_x[0] > meanEdge/2) && (fabs(center_y[0] - center_y[1]) < 0.05)) score += 0.01; } // Score distances and crossings for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { double x = center_x[subpopIndex]; double y = center_y[subpopIndex]; for (int sourceIndex = subpopIndex + 1; sourceIndex < subpopCount; ++sourceIndex) { double dx = x - center_x[sourceIndex]; double dy = y - center_y[sourceIndex]; double distanceSquared = dx * dx + dy * dy; double distance = sqrt(distanceSquared); // being closer than k invokes a penalty if (distance < meanEdge) score -= (meanEdge - distance); // on the other hand, distance between connected subpops is very bad; this is above all what we want to optimize if (connected[subpopIndex * subpopCount + sourceIndex]) { if (distance > meanEdge) score -= (distance - meanEdge); // Detect crossings for (int secondSubpop = subpopIndex + 1; secondSubpop < subpopCount; ++secondSubpop) for (int secondSource = secondSubpop + 1; secondSource < subpopCount; ++secondSource) if (connected[secondSubpop * subpopCount + secondSource]) { double x0 = x, x1 = center_x[sourceIndex], x2 = center_x[secondSubpop], x3 = center_x[secondSource]; double y0 = y, y1 = center_y[sourceIndex], y2 = center_y[secondSubpop], y3 = center_y[secondSource]; // I test intersection with slightly shortened line segments, because I don't want endpoints that touch to be marked as intersections if (is_line_intersection(x0*0.99 + x1*0.01, y0*0.99 + y1*0.01, x0*0.01 + x1*0.99, y0*0.01 + y1*0.99, x2*0.99 + x3*0.01, y2*0.99 + y3*0.01, x2*0.01 + x3*0.99, y2*0.01 + y3*0.99)) score -= 100; } } } } return score; } // This is a simple implementation of the algorithm of Fruchterman and Reingold 1991; // there are better algorithms out there, but this one is simple... - (void)optimizeSubpopPositionsWithController:(SLiMWindowController *)controller { Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; int subpopCount = (int)pop.subpops_.size(); if (subpopCount == 0) return; double width = 0.58, length = 0.58; // allows for the radii of the vertices at max subpop size double area = width * length; double k = sqrt(area / subpopCount); double kSquared = k * k; BOOL *connected; connected = (BOOL *)calloc(subpopCount * subpopCount, sizeof(BOOL)); // We start by figuring out connectivity auto subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; for (const std::pair &fractions_pair : subpop->migrant_fractions_) { slim_objectid_t migrant_source_id = fractions_pair.first; // We need to get from the source ID to the index of the source subpop in the pop array auto sourceIter = pop.subpops_.begin(); int sourceIndex; for (sourceIndex = 0; sourceIndex < subpopCount; ++sourceIndex) { if ((*sourceIter).first == migrant_source_id) break; ++sourceIter; } if (sourceIndex == subpopCount) { free(connected); return; } // Mark the connection bidirectionally connected[subpopIndex * subpopCount + sourceIndex] = YES; connected[sourceIndex * subpopCount + subpopIndex] = YES; } ++subpopIter; } double *pos_x, *pos_y; // vertex positions double *disp_x, *disp_y; // vertex forces/displacements double *best_x, *best_y; // best vertex positions from multiple runs double best_score = -INFINITY; pos_x = (double *)malloc(sizeof(double) * subpopCount); pos_y = (double *)malloc(sizeof(double) * subpopCount); disp_x = (double *)malloc(sizeof(double) * subpopCount); disp_y = (double *)malloc(sizeof(double) * subpopCount); best_x = (double *)malloc(sizeof(double) * subpopCount); best_y = (double *)malloc(sizeof(double) * subpopCount); // We do multiple separate runs from different starting configurations, to try to find the optimal solution for (int trialIteration = 0; trialIteration < 50; ++trialIteration) { double temperature = width / 5.0; // initialize positions; this is basically the G := (V,E) step of the Fruchterman & Reingold algorithm for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { pos_x[subpopIndex] = (random() / (double)INT32_MAX) * width - width/2; pos_y[subpopIndex] = (random() / (double)INT32_MAX) * length - length/2; } // Then we do the core loop of the Fruchterman & Reingold algorithm, which calculates forces and displacements for (int optimizeIteration = 1; optimizeIteration < 1000; ++optimizeIteration) { // Calculate repulsive forces for (int v = 0; v < subpopCount; ++v) { disp_x[v] = 0.0; disp_y[v] = 0.0; for (int u = 0; u < subpopCount; ++u) { if (u != v) { double delta_x = pos_x[v] - pos_x[u]; double delta_y = pos_y[v] - pos_y[u]; double delta_magnitude_squared = delta_x * delta_x + delta_y * delta_y; double multiplier = kSquared / delta_magnitude_squared; // This is a speed-optimized version of the pseudocode version commented out below disp_x[v] += delta_x * multiplier; disp_y[v] += delta_y * multiplier; //double delta_magnitude = sqrt(delta_magnitude_squared); //disp_x[v] += (delta_x / delta_magnitude) * (kSquared / delta_magnitude); //disp_y[v] += (delta_y / delta_magnitude) * (kSquared / delta_magnitude); } } } // Calculate attractive forces for (int v = 0; v < subpopCount; ++v) { for (int u = v + 1; u < subpopCount; ++u) { if (connected[v * subpopCount + u]) { // There is an edge between u and v double delta_x = pos_x[v] - pos_x[u]; double delta_y = pos_y[v] - pos_y[u]; double delta_magnitude_squared = delta_x * delta_x + delta_y * delta_y; double delta_magnitude = sqrt(delta_magnitude_squared); double multiplier = (delta_magnitude_squared / k) / delta_magnitude; double delta_multiplier_x = delta_x * multiplier; double delta_multiplier_y = delta_y * multiplier; disp_x[v] -= delta_multiplier_x; disp_y[v] -= delta_multiplier_y; disp_x[u] += delta_multiplier_x; disp_y[u] += delta_multiplier_y; } } } // Limit max displacement to temperature t and prevent displacement outside frame for (int v = 0; v < subpopCount; ++v) { double delta_x = disp_x[v]; double delta_y = disp_y[v]; double delta_magnitude_squared = delta_x * delta_x + delta_y * delta_y; double delta_magnitude = sqrt(delta_magnitude_squared); if (delta_magnitude < temperature) { pos_x[v] += disp_x[v]; pos_y[v] += disp_y[v]; } else { pos_x[v] += (disp_x[v] / delta_magnitude) * temperature; pos_y[v] += (disp_y[v] / delta_magnitude) * temperature; } if (pos_x[v] < -width/2) pos_x[v] = -width/2; if (pos_y[v] < -length/2) pos_y[v] = -length/2; if (pos_x[v] > width/2) pos_x[v] = width/2; if (pos_y[v] > length/2) pos_y[v] = length/2; } // reduce the temperature as the layout approaches a better configuration // Fruchterman & Reingold are vague about exactly what they did here, but there is a rapid cooling phase (quenching) // and then a constant low-temperature phase (simmering); I've taken a guess at what that might look like temperature = temperature * 0.95; if (temperature < 0.002) temperature = 0.002; } // Test the final candidate and keep the best candidate double candidate_score = [self scorePositionsWithX:pos_x y:pos_y connected:connected count:subpopCount]; if (candidate_score > best_score) { for (int v = 0; v < subpopCount; ++v) { best_x[v] = pos_x[v]; best_y[v] = pos_y[v]; } best_score = candidate_score; //NSLog(@"better candidate, new score == %f", best_score); } } // Finally, we set the positions we have arrived at back into the subpops subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; subpop->gui_center_x_ = best_x[subpopIndex] + 0.5; subpop->gui_center_y_ = best_y[subpopIndex] + 0.5; subpop->gui_center_from_user_ = false; // optimization overrides previously set display settings ++subpopIter; } free(pos_x); free(pos_y); free(disp_x); free(disp_y); free(connected); free(best_x); free(best_y); } - (void)drawGraphInInteriorRect:(NSRect)interiorRect withController:(SLiMWindowController *)controller { Community &community = *controller->community; Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; int subpopCount = (int)pop.subpops_.size(); if (subpopCount == 0) { [self drawMessage:@"no subpopulations" inRect:interiorRect]; return; } // First, we transform our coordinate system so that a square of size (1,1) fits maximally and centered [NSGraphicsContext saveGraphicsState]; NSAffineTransform *transform = [NSAffineTransform transform]; if (interiorRect.size.width > interiorRect.size.height) { [transform translateXBy:interiorRect.origin.x yBy:interiorRect.origin.y]; [transform translateXBy:SLIM_SCREEN_ROUND((interiorRect.size.width - interiorRect.size.height) / 2) yBy:0]; [transform scaleBy:interiorRect.size.height]; } else { [transform translateXBy:interiorRect.origin.x yBy:interiorRect.origin.y]; [transform translateXBy:0 yBy:SLIM_SCREEN_ROUND((interiorRect.size.height - interiorRect.size.width) / 2)]; [transform scaleBy:interiorRect.size.width]; } [transform concat]; // test frame //[[NSColor blackColor] set]; //NSFrameRectWithWidth(NSMakeRect(0, 0, 1, 1), 0.002); if (subpopCount == 1) { auto subpopIter = pop.subpops_.begin(); // a single subpop is shown as a circle at the center [self drawSubpop:(*subpopIter).second withID:(*subpopIter).first centeredAt:NSMakePoint(0.5, 0.5) controller:controller]; } else if (subpopCount > 1) { // first we distribute our subpops in a ring BOOL allUserConfigured = true; { auto subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; double theta = (M_PI * 2.0 / subpopCount) * subpopIndex + M_PI_2; if (!subpop->gui_center_from_user_) { subpop->gui_center_x_ = 0.5 - cos(theta) * 0.29; subpop->gui_center_y_ = 0.5 + sin(theta) * 0.29; allUserConfigured = false; } ++subpopIter; } } // if position optimization is on, we do that to optimize the positions of the subpops if ((community.ModelType() == SLiMModelType::kModelTypeWF) && _optimizePositions && (subpopCount > 2)) [self optimizeSubpopPositionsWithController:controller]; if (!allUserConfigured) { // then do some sizing, to figure out the maximum extent of our subpops NSRect boundingBox = NSZeroRect; { auto subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; NSPoint center = NSMakePoint(subpop->gui_center_x_, subpop->gui_center_y_); NSRect subpopRect = [self rectForSubpop:subpop centeredAt:center]; boundingBox = ((subpopIndex == 0) ? subpopRect : NSUnionRect(boundingBox, subpopRect)); ++subpopIter; } } // then we translate our coordinate system so that the subpops are centered within our (0, 0, 1, 1) box double offsetX = ((1.0 - boundingBox.size.width) / 2.0) - boundingBox.origin.x; double offsetY = ((1.0 - boundingBox.size.height) / 2.0) - boundingBox.origin.y; NSAffineTransform *offsetTransform = [NSAffineTransform transform]; [offsetTransform translateXBy:offsetX yBy:offsetY]; [offsetTransform concat]; } // then we draw the subpops { auto subpopIter = pop.subpops_.begin(); for (int subpopIndex = 0; subpopIndex < subpopCount; ++subpopIndex) { Subpopulation *subpop = (*subpopIter).second; slim_objectid_t subpopID = (*subpopIter).first; NSPoint center = NSMakePoint(subpop->gui_center_x_, subpop->gui_center_y_); [self drawSubpop:subpop withID:subpopID centeredAt:center controller:controller]; ++subpopIter; } } // in the multipop case, we need to draw migration arrows, too { for (auto destSubpopIter = pop.subpops_.begin(); destSubpopIter != pop.subpops_.end(); ++destSubpopIter) { Subpopulation *destSubpop = (*destSubpopIter).second; std::map &destMigrants = (community.ModelType() == SLiMModelType::kModelTypeWF) ? destSubpop->migrant_fractions_ : destSubpop->gui_migrants_; for (auto sourceSubpopIter = destMigrants.begin(); sourceSubpopIter != destMigrants.end(); ++sourceSubpopIter) { slim_objectid_t sourceSubpopID = (*sourceSubpopIter).first; auto sourceSubpopPair = pop.subpops_.find(sourceSubpopID); if (sourceSubpopPair != pop.subpops_.end()) { Subpopulation *sourceSubpop = sourceSubpopPair->second; double migrantFraction = (*sourceSubpopIter).second; // The gui_migrants_ map is raw migration counts, which need to be converted to a fraction of the sourceSubpop pre-migration size if (community.ModelType() == SLiMModelType::kModelTypeNonWF) { if (sourceSubpop->gui_premigration_size_ <= 0) continue; migrantFraction /= sourceSubpop->gui_premigration_size_; if (migrantFraction < 0.0) migrantFraction = 0.0; if (migrantFraction > 1.0) migrantFraction = 1.0; } [self drawArrowFromSubpop:sourceSubpop toSubpop:destSubpop migrantFraction:migrantFraction]; } } } } } // We're done with our transformed coordinate system [NSGraphicsContext restoreGraphicsState]; } - (IBAction)toggleOptimizedPositions:(id)sender { [self setOptimizePositions:![self optimizePositions]]; [self setNeedsDisplay:YES]; } - (void)subclassAddItemsToMenu:(NSMenu *)menu forEvent:(NSEvent *)theEvent { if (menu) { NSMenuItem *menuItem = [menu addItemWithTitle:([self optimizePositions] ? @"Standard Positions" : @"Optimized Positions") action:@selector(toggleOptimizedPositions:) keyEquivalent:@""]; [menuItem setTarget:self]; } } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; SLiMWindowController *controller = [self slimWindowController]; if (!controller) return NO; Species *displaySpecies = [self focalDisplaySpecies]; Population &pop = displaySpecies->population_; if (sel == @selector(toggleOptimizedPositions:)) { // If any subpop has a user-defined center, disable position optimization; it doesn't know how to // handle those, and there's no way to revert back after it messes things up, and so forth for (auto subpopIter = pop.subpops_.begin(); subpopIter != pop.subpops_.end(); ++subpopIter) if (((*subpopIter).second)->gui_center_from_user_) return NO; } return YES; } @end ================================================ FILE: SLiMgui/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "AppIcon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "AppIcon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "AppIcon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "AppIcon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "AppIcon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "AppIcon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "AppIcon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "AppIcon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "AppIcon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: SLiMgui/PopulationView.h ================================================ // // PopulationView.h // SLiM // // Created by Ben Haller on 1/21/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #include "subpopulation.h" #include typedef struct { int backgroundType; // 0 == black, 1 == gray, 2 == white, 3 == named spatial map; if no preference has been set, no entry will exist std::string spatialMapName; // the name of the spatial map chosen, for backgroundType == 3 } PopulationViewBackgroundSettings; @interface PopulationView : NSOpenGLView { // display mode: 0 == individuals (non-spatial), 1 == individuals (spatial) int displayMode; // display background preferences, kept indexed by subpopulation id std::map backgroundSettings; slim_objectid_t lastContextMenuSubpopID; // subview tiling, kept indexed by subpopulation id std::map subpopTiles; } // Outlets connected to objects in PopulationViewOptionsSheet.xib @property (nonatomic, retain) IBOutlet NSWindow *displayOptionsSheet; @property (nonatomic, assign) IBOutlet NSTextField *binCountTextField; @property (nonatomic, assign) IBOutlet NSTextField *fitnessMinTextField; @property (nonatomic, assign) IBOutlet NSTextField *fitnessMaxTextField; @property (nonatomic, assign) IBOutlet NSButton *okButton; - (BOOL)tileSubpopulations:(std::vector &)selectedSubpopulations; @end // This subclass is for displaying an error message in the population view, which is hard to do in an NSOpenGLView @interface PopulationErrorView : NSView { } @end ================================================ FILE: SLiMgui/PopulationView.mm ================================================ // // PopulationView.m // SLiM // // Created by Ben Haller on 1/21/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "PopulationView.h" #import "SLiMWindowController.h" #import "CocoaExtra.h" #import #include #include #include "community.h" static const int kMaxGLRects = 2000; // 2000 rects static const int kMaxVertices = kMaxGLRects * 4; // 4 vertices each @implementation PopulationView - (void)initializeDisplayOptions { displayMode = -1; // don't know yet whether the model is spatial or not, which will determine our initial choice } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { [self initializeDisplayOptions]; } return self; } - (instancetype)initWithFrame:(NSRect)frameRect { if (self = [super initWithFrame:frameRect]) { [self initializeDisplayOptions]; } return self; } - (void)dealloc { //NSLog(@"[PopulationView dealloc]"); [self setDisplayOptionsSheet:nil]; [super dealloc]; } /* // BCH 4/19/2019: these subclass methods are not needed, and are generating a warning in Xcode 10.2, so I'm removing them // Called after being summoned from a NIB/XIB. - (void)prepareOpenGL { } // Adjust the OpenGL viewport in response to window resize - (void)reshape { } */ - (void)drawViewFrameInBounds:(NSRect)bounds { int ox = (int)bounds.origin.x, oy = (int)bounds.origin.y; glColor3f(0.77f, 0.77f, 0.77f); glRecti(ox, oy, ox + 1, oy + (int)bounds.size.height); glRecti(ox + 1, oy, ox + (int)bounds.size.width - 1, oy + 1); glRecti(ox + (int)bounds.size.width - 1, oy, ox + (int)bounds.size.width, oy + (int)bounds.size.height); glRecti(ox + 1, oy + (int)bounds.size.height - 1, ox + (int)bounds.size.width - 1, oy + (int)bounds.size.height); } - (void)drawIndividualsFromSubpopulation:(Subpopulation *)subpop inArea:(NSRect)bounds { // // NOTE this code is parallel to the code in canDisplayIndividualsFromSubpopulation:inArea: and should be maintained in parallel // double scalingFactor = 0.8; // used to be controller->fitnessColorScale; slim_popsize_t subpopSize = subpop->parent_subpop_size_; int squareSize, viewColumns = 0, viewRows = 0; // first figure out the biggest square size that will allow us to display the whole subpopulation for (squareSize = 20; squareSize > 1; --squareSize) { viewColumns = (int)floor((bounds.size.width - 3) / squareSize); viewRows = (int)floor((bounds.size.height - 3) / squareSize); if (viewColumns * viewRows > subpopSize) { // If we have an empty row at the bottom, then break for sure; this allows us to look nice and symmetrical if ((subpopSize - 1) / viewColumns < viewRows - 1) break; // Otherwise, break only if we are getting uncomfortably small; otherwise, let's drop down one square size to allow symmetry if (squareSize <= 5) break; } } if (squareSize > 1) { int squareSpacing = 0; // Convert square area to space between squares if possible if (squareSize > 2) { --squareSize; ++squareSpacing; } if (squareSize > 5) { --squareSize; ++squareSpacing; } double excessSpaceX = bounds.size.width - ((squareSize + squareSpacing) * viewColumns - squareSpacing); double excessSpaceY = bounds.size.height - ((squareSize + squareSpacing) * viewRows - squareSpacing); int offsetX = (int)floor(excessSpaceX / 2.0); int offsetY = (int)floor(excessSpaceY / 2.0); // If we have an empty row at the bottom, then we can use the same value for offsetY as for offsetX, for symmetry if ((subpopSize - 1) / viewColumns < viewRows - 1) offsetY = offsetX; NSRect individualArea = NSMakeRect(bounds.origin.x + offsetX, bounds.origin.y + offsetY, bounds.size.width - offsetX, bounds.size.height - offsetY); static float *glArrayVertices = nil; static float *glArrayColors = nil; int individualArrayIndex, displayListIndex; float *vertices = NULL, *colors = NULL; // Set up the vertex and color arrays if (!glArrayVertices) glArrayVertices = (float *)malloc(kMaxVertices * 2 * sizeof(float)); // 2 floats per vertex, kMaxVertices vertices if (!glArrayColors) glArrayColors = (float *)malloc(kMaxVertices * 4 * sizeof(float)); // 4 floats per color, kMaxVertices colors // Set up to draw rects displayListIndex = 0; vertices = glArrayVertices; glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); colors = glArrayColors; glEnableClientState(GL_COLOR_ARRAY); glColorPointer(4, GL_FLOAT, 0, glArrayColors); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... float left = (float)(individualArea.origin.x + (individualArrayIndex % viewColumns) * (squareSize + squareSpacing)); float top = (float)(individualArea.origin.y + (individualArrayIndex / viewColumns) * (squareSize + squareSpacing)); float right = left + squareSize; float bottom = top + squareSize; *(vertices++) = left; *(vertices++) = top; *(vertices++) = left; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = top; // dark gray default, for a fitness of NaN; should never happen float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; Individual &individual = *subpop->parent_individuals_[individualArrayIndex]; if (Individual::s_any_individual_color_set_ && individual.color_set_) { colorRed = individual.colorR_ / 255.0F; colorGreen = individual.colorG_ / 255.0F; colorBlue = individual.colorB_ / 255.0F; } else { // use individual trait values to determine color; we use fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring double fitness = individual.cached_unscaled_fitness_; if (!std::isnan(fitness)) RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); } for (int j = 0; j < 4; ++j) { *(colors++) = colorRed; *(colors++) = colorGreen; *(colors++) = colorBlue; *(colors++) = colorAlpha; } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } } // Draw any leftovers if (displayListIndex) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); } // Draw a gear // This is an experiment with drawing an "action" button on top of the view contents using OpenGL. // The results are decent on a Retina display, but not great on a regular display. In any case // I think having the action button drawn on top of view contents ends up looking strange. // Unfortunately there doesn't seem to be anywhere good to put such buttons; so we'll stick with // context menus for now. BCH 1/27/2019 #if 0 { double cx = bounds.size.width - 13.0, cy = 13.0; double radius = 8.0; float button_color = 1.0f; float outline_color = 0.60f; // ***** draw the off-white button disc as a triangle fan vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; // center point *(vertices++) = (float)cx; *(vertices++) = (float)cy; displayListIndex++; // fan points for (int j = 0; j <= 32; ++j) { double angle = (j / (32.0)) * (2.0 * M_PI); double r = radius * 1.30; *(vertices++) = (float)(cx + cos(angle) * r); *(vertices++) = (float)(cy + sin(angle) * r); displayListIndex++; } // colors for (int j = 0; j < displayListIndex; ++j) { *(colors++) = button_color; *(colors++) = button_color; *(colors++) = button_color; *(colors++) = 1.0f; } glDrawArrays(GL_TRIANGLE_FAN, 0, displayListIndex); // ***** outline the button with gray colors = glArrayColors; for (int j = 0; j < displayListIndex; ++j) { *(colors++) = outline_color; *(colors++) = outline_color; *(colors++) = outline_color; *(colors++) = 1.0f; } glLineWidth(2.0); glEnable(GL_LINE_SMOOTH); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDrawArrays(GL_LINE_LOOP, 1, displayListIndex - 2); glLineWidth(1.0); glDisable(GL_LINE_SMOOTH); glDisable(GL_BLEND); // ***** draw the gear polygon as a triangle fan vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; // center point *(vertices++) = (float)cx; *(vertices++) = (float)cy; displayListIndex++; // fan points for (int j = 0; j <= 8 * 6; ++j) { double angle = (j / (8.0 * 6.0)) * (2.0 * M_PI); int tooth_step = (j + 1) % 6; // 6 steps per tooth double r = (tooth_step < 3) ? radius : radius * 0.7; *(vertices++) = (float)(cx + cos(angle) * r); *(vertices++) = (float)(cy + sin(angle) * r); displayListIndex++; } // colors for (int j = 0; j < displayListIndex; ++j) { *(colors++) = 0.3f; *(colors++) = 0.3f; *(colors++) = 0.3f; *(colors++) = 1.0f; } glDrawArrays(GL_TRIANGLE_FAN, 0, displayListIndex); // ***** draw the gear outline, to antialias it colors = glArrayColors; for (int j = 0; j < displayListIndex; ++j) { *(colors++) = button_color; *(colors++) = button_color; *(colors++) = button_color; *(colors++) = 1.0f; } glLineWidth(1.0); glEnable(GL_LINE_SMOOTH); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDrawArrays(GL_LINE_LOOP, 1, displayListIndex - 2); glLineWidth(1.0); glDisable(GL_LINE_SMOOTH); glDisable(GL_BLEND); // ***** draw the interior circle vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; // center point *(vertices++) = (float)cx; *(vertices++) = (float)cy; displayListIndex++; // fan points for (int j = 0; j <= 16; ++j) { double angle = (j / 16.0) * (2.0 * M_PI); double r = radius * 0.25; *(vertices++) = (float)(cx + cos(angle) * r); *(vertices++) = (float)(cy + sin(angle) * r); displayListIndex++; } // colors for (int j = 0; j < displayListIndex; ++j) { *(colors++) = button_color; *(colors++) = button_color; *(colors++) = button_color; *(colors++) = 1.0f; } glDrawArrays(GL_TRIANGLE_FAN, 0, displayListIndex); // ***** draw the inner circle outline, to antialias it glLineWidth(1.0); glEnable(GL_LINE_SMOOTH); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDrawArrays(GL_LINE_LOOP, 1, displayListIndex - 2); glLineWidth(1.0); glDisable(GL_LINE_SMOOTH); glDisable(GL_BLEND); } #endif glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); } else { // This is what we do if we cannot display a subpopulation because there are too many individuals in it to display glColor3f(0.9f, 0.9f, 1.0f); int ox = (int)bounds.origin.x, oy = (int)bounds.origin.y; glRecti(ox + 1, oy + 1, ox + (int)bounds.size.width - 1, oy + (int)bounds.size.height - 1); } } #define SLIM_MAX_HISTOGRAM_BINS 100 - (void)cacheDisplayBufferForMap:(SpatialMap *)background_map subpopulation:(Subpopulation *)subpop { // Cache a display buffer for the given background map. This method should be called only in the 2D "xy" // case; in the 1D case we can't know the maximum width ahead of time, so we just draw rects without caching, // and in the 3D "xyz" case we don't know which z-slice to use so we can't display the spatial map. // The window might be too narrow to display a full-size map right now, but we want to premake a full-size map. // The sizing logic here is taken from drawRect:, assuming that we are not constrained in width. // By the way, it may occur to the reader to wonder why we keep the buffer as uint8_t values, given that we // convert to and from uin8_t for the display code that uses float RGB components. My rationale is that // this drastically cuts the amount of memory that has to be accessed, and that the display code, in particular, // is probably memory-bound since most of the work is done in the GPU. I haven't done any speed tests to // confirm that hunch, though. In any case, it's plenty fast and I don't see significant display artifacts. NSRect full_bounds = NSInsetRect([self bounds], 1, 1); int max_height = (int)full_bounds.size.height; double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; double subpopAspect = bounds_x_size / bounds_y_size; int max_width = (int)round(max_height * subpopAspect); // If we have a display buffer of the wrong size, free it. This should only happen when the user changes // the Subpopulation's spatialBounds after it has displayed; it should not happen with a window resize. // The user has no way to change the map or the colormap except to set a whole new map, which will also // result in the old one being freed, so we're already safe in those circumstances. if (background_map->display_buffer_ && ((background_map->buffer_width_ != max_width) || (background_map->buffer_height_ != max_height))) { free(background_map->display_buffer_); background_map->display_buffer_ = nullptr; } // Now allocate and validate the display buffer if we haven't already done so. if (!background_map->display_buffer_) { uint8_t *display_buf = (uint8_t *)malloc(max_width * max_height * 3 * sizeof(uint8_t)); background_map->display_buffer_ = display_buf; background_map->buffer_width_ = max_width; background_map->buffer_height_ = max_height; uint8_t *buf_ptr = display_buf; int64_t xsize = background_map->grid_size_[0]; int64_t ysize = background_map->grid_size_[1]; double *values = background_map->values_; bool interpolate = background_map->interpolate_; for (int y = 0; y < max_height; y++) { for (int x = 0; x < max_width; x++) { // Look up the nearest map point and get its value; interpolate if requested double x_fraction = (x + 0.5) / max_width; // pixel center double y_fraction = (y + 0.5) / max_height; // pixel center double value; if (interpolate) { double x_map = x_fraction * (xsize - 1); double y_map = y_fraction * (ysize - 1); int x1_map = (int)floor(x_map); int y1_map = (int)floor(y_map); int x2_map = (int)ceil(x_map); int y2_map = (int)ceil(y_map); double fraction_x2 = x_map - x1_map; double fraction_x1 = 1.0 - fraction_x2; double fraction_y2 = y_map - y1_map; double fraction_y1 = 1.0 - fraction_y2; double value_x1_y1 = values[x1_map + y1_map * xsize] * fraction_x1 * fraction_y1; double value_x2_y1 = values[x2_map + y1_map * xsize] * fraction_x2 * fraction_y1; double value_x1_y2 = values[x1_map + y2_map * xsize] * fraction_x1 * fraction_y2; double value_x2_y2 = values[x2_map + y2_map * xsize] * fraction_x2 * fraction_y2; value = value_x1_y1 + value_x2_y1 + value_x1_y2 + value_x2_y2; } else { int x_map = (int)round(x_fraction * (xsize - 1)); int y_map = (int)round(y_fraction * (ysize - 1)); value = values[x_map + y_map * xsize]; } // Given the (interpolated?) value, look up the color, interpolating if necessary double rgb[3]; background_map->ColorForValue(value, rgb); // Write the color values to the buffer *(buf_ptr++) = (uint8_t)round(rgb[0] * 255.0); *(buf_ptr++) = (uint8_t)round(rgb[1] * 255.0); *(buf_ptr++) = (uint8_t)round(rgb[2] * 255.0); } } } } - (void)_drawBackgroundSpatialMap:(SpatialMap *)background_map inBounds:(NSRect)bounds forSubpopulation:(Subpopulation *)subpop { // We have a spatial map with a color map, so use it to draw the background int bounds_x1 = (int)bounds.origin.x; int bounds_y1 = (int)bounds.origin.y; int bounds_x2 = (int)(bounds.origin.x + bounds.size.width); int bounds_y2 = (int)(bounds.origin.y + bounds.size.height); //glColor3f(0.0, 0.0, 0.0); //glRecti(bounds_x1, bounds_y1, bounds_x2, bounds_y2); static float *glArrayVertices = nil; static float *glArrayColors = nil; int displayListIndex; float *vertices = NULL, *colors = NULL; // Set up the vertex and color arrays if (!glArrayVertices) glArrayVertices = (float *)malloc(kMaxVertices * 2 * sizeof(float)); // 2 floats per vertex, kMaxVertices vertices if (!glArrayColors) glArrayColors = (float *)malloc(kMaxVertices * 4 * sizeof(float)); // 4 floats per color, kMaxVertices colors // Set up to draw rects displayListIndex = 0; vertices = glArrayVertices; glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); colors = glArrayColors; glEnableClientState(GL_COLOR_ARRAY); glColorPointer(4, GL_FLOAT, 0, glArrayColors); if (background_map->spatiality_ == 1) { // This is the spatiality "x" and "y" cases; they are the only 1D spatiality values for which SLiMgui will draw // In the 1D case we can't cache a display buffer, since we don't know what aspect ratio to use, so we just // draw rects. Whether those rects are horizontal or vertical will depend on the spatiality of the map. Most // of the code is identical, though, because of the way we handle dimensions, so we share the two cases here. bool spatiality_is_x = (background_map->spatiality_string_ == "x"); int64_t xsize = background_map->grid_size_[0]; double *values = background_map->values_; if (background_map->interpolate_) { // Interpolation, so we need to draw every line individually int min_coord = (spatiality_is_x ? bounds_x1 : bounds_y1); int max_coord = (spatiality_is_x ? bounds_x2 : bounds_y2); for (int x = min_coord; x < max_coord; ++x) { double x_fraction = (x + 0.5 - min_coord) / (max_coord - min_coord); // values evaluated at pixel centers double x_map = x_fraction * (xsize - 1); int x1_map = (int)floor(x_map); int x2_map = (int)ceil(x_map); double fraction_x2 = x_map - x1_map; double fraction_x1 = 1.0 - fraction_x2; double value_x1 = values[x1_map] * fraction_x1; double value_x2 = values[x2_map] * fraction_x2; double value = value_x1 + value_x2; int x1, x2, y1, y2; if (spatiality_is_x) { x1 = x; x2 = x + 1; y1 = bounds_y1; y2 = bounds_y2; } else { y1 = (max_coord - 1) - x + min_coord; // flip for y, to use Cartesian coordinates y2 = y1 + 1; x1 = bounds_x1; x2 = bounds_x2; } float rgb[3]; background_map->ColorForValue(value, rgb); //glColor3f(red, green, blue); //glRecti(x1, y1, x2, y2); *(vertices++) = x1; *(vertices++) = y1; *(vertices++) = x1; *(vertices++) = y2; *(vertices++) = x2; *(vertices++) = y2; *(vertices++) = x2; *(vertices++) = y1; for (int j = 0; j < 4; ++j) { *(colors++) = rgb[0]; *(colors++) = rgb[1]; *(colors++) = rgb[2]; *(colors++) = 1.0; } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } } } else { // No interpolation, so we can draw whole grid blocks for (int x = 0; x < xsize; x++) { double value = (spatiality_is_x ? values[x] : values[(xsize - 1) - x]); // flip for y, to use Cartesian coordinates int x1, x2, y1, y2; if (spatiality_is_x) { x1 = (int)round(((x - 0.5) / (xsize - 1)) * bounds.size.width + bounds.origin.x); x2 = (int)round(((x + 0.5) / (xsize - 1)) * bounds.size.width + bounds.origin.x); if (x1 < bounds_x1) x1 = bounds_x1; if (x2 > bounds_x2) x2 = bounds_x2; y1 = bounds_y1; y2 = bounds_y2; } else { y1 = (int)round(((x - 0.5) / (xsize - 1)) * bounds.size.height + bounds.origin.y); y2 = (int)round(((x + 0.5) / (xsize - 1)) * bounds.size.height + bounds.origin.y); if (y1 < bounds_y1) y1 = bounds_y1; if (y2 > bounds_y2) y2 = bounds_y2; x1 = bounds_x1; x2 = bounds_x2; } float rgb[3]; background_map->ColorForValue(value, rgb); //glColor3f(red, green, blue); //glRecti(x1, y1, x2, y2); *(vertices++) = x1; *(vertices++) = y1; *(vertices++) = x1; *(vertices++) = y2; *(vertices++) = x2; *(vertices++) = y2; *(vertices++) = x2; *(vertices++) = y1; for (int j = 0; j < 4; ++j) { *(colors++) = rgb[0]; *(colors++) = rgb[1]; *(colors++) = rgb[2]; *(colors++) = 1.0; } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } } } } else // if (background_map->spatiality_ == 2) { // This is the spatiality "xy" case; it is the only 2D spatiality for which SLiMgui will draw // First, cache the display buffer if needed. If this succeeds, we'll use it. // It should always succeed, so the tile-drawing code below is dead code, kept for parallelism with the 1D case. [self cacheDisplayBufferForMap:background_map subpopulation:subpop]; uint8_t *display_buf = background_map->display_buffer_; if (display_buf) { // Use a cached display buffer to draw. int buf_width = background_map->buffer_width_; int buf_height = background_map->buffer_height_; bool display_full_size = (((int)bounds.size.width == buf_width) && ((int)bounds.size.height == buf_height)); float scale_x = (float)(bounds.size.width / buf_width); float scale_y = (float)(bounds.size.height / buf_height); // Then run through the pixels in the display buffer and draw them; this could be done // with some sort of OpenGL image-drawing method instead, but it's actually already // remarkably fast, at least on my machine, and drawing an image with OpenGL seems very // gross, and I tried it once before and couldn't get it to work well... for (int y = 0; y < buf_height; y++) { // We flip the buffer vertically; it's the simplest way to get it into the right coordinate space uint8_t *buf_ptr = display_buf + ((buf_height - 1) - y) * buf_width * 3; for (int x = 0; x < buf_width; x++) { float red = *(buf_ptr++) / 255.0f; float green = *(buf_ptr++) / 255.0f; float blue = *(buf_ptr++) / 255.0f; float left, right, top, bottom; if (display_full_size) { left = bounds_x1 + x; right = left + 1.0f; top = bounds_y1 + y; bottom = top + 1.0f; } else { left = bounds_x1 + x * scale_x; right = bounds_x1 + (x + 1) * scale_x; top = bounds_y1 + y * scale_y; bottom = bounds_y1 + (y + 1) * scale_y; } *(vertices++) = left; *(vertices++) = top; *(vertices++) = left; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = top; for (int j = 0; j < 4; ++j) { *(colors++) = red; *(colors++) = green; *(colors++) = blue; *(colors++) = 1.0; } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } } } } else { // Draw rects for each map tile, without caching. Not as slow as you might expect, // but for really big maps it does get cumbersome. This is dead code now, overridden // by the buffer-drawing code above, which also handles interpolation correctly. int64_t xsize = background_map->grid_size_[0]; int64_t ysize = background_map->grid_size_[1]; double *values = background_map->values_; int n_colors = background_map->n_colors_; for (int y = 0; y < ysize; y++) { int y1 = (int)round(((y - 0.5) / (ysize - 1)) * bounds.size.height + bounds.origin.y); int y2 = (int)round(((y + 0.5) / (ysize - 1)) * bounds.size.height + bounds.origin.y); if (y1 < bounds_y1) y1 = bounds_y1; if (y2 > bounds_y2) y2 = bounds_y2; // Flip our display, since our coordinate system is flipped relative to our buffer double *values_row = values + ((ysize - 1) - y) * xsize; for (int x = 0; x < xsize; x++) { double value = *(values_row + x); int x1 = (int)round(((x - 0.5) / (xsize - 1)) * bounds.size.width + bounds.origin.x); int x2 = (int)round(((x + 0.5) / (xsize - 1)) * bounds.size.width + bounds.origin.x); if (x1 < bounds_x1) x1 = bounds_x1; if (x2 > bounds_x2) x2 = bounds_x2; float value_fraction = (background_map->colors_min_ < background_map->colors_max_) ? (float)((value - background_map->colors_min_) / (background_map->colors_max_ - background_map->colors_min_)) : 0.0f; float color_index = value_fraction * (n_colors - 1); int color_index_1 = (int)floorf(color_index); int color_index_2 = (int)ceilf(color_index); if (color_index_1 < 0) color_index_1 = 0; if (color_index_1 >= n_colors) color_index_1 = n_colors - 1; if (color_index_2 < 0) color_index_2 = 0; if (color_index_2 >= n_colors) color_index_2 = n_colors - 1; float color_2_weight = color_index - color_index_1; float color_1_weight = 1.0f - color_2_weight; float red1 = background_map->red_components_[color_index_1]; float green1 = background_map->green_components_[color_index_1]; float blue1 = background_map->blue_components_[color_index_1]; float red2 = background_map->red_components_[color_index_2]; float green2 = background_map->green_components_[color_index_2]; float blue2 = background_map->blue_components_[color_index_2]; float red = red1 * color_1_weight + red2 * color_2_weight; float green = green1 * color_1_weight + green2 * color_2_weight; float blue = blue1 * color_1_weight + blue2 * color_2_weight; //glColor3f(red, green, blue); //glRecti(x1, y1, x2, y2); *(vertices++) = x1; *(vertices++) = y1; *(vertices++) = x1; *(vertices++) = y2; *(vertices++) = x2; *(vertices++) = y2; *(vertices++) = x2; *(vertices++) = y1; for (int j = 0; j < 4; ++j) { *(colors++) = red; *(colors++) = green; *(colors++) = blue; *(colors++) = 1.0; } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } //std::cout << "x = " << x << ", y = " << y << ", value = " << value << ": color_index = " << color_index << ", color_index_1 = " << color_index_1 << ", color_index_2 = " << color_index_2 << ", color_1_weight = " << color_1_weight << ", color_2_weight = " << color_2_weight << ", red = " << red << std::endl; } } } } // Draw any leftovers if (displayListIndex) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); } glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); #if 0 // Experimental feature: draw boxes showing where the grid nodes are, since that is rather confusing! NSRect individualArea = NSMakeRect(bounds.origin.x, bounds.origin.y, bounds.size.width - 1, bounds.size.height - 1); int64_t xsize = background_map->grid_size_[0]; int64_t ysize = background_map->grid_size_[1]; double *values = background_map->values_; if ((xsize <= 51) && (ysize <= 51)) { // Set up to draw rects displayListIndex = 0; vertices = glArrayVertices; glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); colors = glArrayColors; glEnableClientState(GL_COLOR_ARRAY); glColorPointer(4, GL_FLOAT, 0, glArrayColors); // first pass we draw squares to make outlines, second pass we draw the interiors in color for (int pass = 0; pass <= 1; ++pass) { for (int x = 0; x < xsize; ++x) { for (int y = 0; y < ysize; ++y) { float position_x = x / (float)(xsize - 1); // 0 to 1 float position_y = y / (float)(ysize - 1); // 0 to 1 float centerX = (float)(individualArea.origin.x + round(position_x * individualArea.size.width) + 0.5); float centerY = (float)(individualArea.origin.y + individualArea.size.height - round(position_y * individualArea.size.height) + 0.5); const float margin = ((pass == 0) ? 5.5f : 3.5f); float left = centerX - margin; float top = centerY - margin; float right = centerX + margin; float bottom = centerY + margin; if (left < individualArea.origin.x) left = (float)individualArea.origin.x; if (top < individualArea.origin.y) top = (float)individualArea.origin.y; if (right > individualArea.origin.x + individualArea.size.width) right = (float)(individualArea.origin.x + individualArea.size.width); if (bottom > individualArea.origin.y + individualArea.size.height) bottom = (float)(individualArea.origin.y + individualArea.size.height); *(vertices++) = left; *(vertices++) = top; *(vertices++) = left; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = top; if (pass == 0) { for (int j = 0; j < 4; ++j) { *(colors++) = 1.0; *(colors++) = 0.25; *(colors++) = 0.25; *(colors++) = 1.0; } } else { // look up the map's color at this grid point float rgb[3]; double value = values[x + y * xsize]; background_map->ColorForValue(value, rgb); for (int j = 0; j < 4; ++j) { *(colors++) = rgb[0]; *(colors++) = rgb[1]; *(colors++) = rgb[2]; *(colors++) = 1.0; } } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } } } } // Draw any leftovers if (displayListIndex) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); } glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); } #endif } - (void)chooseDefaultBackgroundSettings:(PopulationViewBackgroundSettings *)background map:(SpatialMap **)returnMap forSubpopulation:(Subpopulation *)subpop { // black by default background->backgroundType = 0; // if there are spatial maps defined, we try to choose one, requiring "x" or "y" or "xy", and requiring // a color map to be defined, and preferring 2D over 1D, providing the same default behavior as SLiM 2.x SpatialMapMap &spatial_maps = subpop->spatial_maps_; SpatialMap *background_map = nullptr; std::string background_map_name; for (const auto &map_pair : spatial_maps) { SpatialMap *map = map_pair.second; // a map must be "x", "y", or "xy", and must have a defined color map, for us to choose it as a default at all if (((map->spatiality_string_ == "x") || (map->spatiality_string_ == "y") || (map->spatiality_string_ == "xy")) && (map->n_colors_ > 0)) { // the map is usable, so now we check whether it's better than the map we previously found, if any if ((!background_map) || (map->spatiality_ > background_map->spatiality_)) { background_map = map; background_map_name = map_pair.first; } } } if (background_map) { background->backgroundType = 3; background->spatialMapName = background_map_name; *returnMap = background_map; } } - (void)drawSpatialBackgroundInBounds:(NSRect)bounds forSubpopulation:(Subpopulation *)subpop dimensionality:(int)dimensionality { auto backgroundIter = backgroundSettings.find(subpop->subpopulation_id_); PopulationViewBackgroundSettings background; SpatialMap *background_map = nil; if (backgroundIter == backgroundSettings.end()) { // The user has not made a choice, so choose a temporary default. We don't want this choice to "stick", // so that we can, e.g., begin as black and then change to a spatial map if one is defined. [self chooseDefaultBackgroundSettings:&background map:&background_map forSubpopulation:subpop]; } else { // The user has made a choice; verify that it is acceptable, and then use it. background = backgroundIter->second; if (background.backgroundType == 3) { SpatialMapMap &spatial_maps = subpop->spatial_maps_; auto map_iter = spatial_maps.find(background.spatialMapName); if (map_iter != spatial_maps.end()) { background_map = map_iter->second; // if the user somehow managed to choose a map that is not of an acceptable dimensionality, reject it here if ((background_map->spatiality_string_ != "x") && (background_map->spatiality_string_ != "y") && (background_map->spatiality_string_ != "xy")) background_map = nil; } } // if we're supposed to use a background map but we couldn't find it, or it's unacceptable, revert to black if ((background.backgroundType == 3) && !background_map) background.backgroundType = 0; } if ((background.backgroundType == 3) && background_map) { [self _drawBackgroundSpatialMap:background_map inBounds:bounds forSubpopulation:subpop]; } else { // No background map, so just clear to the preferred background color int backgroundColor = background.backgroundType; if (backgroundColor == 0) glColor3f(0.0, 0.0, 0.0); else if (backgroundColor == 1) glColor3f(0.3f, 0.3f, 0.3f); else if (backgroundColor == 2) glColor3f(1.0, 1.0, 1.0); else glColor3f(0.0, 0.0, 0.0); glRecti((int)bounds.origin.x, (int)bounds.origin.y, (int)(bounds.origin.x + bounds.size.width), (int)(bounds.origin.y + bounds.size.height)); } } - (void)drawSpatialIndividualsFromSubpopulation:(Subpopulation *)subpop inArea:(NSRect)bounds dimensionality:(int)dimensionality { SLiMWindowController *controller = [[self window] windowController]; double scalingFactor = 0.8; // used to be controller->fitnessColorScale; slim_popsize_t subpopSize = subpop->parent_subpop_size_; double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; NSRect individualArea = NSMakeRect(bounds.origin.x, bounds.origin.y, bounds.size.width - 1, bounds.size.height - 1); static float *glArrayVertices = nil; static float *glArrayColors = nil; int individualArrayIndex, displayListIndex; float *vertices = NULL, *colors = NULL; // Set up the vertex and color arrays if (!glArrayVertices) glArrayVertices = (float *)malloc(kMaxVertices * 2 * sizeof(float)); // 2 floats per vertex, kMaxVertices vertices if (!glArrayColors) glArrayColors = (float *)malloc(kMaxVertices * 4 * sizeof(float)); // 4 floats per color, kMaxVertices colors // Set up to draw rects displayListIndex = 0; vertices = glArrayVertices; glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); colors = glArrayColors; glEnableClientState(GL_COLOR_ARRAY); glColorPointer(4, GL_FLOAT, 0, glArrayColors); // First we outline all individuals if (dimensionality == 1) srandom(controller->community->Tick()); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... Individual &individual = *subpop->parent_individuals_[individualArrayIndex]; float position_x, position_y; if (dimensionality == 1) { position_x = (float)((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = (float)(random() / (double)INT32_MAX); if ((position_x < 0.0) || (position_x > 1.0)) // skip points that are out of bounds continue; } else { position_x = (float)((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = (float)((individual.spatial_y_ - bounds_y0) / bounds_y_size); if ((position_x < 0.0) || (position_x > 1.0) || (position_y < 0.0) || (position_y > 1.0)) // skip points that are out of bounds continue; } float centerX = (float)(individualArea.origin.x + round(position_x * individualArea.size.width) + 0.5); float centerY = (float)(individualArea.origin.y + individualArea.size.height - round(position_y * individualArea.size.height) + 0.5); float left = centerX - 2.5f; float top = centerY - 2.5f; float right = centerX + 2.5f; float bottom = centerY + 2.5f; if (left < individualArea.origin.x) left = (float)individualArea.origin.x; if (top < individualArea.origin.y) top = (float)individualArea.origin.y; if (right > individualArea.origin.x + individualArea.size.width + 1) right = (float)(individualArea.origin.x + individualArea.size.width + 1); if (bottom > individualArea.origin.y + individualArea.size.height + 1) bottom = (float)(individualArea.origin.y + individualArea.size.height + 1); *(vertices++) = left; *(vertices++) = top; *(vertices++) = left; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = top; for (int j = 0; j < 4; ++j) { *(colors++) = 0.25; *(colors++) = 0.25; *(colors++) = 0.25; *(colors++) = 1.0; } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } } // Then we draw all individuals if (dimensionality == 1) srandom(controller->community->Tick()); for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) { // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... Individual &individual = *subpop->parent_individuals_[individualArrayIndex]; float position_x, position_y; if (dimensionality == 1) { position_x = (float)((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = (float)(random() / (double)INT32_MAX); if ((position_x < 0.0) || (position_x > 1.0)) // skip points that are out of bounds continue; } else { position_x = (float)((individual.spatial_x_ - bounds_x0) / bounds_x_size); position_y = (float)((individual.spatial_y_ - bounds_y0) / bounds_y_size); if ((position_x < 0.0) || (position_x > 1.0) || (position_y < 0.0) || (position_y > 1.0)) // skip points that are out of bounds continue; } float centerX = (float)(individualArea.origin.x + round(position_x * individualArea.size.width) + 0.5); float centerY = (float)(individualArea.origin.y + individualArea.size.height - round(position_y * individualArea.size.height) + 0.5); float left = centerX - 1.5f; float top = centerY - 1.5f; float right = centerX + 1.5f; float bottom = centerY + 1.5f; // clipping deliberately not done here; because individual rects are 3x3, they will fall at most one pixel // outside our drawing area, and thus the flaw will be covered by the view frame when it overdraws *(vertices++) = left; *(vertices++) = top; *(vertices++) = left; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = bottom; *(vertices++) = right; *(vertices++) = top; // dark gray default, for a fitness of NaN; should never happen float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; if (Individual::s_any_individual_color_set_ && individual.color_set_) { colorRed = individual.colorR_ / 255.0F; colorGreen = individual.colorG_ / 255.0F; colorBlue = individual.colorB_ / 255.0F; } else { // use individual trait values to determine color; we used fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks // we normalize fitness values with subpopFitnessScaling so individual fitness, unscaled by subpopulation fitness, is used for coloring double fitness = individual.cached_unscaled_fitness_; if (!std::isnan(fitness)) RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); } for (int j = 0; j < 4; ++j) { *(colors++) = colorRed; *(colors++) = colorGreen; *(colors++) = colorBlue; *(colors++) = colorAlpha; } displayListIndex++; // If we've filled our buffers, get ready to draw more if (displayListIndex == kMaxGLRects) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); // And get ready to draw more vertices = glArrayVertices; colors = glArrayColors; displayListIndex = 0; } } // Draw any leftovers if (displayListIndex) { // Draw our arrays glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); } glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); } - (void)drawRect:(NSRect)rect { // // NOTE this code is parallel to code in tileSubpopulations: and both should be maintained! // NSRect bounds = [self bounds]; SLiMWindowController *controller = [[self window] windowController]; Species *displaySpecies = [controller focalDisplaySpecies]; std::vector selectedSubpopulations = [controller selectedSubpopulations]; int selectedSubpopCount = (int)(selectedSubpopulations.size()); // Decide on our display mode if (displaySpecies && (controller->community->Tick() >= 1)) { if (displayMode == -1) displayMode = ((displaySpecies->spatial_dimensionality_ == 0) ? 0 : 1); if ((displayMode == 1) && (displaySpecies->spatial_dimensionality_ == 0)) displayMode = 0; } // Update the viewport; using backingBounds here instead of bounds makes the view hi-res-aware, // while still remaining point-based since the ortho projection we use below uses bounds. NSRect backingBounds = [self convertRectToBacking:bounds]; glViewport(0, 0, (int)backingBounds.size.width, (int)backingBounds.size.height); // Update the projection glMatrixMode(GL_PROJECTION); GLKMatrix4 orthoMat = GLKMatrix4MakeOrtho(0.0, (int)bounds.size.width, (int)bounds.size.height, 0.0, -1.0f, 1.0f); glLoadMatrixf(orthoMat.m); glMatrixMode(GL_MODELVIEW); if (selectedSubpopCount == 0) { // clear to a shade of gray glColor3f(0.9f, 0.9f, 0.9f); glRecti(0, 0, (int)bounds.size.width, (int)bounds.size.height); // Frame our view [self drawViewFrameInBounds:bounds]; } else if (selectedSubpopCount > 10) { // We should be hidden in this case, but just in case, let's draw... // clear to our "too much information" shade glColor3f(0.9f, 0.9f, 1.0f); glRecti(0, 0, (int)bounds.size.width, (int)bounds.size.height); // Frame our view [self drawViewFrameInBounds:bounds]; } else { if (selectedSubpopulations.size() > 1) { // Clear to background gray; FIXME at present this hard-codes the OS X 10.10 background color... glColor3f(0.93f, 0.93f, 0.93f); glRecti(0, 0, (int)bounds.size.width, (int)bounds.size.height); } for (Subpopulation *subpop : selectedSubpopulations) { auto tileIter = subpopTiles.find(subpop->subpopulation_id_); if (tileIter != subpopTiles.end()) { NSRect tileBounds = tileIter->second; if ((displayMode == 1) && (displaySpecies->spatial_dimensionality_ == 1)) { [self drawSpatialBackgroundInBounds:tileBounds forSubpopulation:subpop dimensionality:displaySpecies->spatial_dimensionality_]; [self drawSpatialIndividualsFromSubpopulation:subpop inArea:NSInsetRect(tileBounds, 1, 1) dimensionality:displaySpecies->spatial_dimensionality_]; [self drawViewFrameInBounds:tileBounds]; } else if ((displayMode == 1) && (displaySpecies->spatial_dimensionality_ > 1)) { // clear to a shade of gray glColor3f(0.9f, 0.9f, 0.9f); glRecti((int)tileBounds.origin.x, (int)tileBounds.origin.y, (int)(tileBounds.origin.x + tileBounds.size.width), (int)(tileBounds.origin.y + tileBounds.size.height)); // Frame our view [self drawViewFrameInBounds:tileBounds]; // Now determine a subframe and draw spatial information inside that. NSRect spatialDisplayBounds = [self spatialDisplayBoundsForSubpopulation:subpop tileBounds:tileBounds]; [self drawSpatialBackgroundInBounds:spatialDisplayBounds forSubpopulation:subpop dimensionality:displaySpecies->spatial_dimensionality_]; [self drawSpatialIndividualsFromSubpopulation:subpop inArea:spatialDisplayBounds dimensionality:displaySpecies->spatial_dimensionality_]; [self drawViewFrameInBounds:NSInsetRect(spatialDisplayBounds, -1, -1)]; } else // displayMode == 0 { // Clear to white glColor3f(1.0, 1.0, 1.0); glRecti((int)tileBounds.origin.x, (int)tileBounds.origin.y, (int)(tileBounds.origin.x + tileBounds.size.width), (int)(tileBounds.origin.y + tileBounds.size.height)); [self drawViewFrameInBounds:tileBounds]; [self drawIndividualsFromSubpopulation:subpop inArea:tileBounds]; } } } } [[self openGLContext] flushBuffer]; } // // Subarea tiling // #pragma mark Subarea tiling - (BOOL)tileSubpopulations:(std::vector &)selectedSubpopulations { // // NOTE this code is parallel to code in drawRect: and both should be maintained! // // We will decide upon new tiles for our subpopulations here, so start out empty subpopTiles.clear(); NSRect bounds = [self bounds]; SLiMWindowController *controller = [[self window] windowController]; Species *displaySpecies = [controller focalDisplaySpecies]; int selectedSubpopCount = (int)selectedSubpopulations.size(); // Decide on our display mode if (displaySpecies && (controller->community->Tick() >= 1)) { if (displayMode == -1) displayMode = ((displaySpecies->spatial_dimensionality_ == 0) ? 0 : 1); if ((displayMode == 1) && (displaySpecies->spatial_dimensionality_ == 0)) displayMode = 0; } if (selectedSubpopCount == 0) { return YES; } else if (selectedSubpopCount > 10) { return NO; } else if (selectedSubpopCount == 1) { Subpopulation *selectedSubpop = selectedSubpopulations[0]; subpopTiles.emplace(selectedSubpop->subpopulation_id_, bounds); if ((displayMode == 1) && (displaySpecies->spatial_dimensionality_ == 1)) { return YES; } else if ((displayMode == 1) && (displaySpecies->spatial_dimensionality_ > 1)) { return YES; } else { return [self canDisplayIndividualsFromSubpopulation:selectedSubpop inArea:bounds]; } } else if (displayMode == 1) { // spatial display adaptively finds the layout the maximizes the pixel area covered, and cannot fail int64_t bestTotalExtent = 0; for (int rowCount = 1; rowCount <= selectedSubpopCount; ++rowCount) { int columnCount = (int)ceil(selectedSubpopCount / (double)rowCount); int interBoxSpace = 5; int totalInterboxHeight = interBoxSpace * (rowCount - 1); int totalInterboxWidth = interBoxSpace * (columnCount - 1); double boxWidth = (bounds.size.width - totalInterboxWidth) / (double)columnCount; double boxHeight = (bounds.size.height - totalInterboxHeight) / (double)rowCount; std::map candidateTiles; int64_t totalExtent = 0; for (int subpopIndex = 0; subpopIndex < selectedSubpopCount; subpopIndex++) { int columnIndex = subpopIndex % columnCount; int rowIndex = subpopIndex / columnCount; double boxLeft = round(bounds.origin.x + columnIndex * (interBoxSpace + boxWidth)); double boxRight = round(bounds.origin.x + columnIndex * (interBoxSpace + boxWidth) + boxWidth); double boxTop = round(bounds.origin.y + rowIndex * (interBoxSpace + boxHeight)); double boxBottom = round(bounds.origin.y + rowIndex * (interBoxSpace + boxHeight) + boxHeight); NSRect boxBounds = NSMakeRect(boxLeft, boxTop, boxRight - boxLeft, boxBottom - boxTop); Subpopulation *subpop = selectedSubpopulations[subpopIndex]; candidateTiles.emplace(subpop->subpopulation_id_, boxBounds); // find out what pixel area actually gets used by this box, and use that to choose the optimal layout NSRect spatialDisplayBounds = [self spatialDisplayBoundsForSubpopulation:subpop tileBounds:boxBounds]; int64_t extent = (int64_t)spatialDisplayBounds.size.width * (int64_t)spatialDisplayBounds.size.height; totalExtent += extent; } if (totalExtent > bestTotalExtent) { bestTotalExtent = totalExtent; std::swap(subpopTiles, candidateTiles); } } return YES; } else // displayMode == 0 { // non-spatial display always uses vertically stacked maximum-width tiles, but can fail if they are too small int interBoxSpace = 5; int totalInterbox = interBoxSpace * (selectedSubpopCount - 1); double boxHeight = (bounds.size.height - totalInterbox) / (double)selectedSubpopCount; for (int subpopIndex = 0; subpopIndex < selectedSubpopCount; subpopIndex++) { double boxTop = round(bounds.origin.y + subpopIndex * (interBoxSpace + boxHeight)); double boxBottom = round(bounds.origin.y + subpopIndex * (interBoxSpace + boxHeight) + boxHeight); NSRect boxBounds = NSMakeRect(bounds.origin.x, boxTop, bounds.size.width, boxBottom - boxTop); Subpopulation *subpop = selectedSubpopulations[subpopIndex]; subpopTiles.emplace(subpop->subpopulation_id_, boxBounds); if (![self canDisplayIndividualsFromSubpopulation:subpop inArea:boxBounds]) { subpopTiles.clear(); return NO; } } return YES; } } - (BOOL)canDisplayIndividualsFromSubpopulation:(Subpopulation *)subpop inArea:(NSRect)bounds { // // NOTE this code is parallel to the code in drawIndividualsFromSubpopulation:inArea: and should be maintained in parallel // slim_popsize_t subpopSize = subpop->parent_subpop_size_; int squareSize, viewColumns = 0, viewRows = 0; // first figure out the biggest square size that will allow us to display the whole subpopulation for (squareSize = 20; squareSize > 1; --squareSize) { viewColumns = (int)floor((bounds.size.width - 3) / squareSize); viewRows = (int)floor((bounds.size.height - 3) / squareSize); if (viewColumns * viewRows > subpopSize) { // If we have an empty row at the bottom, then break for sure; this allows us to look nice and symmetrical if ((subpopSize - 1) / viewColumns < viewRows - 1) break; // Otherwise, break only if we are getting uncomfortably small; otherwise, let's drop down one square size to allow symmetry if (squareSize <= 5) break; } } return (squareSize > 1); } - (NSRect)spatialDisplayBoundsForSubpopulation:(Subpopulation *)subpop tileBounds:(NSRect)tileBounds { // Determine a subframe for drawing spatial information inside. The subframe we use is the maximal subframe // with integer boundaries that preserves, as closely as possible, the aspect ratio of the subpop's bounds. NSRect spatialDisplayBounds = NSInsetRect(tileBounds, 1, 1); double displayAspect = spatialDisplayBounds.size.width / spatialDisplayBounds.size.height; double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; double subpopAspect = bounds_x_size / bounds_y_size; if (subpopAspect > displayAspect) { // The display bounds will need to shrink vertically to match the subpop double idealSize = round(spatialDisplayBounds.size.width / subpopAspect); double roundedOffset = round((spatialDisplayBounds.size.height - idealSize) / 2.0); spatialDisplayBounds.origin.y += roundedOffset; spatialDisplayBounds.size.height = idealSize; } else if (subpopAspect < displayAspect) { // The display bounds will need to shrink horizontally to match the subpop double idealSize = round(spatialDisplayBounds.size.height * subpopAspect); double roundedOffset = round((spatialDisplayBounds.size.width - idealSize) / 2.0); spatialDisplayBounds.origin.x += roundedOffset; spatialDisplayBounds.size.width = idealSize; } return spatialDisplayBounds; } // // Actions // #pragma mark Actions - (IBAction)setDisplayStyle:(id)sender { NSMenuItem *senderMenuItem = (NSMenuItem *)sender; displayMode = (int)[senderMenuItem tag]; [self setNeedsDisplay:YES]; [[[self window] windowController] updatePopulationViewHiding]; } - (IBAction)setDisplayBackground:(id)sender { NSMenuItem *senderMenuItem = (NSMenuItem *)sender; int newDisplayBackground = (int)([senderMenuItem tag] - 10); auto backgroundIter = backgroundSettings.find(lastContextMenuSubpopID); PopulationViewBackgroundSettings *background = ((backgroundIter == backgroundSettings.end()) ? nil : &backgroundIter->second); std::string mapName; // If the user has selected a spatial map, extract its name if (newDisplayBackground == 3) { NSString *menuItemTitle = [senderMenuItem title]; NSArray *parts = [menuItemTitle componentsSeparatedByString:@"\""]; if ([parts count] == 5) { NSString *mapNameString = [parts objectAtIndex:1]; const char *mapNameString_inner = [mapNameString UTF8String]; mapName = std::string(mapNameString_inner); } if (mapName.length() == 0) return; } if (background) { background->backgroundType = newDisplayBackground; background->spatialMapName = mapName; [self setNeedsDisplay:YES]; } else { backgroundSettings.emplace(lastContextMenuSubpopID, PopulationViewBackgroundSettings{newDisplayBackground, mapName}); [self setNeedsDisplay:YES]; } } - (NSMenu *)menuForEvent:(NSEvent *)theEvent { SLiMWindowController *controller = [[self window] windowController]; Species *displaySpecies = [controller focalDisplaySpecies]; bool disableAll = false; // When the simulation is not valid and initialized, the context menu is disabled if (!displaySpecies || (controller->community->Tick() < 1)) disableAll = true; NSMenu *menu = [[NSMenu alloc] initWithTitle:@"population_menu"]; NSMenuItem *menuItem; [menu setAutoenablesItems:NO]; menuItem = [menu addItemWithTitle:@"Display Individuals (non-spatial)" action:@selector(setDisplayStyle:) keyEquivalent:@""]; [menuItem setTag:0]; [menuItem setTarget:self]; [menuItem setEnabled:!disableAll]; menuItem = [menu addItemWithTitle:@"Display Individuals (spatial)" action:@selector(setDisplayStyle:) keyEquivalent:@""]; [menuItem setTag:1]; [menuItem setTarget:self]; [menuItem setEnabled:(!disableAll && (displaySpecies->spatial_dimensionality_ > 0))]; menuItem = [menu addItemWithTitle:@"Display Fitness Line Plot (per subpopulation)..." action:@selector(setDisplayStyle:) keyEquivalent:@""]; [menuItem setTag:2]; [menuItem setTarget:self]; [menuItem setEnabled:!disableAll]; menuItem = [menu addItemWithTitle:@"Display Fitness Bar Plot (aggregated)..." action:@selector(setDisplayStyle:) keyEquivalent:@""]; [menuItem setTag:3]; [menuItem setTarget:self]; [menuItem setEnabled:!disableAll]; // Check the item corresponding to our current display preference, if any if (!disableAll) { menuItem = [menu itemWithTag:displayMode]; [menuItem setState:NSOnState]; } // If we're displaying spatially, provide background options (colors, spatial maps) if (!disableAll && (displaySpecies->spatial_dimensionality_ > 0) && (displayMode == 1)) { // determine which subpopulation the click was in std::vector selectedSubpopulations = [controller selectedSubpopulations]; Subpopulation *subpopForEvent = nullptr; NSPoint eventPoint = [theEvent locationInWindow]; NSPoint viewPoint = [self convertPoint:eventPoint fromView:nil]; // our tile coordinates are in the OpenGL coordinate system, which has the origin at top left viewPoint.y = [self bounds].size.height - viewPoint.y; for (Subpopulation *subpop : selectedSubpopulations) { slim_objectid_t subpop_id = subpop->subpopulation_id_; auto tileIter = subpopTiles.find(subpop_id); if (tileIter != subpopTiles.end()) { NSRect tileRect = tileIter->second; if (NSPointInRect(viewPoint, tileRect)) { subpopForEvent = subpop; break; } } } if (subpopForEvent) { [menu addItem:[NSMenuItem separatorItem]]; menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"Background for p%d:", subpopForEvent->subpopulation_id_] action:NULL keyEquivalent:@""]; [menuItem setTag:-1]; [menuItem setEnabled:false]; menuItem = [menu addItemWithTitle:@"Black Background" action:@selector(setDisplayBackground:) keyEquivalent:@""]; [menuItem setTag:10]; [menuItem setTarget:self]; [menuItem setEnabled:!disableAll]; menuItem = [menu addItemWithTitle:@"Gray Background" action:@selector(setDisplayBackground:) keyEquivalent:@""]; [menuItem setTag:11]; [menuItem setTarget:self]; [menuItem setEnabled:!disableAll]; menuItem = [menu addItemWithTitle:@"White Background" action:@selector(setDisplayBackground:) keyEquivalent:@""]; [menuItem setTag:12]; [menuItem setTarget:self]; [menuItem setEnabled:!disableAll]; // look for spatial maps to offer as choices; need to scan the defined maps for the ones we can use SpatialMapMap &spatial_maps = subpopForEvent->spatial_maps_; for (const auto &map_pair : spatial_maps) { SpatialMap *map = map_pair.second; // We used to display only maps with a color scale; now we just make up a color scale if none is given. Only // "x", "y", and "xy" maps are considered displayable; We can't display a z coordinate, and we can't display // even the x or y portion of "xz", "yz", and "xyz" maps since we don't know which z-slice to use. bool displayable = ((map->spatiality_string_ == "x") || (map->spatiality_string_ == "y") || (map->spatiality_string_ == "xy")); NSString *menuItemTitle; if (map->spatiality_ == 1) menuItemTitle = [NSString stringWithFormat:@"Spatial Map \"%s\" (\"%s\", %d)", map_pair.first.c_str(), map->spatiality_string_.c_str(), (int)map->grid_size_[0]]; else if (map->spatiality_ == 2) menuItemTitle = [NSString stringWithFormat:@"Spatial Map \"%s\" (\"%s\", %d×%d)", map_pair.first.c_str(), map->spatiality_string_.c_str(), (int)map->grid_size_[0], (int)map->grid_size_[1]]; else // (map->spatiality_ == 3) menuItemTitle = [NSString stringWithFormat:@"Spatial Map \"%s\" (\"%s\", %d×%d×%d)", map_pair.first.c_str(), map->spatiality_string_.c_str(), (int)map->grid_size_[0], (int)map->grid_size_[1], (int)map->grid_size_[2]]; menuItem = [menu addItemWithTitle:menuItemTitle action:@selector(setDisplayBackground:) keyEquivalent:@""]; [menuItem setTag:13]; [menuItem setTarget:self]; [menuItem setEnabled:(!disableAll && displayable)]; } // check the menu item for the preferred display option; if we're in auto mode, don't check anything (could put a dash by the currently chosen style?) auto backgroundIter = backgroundSettings.find(subpopForEvent->subpopulation_id_); PopulationViewBackgroundSettings *background = ((backgroundIter == backgroundSettings.end()) ? nil : &backgroundIter->second); if (background) { menuItem = nil; if (background->backgroundType == 3) { // We have to find the menu item to check by scanning and looking for the right title NSString *chosenMapPrefix = [NSString stringWithFormat:@"Spatial Map \"%s\" (\"", background->spatialMapName.c_str()]; NSArray *menuItems = [menu itemArray]; for (NSMenuItem *item : menuItems) { if ([[item title] hasPrefix:chosenMapPrefix]) { menuItem = item; break; } } } else { // We can find the menu item to check by tag menuItem = [menu itemWithTag:background->backgroundType + 10]; } [menuItem setState:NSOnState]; } // remember which subpopulation this context menu is for lastContextMenuSubpopID = subpopForEvent->subpopulation_id_; } } return [menu autorelease]; } @end @implementation PopulationErrorView - (void)drawMessage:(NSString *)messageString inRect:(NSRect)rect { static NSDictionary *attrs = nil; if (!attrs) attrs = [@{NSFontAttributeName : [NSFont fontWithName:@"Times New Roman" size:14], NSForegroundColorAttributeName : [NSColor colorWithCalibratedWhite:0.4 alpha:1.0]} retain]; NSAttributedString *attrMessage = [[NSAttributedString alloc] initWithString:messageString attributes:attrs]; NSPoint centerPoint = NSMakePoint(rect.origin.x + rect.size.width / 2, rect.origin.y + rect.size.height / 2); NSSize messageSize = [attrMessage size]; NSPoint drawPoint = NSMakePoint(centerPoint.x - messageSize.width / 2.0, centerPoint.y - messageSize.height / 2.0); [attrMessage drawAtPoint:drawPoint]; [attrMessage release]; } - (void)drawRect:(NSRect)rect { NSRect bounds = [self bounds]; // Erase background [[NSColor colorWithDeviceWhite:0.9 alpha:1.0] set]; NSRectFill(bounds); // Frame the view [[NSColor colorWithDeviceWhite:0.77 alpha:1.0] set]; NSFrameRect(bounds); // Draw the message [self drawMessage:@"too many subpops\n or individuals\n to display –\n control-click to\n select a different\n display mode" inRect:NSInsetRect(bounds, 1, 1)]; } - (BOOL)isOpaque { return YES; } - (NSMenu *)menuForEvent:(NSEvent *)theEvent { SLiMWindowController *controller = [[self window] windowController]; PopulationView *popView = controller->populationView; return [popView menuForEvent:theEvent]; } @end ================================================ FILE: SLiMgui/Recipes/Recipe 10.1 - Temporally varying selection.txt ================================================ // Keywords: environmental change initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // beneficial initializeGenomicElementType("g1", c(m1,m2), c(0.995,0.005)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 2000:3999 mutationEffect(m2) { return 1.0; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 10.2 - Spatially varying selection.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "e", 0.1); // deleterious in p2 initializeGenomicElementType("g1", c(m1,m2), c(0.99,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); p1.setMigrationRates(p2, 0.1); // weak migration p2 -> p1 p2.setMigrationRates(p1, 0.5); // strong migration p1 -> p2 } mutationEffect(m2, p2) { return 1/effect; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis I.txt ================================================ // Keywords: gene interactions initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // epistatic mut 1 initializeMutationType("m3", 0.5, "f", 0.1); // epistatic mut 2 initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); // epistatic locus 1 initializeGenomicElementType("g3", m3, 1); // epistatic locus 2 initializeGenomicElement(g1, 0, 10000); initializeGenomicElement(g2, 10001, 13000); initializeGenomicElement(g1, 13001, 70000); initializeGenomicElement(g3, 70001, 73000); initializeGenomicElement(g1, 73001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m3) { if (individual.countOfMutationsOfType(m2)) return 0.5; else return effect; } ================================================ FILE: SLiMgui/Recipes/Recipe 10.3.1 - Fitness as a function of genomic background, Epistasis II.txt ================================================ // Keywords: gene interactions initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // epistatic mut 1 m2.convertToSubstitution = F; initializeMutationType("m3", 0.5, "f", 0.1); // epistatic mut 2 m3.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); // epistatic locus 1 initializeGenomicElementType("g3", m3, 1); // epistatic locus 2 initializeGenomicElement(g1, 0, 10000); initializeGenomicElement(g2, 10001, 13000); initializeGenomicElement(g1, 13001, 70000); initializeGenomicElement(g3, 70001, 73000); initializeGenomicElement(g1, 73001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m3) { if (individual.countOfMutationsOfType(m2)) return 0.5; else return effect; } ================================================ FILE: SLiMgui/Recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection I.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // balanced initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m2) { return 1.5 - sim.mutationFrequencies(p1, mut); } ================================================ FILE: SLiMgui/Recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection II.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // positive freq. dep. initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m2) { return 1.0 + sim.mutationFrequencies(p1, mut); } ================================================ FILE: SLiMgui/Recipes/Recipe 10.4.1 - Fitness as a function of population composition, Frequency-dependent selection III.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // positive freq. dep. initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } mutationEffect(m2) { dominance = asInteger(homozygous) * 0.5 + 0.5; return 1.0 + sim.mutationFrequencies(p1, mut) * dominance; } ================================================ FILE: SLiMgui/Recipes/Recipe 10.4.2 - Fitness as a function of population composition, Cultural effects on fitness.txt ================================================ // Keywords: culture, non-genetic inheritance initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // lactase-promoting m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(0.99,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 10000 early() { sim.simulationFinished(); } late() { // Assign a cultural group: milk-drinker == T, non-milk-drinker == F p1.individuals.tagL0 = (runif(1000) < 0.5); } mutationEffect(m2) { if (individual.tagL0) return effect; // beneficial for milk-drinkers else return 1.0; // neutral for non-milk-drinkers } ================================================ FILE: SLiMgui/Recipes/Recipe 10.4.3 - Fitness as a function of population composition, Kin selection and the green-beard effect.txt ================================================ // Keywords: kin selection, inclusive fitness, selfish gene, greenbeard effect initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.0); // green-beard m2.convertToSubstitution = F; m2.color = "red"; initializeGenomicElementType("g1", m1, 1); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1 late() { target = sample(p1.haplosomes, 100); target.addNewDrawnMutation(m2, 10000); } 1: late() { p1.individuals.tag = 0; for (rep in 1:50) { individuals = sample(p1.individuals, 2); i0 = individuals[0]; i1 = individuals[1]; i0greenbeards = i0.countOfMutationsOfType(m2); i1greenbeards = i1.countOfMutationsOfType(m2); if (i0greenbeards & i1greenbeards) { alleleSum = i0greenbeards + i1greenbeards; i0.tag = i0.tag - alleleSum; // cost to i0 i1.tag = i1.tag + alleleSum * 2; // benefit to i1 } } } mutationEffect(m2) { return 1.0 + individual.tag / 10; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 10.5 - Changing selection coefficients with setSelectionCoeff().txt ================================================ // Keywords: environmental change, temporal change initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 1.0, "f", 0.1); // balanced initializeGenomicElementType("g1", c(m1,m2), c(999,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); for (mut in m2muts, freq in freqs) mut.setSelectionCoeff(0.5 - freq); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 10.6 - Varying the dominance coefficient among mutations I.txt ================================================ // Keywords: dominance coefficients, mutation() initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.05); initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } mutation(m2) { mut.setValue("dom", runif(1)); return T; } mutationEffect(m2) { if (homozygous) return 1.0 + mut.selectionCoeff; else return 1.0 + mut.getValue("dom") * mut.selectionCoeff; } 100000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 10.6 - Varying the dominance coefficient among mutations II.txt ================================================ // Keywords: dominance coefficients, mutation() initialize() { initializeMutationRate(1e-7); for (i in 0:10) initializeMutationType(i, i * 0.1, "n", 0.0, 0.02); initializeGenomicElementType("g1", m0, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } mutation(m0) { s = mut.selectionCoeff; d = asInteger(min(floor(abs(s) * 100.0), 10.0)); mut.setMutationType(d); return T; } 100000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 11.1 - Assortative mating.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.setValue("FST", 0.0); sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); p1.setMigrationRates(p2, 0.1); p2.setMigrationRates(p1, 0.1); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } mutationEffect(m2, p2) { return 0.2; } 2000: early() { // tag all individuals with their m2 mutation count inds = sim.subpopulations.individuals; inds.tag = inds.countOfMutationsOfType(m2); // precalculate the mating weights vectors for (subpop in c(p1,p2)) { has_m2 = (subpop.individuals.tag > 0); subpop.setValue("weights1", ifelse(has_m2, 2.0, 1.0)); subpop.setValue("weights2", ifelse(has_m2, 0.5, 1.0)); } } 2000: mateChoice() { if (individual.tag > 0) return weights * sourceSubpop.getValue("weights1"); else return weights * sourceSubpop.getValue("weights2"); } 10000: late() { FST = calcFST(p1.haplosomes, p2.haplosomes); sim.setValue("FST", sim.getValue("FST") + FST); } 19999 late() { cat("Mean FST at equilibrium: " + (sim.getValue("FST") / 10000)); sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 11.2 - Sequential mate search I.txt ================================================ // Keywords: choosy, choosiness, ornament, mate choice initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", -0.025); // ornamental initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1:10001 early() { if (sim.cycle % 1000 == 1) { fixedMuts = sum(sim.substitutions.mutationType == m2); osize = fixedMuts * 2 + p1.individuals.countOfMutationsOfType(m2); catn(sim.cycle + ": Mean ornament size == " + mean(osize)); } } mateChoice() { fixedMuts = sum(sim.substitutions.mutationType == m2); for (attempt in 1:5) { mate = sample(p1.individuals, 1, T, weights); osize = fixedMuts * 2 + mate.countOfMutationsOfType(m2); if (runif(1) < log(osize + 1) * 0.1 + attempt * 0.1) return mate; } return float(0); } ================================================ FILE: SLiMgui/Recipes/Recipe 11.2 - Sequential mate search II.txt ================================================ // Keywords: choosy, choosiness, ornament, mate choice initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", -0.025); // ornamental initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1:10001 early() { fixedMuts = sum(sim.substitutions.mutationType == m2); inds = p1.individuals; osize = fixedMuts * 2 + inds.countOfMutationsOfType(m2); inds.tagF = log(osize + 1) * 0.1; if (sim.cycle % 1000 == 1) catn(sim.cycle + ": Mean ornament size == " + mean(osize)); } mateChoice() { for (attempt in 1:5) { mate = sample(p1.individuals, 1, T, weights); if (runif(1) < mate.tagF + attempt * 0.1) return mate; } return float(0); } ================================================ FILE: SLiMgui/Recipes/Recipe 11.3 - Gametophytic self-incompatibility.txt ================================================ // Keywords: modifyChild(), plants, pollen initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.0); // S-locus mutations initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElementType("g2", m2, 1.0); initializeGenomicElement(g1, 0, 20000); initializeGenomicElement(g2, 20001, 21000); initializeGenomicElement(g1, 21001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { cat("m1 mutation count: " + sim.countOfMutationsOfType(m1) + "\n"); cat("m2 mutation count: " + sim.countOfMutationsOfType(m2) + "\n"); } modifyChild(p1) { pollenSMuts = child.haploidGenome2.mutationsOfType(m2); styleSMuts1 = parent1.haploidGenome1.mutationsOfType(m2); styleSMuts2 = parent1.haploidGenome2.mutationsOfType(m2); if (identical(pollenSMuts, styleSMuts1)) if (runif(1) < 0.99) return F; if (identical(pollenSMuts, styleSMuts2)) if (runif(1) < 0.99) return F; return T; } ================================================ FILE: SLiMgui/Recipes/Recipe 12.1 - Social learning of cultural traits.txt ================================================ // Keywords: non-genetic inheritance initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.1); // lactase-promoting m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(0.99,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); p1.individuals.tagL0 = (runif(1000) < 0.5); } modifyChild() { parentCulture = mean(c(parent1.tagL0, parent2.tagL0)); childCulture = (runif(1) < 0.1 + 0.8 * parentCulture); child.tagL0 = childCulture; return T; } mutationEffect(m2) { if (individual.tagL0) return effect; // beneficial for milk-drinkers else return 1.0; // neutral for non-milk-drinkers } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 12.2 - Lethal epistasis I.txt ================================================ // Keywords: gene interaction initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.5); // mutation A m2.convertToSubstitution = F; initializeMutationType("m3", 0.5, "f", 0.5); // mutation B m3.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1 late() { sample(p1.haplosomes, 20).addNewDrawnMutation(m2, 10000); // add A sample(p1.haplosomes, 20).addNewDrawnMutation(m3, 20000); // add B } modifyChild() { hasMutA = any(child.haplosomes.countOfMutationsOfType(m2) > 0); hasMutB = any(child.haplosomes.countOfMutationsOfType(m3) > 0); if (hasMutA & hasMutB) return F; return T; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 12.2 - Lethal epistasis II.txt ================================================ // Keywords: gene interaction initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.5); // mutation A m2.convertToSubstitution = F; initializeMutationType("m3", 0.5, "f", 0.5); // mutation B m3.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1 late() { sample(p1.haplosomes, 20).addNewDrawnMutation(m2, 10000); // add A sample(p1.haplosomes, 20).addNewDrawnMutation(m3, 20000); // add B } modifyChild() { mutACount = sum(child.haplosomes.countOfMutationsOfType(m2)); mutBCount = sum(child.haplosomes.countOfMutationsOfType(m3)); if ((mutACount == 2) & (mutBCount == 2)) return F; return T; } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 12.3 - Simulating gene drive.txt ================================================ // Keywords: migration, dispersal, CRISPR gene drive initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", -0.1); // MCR complex initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { for (i in 0:5) sim.addSubpop(i, 500); for (i in 1:5) sim.subpopulations[i].setMigrationRates(i-1, 0.001); for (i in 0:4) sim.subpopulations[i].setMigrationRates(i+1, 0.1); } 100 late() { p0.haplosomes[0:49].addNewDrawnMutation(m2, 10000); } 100:10000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = any(sim.substitutions.mutationType == m2); cat(ifelse(fixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } mutationEffect(m2) { return 1.5 - subpop.id * 0.15; } 100:10000 modifyChild() { mut = sim.mutationsOfType(m2); if (size(mut) == 1) { hasMutOnChromosome1 = child.haploidGenome1.containsMutations(mut); hasMutOnChromosome2 = child.haploidGenome2.containsMutations(mut); if (hasMutOnChromosome1 & !hasMutOnChromosome2) child.haploidGenome2.addMutations(mut); else if (hasMutOnChromosome2 & !hasMutOnChromosome1) child.haploidGenome1.addMutations(mut); } return T; } ================================================ FILE: SLiMgui/Recipes/Recipe 12.4 - Suppressing hermaphroditic selfing.txt ================================================ // Keywords: selfing // NOTE: This model is now obsolete! Is it now recommended // that you use this call in your initialize() callback // instead of following this recipe: // // initializeSLiMOptions(preventIncidentalSelfing=T); // // This recipe still works, but is much slower than using // that configuration flag. initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } modifyChild() { // prevent hermaphroditic selfing if (parent1 == parent2) return F; return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 12.5 - Tracking separate sexes in script.txt ================================================ // Keywords: separate sexes, sexual model, sex chromosomes, sex ratio, Wolbachia initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.individuals.tagL0 = repEach(c(F,T), 250); // f==F, m==T } modifyChild() { if (parent1.tagL0 == parent2.tagL0) return F; child.tagL0 = (runif(1) <= 0.5); return T; } 1: late() { catn("Sex ratio (M:M+F): " + mean(p1.individuals.tagL0)); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.1 - Polygenic selection.txt ================================================ // Keywords: quantitative trait, polygenic selection initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.0); // QTLs m2.convertToSubstitution = F; m2.color = "red"; initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); initializeGenomicElement(g1, 0, 20000); initializeGenomicElement(g2, 20001, 30000); initializeGenomicElement(g1, 30001, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } fitnessEffect() { phenotype = individual.countOfMutationsOfType(m2); return 1.5 - (phenotype - 10.0)^2 * 0.005; } 5000 late() { print(sim.mutationFrequencies(NULL, sim.mutationsOfType(m2))); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.2 - A simple model of variable QTL effect sizes.txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.5); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1); initializeGenomicElementType("g2", m2, 1); initializeGenomicElement(g1, 0, 20000); initializeGenomicElement(g2, 20001, 30000); initializeGenomicElement(g1, 30001, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.5 - (phenotypes - 10.0)^2 * 0.005; if (sim.cycle % 100 == 0) catn(sim.cycle + ": Mean phenotype == " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.3 - A model of discrete QTL effects across multiple chromosomes.txt ================================================ // Keywords: migration, dispersal, QTL, quantitative trait loci initialize() { // neutral mutations in non-coding regions initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); // mutations representing alleles in QTLs scriptForQTLs = "if (runif(1) < 0.5) -1; else 1;"; initializeMutationType("m2", 0.5, "s", scriptForQTLs); initializeGenomicElementType("g2", m2, 1.0); m2.convertToSubstitution = F; m2.mutationStackPolicy = "l"; // set up our chromosome: 10 QTLs, surrounded by neutral regions defineConstant("C", 10); // number of QTLs / chromosomes defineConstant("W", 1000); // size of neutral buffer on each side for (i in 1:C) { initializeChromosome(i, W*2 + 1); initializeGenomicElement(g1, 0, W-1); initializeGenomicElement(g2, W, W); initializeGenomicElement(g1, W+1, W+1 + W-1); initializeMutationRate(1e-6); initializeRecombinationRate(1e-8); } } 1 late() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); // set up migration; comment these out for zero gene flow p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p1, 0.01); // optional: give m2 mutations to everyone, as standing variation individuals = sim.subpopulations.individuals; for (i in 1:C) { g = sim.subpopulations.individuals.haplosomesForChromosomes(i); isPlus = asLogical(rbinom(size(g), 1, 0.5)); g[isPlus].addNewMutation(m2, 1.0, W); g[!isPlus].addNewMutation(m2, -1.0, W); } } mutationEffect(m2) { return 1.0; } 1: late() { // evaluate and save the additive effects of QTLs for (subpop in c(p1,p2)) { inds = subpop.individuals; phenotype = inds.sumOfMutationsOfType(m2); optimum = (subpop == p1 ? 10.0 else -10.0); inds.fitnessScaling = 1.0 + dnorm(optimum - phenotype, 0.0, 5.0); inds.tagF = phenotype; } } mateChoice() { phenotype = individual.tagF; others = sourceSubpop.individuals.tagF; return weights * dnorm(others, phenotype, 5.0); } c(2,2001) early() { cat("-------------------------------\n"); cat("Output for end of cycle " + (sim.cycle - 1) + ":\n\n"); // Output population fitness values cat("p1 mean fitness = " + mean(p1.cachedFitness(NULL)) + "\n"); cat("p2 mean fitness = " + mean(p2.cachedFitness(NULL)) + "\n"); // Output population additive QTL-based phenotypes cat("p1 mean phenotype = " + mean(p1.individuals.tagF) + "\n"); cat("p2 mean phenotype = " + mean(p2.individuals.tagF) + "\n"); // Output frequencies of +1/-1 alleles at the QTLs muts = sim.mutationsOfType(m2); plus = muts[muts.selectionCoeff == 1.0]; minus = muts[muts.selectionCoeff == -1.0]; cat("\nOverall frequencies:\n\n"); for (i in 1:C) { iPlus = plus[plus.chromosome.id == i]; iMinus = minus[minus.chromosome.id == i]; pf = sum(sim.mutationFrequencies(NULL, iPlus)); mf = sum(sim.mutationFrequencies(NULL, iMinus)); pf1 = sum(sim.mutationFrequencies(p1, iPlus)); mf1 = sum(sim.mutationFrequencies(p1, iMinus)); pf2 = sum(sim.mutationFrequencies(p2, iPlus)); mf2 = sum(sim.mutationFrequencies(p2, iMinus)); cat(" QTL " + i + ": f(+) == " + pf + ", f(-) == " + mf + "\n"); cat(" in p1: f(+) == " + pf1 + ", f(-) == " + mf1 + "\n"); cat(" in p2: f(+) == " + pf2 + ", f(-) == " + mf2 + "\n\n"); } } ================================================ FILE: SLiMgui/Recipes/Recipe 13.4 - A quantitative genetics model with heritability.txt ================================================ // Keywords: QTLs, quantitative trait loci, heritability, environmental variance, breeding values, additive genetic variance initialize() { defineConstant("h2", 0.1); // target heritability initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 1000); } 1: late() { // sum the additive effects of QTLs inds = sim.subpopulations.individuals; additive = inds.sumOfMutationsOfType(m2); // model environmental variance, according to the target heritability V_A = sd(additive)^2; V_E = (V_A - h2 * V_A) / h2; // from h2 == V_A / (V_A + V_E) env = rnorm(size(inds), 0.0, sqrt(V_E)); // set fitness effects and remember phenotypes phenotypes = additive + env; inds.fitnessScaling = 1.0 + dnorm(10.0 - phenotypes, 0.0, 5.0); inds.tagF = phenotypes; } mutationEffect(m2) { return 1.0; // QTLs are neutral; fitness effects are handled below } 1:100000 late() { if (sim.cycle == 1) cat("Mean phenotype:\n"); meanPhenotype = mean(p1.individuals.tagF); cat(format("%.2f", meanPhenotype)); // Run until we reach the fitness peak if (abs(meanPhenotype - 10.0) > 0.1) { cat(", "); return; } cat("\n\n-------------------------------\n"); cat("QTLs at cycle " + sim.cycle + ":\n\n"); qtls = sim.mutationsOfType(m2); f = sim.mutationFrequencies(NULL, qtls); s = qtls.selectionCoeff; p = qtls.position; o = qtls.originTick; indices = order(f, F); for (i in indices) cat(" " + p[i] + ": s = " + s[i] + ", f == " + f[i] + ", o == " + o[i] + "\n"); sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.5 - A QTL-based model with two quantitative phenotypic traits and pleiotropy.txt ================================================ // Keywords: QTL, quantitative trait loci, pleiotropy, M-matrix, live plotting, mutation() function (void)updatePlot(void) { if (exists("slimgui")) { plot = slimgui.createPlot("Adaptive Walk", xrange=c(-10,30), yrange=c(-30,10), xlab="phenotype 1", ylab="phenotype 2"); plot.points(0, 0, symbol=21, color="red", size=3.0); plot.points(20, -20, symbol=21, color="green", size=3.0); plot.text(0, 4, "start", size=15); plot.text(20, -24, "optimum", size=15); plot.lines(HIST[,0], HIST[,1], "black"); plot.points(HIST[,0], HIST[,1], 16, "black", size=0.5); } } initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", 0.0); // QTLs m2.convertToSubstitution = F; m2.color = "red"; // g1 is a neutral region, g2 is a QTL initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElementType("g2", c(m1,m2), c(1.0, 0.1)); // chromosome of length 100 kb with two QTL regions initializeGenomicElement(g1, 0, 39999); initializeGenomicElement(g2, 40000, 49999); initializeGenomicElement(g1, 50000, 79999); initializeGenomicElement(g2, 80000, 89999); initializeGenomicElement(g1, 90000, 99999); initializeRecombinationRate(1e-8); // QTL-related constants used below defineConstant("QTL_mu", c(0, 0)); defineConstant("QTL_cov", 0.25); defineConstant("QTL_sigma", matrix(c(1,QTL_cov,QTL_cov,1), nrow=2)); defineConstant("QTL_optima", c(20, -20)); catn("\nQTL DFE means: "); print(QTL_mu); catn("\nQTL DFE variance-covariance matrix: "); print(QTL_sigma); } 1 late() { sim.addSubpop("p1", 500); defineGlobal("HIST", matrix(c(0.0, 0.0), nrow=1)); updatePlot(); } mutation(m2) { // draw mutational effects for the new m2 mutation effects = rmvnorm(1, QTL_mu, QTL_sigma); mut.setValue("e0", effects[0]); mut.setValue("e1", effects[1]); // remember all drawn effects, for our final output old_effects = sim.getValue("all_effects"); sim.setValue("all_effects", rbind(old_effects, effects)); return T; } late() { for (ind in sim.subpopulations.individuals) { // construct phenotypes from additive effects of QTL mutations muts = ind.haplosomes.mutationsOfType(m2); phenotype0 = size(muts) ? sum(muts.getValue("e0")) else 0.0; phenotype1 = size(muts) ? sum(muts.getValue("e1")) else 0.0; ind.setValue("phenotype0", phenotype0); ind.setValue("phenotype1", phenotype1); // calculate fitness effects effect0 = 1.0 + dnorm(QTL_optima[0] - phenotype0, 0.0, 20.0) * 10.0; effect1 = 1.0 + dnorm(QTL_optima[1] - phenotype1, 0.0, 20.0) * 10.0; ind.fitnessScaling = effect0 * effect1; } } 1:1000000 late() { // output, run every 1000 cycles if (sim.cycle % 1000 != 0) return; // print final phenotypes versus their optima inds = sim.subpopulations.individuals; p0_mean = mean(inds.getValue("phenotype0")); p1_mean = mean(inds.getValue("phenotype1")); catn(); catn("Cycle: " + sim.cycle); catn("Mean phenotype 0: " + p0_mean + " (" + QTL_optima[0] + ")"); catn("Mean phenotype 1: " + p1_mean + " (" + QTL_optima[1] + ")"); // update our plot defineGlobal("HIST", rbind(HIST, c(p0_mean, p1_mean))); updatePlot(); // keep running until we get within 10% of both optima if ((abs(p0_mean - QTL_optima[0]) > abs(0.1 * QTL_optima[0])) | (abs(p1_mean - QTL_optima[1]) > abs(0.1 * QTL_optima[1]))) return; // we are done with the main adaptive walk; print final output // get the QTL mutations and their frequencies m2muts = sim.mutationsOfType(m2); m2freqs = sim.mutationFrequencies(NULL, m2muts); // sort those vectors by frequency o = order(m2freqs, ascending=F); m2muts = m2muts[o]; m2freqs = m2freqs[o]; // get the effect sizes m2e0 = m2muts.getValue("e0"); m2e1 = m2muts.getValue("e1"); // now output a list of the QTL mutations and their effect sizes catn("\nQTL mutations (f: e0, e1):"); for (i in seqAlong(m2muts)) catn(m2freqs[i] + ": " + m2e0[i] + ", " + m2e1[i]); // output covariances fixed_m2 = m2muts[m2freqs == 1.0]; cov_fixed = cov(fixed_m2.getValue("e0"), fixed_m2.getValue("e1")); effects = sim.getValue("all_effects"); cov_drawn = cov(drop(effects[,0]), drop(effects[,1])); catn("\nCovariance of effects among fixed QTLs: " + cov_fixed); catn("\nCovariance of effects specified by the QTL DFE: " + QTL_cov); catn("\nCovariance of effects across all QTL draws: " + cov_drawn); sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.6 - A variety of fitness functions I (stabilizing selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); scale = dnorm(5.0, 5.0, 2.0); inds.fitnessScaling = 1.0 + dnorm(phenotypes, 5.0, 2.0) / scale; if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.6 - A variety of fitness functions II (directional selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + phenotypes * 0.12; if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.6 - A variety of fitness functions III (disruptive selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); scale = dnorm(0.0, 0.0, 2.0); inds.fitnessScaling = 1.5 - dnorm(phenotypes, 0.0, 2.0) / scale; if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 5000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.6 - A variety of fitness functions IV (truncation selection).txt ================================================ // Keywords: quantitative trait initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 5000); } 10000 late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); cat("Phenotypes: " + mean(phenotypes)); } 10001: late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = ifelse(phenotypes < 0.0, 0.0, 1.0); if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); } 15000 late() { m2muts = sim.mutationsOfType(m2); freqs = sim.mutationFrequencies(NULL, m2muts); effects = m2muts.selectionCoeff; catn(); print(cbind(freqs, effects)); } ================================================ FILE: SLiMgui/Recipes/Recipe 13.7 - Negative frequency-dependence on a quantitative trait.txt ================================================ // Keywords: quantitative trait, negative frequency-dependence, squashed stabilizing selection initialize() { defineConstant("COMP", T); // is competition enabled? initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 0.15); // QTLs m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } mutationEffect(m2) { return 1.0; } 1 early() { sim.addSubpop("p1", 500); cat("Phenotypes: 0"); } 1:5000 late() { inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m2); stabilizing = dnorm(phenotypes, 5.0, 2.0) / dnorm(5.0, 5.0, 2.0); competition = sapply(phenotypes, "sum(dnorm(phenotypes, applyValue, 0.3));"); competition = 1.0 - (competition / size(inds)) / dnorm(0.0, 0.0, 0.3); inds.fitnessScaling = 1.0 + stabilizing * (COMP ? competition else 1.0); if (sim.cycle % 10 == 0) cat(", " + mean(phenotypes)); if (exists("slimgui")) { // plot the relative densities x1 = -2; x2 = 12; step = 0.5; centers = seq(from=x1, to=x2 + step*0.01, by=step); breaks = seq(from=x1 - step/2, to=x2 + step*0.51, by=step); intervals = findInterval(phenotypes, breaks, allInside=T); counts = tabulate(intervals, length(centers) - 1); density = counts / max(counts); plot_pheno = slimgui.createPlot("Phenotypic Distribution", xrange=c(-0.5, 10.5), yrange=c(-0.05, 1.05), xlab="Phenotypic trait value", ylab="Relative density", width=500, height=250); plot_pheno.axis(1, at=c(0,5,10)); plot_pheno.abline(v=5.0, color="cornflowerblue", lwd=2); plot_pheno.lines(centers, density, lwd=2); // plot the fitness function pheno_vals = seq(-2, 12, by=0.1); stabilizing = dnorm(pheno_vals, 5.0, 2.0) / dnorm(5.0, 5.0, 2.0); competition = sapply(pheno_vals, "sum(dnorm(phenotypes, applyValue, 0.3));"); competition = 1.0 - (competition / size(inds)) / dnorm(0.0, 0.0, 0.3); fitness = 1.0 + stabilizing * (COMP ? competition else 1.0); plot_fit = slimgui.createPlot("Fitness Function", xrange=c(-0.5, 10.5), yrange=c(0.95, 2.05), xlab="Phenotypic trait value", ylab="Fitness", width=500, height=250); plot_fit.axis(1, at=c(0,5,10)); plot_fit.abline(v=5.0, color="cornflowerblue", lwd=2); plot_fit.lines(pheno_vals, fitness, lwd=2); } } ================================================ FILE: SLiMgui/Recipes/Recipe 14.1 - Relatedness, inbreeding, and heterozygosity.txt ================================================ // Keywords: initialize() { initializeSLiMOptions(keepPedigrees = T); initializeMutationRate(1e-5); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 100); } mateChoice() { // Prefer relatives as mates return weights * (individual.relatedness(sourceSubpop.individuals) + 0.01); } 1000 late() { // Print mean heterozygosity across the population heterozygosity = calcHeterozygosity(p1.haplosomes); cat("Mean heterozygosity = " + heterozygosity + "\n"); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.10 - Modeling transposable elements.txt ================================================ // Keywords: initialize() { defineConstant("L", 1e6); // chromosome length defineConstant("teInitialCount", 100); // initial number of TEs defineConstant("teJumpP", 0.0001); // TE jump probability defineConstant("teDisableP", 0.00005); // disabling mut probability initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); // transposon mutation type; also neutral, but red initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "#FF0000"; // disabled transposon mutation type; dark red initializeMutationType("m3", 0.5, "f", 0.0); m3.convertToSubstitution = F; m3.color = "#700000"; } 1 late() { sim.addSubpop("p1", 500); sim.tag = 0; // the next unique tag value to use for TEs // create some transposons at random positions haplosomes = sim.subpopulations.haplosomes; positions = rdunif(teInitialCount, 0, L-1); for (teIndex in 0:(teInitialCount-1)) { pos = positions[teIndex]; mut = haplosomes.addNewDrawnMutation(m2, pos); mut.tag = sim.tag; sim.tag = sim.tag + 1; } } modifyChild() { // disable transposons with rate teDisableP for (haplosome in child.haplosomes) { tes = haplosome.mutationsOfType(m2); teCount = tes.size(); mutatedCount = teCount ? rpois(1, teCount * teDisableP) else 0; if (mutatedCount) { mutatedTEs = sample(tes, mutatedCount); for (te in mutatedTEs) { all_disabledTEs = sim.mutationsOfType(m3); disabledTE = all_disabledTEs[all_disabledTEs.tag == te.tag]; if (size(disabledTE)) { // use the existing disabled TE mutation haplosome.removeMutations(te); haplosome.addMutations(disabledTE); next; } // make a new disabled TE mutation with the right tag haplosome.removeMutations(te); disabledTE = haplosome.addNewDrawnMutation(m3, te.position); disabledTE.tag = te.tag; } } } return T; } late() { // make active transposons copy themselves with rate teJumpP for (individual in sim.subpopulations.individuals) { for (haplosome in individual.haplosomes) { tes = haplosome.mutationsOfType(m2); teCount = tes.size(); jumpCount = teCount ? rpois(1, teCount * teJumpP) else 0; if (jumpCount) { jumpTEs = sample(tes, jumpCount); for (te in jumpTEs) { // make a new TE mutation pos = rdunif(1, 0, L-1); jumpTE = haplosome.addNewDrawnMutation(m2, pos); jumpTE.tag = sim.tag; sim.tag = sim.tag + 1; } } } } } 5000 late() { // print information on each TE, including the fraction of it disabled all_tes = sortBy(sim.mutationsOfType(m2), "position"); all_disabledTEs = sortBy(sim.mutationsOfType(m3), "position"); haplosomeCount = size(sim.subpopulations.haplosomes); catn("Active TEs:"); for (te in all_tes) { cat(" TE at " + te.position + ": "); active = sim.mutationCounts(NULL, te); disabledTE = all_disabledTEs[all_disabledTEs.tag == te.tag]; if (size(disabledTE) == 0) { disabled = 0; } else { disabled = sim.mutationCounts(NULL, disabledTE); all_disabledTEs = all_disabledTEs[all_disabledTEs != disabledTE]; } total = active + disabled; cat("frequency " + format("%0.3f", total / haplosomeCount) + ", "); catn(round(active / total * 100) + "% active"); } catn("\nCompletely disabled TEs: "); for (te in all_disabledTEs) { freq = sim.mutationFrequencies(NULL, te); cat(" TE at " + te.position + ": "); catn("frequency " + format("%0.3f", freq)); } } ================================================ FILE: SLiMgui/Recipes/Recipe 14.11 - Modeling opposite ends of a chromosome I.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeGenomicElement(g1, 9900000, 9999999); initializeRecombinationRate(1.5e-7); } 1 early() { sim.addSubpop("p1", 500); } 200000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.11 - Modeling opposite ends of a chromosome II.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeGenomicElement(g1, 100000, 199999); rates = c(1.5e-7, 0.473642, 1.5e-7); ends = c(99999, 100000, 199999); initializeRecombinationRate(rates, ends); } 1 early() { sim.addSubpop("p1", 500); } 200000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.12 - Visualizing ancestry and admixture with mutation() callbacks.txt ================================================ // Keywords: mutation types, tracking, true local ancestry, admixture, introgression, mutation() initialize() { initializeMutationRate(1e-8); // neutral and beneficial for p1 initializeMutationType("m1", 0.5, "f", 0.0); m1.color = "yellow"; m1.colorSubstitution = "yellow"; initializeMutationType("m2", 0.5, "f", 0.1); m2.color = "red"; m2.colorSubstitution = "red"; // neutral and beneficial for p2 initializeMutationType("m3", 0.5, "f", 0.0); m3.color = "blue"; m3.colorSubstitution = "blue"; initializeMutationType("m4", 0.5, "f", 0.1); m4.color = "green"; m4.colorSubstitution = "green"; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.0001)); initializeGenomicElement(g1, 0, 9999999); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 1000); sim.addSubpop("p2", 1000); } mutation(m1, p2) { // use m3 instead of m1, in p2 mut.setMutationType(m3); return T; } mutation(m2, p2) { // use m4 instead of m2, in p2 mut.setMutationType(m4); return T; } 10000 early() { sim.chromosome.setMutationRate(0.0); p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p1, 0.01); } 15000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.13 - Modeling biallelic loci with a mutation() callback I.txt ================================================ // Keywords: identity by state, uniquing, unique down, back mutation, two alleles initialize() { // an m2 mutation is "A" initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "red"; // an m3 mutation is "a" initializeMutationType("m3", 0.5, "f", 0.0); m3.convertToSubstitution = F; m3.color = "cornflowerblue"; // enforce mutually exclusive replacement c(m2,m3).mutationStackGroup = 1; c(m2,m3).mutationStackPolicy = 'l'; initializeGenomicElementType("g1", c(m2,m3), c(1.0,1.0)); initializeGenomicElement(g1, 0, 99); initializeMutationRate(1e-4); initializeRecombinationRate(0.5); } 1 early() { sim.addSubpop("p1", 100); // create the canonical mutation objects target = p1.haplosomes[0]; target.addNewDrawnMutation(m2, 0:99); defineConstant("MUT2", target.mutations); target.removeMutations(); target.addNewDrawnMutation(m3, 0:99); defineConstant("MUT3", target.mutations); target.removeMutations(); // start homozygous "aa" at every position p1.haplosomes.addMutations(MUT3); // log results log = community.createLogFile("freq.csv", logInterval=10); log.addTick(); log.addMeanSDColumns("freq", "sim.mutationFrequencies(NULL, MUT2);"); } mutation(m2) { // unique down to the canonical m2 mutation return MUT2[mut.position]; } mutation(m3) { // unique down to the canonical m3 mutation return MUT3[mut.position]; } 50000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 14.13 - Modeling biallelic loci with a mutation() callback II.txt ================================================ // Keywords: identity by state, uniquing, unique down, back mutation, two alleles initialize() { // m2 models a biallelic locus; an m2 mutation is "A", // absence of an m2 mutation is "a"; "aa" is neutral initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "red"; // m3 is used for new mutations; new m3 mutations get // uniqued down to the correct biallelic m2 state initializeMutationType("m3", 0.5, "f", 0.0); m3.convertToSubstitution = F; m3.color = "cornflowerblue"; initializeGenomicElementType("g1", m3, 1.0); initializeGenomicElement(g1, 0, 99); initializeMutationRate(1e-4); initializeRecombinationRate(0.5); } 1 early() { sim.addSubpop("p1", 100); // create the permanent m2 mutation objects we will use target = p1.haplosomes[0]; target.addNewDrawnMutation(m2, 0:99); defineConstant("MUT", target.mutations); // then remove them; start with "aa" for all individuals target.removeMutations(); // log results log = community.createLogFile("freq.csv", logInterval=10); log.addTick(); log.addMeanSDColumns("freq", "sim.mutationFrequencies(NULL, MUT);"); } mutation(m3) { // if we already have an m2 mutation at the site, allow // the new m3 mutation; we will remove the stack below if (haplosome.containsMarkerMutation(m2, mut.position)) return T; // no m2 mutation is present, so unique down return MUT[mut.position]; } late() { // implement back-mutations from A to a m3muts = sim.mutationsOfType(m3); // do we have any m3 mutations segregating? // if so, we have m2/m3 stacked mutations to remove if (m3muts.length() > 0) { haplosomes = sim.subpopulations.haplosomes; counts = haplosomes.countOfMutationsOfType(m3); hasStacked = haplosomes[counts > 0]; for (haplosome in hasStacked) { stacked_m3 = haplosome.mutationsOfType(m3); stackPositions = stacked_m3.position; all_m2 = haplosome.mutationsOfType(m2); s = (match(all_m2.position, stackPositions) >= 0); stacked_m2 = all_m2[s]; haplosome.removeMutations(c(stacked_m3, stacked_m2)); } } } 50000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 14.14 - Modeling biallelic loci in script.txt ================================================ // Keywords: identity by state, uniquing, unique down, back mutation, two alleles // biallelicFrequencies() is used by the LogFile function (float)biallelicFrequencies(void) { g = NULL; for (ind in p1.individuals) g = rbind(g, ind.getValue('G1'), ind.getValue('G2')); f = apply(g, 1, "mean(applyValue);"); return f; } initialize() { defineConstant("MU", 1e-4); defineConstant("L", 100); } 1 early() { sim.addSubpop("p1", 100); // all individuals start in the "wild-type" state // genomic state is kept in two vectors, G1 and G2 p1.individuals.setValue("G1", rep(F, L)); p1.individuals.setValue("G2", rep(F, L)); // log results log = community.createLogFile("freq.csv", logInterval=10); log.addTick(); log.addMeanSDColumns("freq", "biallelicFrequencies();"); } modifyChild() { // inherit biallelic loci from parents with recombination and mutation parentG1 = parent1.getValue("G1"); parentG2 = parent1.getValue("G2"); recombined = ifelse(rbinom(L, 1, 0.5) == 0, parentG1, parentG2); mutated = ifelse(rbinom(L, 1, MU) == 1, !recombined, recombined); child.setValue("G1", mutated); parentG1 = parent2.getValue("G1"); parentG2 = parent2.getValue("G2"); recombined = ifelse(rbinom(L, 1, 0.5) == 0, parentG1, parentG2); mutated = ifelse(rbinom(L, 1, MU) == 1, !recombined, recombined); child.setValue("G2", mutated); return T; } 50000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 14.15 - Using runs of homozygosity (ROH) to track inbreeding.txt ================================================ // Keywords: runs of homozygosity, ROH, F_ROH, inbreeding, autozygosity initialize() { initializeMutationRate(5e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e9-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); sim.addSubpop("p2", 100); sim.addSubpop("p3", 100); p1.setMigrationRates(p2, 0.1); p2.setMigrationRates(p1, 0.001); p1.setMigrationRates(p3, 0.001); p3.setMigrationRates(p1, 0.1); // start our histories empty for (subpop in c(p1,p2,p3)) { subpop.setValue("FROH_hist", float(0)); subpop.setValue("FROH_hist_tick", integer(0)); } } 500 early() { p2.setSubpopulationSize(20); } 700 early() { p2.setSubpopulationSize(100); } 1400 early() { p3.setSubpopulationSize(20); } 1600 early() { p3.setSubpopulationSize(500); } 100:2000 late() { // in SLiMgui, make a plot every ten ticks if (exists("slimgui") & (community.tick % 10 == 0)) { plot = slimgui.createPlot("Froh history", xrange=c(1, community.estimatedLastTick()), yrange=c(0.0,0.5), xlab="Tick", ylab="Mean Froh", width=500, height=300); plot.addLegend("topRight"); for (subpop in c(p1,p2,p3), color in c("red", "cornflowerblue", "chartreuse3")) { mean_FROH = calcMeanFroh(subpop.individuals); FROH_hist = c(subpop.getValue("FROH_hist"), mean_FROH); FROH_hist_tick = c(subpop.getValue("FROH_hist_tick"), community.tick); subpop.setValue("FROH_hist", FROH_hist); subpop.setValue("FROH_hist_tick", FROH_hist_tick); plot.lines(x=FROH_hist_tick, y=FROH_hist, color=color, lwd=2.0); plot.legendLineEntry("p" + subpop.id, color, lwd=2.0); } } } ================================================ FILE: SLiMgui/Recipes/Recipe 14.16 - Visualizing linkage disequilibrium.txt ================================================ // Keywords: linkage disequilibrium, LD, custom plotting, recombination, calcLD initialize() { setSeed(2180149919968428688); defineConstant("L", 1e7); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.1).color="red"; // used for MUT1 initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 5000 late() { // create the MUT1 mutation that we will track over time mut1 = sample(p1.haplosomes, 1).addNewDrawnMutation(m2, asInteger(L/2)); defineGlobal("MUT1", mut1); } 5000:10000 late() { allMutsMAF = sim.mutations[sim.mutationFrequencies(p1) >= 0.10]; muts = sortBy(allMutsMAF, "position"); if (size(muts) == 0) return; if (!MUT1.isSegregating) { catn("The focal mutation fixed or was lost."); sim.simulationFinished(); return; } ld = calcLD_D(MUT1, muts) * 10; // scale up to make it more visible r = calcLD_Rsquared(MUT1, muts, squared=F); r2 = calcLD_Rsquared(MUT1, muts); p = muts.position; plot = slimgui.createPlot("LD versus R2", xrange=c(0,L-1), yrange=c(-1,1), xlab="tick", ylab="metric", width=800, height=300); plot.abline(v=MUT1.position, color="red", lwd=2); plot.points(p, ld, symbol=16, color="cornflowerblue", size=0.5, alpha=0.1); plot.points(p, r, symbol=16, color="chartreuse3", size=0.5, alpha=0.1); plot.points(p, r2, symbol=16, color="black", size=0.5, alpha=0.1); f = rep(1/201, 201); // running average filter, 201 mutations wide plot.lines(p, filter(ld, f, outside=T), color="cornflowerblue", lwd=2); plot.lines(p, filter(r, f, outside=T), color="chartreuse3", lwd=2); plot.lines(p, filter(r2, f, outside=T), color="black", lwd=2); plot.addLegend("topRight"); plot.legendPointEntry("R2", symbol=16, color="black", size=0.5); plot.legendPointEntry("R", symbol=16, color="chartreuse3", size=0.5); plot.legendPointEntry("LD*10", symbol=16, color="cornflowerblue", size=0.5); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.2 - Mortality-based fitness I.txt ================================================ // Keywords: death, survival initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", -0.005); // deleterious m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } mutationEffect(m2) { // convert fecundity-based selection to survival-based selection if (runif(1) < effect) return 1.0; else return 0.0; } 10000 late() { sim.outputMutations(sim.mutationsOfType(m2)); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.2 - Mortality-based fitness II.txt ================================================ // Keywords: death, survival initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { // initially, everybody lives sim.subpopulations.individuals.tagL0 = F; // here be dragons sample(sim.subpopulations.individuals, 100).tagL0 = T; } fitnessEffect() { // individuals tagged for death die here if (individual.tagL0) return 0.0; else return 1.0; } 10000 late() { sim.outputFull(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.2 - Mortality-based fitness III.txt ================================================ // Keywords: fitness, death, survival initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { // here be dragons sample(sim.subpopulations.individuals, 100).fitnessScaling = 0.0; } 10000 late() { sim.outputFull(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.3 - Reading initial simulation state from an MS output file I.txt ================================================ // Keywords: MS format initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 20000 late() { p1.outputMSSample(2000, replace=F, filePath="ms.txt"); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.3 - Reading initial simulation state from an MS output file II.txt ================================================ // Keywords: MS format initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 1000); // READ MS FORMAT INITIAL STATE lines = readFile("ms.txt"); index = 0; // skip lines until reaching the // line, then skip that line while (lines[index] != "//") index = index + 1; index = index + 1; if (index + 2 + p1.individualCount * 2 > size(lines)) stop("File is too short; terminating."); // next line should be segsites: segsitesLine = lines[index]; index = index + 1; parts = strsplit(segsitesLine); if (size(parts) != 2) stop("Malformed segsites."); if (parts[0] != "segsites:") stop("Missing segsites."); segsites = asInteger(parts[1]); // and next is positions: positionsLine = lines[index]; index = index + 1; parts = strsplit(positionsLine); if (size(parts) != segsites + 1) stop("Malformed positions."); if (parts[0] != "positions:") stop("Missing positions."); positions = asFloat(parts[1:(size(parts)-1)]); // create all mutations in a haplosome in a dummy subpopulation sim.addSubpop("p2", 1); g = p2.haplosomes[0]; L = sim.chromosomes.lastPosition; intPositions = asInteger(round(positions * L)); muts = g.addNewMutation(m1, 0.0, intPositions); // add the appropriate mutations to each haplosome for (g in p1.haplosomes) { f = asLogical(asInteger(strsplit(lines[index], ""))); index = index + 1; g.addMutations(muts[f]); } // remove the dummy subpopulation p2.setSubpopulationSize(0); // (optional) set the tick and cycle to match the save point community.tick = 20000; sim.cycle = 20000; } 30000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.4 - Modeling chromosomal inversions with a recombination() callback.txt ================================================ // Keywords: recombination suppression, inversion, gamete generation, meiosis initialize() { defineConstant("L", 1000000); defineConstant("INV_LENGTH", 500000); defineConstant("INV_START", asInteger(L/2 - INV_LENGTH/2)); defineConstant("INV_END", INV_START + INV_LENGTH - 1); defineConstant("N", 500); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral sites initializeMutationType("m2", 0.5, "f", 0.0); // start marker initializeMutationType("m3", 0.5, "f", 0.0); // end marker c(m2,m3).convertToSubstitution = T; c(m2,m3).color = "red"; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", N); // give some haplosomes an inversion inverted = sample(p1.haplosomes, 100); inverted.addNewDrawnMutation(m2, INV_START); inverted.addNewDrawnMutation(m3, INV_END); } mutationEffect(m2) { // fitness of the inversion is frequency-dependent f = sim.mutationFrequencies(NULL, mut); return 1.0 - (f - 0.5) * 0.2; } 5000 late() { sim.outputFixedMutations(); // Assess fixation inside vs. outside the inversion pos = sim.substitutions.position; catn(sum((pos >= INV_START) & (pos < INV_END)) + " inside."); catn(sum((pos < INV_START) | (pos >= INV_END)) + " outside."); } recombination() { gm1 = haplosome1.containsMarkerMutation(m2, INV_START); gm2 = haplosome2.containsMarkerMutation(m2, INV_START); if (!(gm1 | gm2)) { // homozygote non-inverted return F; } inInv = (breakpoints > INV_START) & (breakpoints <= INV_END); if (sum(inInv) % 2 == 0) { return F; } if (gm1 & gm2) { // homozygote inverted left = (breakpoints == INV_START); right = (breakpoints == INV_END + 1); breakpoints = sort(c(breakpoints[!(left | right)], c(INV_START, INV_END + 1)[c(sum(left) == 0, sum(right) == 0)])); return T; } else { // heterozygote inverted: resample to get an even number of breakpoints // this is *recursive*: it calls this recombination callback again! breakpoints = sim.chromosomes.drawBreakpoints(individual); } return T; } ================================================ FILE: SLiMgui/Recipes/Recipe 14.5 - Estimating model parameters with ABC.txt ================================================ // Keywords: Approximate Bayesian computation, MCMC, parameter estimation initialize() { initializeMutationRate(mu); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 999999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000 late() { cat(sim.mutations.size() + "\n"); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.6 - Tracking local ancestry along the chromosome.txt ================================================ // Keywords: migration, dispersal, admixture, ancestry, introgression initialize() { defineConstant("Z", 1e9); // last chromosome position defineConstant("I", 1e6); // interval between markers initializeMutationType("m1", 0.5, "f", 0.0); // p1 marker initializeMutationType("m2", 0.5, "f", 0.0); // p2 marker initializeMutationType("m3", 0.5, "f", 0.0); // p3 marker initializeMutationType("m4", 0.5, "e", 0.5); // beneficial c(m1,m2,m3,m4).color = c("red", "green", "blue", "white"); c(m1,m2,m3).convertToSubstitution = F; initializeGenomicElementType("g1", m4, 1.0); initializeGenomicElement(g1, 0, Z); initializeMutationRate(0); initializeRecombinationRate(1e-9); } 1 late() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); sim.addSubpop("p3", 500); // set up markers in each subpopulation, and add a beneficial mutation positions = seq(from=0, to=Z, by=I); defineConstant("M", size(positions)); catn("Modeling " + M + " ancestry markers."); for (subpop in c(p1,p2,p3), muttype in c(m1,m2,m3), symbol in c("M1","M2","M3")) { haplosomes = subpop.haplosomes; muts = haplosomes.addNewDrawnMutation(muttype, positions); defineConstant(symbol, muts); mut = haplosomes.addNewDrawnMutation(m4, integerDiv(Z, 2)); catn("Beneficial mutation: s == " + mut.selectionCoeff); } // set up circular migration between the subpops p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p3, 0.01); p3.setMigrationRates(p1, 0.01); } :100000 late() { if (exists("slimgui")) { plot = slimgui.createPlot("Local ancestry", c(0,1), c(0,1), xlab="Position", ylab="Ancestry fraction", width=700, height=250); plot.addLegend(labelSize=14, graphicsWidth=20); plot.legendLineEntry("p1 ancestry", "red", lwd=3); plot.legendLineEntry("p2 ancestry", "green", lwd=3); plot.legendLineEntry("p3 ancestry", "blue", lwd=3); plot.abline(v=0.5, color="black", lwd=2); for (col in c(m1,m2,m3).color, symbol in c("M1","M2","M3")) { mutlist = executeLambda(symbol + ";"); freqs = sim.mutationFrequencies(NULL, mutlist); plot.lines(seq(0, 1, length=size(freqs)), freqs, color=col, lwd=3); plot.abline(h=mean(freqs), color=col); } } if (sim.countOfMutationsOfType(m4) == 0) sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.7 - Live plotting with R using system().txt ================================================ // Keywords: live plotting initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); if (fileExists("/usr/bin/Rscript")) defineConstant("RSCRIPT", "/usr/bin/Rscript"); else if (fileExists("/usr/local/bin/Rscript")) defineConstant("RSCRIPT", "/usr/local/bin/Rscript"); else stop("Couldn't find Rscript."); } 1 early() { sim.addSubpop("p1", 5000); sim.setValue("fixed", NULL); defineConstant("pngPath", writeTempFile("plot_", ".png", "")); // If we're running in SLiMgui, open a plot window if (exists("slimgui")) slimgui.openDocument(pngPath); } 1: early() { if (sim.cycle % 10 == 0) { count = sim.substitutions.size(); sim.setValue("fixed", c(sim.getValue("fixed"), count)); } if (sim.cycle % 1000 != 0) return; y = sim.getValue("fixed"); rstr = paste('{', 'x <- (1:' + size(y) + ') * 10', 'y <- c(' + paste(y, sep=", ") + ')', 'png(width=4, height=4, units="in", res=72, file="' + pngPath + '")', 'par(mar=c(4.0, 4.0, 1.5, 1.5))', 'plot(x=x, y=y, xlim=c(0, 50000), ylim=c(0, 500), type="l",', 'xlab="Generation", ylab="Fixed mutations", cex.axis=0.95,', 'cex.lab=1.2, mgp=c(2.5, 0.7, 0), col="red", lwd=2,', 'xaxp=c(0, 50000, 2))', 'box()', 'dev.off()', '}', sep="\n"); scriptPath = writeTempFile("plot_", ".R", rstr); system(RSCRIPT, args=scriptPath); deleteFile(scriptPath); } 50000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.8 - Using mutation rate variation to model varying functional density.txt ================================================ // Keywords: mutation rate map, mutation map initialize() { initializeMutationType("m1", 0.5, "f", -0.01); // deleterious initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // Use the mutation rate map to vary functional density ends = c(20000, 30000, 70000, 90000, 99999); densities = c(1e-9, 2e-8, 1e-9, 5e-8, 1e-9); initializeMutationRate(densities, ends); } 1 early() { sim.addSubpop("p1", 500); } 200000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 14.9 - Modeling microsatellites.txt ================================================ // Keywords: initialize() { defineConstant("L", 1e6); // chromosome length defineConstant("msatCount", 10); // number of microsats defineConstant("msatMu", 0.0001); // mutation rate per microsat defineConstant("msatUnique", T); // T = unique msats, F = lineages initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); // microsatellite mutation type; also neutral, but magenta initializeMutationType("m2", 0.5, "f", 0.0); m2.convertToSubstitution = F; m2.color = "#900090"; } 1 late() { sim.addSubpop("p1", 500); // create some microsatellites at random positions haplosomes = sim.subpopulations.haplosomes; positions = rdunif(msatCount, 0, L-1); repeats = rpois(msatCount, 20) + 5; for (msatIndex in 0:(msatCount-1)) { pos = positions[msatIndex]; mut = haplosomes.addNewDrawnMutation(m2, pos); mut.tag = repeats[msatIndex]; } // remember the microsat positions for later defineConstant("msatPositions", positions); } modifyChild() { // mutate microsatellites with rate msatMu for (haplosome in child.haplosomes) { mutCount = rpois(1, msatMu * msatCount); if (mutCount) { mutSites = sample(msatPositions, mutCount); msats = haplosome.mutationsOfType(m2); for (mutSite in mutSites) { msat = msats[msats.position == mutSite]; repeats = msat.tag; // modify the number of repeats by adding -1 or +1 repeats = repeats + (rdunif(1, 0, 1) * 2 - 1); if (repeats < 5) next; // if we're uniquing microsats, do so now if (msatUnique) { all_msats = sim.mutationsOfType(m2); msatsAtSite = all_msats[all_msats.position == mutSite]; matchingMut = msatsAtSite[msatsAtSite.tag == repeats]; if (matchingMut.size() == 1) { haplosome.removeMutations(msat); haplosome.addMutations(matchingMut); next; } } // make a new mutation with the new repeat count haplosome.removeMutations(msat); msat = haplosome.addNewDrawnMutation(m2, mutSite); msat.tag = repeats; } } } return T; } 10000 late() { // print frequency information for each microsatellite site all_msats = sim.mutationsOfType(m2); for (pos in sort(msatPositions)) { catn("Microsatellite at " + pos + ":"); msatsAtPos = all_msats[all_msats.position == pos]; for (msat in sortBy(msatsAtPos, "tag")) catn(" variant with " + msat.tag + " repeats: " + sim.mutationFrequencies(NULL, msat)); } } ================================================ FILE: SLiMgui/Recipes/Recipe 15.1 - A minimal nonWF model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.10 - Recording a pedigree.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 10); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // delete any existing pedigree log files deleteFile("mating.txt"); deleteFile("death.txt"); } reproduction() { // choose a mate and generate an offspring mate = subpop.sampleIndividuals(1); child = subpop.addCrossed(individual, mate); child.tag = sim.tag; sim.tag = sim.tag + 1; // log the mating line = paste(community.tick, individual.tag, mate.tag, child.tag); writeFile("mating.txt", line, append=T); } 1 early() { sim.addSubpop("p1", 10); // provide initial tags and remember the next tag value p1.individuals.tag = 1:10; sim.tag = 11; } early() { // density-dependence p1.fitnessScaling = K / p1.individualCount; } survival() { if (!surviving) { // log the death line = community.tick + " " + individual.tag; writeFile("death.txt", line, append=T); } return NULL; } 100 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.11 - Dynamic population structure in nonWF models.txt ================================================ // Keywords: split, join, vicariance, founder, founding, merge, assimilation, admixture initialize() { initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // each subpopulation reproduces within itself subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { // start with two subpops that grow to different sizes sim.addSubpop("p1", 10).setValue("K", 500); sim.addSubpop("p2", 10).setValue("K", 600); } early() { // density-dependent regulation for each subpop for (subpop in sim.subpopulations) { K = subpop.getValue("K"); subpop.fitnessScaling = K / subpop.individualCount; } } 5000 late() { } // // Join p1 and p2 to form p3 in tick 1000 // 999 late() { // create a zero-size subpop for the join sim.addSubpop("p3", 0).setValue("K", 750); } 1000 reproduction() { // generate juveniles to seed p3 founderCount = rdunif(1, 10, 20); p1_inds = p1.individuals; p2_inds = p2.individuals; all_inds = c(p1_inds, p2_inds); for (i in seqLen(founderCount)) { // select a first parent with equal probabilities parent1 = sample(all_inds, 1); // select a second parent with a bias toward p2 if (rdunif(1) < 0.2) parent2 = sample(p1_inds, 1); else parent2 = sample(p2_inds, 1); // generate the offspring into p3 p3.addCrossed(parent1, parent2); } // we're done, don't run again this tick self.active = 0; } 1000 early() { // get rid of p1 and p2 now c(p1,p2).fitnessScaling = 0.0; } // // Split p3 to form a new founder subpop p4 in 2000 // 1999 late() { // create a zero-size subpop for the split sim.addSubpop("p4", 0).setValue("K", 100); } 2000 reproduction() { // generate juveniles to seed p4 founderCount = rdunif(1, 10, 20); all_inds = p3.individuals; for (i in seqLen(founderCount)) { // select parent1/parent2 with equal probabilities parent1 = sample(all_inds, 1); parent2 = sample(all_inds, 1); // generate the offspring into p4 p4.addCrossed(parent1, parent2); } // we're done, don't run again this tick self.active = 0; } ================================================ FILE: SLiMgui/Recipes/Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model I.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, Wright-Fisher, non-overlapping generations, discrete generations initialize() { initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 0.0, "f", -0.5); initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.05)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { K = sim.getValue("K"); // parents are chosen randomly, irrespective of fitness parents1 = p1.sampleIndividuals(K, replace=T); parents2 = p1.sampleIndividuals(K, replace=T); for (i in seqLen(K)) p1.addCrossed(parents1[i], parents2[i]); self.active = 0; } 1 early() { sim.setValue("K", 500); sim.addSubpop("p1", sim.getValue("K")); } early() { // parents die; offspring survive proportional to fitness inds = sim.subpopulations.individuals; inds[inds.age > 0].fitnessScaling = 0.0; } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.12 - Implementing a Wright-Fisher model with a nonWF model II.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, Wright-Fisher, non-overlapping generations, discrete generations initialize() { initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.0, "f", -0.5); c(m1,m2).convertToSubstitution = T; initializeGenomicElementType("g1", c(m1, m2), c(1.0, 0.05)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { K = sim.getValue("K"); // parents are chosen proportional to fitness inds = p1.individuals; fitness = p1.cachedFitness(NULL); parents1 = sample(inds, K, replace=T, weights=fitness); parents2 = sample(inds, K, replace=T, weights=fitness); for (i in seqLen(K)) p1.addCrossed(parents1[i], parents2[i]); self.active = 0; } 1 early() { sim.setValue("K", 500); sim.addSubpop("p1", sim.getValue("K")); } survival() { // survival is independent of fitness; parents die, offspring live return (individual.age == 0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.13 - Range expansion in a stepping-stone model I.txt ================================================ // Keywords: range expansion, colonization, population spread, migration initialize() { defineConstant("K", 1000); // carrying capacity per subpop defineConstant("N", 10); // number of subpopulations defineConstant("M", 0.01); // migration probability defineConstant("R", 1.04); // mean reproduction (as first parent) initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // individuals reproduce locally, without dispersal litterSize = rpois(1, R); for (i in seqLen(litterSize)) { // generate each offspring with an independently drawn mate mate = subpop.sampleIndividuals(1, exclude=individual); if (mate.size()) subpop.addCrossed(individual, mate); } } 1 early() { // create an initial population of 100 individuals, the rest empty for (i in seqLen(N)) sim.addSubpop(i, (i == 0) ? 100 else 0); } 1 late() { // set up a log file log = community.createLogFile("sim_log.txt", sep="\t", logInterval=10); log.addCycle(); log.addPopulationSize(); log.addMeanSDColumns("size", "sim.subpopulations.individualCount;"); log.addCustomColumn("pop_migrants", "sum(sim.subpopulations.individuals.migrant);"); log.addMeanSDColumns("migrants", "sapply(sim.subpopulations, 'sum(applyValue.individuals.migrant);');"); } early() { inds = sim.subpopulations.individuals; // non-overlapping generations; kill off the parental generation ages = inds.age; inds[ages > 0].fitnessScaling = 0.0; inds = inds[ages == 0]; // pre-plan migration of individuals to adjacent subpops numMigrants = rbinom(1, inds.size(), M); if (numMigrants) { migrants = sample(inds, numMigrants); currentSubpopID = migrants.subpopulation.id; displacement = -1 + rbinom(migrants.size(), 1, 0.5) * 2; // -1 or +1 newSubpopID = currentSubpopID + displacement; actuallyMoving = (newSubpopID >= 0) & (newSubpopID < N); if (sum(actuallyMoving)) { migrants = migrants[actuallyMoving]; newSubpopID = newSubpopID[actuallyMoving]; // do the pre-planned moves into each subpop in bulk for (subpop in sim.subpopulations) subpop.takeMigrants(migrants[newSubpopID == subpop.id]); } } // post-migration density-dependent fitness for each subpop for (subpop in sim.subpopulations) { juvenileCount = sum(subpop.individuals.age == 0); subpop.fitnessScaling = K / juvenileCount; } } 1001 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.13 - Range expansion in a stepping-stone model II.txt ================================================ // Keywords: range expansion, colonization, population spread, migration initialize() { defineConstant("K", 1000); // carrying capacity per subpop defineConstant("N", 10); // number of subpopulations defineConstant("M", 0.01); // migration probability defineConstant("R", 1.04); // mean reproduction (as first parent) initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // individuals reproduce locally, without dispersal litterSize = rpois(1, R); for (i in seqLen(litterSize)) { // generate each offspring with an independently drawn mate mate = subpop.sampleIndividuals(1, exclude=individual); if (mate.size()) subpop.addCrossed(individual, mate); } } 1 early() { // create an initial population of 100 individuals, the rest empty for (i in seqLen(N)) sim.addSubpop(i, (i == 0) ? 100 else 0); } 1 late() { // set up a log file log = community.createLogFile("sim_log.txt", sep="\t", logInterval=10); log.addCycle(); log.addPopulationSize(); log.addMeanSDColumns("size", "sim.subpopulations.individualCount;"); log.addCustomColumn("pop_migrants", "sum(sim.subpopulations.individuals.migrant);"); log.addMeanSDColumns("migrants", "sapply(sim.subpopulations, 'sum(applyValue.individuals.migrant);');"); } early() { // non-overlapping generations; kill off the parental generation inds = sim.subpopulations.individuals; sim.killIndividuals(inds[inds.age > 0]); // pre-migration density-dependent fitness for each subpop for (subpop in sim.subpopulations) subpop.fitnessScaling = K / subpop.individualCount; } survival() { // honor SLiM's survival decision if (!surviving) return NULL; // migrate with probability M if (runif(1) >= M) return NULL; // migrate the focal individual to an adjacent subpop subpops = sim.subpopulations; newSubpopID = subpop.id + (-1 + rbinom(1, 1, 0.5) * 2); // -1 or +1 newSubpop = subpops[subpops.id == newSubpopID]; if (newSubpop.size()) return newSubpop; return NULL; } 1001 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.14 - Logistic population growth with the Beverton-Holt model.txt ================================================ // Keywords: logistic growth, logistic population model, carrying capacity, density dependence initialize() { defineConstant("K", 50000); defineConstant("R", 1.1); defineConstant("M", K / (R - 1)); initializeSLiMModelType("nonWF"); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 50); // the "simple model" sim.addSubpop("p2", 50); // Beverton-Holt influencing fecundity sim.addSubpop("p3", 50); // Beverton-Holt influencing survival log = community.createLogFile("sim_log.txt", logInterval=1); log.addCycle(); log.addSubpopulationSize(p1); log.addSubpopulationSize(p2); log.addSubpopulationSize(p3); } reproduction(p1) { // p1 simply reproduces with a mean litter size of R, like p3 litterSize = rpois(1, R); for (i in seqLen(litterSize)) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } reproduction(p2) { // p2 reproduces up to the Beverton-Holt equation's target n_t = subpop.individualCount; n_t_plus_1 = (R * n_t) / (1 + n_t / M); mean_litter_size = n_t_plus_1 / n_t; litterSize = rpois(1, mean_litter_size); for (i in seqLen(litterSize)) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } reproduction(p3) { // p3 simply reproduces with a mean litter size of R, like p1 litterSize = rpois(1, R); for (i in seqLen(litterSize)) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } early() { // p1 uses the "simple model" with non-overlapping generations inds = p1.individuals; inds[inds.age > 0].fitnessScaling = 0.0; n_t_plus_pt5 = sum(inds.age == 0); p1.fitnessScaling = K / n_t_plus_pt5; // p2 has selection only to achieve non-overlapping generations inds = p2.individuals; inds[inds.age > 0].fitnessScaling = 0.0; // p3 uses the Beverton-Holt equation for survival inds = p3.individuals; inds[inds.age > 0].fitnessScaling = 0.0; n_t_plus_pt5 = sum(inds.age == 0); p3.fitnessScaling = 1 / (1 + (n_t_plus_pt5 / R) / M); } 200 late() { // log out the final row before plotting log = community.logFiles; log.logRow(); log.setLogInterval(NULL); // make a final plot if (exists("slimgui")) { cycle_data = slimgui.logFileData(log, "cycle"); p1_data = slimgui.logFileData(log, "p1_num_individuals"); p2_data = slimgui.logFileData(log, "p2_num_individuals"); p3_data = slimgui.logFileData(log, "p3_num_individuals"); plot = slimgui.createPlot("Population Growth", xlab="Generation", ylab="Population size", width=500, height=250); plot.abline(h=50000, color="#999999", lwd=1.0); plot.lines(cycle_data, p1_data, "cornflowerblue", lwd=2); plot.lines(cycle_data, p2_data, "red", lwd=2); plot.lines(cycle_data, p3_data, "chartreuse3", lwd=2); plot.addLegend("topLeft", inset=0, labelSize=13); plot.legendLineEntry("p1", "cornflowerblue", lwd=2); plot.legendLineEntry("p2", "red", lwd=2); plot.legendLineEntry("p3", "chartreuse3", lwd=2); plot.write("Population Growth.pdf"); } } ================================================ FILE: SLiMgui/Recipes/Recipe 15.2 - Age structure (a life table model).txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 30); defineConstant("L", c(0.7, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0)); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { if (individual.age > 2) { mate = subpop.sampleIndividuals(1, minAge=3); subpop.addCrossed(individual, mate); } } 1 early() { sim.addSubpop("p1", 10); p1.individuals.age = rdunif(10, min=0, max=7); } early() { // life table based individual mortality inds = p1.individuals; ages = inds.age; mortality = L[ages]; survival = 1 - mortality; inds.fitnessScaling = survival; // density-dependence, factoring in individual mortality p1.fitnessScaling = K / (p1.individualCount * mean(survival)); } late() { // print our age distribution after mortality catn(sim.cycle + ": " + paste(sort(p1.individuals.age))); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.3 - Handling all reproduction at once with big bang reproduction.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { for (s in sim.subpopulations) { for (ind in s.individuals) { s.addCrossed(ind, s.sampleIndividuals(1)); } } self.active = 0; } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.4 - Monogamous mating and variation in litter size.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // randomize the order of p1.individuals parents = sample(p1.individuals, p1.individualCount); // draw monogamous pairs and generate litters for (i in seq(0, p1.individualCount - 2, by=2)) { parent1 = parents[i]; parent2 = parents[i + 1]; p1.addCrossed(parent1, parent2, count=rpois(1, 2.7)); } // disable this callback for this cycle self.active = 0; } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.5 - Beneficial mutations and absolute fitness.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 1.0, "f", 0.5); // dominant beneficial initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { for (i in 1:5) subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); } 100 early() { mutant = sample(p1.individuals.haplosomes, 10); mutant.addNewDrawnMutation(m2, 10000); } early() { p1.fitnessScaling = K / p1.individualCount; } late() { inds = p1.individuals; catn(sim.cycle + ": " + size(inds) + " (" + max(inds.age) + ")"); } 2000 late() { sim.outputFull(ages=T); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.6 - A metapopulation extinction-colonization model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, migration, dispersal initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 50); // carrying capacity per subpop defineConstant("N", 10); // number of subpopulations defineConstant("m", 0.01); // migration rate defineConstant("e", 0.1); // subpopulation extinction rate initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { for (i in 1:N) sim.addSubpop(i, (i == 1) ? 10 else 0); } early() { // random migration nIndividuals = sum(sim.subpopulations.individualCount); nMigrants = rpois(1, nIndividuals * m); migrants = sample(sim.subpopulations.individuals, nMigrants); for (migrant in migrants) { do dest = sample(sim.subpopulations, 1); while (dest == migrant.subpopulation); dest.takeMigrants(migrant); } // density-dependence and random extinctions for (subpop in sim.subpopulations) { if (runif(1) < e) sim.killIndividuals(subpop.individuals); else subpop.fitnessScaling = K / subpop.individualCount; } } late() { if (sum(sim.subpopulations.individualCount) == 0) stop("Global extinction in cycle " + sim.cycle + "."); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.7 - Habitat choice.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, migration, dispersal initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 0.5, "e", 0.1); // deleterious in p2 m2.color = "red"; initializeMutationType("m3", 0.5, "e", 0.1); // deleterious in p1 m3.color = "green"; initializeGenomicElementType("g1", c(m1,m2,m3), c(0.98,0.01,0.01)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { dest = sample(sim.subpopulations, 1); dest.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); sim.addSubpop("p2", 10); } early() { // habitat choice inds = sim.subpopulations.individuals; inds_m2 = inds.countOfMutationsOfType(m2); inds_m3 = inds.countOfMutationsOfType(m3); pref_p1 = 0.5 + (inds_m2 - inds_m3) * 0.1; pref_p1 = pmax(pmin(pref_p1, 1.0), 0.0); inertia = ifelse(inds.subpopulation.id == 1, 1.0, 0.0); pref_p1 = pref_p1 * 0.75 + inertia * 0.25; choice = ifelse(runif(inds.size()) < pref_p1, 1, 2); moving = inds[choice != inds.subpopulation.id]; from_p1 = moving[moving.subpopulation == p1]; from_p2 = moving[moving.subpopulation == p2]; p2.takeMigrants(from_p1); p1.takeMigrants(from_p2); } early() { p1.fitnessScaling = K / p1.individualCount; p2.fitnessScaling = K / p2.individualCount; } mutationEffect(m2, p2) { return 1/effect; } mutationEffect(m3, p1) { return 1/effect; } 1000 late() { for (id in 1:2) { subpop = sim.subpopulations[sim.subpopulations.id == id]; s = subpop.individualCount; inds = subpop.individuals; c2 = sum(inds.countOfMutationsOfType(m2)); c3 = sum(inds.countOfMutationsOfType(m3)); catn("subpop " + id + " (" + s + "): " + c2 + " m2, " + c3 + " m3"); } } ================================================ FILE: SLiMgui/Recipes/Recipe 15.8 - Evolutionary rescue after environmental change.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, QTL, quantitative trait loci initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); defineConstant("opt1", 0.0); defineConstant("opt2", 10.0); defineConstant("Tdelta", 10000); initializeMutationType("m1", 0.5, "n", 0.0, 1.0); // QTL initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { subpop.addCrossed(individual, subpop.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 500); } early() { // QTL-based fitness inds = sim.subpopulations.individuals; phenotypes = inds.sumOfMutationsOfType(m1); optimum = (sim.cycle < Tdelta) ? opt1 else opt2; deviations = optimum - phenotypes; fitnessFunctionMax = dnorm(0.0, 0.0, 5.0); adaptation = dnorm(deviations, 0.0, 5.0) / fitnessFunctionMax; inds.fitnessScaling = 0.1 + adaptation * 0.9; inds.tagF = phenotypes; // just for output below // density-dependence with a maximum benefit at low density p1.fitnessScaling = min(K / p1.individualCount, 1.5); } mutationEffect(m1) { return 1.0; } late() { if (p1.individualCount == 0) { // stop at extinction catn("Extinction in cycle " + sim.cycle + "."); sim.simulationFinished(); } else { // output the phenotypic mean and pop size phenotypes = p1.individuals.tagF; cat(sim.cycle + ": " + p1.individualCount + " individuals"); cat(", phenotype mean " + mean(phenotypes)); if (size(phenotypes) > 1) cat(" (sd " + sd(phenotypes) + ")"); catn(); } } 20000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 15.9 - Litter size and parental investment.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, sexual, QTL, quantitative trait loci, reproduction() initialize() { initializeSLiMModelType("nonWF"); initializeSex(); defineConstant("K", 500); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeMutationType("m2", 0.5, "n", 0.0, 0.3); // QTL initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.1)); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction(NULL, "F") { mate = subpop.sampleIndividuals(1, sex="M"); if (mate.size()) { qtlValue = individual.tagF; expectedLitterSize = max(0.0, qtlValue + 3); litterSize = rpois(1, expectedLitterSize); penalty = 3.0 / litterSize; for (i in seqLen(litterSize)) { offspring = subpop.addCrossed(individual, mate); offspring.setValue("penalty", rgamma(1, penalty, 20)); } } } 1 early() { sim.addSubpop("p1", 500); p1.individuals.setValue("penalty", 1.0); } early() { // non-overlapping generations inds = sim.subpopulations.individuals; inds[inds.age > 0].fitnessScaling = 0.0; inds = inds[inds.age == 0]; // focus on juveniles // QTL calculations inds.tagF = inds.sumOfMutationsOfType(m2); // parental investment fitness penalties inds.fitnessScaling = inds.getValue("penalty"); // density-dependence for juveniles p1.fitnessScaling = K / size(inds); } mutationEffect(m2) { return 1.0; } late() { // output the phenotypic mean and pop size qtlValues = p1.individuals.tagF; expectedSizes = pmax(0.0, qtlValues + 3); cat(sim.cycle + ": " + p1.individualCount + " individuals"); cat(", mean litter size " + mean(expectedSizes)); catn(); } 20000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.1 - Pollen flow.txt ================================================ // Keywords: nonWF, non-Wright-Fisher initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 200); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // determine how many ovules were fertilized, out of the total fertilizedOvules = rbinom(1, 30, 0.5); // determine the pollen source for each fertilized ovule other = (subpop == p1) ? p2 else p1; pollenSources = ifelse(runif(fertilizedOvules) < 0.99, subpop, other); // generate seeds from each fertilized ovule // the ovule belongs to individual, the pollen comes from source for (source in pollenSources) subpop.addCrossed(individual, source.sampleIndividuals(1)); } 1 early() { sim.addSubpop("p1", 10); sim.addSubpop("p2", 10); } early() { for (subpop in sim.subpopulations) subpop.fitnessScaling = K / subpop.individualCount; } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.10 - Modeling pseudo-autosomal regions (PARs) with addMultiRecombinant().txt ================================================ // Keywords: multiple chromosomes, pseudo-autosomal region (PAR) initialize() { defineConstant("A1_LEN", 2e7); defineConstant("PAR1_LEN", 2771479); defineConstant("PAR2_LEN", 329513); defineConstant("X_LEN", 156040895 - (PAR1_LEN + PAR2_LEN)); defineConstant("Y_LEN", 57227415 - (PAR1_LEN + PAR2_LEN)); defineConstant("MU", 1e-8); defineConstant("R", 1e-7); defineConstant("N", 500); defineConstant("REC", N*10); // start recording at this tick defineConstant("RUNTIME", N*50); // finish at this tick initializeSLiMModelType("nonWF"); initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0).convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); for (id in 1:5, length in c(A1_LEN, PAR1_LEN, X_LEN, Y_LEN, PAR2_LEN), type in c("A","A","X","Y","A"), symbol in c("A1","P1","X","Y","P2")) { chr = initializeChromosome(id, length, type=type, symbol=symbol); initializeGenomicElement(g1); initializeMutationRate(MU); initializeRecombinationRate(R); defineConstant(paste0("CHR_", symbol), chr); } } 1 late() { sim.addSubpop("p1", N); } REC late() { log = community.createLogFile("PAR_Ne.csv", logInterval=10); log.addTick(); log.addCustomColumn("Ne_A1", "estimateNe_Heterozygosity(p1, CHR_A1);"); log.addCustomColumn("Ne_P1", "estimateNe_Heterozygosity(p1, CHR_P1);"); log.addCustomColumn("Ne_X", "estimateNe_Heterozygosity(p1, CHR_X);"); log.addCustomColumn("Ne_Y", "estimateNe_Heterozygosity(p1, CHR_Y);"); log.addCustomColumn("Ne_P2", "estimateNe_Heterozygosity(p1, CHR_P2);"); defineConstant("LOG", log); } reproduction() { for (i in seqLen(N)) { parentF = p1.sampleIndividuals(1, sex="F"); parentM = p1.sampleIndividuals(1, sex="M"); // generate breakpoints for the female parent (X recombines) breaks_F_P1 = CHR_P1.drawBreakpoints(parent=parentF); breaks_F_X = CHR_X.drawBreakpoints(parent=parentF); breaks_F_P2 = CHR_P2.drawBreakpoints(parent=parentF); // generate breakpoints for the male parent (only PARs recombine) breaks_M_P1 = CHR_P1.drawBreakpoints(parent=parentM); breaks_M_P2 = CHR_P2.drawBreakpoints(parent=parentM); // get the haplosomes for each chromosome in each parent strands_F_P1 = parentF.haplosomesForChromosomes(CHR_P1); // 2 strands_F_X = parentF.haplosomesForChromosomes(CHR_X); // 2 strands_F_P2 = parentF.haplosomesForChromosomes(CHR_P2); // 2 strands_M_P1 = parentM.haplosomesForChromosomes(CHR_P1); // 2 strands_M_X = parentM.haplosomesForChromosomes(CHR_X)[0]; // 1 strands_M_Y = parentM.haplosomesForChromosomes(CHR_Y); // 1 strands_M_P2 = parentM.haplosomesForChromosomes(CHR_P2); // 2 // choose initial copy strand indices for PAR1 in both (coin flip) initial_F_P1 = rbinom(1, 1, 0.5); initial_M_P1 = rbinom(1, 1, 0.5); // generate the inheritance dictionary for PAR1 s1_F_P1 = strands_F_P1[initial_F_P1]; s2_F_P1 = strands_F_P1[1 - initial_F_P1]; s1_M_P1 = strands_M_P1[initial_M_P1]; s2_M_P1 = strands_M_P1[1 - initial_M_P1]; pattern = sim.addPatternForRecombinant(CHR_P1, NULL, s1_F_P1, s2_F_P1, breaks_F_P1, s1_M_P1, s2_M_P1, breaks_M_P1, randomizeStrands=F); // the initial strand for the X in the female follows from the // above, because PAR1 is physically linked to the start of the // X; if an odd number of crossovers occurred, switch strands initial_F_X = initial_F_P1; if (size(breaks_F_P1) % 2 == 1) initial_F_X = 1 - initial_F_X; if (runif(1) < R) initial_F_X = 1 - initial_F_X; // do the same for the male, but the "initial strand" is the // X if 0, the Y if 1, and it determines the offspring sex initial_M_XY = initial_M_P1; if (size(breaks_M_P1) % 2 == 1) initial_M_XY = 1 - initial_M_XY; if (runif(1) < R) initial_M_XY = 1 - initial_M_XY; sex = ((initial_M_XY == 0) ? "F" else "M"); // generate the inheritance dictionaries for the X and Y s1_F_X = strands_F_X[initial_F_X]; s2_F_X = strands_F_X[1 - initial_F_X]; if (sex == "F") { sim.addPatternForRecombinant(CHR_X, pattern, s1_F_X, s2_F_X, breaks_F_X, strands_M_X, NULL, NULL, sex=sex, randomizeStrands=F); sim.addPatternForNull(CHR_Y, pattern, sex=sex); } else { sim.addPatternForRecombinant(CHR_X, pattern, s1_F_X, s2_F_X, breaks_F_X, NULL, NULL, NULL, sex=sex, randomizeStrands=F); sim.addPatternForClone(CHR_Y, pattern, parent=parentM, sex=sex); } // and the initial copy strand for PAR2 follows from the above, // because PAR2 is physically linked to the end of the X/Y; // if an odd number of crossovers occurred, switch strands initial_F_P2 = initial_F_X; if (size(breaks_F_X) % 2 == 1) initial_F_P2 = 1 - initial_F_P2; if (runif(1) < R) initial_F_P2 = 1 - initial_F_P2; initial_M_P2 = initial_M_XY; if (runif(1) < R) initial_M_P2 = 1 - initial_M_P2; // generate the inheritance dictionary for PAR2 s1_F_P2 = strands_F_P2[initial_F_P2]; s2_F_P2 = strands_F_P2[1 - initial_F_P2]; s1_M_P2 = strands_M_P2[initial_M_P2]; s2_M_P2 = strands_M_P2[1 - initial_M_P2]; sim.addPatternForRecombinant(CHR_P2, pattern, s1_F_P2, s2_F_P2, breaks_F_P2, s1_M_P2, s2_M_P2, breaks_M_P2, randomizeStrands=F); // finally, generate the offspring following the pattern dictionary subpop.addMultiRecombinant(pattern, sex=sex, parent1=parentF, parent2=parentM, randomizeStrands=F); } self.active = 0; } 2: early() { // non-overlapping generations adults = p1.subsetIndividuals(minAge=1); sim.killIndividuals(adults); } REC:(RUNTIME+1) early() { // plot results that got logged the previous tick (which ended in 0) if ((community.tick % 10 == 1) & exists("slimgui")) { ticks = slimgui.logFileData(LOG, "tick"); Ne_A1 = slimgui.logFileData(LOG, "Ne_A1"); Ne_P1 = slimgui.logFileData(LOG, "Ne_P1"); Ne_X = slimgui.logFileData(LOG, "Ne_X"); Ne_Y = slimgui.logFileData(LOG, "Ne_Y"); Ne_P2 = slimgui.logFileData(LOG, "Ne_P2"); plot = slimgui.createPlot("Ne Estimates", xrange=c(REC, RUNTIME), yrange=c(0, N * 2), xlab="Tick", ylab="Population size", width=1000, height=400); plot.axis(2, at=c(0, N, N*2)); plot.abline(h=N, color="black", lwd=2.0); plot.lines(ticks, Ne_A1, "chartreuse3", lwd=2.0); plot.abline(h=mean(Ne_A1), color="chartreuse3", lwd=1.0); plot.lines(ticks, Ne_P1, "turquoise3", lwd=2.0); plot.abline(h=mean(Ne_P1), color="turquoise3", lwd=1.0); plot.lines(ticks, Ne_X, "red", lwd=2.0); plot.abline(h=mean(Ne_X), color="red", lwd=1.0); plot.lines(ticks, Ne_Y, "orchid2", lwd=2.0); plot.abline(h=mean(Ne_Y), color="orchid2", lwd=1.0); plot.lines(ticks, Ne_P2, "cornflowerblue", lwd=2.0); plot.abline(h=mean(Ne_P2), color="cornflowerblue", lwd=1.0); plot.addLegend("topLeft", labelSize=12); plot.legendLineEntry("N", "black", lwd=2.0); plot.legendLineEntry("Ne (A1)", "chartreuse3", lwd=2.0); plot.legendLineEntry("Ne (P1)", "turquoise3", lwd=2.0); plot.legendLineEntry("Ne (X)", "red", lwd=2.0); plot.legendLineEntry("Ne (Y)", "orchid2", lwd=2.0); plot.legendLineEntry("Ne (P2)", "cornflowerblue", lwd=2.0); if (community.tick == RUNTIME + 1) { plot.write("Ne_EST.pdf"); sim.simulationFinished(); } } } function (float)estimateNe_Heterozygosity(o$ subpop, [No$ chromosome = NULL]) { if (isNULL(chromosome)) { if (size(sim.chromosomes) == 1) chromosome = sim.chromosomes; else stop("ERROR: in a multi-chrom model, a chromosome must be supplied."); } haplosomes = subpop.haplosomesForChromosomes(chromosome, includeNulls=F); pi = calcHeterozygosity(haplosomes); return pi / (4 * MU); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.11 - Life-long monogamous mating.txt ================================================ // Keywords: monogamy, monogamous mating, nonWF, non-Wright-Fisher initialize() { defineConstant("K", 500); // carrying capacity defineConstant("R_AGE_M", 3); // minimum age of reproduction (male) defineConstant("R_AGE_F", 4); // minimum age of reproduction (female) defineConstant("FECUN", 0.2); // mean fecundity per female per tick initializeSLiMModelType("nonWF"); initializeSex(); } 1 first() { sim.addSubpop("p1", K); p1.individuals.age = rdunif(K, min=0, max=15); // initial variation in age p1.individuals.tag = -1; // mark all individuals as unmated } first() { // find mated individuals whose mate has died, and mark them as unmated mated_individuals = p1.individuals; mated_individuals = mated_individuals[mated_individuals.tag >= 0]; if (size(mated_individuals) > 0) { tags = mated_individuals.tag; tag_counts = tabulate(tags); tags_to_fix = which(tag_counts == 1); unmated_indices = match(tags_to_fix, tags); mated_individuals[unmated_indices].tag = -1; } // find the next tag value to use for new mating pairs next_tag = max(p1.individuals.tag) + 1; // find unmated individuals that are of reproductive age unmated_F = p1.subsetIndividuals(sex="F", tag=-1, minAge=R_AGE_F); unmated_M = p1.subsetIndividuals(sex="M", tag=-1, minAge=R_AGE_M); // pair individuals randomly; some individuals may be left unpaired pair_count = min(size(unmated_F), size(unmated_M)); unmated_F = sample(unmated_F, pair_count, replace=F); unmated_M = sample(unmated_M, pair_count, replace=F); for (f in unmated_F, m in unmated_M, tag in seqLen(pair_count) + next_tag) { f.tag = tag; m.tag = tag; } } reproduction() { // find the subset of individuals that have a mate mated_F = p1.subsetIndividuals(sex="F"); mated_F = mated_F[mated_F.tag >= 0]; mated_M = p1.subsetIndividuals(sex="M"); mated_M = mated_M[mated_M.tag >= 0]; // look up the male for each female, by tag male_indices = match(mated_F.tag, mated_M.tag); mated_M = mated_M[male_indices]; pair_count = size(mated_F); // produce offspring from each mated pair for (f in mated_F, m in mated_M, c in rpois(pair_count, FECUN), new_tag in seqLen(pair_count)) { // re-tag paired individuals to compact tags down f.tag = new_tag; m.tag = new_tag; offspring = p1.addCrossed(f, m, count=c); offspring.tag = -1; // mark offspring as unmated } self.active = 0; // deactivate for the rest of the tick ("big bang") } early() { // density-dependent population regulation p1.fitnessScaling = K / p1.individualCount; } 10000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 16.2 - Following a pedigree.txt ================================================ // Keywords: nonWF, non-Wright-Fisher function (+)readMatrix(s$ path, [string$ sep = ","]) { if (!fileExists(path)) stop("readMatrix(): File not found at path " + path); df = readCSV(path, colNames=F, sep=sep); m = df.asMatrix(); return m; } initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 10); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // read in the pedigree log files defineConstant("M", readMatrix("mating.txt", sep="")); defineConstant("D", readMatrix("death.txt", sep="")); // extract the ticks for quick lookup defineConstant("Mt", drop(M[,0])); defineConstant("Dt", drop(D[,0])); } reproduction() { // generate all offspring for the tick m = M[Mt == community.tick,]; for (index in seqLen(nrow(m))) { row = m[index,]; ind = subpop.subsetIndividuals(tag=row[,1]); mate = subpop.subsetIndividuals(tag=row[,2]); child = subpop.addCrossed(ind, mate); child.tag = row[,3]; } self.active = 0; } 1 early() { sim.addSubpop("p1", 10); // provide initial tags matching the original model p1.individuals.tag = 1:10; } early() { // execute the predetermined mortality inds = p1.individuals; inds.fitnessScaling = 1.0; d = drop(D[Dt == community.tick, 1]); indices = match(d, inds.tag); inds[indices].fitnessScaling = 0.0; } 100 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.3 - Modeling clonal haploid bacteria with horizontal gene transfer.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, clonal, haploid initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 1e5); // carrying capacity defineConstant("L", 1e5); // chromosome length defineConstant("H", 0.001); // HGT probability initializeMutationType("m1", 1.0, "f", 0.0); // neutral (unused) initializeMutationType("m2", 1.0, "f", 0.1); // beneficial initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, L, type="H"); initializeGenomicElement(g1); initializeMutationRate(0); // no mutation initializeRecombinationRate(0); // no recombination } reproduction() { if (runif(1) < H) { // horizontal gene transfer from a randomly chosen individual HGTsource = p1.sampleIndividuals(1, exclude=individual).haplosomes; // draw two distinct locations; redraw if we get a duplicate do breaks = rdunif(2, max=L-1); while (breaks[0] == breaks[1]); // HGT from breaks[0] forward to breaks[1] on a circular chromosome if (breaks[0] > breaks[1]) breaks = c(0, breaks[1], breaks[0]); subpop.addRecombinant(individual.haplosomes, HGTsource, breaks, NULL, NULL, NULL, randomizeStrands=F); } else { // no horizontal gene transfer; clonal replication subpop.addCloned(individual); } } 1 early() { // start from two bacteria with different beneficial mutations sim.addSubpop("p1", 2); h = p1.individuals.haplosomes; h[0].addNewDrawnMutation(m2, asInteger(L * 0.25)); h[1].addNewDrawnMutation(m2, asInteger(L * 0.75)); } early() { // density-dependent population regulation p1.fitnessScaling = K / p1.individualCount; } late() { // detect fixation/loss of the beneficial mutations muts = sim.mutations; freqs = sim.mutationFrequencies(NULL, muts); if (all(freqs == 1.0)) { catn(sim.cycle + ": " + sum(freqs == 1.0) + " fixed."); sim.simulationFinished(); } } 1e6 late() { catn(sim.cycle + ": no result."); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.4 - Alternation of generations.txt ================================================ // Keywords: alternation of generations, sporophyte, gametophyte, sperm, eggs, diploid, haploid, mating system, fertilization, meiosis, reproduction() initialize() { defineConstant("K", 500); // carrying capacity (diploid) defineConstant("MU", 1e-7); // mutation rate defineConstant("R", 1e-7); // recombination rate defineConstant("L1", 1e5-1); // chromosome end (length - 1) initializeSLiMModelType("nonWF"); initializeSex(); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L1); initializeRecombinationRate(R); } 1 early() { sim.addSubpop("p1", K); sim.addSubpop("p2", 0); } reproduction(p1) { g_1 = individual.haploidGenome1; g_2 = individual.haploidGenome2; for (meiosisCount in 1:5) { if (individual.sex == "M") { breaks = sim.chromosomes.drawBreakpoints(individual); s_1 = p2.addRecombinant(g_1, g_2, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); s_2 = p2.addRecombinant(g_2, g_1, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); breaks = sim.chromosomes.drawBreakpoints(individual); s_3 = p2.addRecombinant(g_1, g_2, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); s_4 = p2.addRecombinant(g_2, g_1, breaks, NULL, NULL, NULL, "M", randomizeStrands=F); } else if (individual.sex == "F") { e = p2.addRecombinant(g_1, g_2, NULL, NULL, NULL, NULL, "F", randomizeStrands=T); } } } reproduction(p2, "F") { mate = p2.sampleIndividuals(1, sex="M", tagL0=F); mate.tagL0 = T; child = p1.addRecombinant(individual.haploidGenome1, NULL, NULL, mate.haploidGenome1, NULL, NULL); } early() { if (sim.cycle % 2 == 0) { p1.fitnessScaling = 0.0; p2.individuals.tagL0 = F; sim.chromosomes.setMutationRate(0.0); } else { p2.fitnessScaling = 0.0; p1.fitnessScaling = K / p1.individualCount; sim.chromosomes.setMutationRate(MU); } } 10000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.5 - Meiotic drive.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, meiotic drive, segregation distortion, intragenomic conflict function (i)driveBreakpoints(o$ gen1, o$ gen2) { // start with default breakpoints generated by the chromosome breaks = sim.chromosomes.drawBreakpoints(); // if both haplosomes have the drive, or neither, then just return gen1has = gen1.containsMarkerMutation(m2, D_pos); gen2has = gen2.containsMarkerMutation(m2, D_pos); if (gen1has == gen2has) return breaks; // will the drive be inherited? do we want it to be? polarity = sum(breaks <= D_pos) % 2; // 0 for gen1, 1 for gen2 polarityI = (gen1has ? 0 else 1); desiredPolarity = (runif(1) < D_prob) ? polarityI else !polarityI; // intervene to produce the outcome we want if (desiredPolarity != polarity) return c(0, breaks); return breaks; } initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); // carrying capacity defineConstant("D_pos", 20000); // meiotic drive allele position defineConstant("D_prob", 0.8); // meiotic drive probability initializeMutationType("m1", 0.5, "f", 0.0); // neutral m1.convertToSubstitution = T; initializeMutationType("m2", 0.1, "f", -0.1); // drive allele m2.color = "red"; m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { m = subpop.sampleIndividuals(1); b1 = driveBreakpoints(individual.haploidGenome1, individual.haploidGenome2); b2 = driveBreakpoints(m.haploidGenome1, m.haploidGenome2); subpop.addRecombinant(individual.haploidGenome1, individual.haploidGenome2, b1, m.haploidGenome1, m.haploidGenome2, b2, randomizeStrands=F); } 1 early() { sim.addSubpop("p1", 10); } early() { p1.fitnessScaling = K / p1.individualCount; } 100 early() { target = sample(p1.haplosomes, 10); target.addNewDrawnMutation(m2, D_pos); } 100:1000 late() { mut = sim.mutationsOfType(m2); if (size(mut) == 0) { catn(sim.cycle + ": LOST"); sim.simulationFinished(); } else if (sim.mutationFrequencies(NULL, mut) == 1.0) { catn(sim.cycle + ": FIXED"); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 16.6 - Sperm storage with a survival() callback.txt ================================================ // Keywords: survival(), sperm storage // This model is loosely based upon a model by Anita Lerch. initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(keepPedigrees=T); initializeSex(); defineConstant("K", 500); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } reproduction(p1) { matureFemales = subpop.subsetIndividuals(sex="F", minAge=7); for (female in matureFemales) { if (female.tag < 0) { // the female has not yet chosen a mate, so choose one now mate = subpop.sampleIndividuals(1, sex="M", minAge=7); } else { // the female has already chosen a mate; look it up by id mate = sim.individualsWithPedigreeIDs(female.tag); } if (mate.size()) { female.tag = mate.pedigreeID; subpop.addCrossed(female, mate, count=rpois(1, 5)); } else { catn(sim.cycle + ": No mate found for tag " + female.tag); } } self.active = 0; } 1 early() { sim.addSubpop("p1", 100); p1inds = p1.individuals; p1inds.age = rdunif(size(p1.individuals), min=0, max=10); p1inds.tag = -1; sim.addSubpop("p1000", 0); // cold storage for dead males } early() { // fix all new female tags; faster to do this vectorized offspringFemales = p1.subsetIndividuals(sex="F", maxAge=0); offspringFemales.tag = -1; // p1 is governed by standard density-dependence p1.fitnessScaling = K / p1.individualCount; // cold storage individuals are kept until unreferenced p1000.individuals.tag = 0; maleRefs = p1.subsetIndividuals(sex="F").tag; maleRefs = maleRefs[maleRefs != -1]; referencedDeadMales = sim.individualsWithPedigreeIDs(maleRefs, p1000); referencedDeadMales.tag = 1; } survival(p1) { // move dying males into cold storage in case they have mated if (!surviving) if (individual.sex == "M") return p1000; return NULL; } survival(p1000) { return (individual.tag == 1); } late() { catn(sim.cycle + ": p1 (" + p1.individualCount + ")" + ", p1000 (" + p1000.individualCount + ")"); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.7 - Tracking separate sexes in script, nonWF style.txt ================================================ // Keywords: automixis, parthenogenesis, sex determination, mating systems, sexual types initialize() { initializeSLiMModelType("nonWF"); defineConstant("K", 500); // carrying capacity initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } reproduction() { // we focus on the reproduction of the females here if (individual.tagL0 == F) { if (runif(1) < 0.7) { // choose a male mate and produce a son or daughter mate = subpop.sampleIndividuals(1, tagL0=T); offspring = subpop.addCrossed(individual, mate); offspring.tagL0 = (runif(1) <= 0.5); } else { // reproduce through automixis to produce a daughter offspring = subpop.addSelfed(individual); offspring.tagL0 = F; } } } 1 early() { sim.addSubpop("p1", K); // assign random sexes (T = male, F = female) p1.individuals.tagL0 = (runif(p1.individualCount) <= 0.5); } early() { p1.fitnessScaling = K / p1.individualCount; } 1:2000 late() { ratio = mean(p1.individuals.tagL0); catn(sim.cycle + ": " + ratio); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.8 - Modeling haplodiploidy with addRecombinant().txt ================================================ // Keywords: mating systems, haplodiploidy, arrhenotoky, bees, wasps, ants, Hymenoptera initialize() { defineConstant("K", 2000); defineConstant("P_OFFSPRING_MALE", 0.8); initializeSLiMModelType("nonWF"); initializeMutationRate(1e-8); initializeMutationType("m1", 0.0, "f", 0.0); m1.convertToSubstitution = T; m1.hemizygousDominanceCoeff = 1.0; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 999999); initializeRecombinationRate(1e-6); initializeSex(); } reproduction(NULL, "F") { gen1 = individual.haploidGenome1; gen2 = individual.haploidGenome2; // decide whether we're generating a haploid male or a diploid female if (rbinom(1, 1, P_OFFSPRING_MALE)) { // didn't find a mate; make a haploid male from an unfertilized egg: // - one haplosome comes from recombination of the female's haplosomes // - the other haplosome is a null haplosome (a placeholder) subpop.addRecombinant(gen1, gen2, NULL, NULL, NULL, NULL, "M", randomizeStrands=T); } else { // found a mate; make a diploid female from a fertilized egg: // - one haplosome comes from recombination of the female's haplosomes // - the other haplosome comes from the mate (a haploid male) mate = subpop.sampleIndividuals(1, sex="M"); subpop.addRecombinant(gen1, gen2, NULL, mate.haploidGenome1, NULL, NULL, "F", randomizeStrands=T); } } 1 early() { // make an initial population with the right genetics mCount = asInteger(K * P_OFFSPRING_MALE); fCount = K - mCount; sim.addSubpop("p1", mCount, sexRatio=1.0, haploid=T); // males sim.addSubpop("p2", fCount, sexRatio=0.0, haploid=F); // females p1.takeMigrants(p2.individuals); p2.removeSubpopulation(); } early() { p1.fitnessScaling = K / p1.individualCount; } 10000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 16.9 - Complex multi-chromosome inheritance with addMultiRecombinant().txt ================================================ // Keywords: multiple chromosomes, inheritance patterns, mating systems initialize() { defineConstant("K", 500); // carrying capacity initializeSLiMModelType("nonWF"); initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); m1.convertToSubstitution = T; ids = 1:7; symbols = c("A", "X", "Y", "P", "Q", "R", "S"); lengths = c(3e6, 2e6, 1e6, 1e6, 1e6, 1e6, 1e6); types = c("A", "X", "Y", "H", "H", "H", "H"); for (id in ids, symbol in symbols, length in lengths, type in types) { initializeChromosome(id, length, type, symbol); initializeMutationRate(1e-7); initializeRecombinationRate(1e-7); initializeGenomicElement(g1); } } reproduction(NULL, "F") { mate = subpop.sampleIndividuals(1, sex="M"); pattern = Dictionary(); sim.addPatternForClone("P", pattern, individual); sim.addPatternForClone("Q", pattern, runif(1) < 0.5 ? individual else mate); sim.addPatternForCross("R", pattern, individual, mate); ind_hapS = individual.haplosomesForChromosomes("S"); mate_hapS = mate.haplosomesForChromosomes("S"); sim.addPatternForRecombinant("S", pattern, ind_hapS, mate_hapS, NULL, NULL, NULL, NULL); subpop.addMultiRecombinant(pattern, parent1=individual, parent2=mate, randomizeStrands=F); } 1 early() { sim.addSubpop("p1", K); } early() { p1.fitnessScaling = K / p1.individualCount; } 1000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.1 - A simple 2D continuous-space model.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); // initial positions are random in ([0,1], [0,1]) p1.individuals.x = runif(p1.individualCount); p1.individuals.y = runif(p1.individualCount); } modifyChild() { // draw a child position near the first parent, within bounds do child.x = parent1.x + rnorm(1, 0, 0.02); while ((child.x < 0.0) | (child.x > 1.0)); do child.y = parent1.y + rnorm(1, 0, 0.02); while ((child.y < 0.0) | (child.y > 1.0)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.10 - A simple biogeographic landscape model.txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial map, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=30.0); i1.setInteractionFunction("n", 5.0, 10.0); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=30.0); i2.setInteractionFunction("n", 1.0, 10.0); } 1 late() { sim.addSubpop("p1", 1000); p1.setSpatialBounds(c(0.0, 0.0, 539.0, 216.0)); // this file is in the recipe archive at http://benhaller.com/slim/SLiM_Recipes.zip mapImage = Image("world_map_540x217.png"); map = p1.defineSpatialMap("world", "xy", 1.0 - mapImage.floatK, valueRange=c(0.0, 1.0), colors=c("#0000CC", "#55FF22")); defineConstant("WORLD", map); // start near a specific map location for (ind in p1.individuals) { ind.x = rnorm(1, 300.0, 1.0); ind.y = rnorm(1, 100.0, 1.0); } } 1: late() { i1.evaluate(p1); inds = sim.subpopulations.individuals; competition = i1.totalOfNeighborStrengths(inds) / size(inds); competition = pmin(competition, 0.99); inds.fitnessScaling = 1.0 - competition; } 2: first() { i2.evaluate(p1); } mateChoice() { return i2.strength(individual); } modifyChild() { do pos = parent1.spatialPosition + rnorm(2, 0, 2.0); while (!p1.pointInBounds(pos)); // prevent dispersal into water if (WORLD.mapValue(pos) == 0.0) return F; child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.11 - Local adaptation on a heterogeneous landscape map.txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial map, reprising boundaries, QTL, quantitative trait loci, spatial competition, spatial mate choice initialize() { defineConstant("SIGMA_C", 0.1); defineConstant("SIGMA_K", 0.5); defineConstant("SIGMA_M", 0.1); defineConstant("N", 500); initializeSLiMOptions(dimensionality="xyz"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.1)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); // competition initializeInteractionType(1, "xyz", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); // mate choice initializeInteractionType(2, "xyz", reciprocal=T, maxDistance=SIGMA_M * 3); i2.setInteractionFunction("n", 1.0, SIGMA_M); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", N); p1.setSpatialBounds(c(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); p1.individuals.setSpatialPosition(p1.pointUniform(N)); p1.individuals.z = 0.0; defineConstant("MAPVALUES", matrix(runif(25, 0, 1), ncol=5)); map = p1.defineSpatialMap("map1", "xy", MAPVALUES, interpolate=T, valueRange=c(0.0, 1.0), colors=c("red", "yellow")); defineConstant("OPTIMUM", map); } modifyChild() { // set offspring position based on parental position do pos = c(parent1.spatialPosition[0:1] + rnorm(2, 0, 0.005), 0.0); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 1: late() { // construct phenotypes and fitness effects from QTLs inds = sim.subpopulations.individuals; phenotype = inds.sumOfMutationsOfType(m2); location = inds.spatialPosition[rep(c(T,T,F), inds.size())]; optimum = OPTIMUM.mapValue(location); inds.fitnessScaling = 1.0 + dnorm(phenotype, optimum, SIGMA_K); inds.z = phenotype; // color individuals according to phenotype inds.color = OPTIMUM.mapColor(phenotype); // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 2: first() { // evaluate mate choice in preparation for reproduction i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.12 - Periodic spatial boundaries.txt ================================================ // Keywords: continuous space, continuous spatial landscape, periodic boundaries initialize() { initializeSLiMOptions(dimensionality="xy", periodicity="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType("i1", "xy", reciprocal=T, maxDistance=0.2); i1.setInteractionFunction("n", 1.0, 0.1); } 1 late() { sim.addSubpop("p1", 2000); p1.individuals.setSpatialPosition(p1.pointUniform(2000)); } late() { i1.evaluate(p1); focus = sample(p1.individuals, 1); s = i1.strength(focus); inds = p1.individuals; for (i in seqAlong(s)) inds[i].color = rgb2color(c(1.0 - s[i], 1.0 - s[i], s[i])); focus.color = "red"; } modifyChild() { pos = parent1.spatialPosition + rnorm(2, 0, 0.02); child.setSpatialPosition(p1.pointPeriodic(pos)); return T; } 1000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.13 - Density-dependent fecundity with summarizeIndividuals().txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial map, density, competition, regulation, fertility, per unit area initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 1000); p1.individuals.setSpatialPosition(p1.pointUniform(1000)); } late() { inds = p1.individuals; bounds = p1.spatialBounds; // make a density map: 0 is empty, 1 is maximum density density = summarizeIndividuals(inds, c(10, 10), bounds, operation="individuals.size();", empty=0.0, perUnitArea=T); density = density / max(density); p1.defineSpatialMap("density", "xy", density, F, range(density), c("black", "orange", "red")); } modifyChild() { pos = parent1.spatialPosition + rnorm(2, 0, 0.01); pos = p1.pointReflected(pos); if (runif(1) < p1.spatialMapValue("density", pos)) return F; child.setSpatialPosition(pos); return T; } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.14 - Directed dispersal with the SpatialMap class.txt ================================================ // Keywords: continuous space, continuous spatial landscape, spatial maps, directed dispersal initialize() { defineConstant("K", 1000); initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy", periodicity="xy"); } 1 late() { sim.addSubpop("p1", K); p1.individuals.setSpatialPosition(c(0.0, 0.0)); p1.individuals.color = rainbow(K); do { m = matrix(rbinom(16, 1, 0.2), ncol=4, byrow=T); m = cbind(m, m[,0]); m = rbind(m, m[0,]); } while ((sum(m) == 0) | (sum(m) == 1)); map = p1.defineSpatialMap("habitat", "xy", m, valueRange=c(0,1), colors=c("black", "white")); defineConstant("MAP", map); } 2 late() { MAP.interpolate(15, method="cubic"); } 3 late() { MAP.rescale(); } 4 late() { MAP.smooth(0.3, "n", 0.1); } 5 late() { MAP.rescale(); } 6 late() { MAP.interpolate = T; } 10 late() { p1.individuals.setSpatialPosition(p1.pointUniform(K)); } 11:100000 late() { inds = p1.individuals; pos = inds.spatialPosition; pos = MAP.sampleNearbyPoint(pos, INF, "n", 0.002); inds.setSpatialPosition(pos); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.15 - Spatial competition and spatial mate choice in a nonWF model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, periodic boundaries, selfing initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy", periodicity="xy"); defineConstant("K", 300); // carrying capacity defineConstant("S", 0.1); // spatial competition distance initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); } 2: first() { // look for mates i2.evaluate(p1); } reproduction() { // choose our nearest neighbor as a mate, within the max distance mate = i2.nearestNeighbors(individual, 1); for (i in seqLen(rpois(1, 0.1))) { if (mate.size()) offspring = subpop.addCrossed(individual, mate); else offspring = subpop.addSelfed(individual); // set offspring position pos = individual.spatialPosition + rnorm(2, 0, 0.02); offspring.setSpatialPosition(p1.pointPeriodic(pos)); } } 1 early() { sim.addSubpop("p1", 1); // random initial positions p1.individuals.setSpatialPosition(p1.pointUniform(1)); } early() { i1.evaluate(p1); // spatial competition provides density-dependent selection inds = p1.individuals; competition = i1.totalOfNeighborStrengths(inds); competition = (competition + 1) / (PI * S^2); inds.fitnessScaling = K / competition; } late() { // move around a bit for (ind in p1.individuals) { newPos = ind.spatialPosition + runif(2, -0.01, 0.01); ind.setSpatialPosition(p1.pointPeriodic(newPos)); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.16 - A spatial model with carrying-capacity density.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, selfing, spatial competition, spatial mate choice initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy"); defineConstant("K", 300); // carrying-capacity density defineConstant("S", 0.1); // SIGMA_S, the spatial interaction width initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S * 3); i1.setInteractionFunction("n", 1.0, S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); } 2: first() { // look for mates i2.evaluate(p1); } reproduction() { // choose our nearest neighbor as a mate, within the max distance mate = i2.nearestNeighbors(individual, 1); for (i in seqLen(rpois(1, 0.1))) { if (mate.size()) offspring = subpop.addCrossed(individual, mate); else offspring = subpop.addSelfed(individual); // set offspring position do pos = individual.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); offspring.setSpatialPosition(pos); } } 1 early() { sim.addSubpop("p1", 1); // random initial positions p1.individuals.setSpatialPosition(p1.pointUniform(1)); } early() { i1.evaluate(p1); // spatial competition provides density-dependent selection inds = p1.individuals; competition = i1.localPopulationDensity(inds); inds.fitnessScaling = K / competition; } late() { // move around a bit for (ind in p1.individuals) { do newPos = ind.spatialPosition + runif(2, -0.01, 0.01); while (!p1.pointInBounds(newPos)); ind.setSpatialPosition(newPos); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.17 - A spatial epidemiological S-I-R model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, periodic boundaries, spatial competition, spatial mate choice, disease, epidemiology, SIR, S-I-R, infection, epidemic, pandemic initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy", periodicity="xy"); defineConstant("K", 10000); // carrying-capacity density defineConstant("S", 0.01); // SIGMA_S, the competition width defineConstant("HEALTH_S", 0); // susceptible defineConstant("HEALTH_I", 1); // infectious defineConstant("HEALTH_R", 2); // recovered defineConstant("FERTILITY", 0.05); defineConstant("INFECTIVITY", 4); defineConstant("RATE_DEATH", 0.3); defineConstant("RATE_CLEAR", 0.05); defineConstant("MAX_AGE", 100.0); initializeMutationType("m1", 0.5, "f", 0.0); m1.convertToSubstitution = T; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S * 3); i1.setInteractionFunction("n", 1.0, S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.05); } 2: first() { // look for mates i2.evaluate(p1); } reproduction() { litterSize = rpois(1, FERTILITY); if (litterSize) { mate = i2.nearestNeighbors(individual, 1); if (mate.size()) for (i in seqLen(litterSize)) { offspring = subpop.addCrossed(individual, mate); // set offspring position and state pos = individual.spatialPosition + rnorm(2, 0, 0.005); offspring.setSpatialPosition(p1.pointPeriodic(pos)); offspring.tag = HEALTH_S; } } } 1 early() { sim.addSubpop("p1", K); p1.individuals.setSpatialPosition(p1.pointUniform(K)); p1.individuals.tag = HEALTH_S; } 100 early() { // seed the infection in a susceptible individual target = p1.sampleIndividuals(1, tag=HEALTH_S); target.tag = HEALTH_I; } early() { i1.evaluate(p1); // spatial competition provides density-dependent selection inds = p1.individuals; competition = i1.totalOfNeighborStrengths(inds); competition = (competition + 1) / (2 * PI * S^2); inds.fitnessScaling = K / competition; // age-based mortality; at age 100 mortality is 100% age_mortality = sqrt((MAX_AGE - inds.age) / MAX_AGE); inds.fitnessScaling = inds.fitnessScaling * age_mortality; // SIR model infected = inds[inds.tag == HEALTH_I]; for (ind in infected) { // make contact with random neighbors each cycle contacts = i1.drawByStrength(ind, rpois(1, INFECTIVITY)); for (contact in contacts) { // if the contact is susceptible, they might get infected if (contact.tag == HEALTH_S) { strength = i1.strength(ind, contact); if (runif(1) < strength) contact.tag = HEALTH_I; } } // die with some probability each cycle if (runif(1) < RATE_DEATH) ind.fitnessScaling = 0.0; // recover with some probability each cycle if (runif(1) < RATE_CLEAR) ind.tag = HEALTH_R; } } late() { inds = p1.individuals; // move around a bit for (ind in inds) { newPos = ind.spatialPosition + runif(2, -0.005, 0.005); ind.setSpatialPosition(p1.pointPeriodic(newPos)); } // color according to health status; S=green, I=red, R=blue inds_tags = inds.tag; inds[inds_tags == HEALTH_S].color = "green"; inds[inds_tags == HEALTH_I].color = "red"; inds[inds_tags == HEALTH_R].color = "blue"; } 1:1000 late() { tags = p1.individuals.tag; cat(sum(tags == HEALTH_S) + ", " + sum(tags == HEALTH_I) + ", " + sum(tags == HEALTH_R) + ", "); if ((sum(tags == HEALTH_I) == 0) & (sim.cycle >= 100)) { catn("\nLOST in cycle " + sim.cycle); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 17.18 - A sexual, age-structured spatial model.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, continuous space, continuous spatial landscape, selfing, spatial competition, spatial mate choice initialize() { initializeSLiMModelType("nonWF"); initializeSLiMOptions(dimensionality="xy"); initializeSex(); defineConstant("K", 300); // carrying-capacity density defineConstant("S", 0.1); // SIGMA_S, the spatial interaction width // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=S * 3); i1.setInteractionFunction("n", 1.0, S); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); i2.setConstraints("receiver", sex="F", minAge=2, maxAge=4); i2.setConstraints("exerter", sex="M", minAge=2); } 1 early() { sim.addSubpop("p1", K); p1.individuals.setSpatialPosition(p1.pointUniform(K)); } 2: first() { // look for mates i2.evaluate(p1); } reproduction(NULL, "F") { // choose our nearest neighbor as a mate, within the max distance mate = i2.nearestInteractingNeighbors(individual, 1); if (mate.size() > 0) subpop.addCrossed(individual, mate, count=rpois(1, 1.5)); } early() { // first, conduct age-related mortality with killIndividuals() inds = p1.individuals; ages = inds.age; inds4 = inds[ages == 4]; inds5 = inds[ages == 5]; inds6 = inds[ages >= 6]; death4 = (runif(inds4.size()) < 0.10); death5 = (runif(inds5.size()) < 0.30); sim.killIndividuals(c(inds4[death4], inds5[death5], inds6)); // disperse prior to density-dependence p1.deviatePositions(NULL, "reprising", INF, "n", 0.02); // spatial competition provides density-dependent selection i1.evaluate(p1); inds = p1.individuals; competition = i1.localPopulationDensity(inds); inds.fitnessScaling = K / competition; } 10000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 17.19 - Modeling indirect competition mediated by resource availability.txt ================================================ // Keywords: resources, foraging, spatial competition, home range, multispecies species all initialize() { initializeSLiMModelType("nonWF"); // Foraging interaction. initializeInteractionType(1, "xy", reciprocal=T, maxDistance=1.5); // Reproduction interaction. initializeInteractionType(2, "xy", reciprocal=T, maxDistance=3, sexSegregation="FM"); } species resourceNode initialize() { initializeSpecies(avatar="🪣", color="cornflowerblue"); initializeSLiMOptions(dimensionality="xy"); } species forager initialize() { initializeSpecies(avatar="🤤", color="red"); initializeSLiMOptions(dimensionality="xy"); initializeSex(); } ticks all 1 early() { // Coordinates for the resource nodes. xs = rep(seq(0.5, 100), 100); ys = repEach(seq(0.5, 100), 100); // Add the resource nodes. resourceNode.addSubpop("p1", 10000); p1.setSpatialBounds(c(0, 0, 100, 100)); p1.individuals.x = xs; p1.individuals.y = ys; p1.individuals.tagF = 10.0; // Initialize the population of foragers. forager.addSubpop("p2", 100000); p2.setSpatialBounds(p1.spatialBounds); p2.individuals.setSpatialPosition(p2.pointUniform(p2.individualCount)); } ticks all 2: first() { // Evaluate the spatial interaction for reproduction. i2.evaluate(p2); } species forager reproduction(NULL, "F") { // Draw the litter size first, and return if it's zero. litterSize = rpois(1, 8); if (litterSize == 0) return; // Draw a random mate from among males in range. mate = i2.drawByStrength(individual, 1, p2); if (size(mate) == 0) return; // Produce the offspring. subpop.addCrossed(individual, mate, count=litterSize); } ticks all 2:100 early() { // Dispersal of new offspring. offspring = p2.subsetIndividuals(maxAge=0); p2.deviatePositions(offspring, "reprising", INF, "n", 1.5); // Evaluate the spatial interaction between resource nodes and foragers. i1.evaluate(c(p2, p1)); // Survival in this model is based entirely on resource availability. p2.individuals.fitnessScaling = 0.0; for (node in p1.individuals) { // Find all foragers within range of the resource node. f = i1.nearestNeighbors(node, p2.individualCount, p2); // Evenly divide resources to all foragers within range. f.fitnessScaling = f.fitnessScaling + node.tagF / size(f); } // In some cases, if the landscape is at very low density, some individuals // might have a fitnessScaling value > 1.0. This value must be capped. p2.individuals.fitnessScaling = pmin(p2.individuals.fitnessScaling, 1.0); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.2 - Spatial competition.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries, spatial competition initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // Set up an interaction for spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // initial positions are random in ([0,1], [0,1]) p1.individuals.x = runif(p1.individualCount); p1.individuals.y = runif(p1.individualCount); } 1: late() { // evaluate interactions before fitness calculations i1.evaluate(p1); } fitnessEffect() { // spatial competition totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // draw a child position near the first parent, within bounds do child.x = parent1.x + rnorm(1, 0, 0.02); while ((child.x < 0.0) | (child.x > 1.0)); do child.y = parent1.y + rnorm(1, 0, 0.02); while ((child.y < 0.0) | (child.y > 1.0)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.3 - Boundaries and boundary conditions I (stopping boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, stopping boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Stopping boundary conditions pos = parent1.spatialPosition + rnorm(2, 0, 0.02); child.setSpatialPosition(p1.pointStopped(pos)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.3 - Boundaries and boundary conditions II (reflecting boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, reflecting boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Reflecting boundary conditions pos = parent1.spatialPosition + rnorm(2, 0, 0.02); child.setSpatialPosition(p1.pointReflected(pos)); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.3 - Boundaries and boundary conditions III (absorbing boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, absorbing boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Absorbing boundary conditions pos = parent1.spatialPosition + rnorm(2, 0, 0.02); if (!p1.pointInBounds(pos)) return F; child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.3 - Boundaries and boundary conditions IV (reprising boundaries).txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } modifyChild() { // Reprising boundary conditions do pos = parent1.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.3 - Boundaries and boundary conditions V (dispersal kernels).txt ================================================ // Keywords: continuous space, continuous spatial landscape, boundaries, boundary conditions, dispersal kernel initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); // competition i1.setInteractionFunction("n", 3.0, 0.1); } 1 late() { sim.addSubpop("p1", 500); // Initial positions are random within spatialBounds p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { // Dispersal and boundary enforcement p1.deviatePositions(NULL, "reprising", INF, "n", 0.02); // Evaluate for competition i1.evaluate(p1); } fitnessEffect() { totalStrength = i1.totalOfNeighborStrengths(individual); return 1.1 - totalStrength / p1.individualCount; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.4 - Mate choice with a spatial kernel.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); i1.setInteractionFunction("n", 3.0, 0.1); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); i2.setInteractionFunction("n", 1.0, 0.02); } 1 late() { sim.addSubpop("p1", 500); p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); inds = sim.subpopulations.individuals; competition = i1.totalOfNeighborStrengths(inds); inds.fitnessScaling = 1.1 - competition / size(inds); } 2: first() { i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } modifyChild() { do pos = parent1.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.5 - Mate choice with a nearest-neighbor search.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries initialize() { initializeSLiMOptions(dimensionality="xy"); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); // spatial competition initializeInteractionType(1, "xy", reciprocal=T, maxDistance=0.3); i1.setInteractionFunction("n", 3.0, 0.1); // spatial mate choice initializeInteractionType(2, "xy", reciprocal=T, maxDistance=0.1); } 1 late() { sim.addSubpop("p1", 500); p1.individuals.setSpatialPosition(p1.pointUniform(500)); } 1: late() { i1.evaluate(p1); inds = sim.subpopulations.individuals; competition = i1.totalOfNeighborStrengths(inds); inds.fitnessScaling = 1.1 - competition / size(inds); } 2: first() { i2.evaluate(p1); } mateChoice() { // nearest-neighbor spatial mate choice neighbors = i2.nearestNeighbors(individual, 3); return (size(neighbors) ? sample(neighbors, 1) else float(0)); } modifyChild() { do pos = parent1.spatialPosition + rnorm(2, 0, 0.02); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 17.6 - Divergence due to phenotypic competition with an interaction() callback.txt ================================================ // Keywords: QTL, quantitative trait loci, phenotypic competition, interaction() initialize() { defineConstant("OPTIMUM", 5.0); defineConstant("SIGMA_K", 1.0); defineConstant("SIGMA_C", 0.4); defineConstant("NORM", dnorm(0.0, mean=0, sd=SIGMA_C)); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); initializeInteractionType(1, "", reciprocal=T); // competition i1.setInteractionFunction("f", 1.0); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", 500); } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + dnorm(phenotypes, OPTIMUM, SIGMA_K); inds.tagF = phenotypes; // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } interaction(i1) { return dnorm(exerter.tagF, receiver.tagF, SIGMA_C) / NORM; } 1:2001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.tagF; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: SLiMgui/Recipes/Recipe 17.7 - Modeling phenotype as a spatial dimension.txt ================================================ // Keywords: QTL, quantitative trait loci, phenotypic competition initialize() { defineConstant("OPTIMUM", 5.0); defineConstant("SIGMA_K", 1.0); defineConstant("SIGMA_C", 0.4); defineConstant("NORM", dnorm(0.0, mean=0, sd=SIGMA_C)); initializeSLiMOptions(dimensionality="x"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); initializeInteractionType(1, "x", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", 500); p1.setSpatialBounds(c(0.0, 10.0)); } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + dnorm(phenotypes, OPTIMUM, SIGMA_K); inds.x = phenotypes; // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 1:2001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.x; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: SLiMgui/Recipes/Recipe 17.8 - Sympatric speciation facilitated by assortative mating.txt ================================================ // Keywords: QTL, quantitative trait loci, phenotypic competition initialize() { defineConstant("OPTIMUM", 5.0); defineConstant("SIGMA_K", 1.0); defineConstant("SIGMA_C", 0.4); defineConstant("SIGMA_M", 0.5); defineConstant("NORM", dnorm(0.0, mean=0, sd=SIGMA_C)); initializeSLiMOptions(dimensionality="x"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.01)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); // competition initializeInteractionType(1, "x", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); // mate choice initializeInteractionType(2, "x", reciprocal=T, maxDistance=SIGMA_M * 3); i2.setInteractionFunction("n", 1.0, SIGMA_M); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", 500); p1.setSpatialBounds(c(0.0, 10.0)); } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); inds.fitnessScaling = 1.0 + dnorm(phenotypes, OPTIMUM, SIGMA_K); inds.x = phenotypes; // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 2: first() { // evaluate mate choice in preparation for reproduction i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } 1:2001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.x; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: SLiMgui/Recipes/Recipe 17.9 - Speciation due to spatial variation in selection.txt ================================================ // Keywords: continuous space, continuous spatial landscape, reprising boundaries, QTL, quantitative trait loci, spatial competition, phenotypic competition, spatial mate choice initialize() { defineConstant("SIGMA_C", 0.1); defineConstant("SIGMA_K", 0.5); defineConstant("SIGMA_M", 0.1); defineConstant("SLOPE", 1.0); defineConstant("N", 500); initializeSLiMOptions(dimensionality="xyz"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "n", 0.0, 1.0); // QTL m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1, m2), c(1, 0.1)); initializeGenomicElement(g1, 0, 1e5 - 1); initializeRecombinationRate(1e-8); // competition initializeInteractionType(1, "xyz", reciprocal=T, maxDistance=SIGMA_C * 3); i1.setInteractionFunction("n", 1.0, SIGMA_C); // mate choice initializeInteractionType(2, "xyz", reciprocal=T, maxDistance=SIGMA_M * 3); i2.setInteractionFunction("n", 1.0, SIGMA_M); } mutationEffect(m2) { return 1.0; } 1 late() { sim.addSubpop("p1", N); p1.setSpatialBounds(c(0.0, 0.0, -SLOPE, 1.0, 1.0, SLOPE)); p1.individuals.setSpatialPosition(p1.pointUniform(N)); p1.individuals.z = 0.0; } modifyChild() { // set offspring position based on parental position do pos = c(parent1.spatialPosition[0:1] + rnorm(2, 0, 0.005), 0.0); while (!p1.pointInBounds(pos)); child.setSpatialPosition(pos); return T; } 1: late() { inds = sim.subpopulations.individuals; // construct phenotypes and fitness effects from QTLs phenotypes = inds.sumOfMutationsOfType(m2); optima = (inds.x - 0.5) * SLOPE; inds.fitnessScaling = 1.0 + dnorm(phenotypes, optima, SIGMA_K); inds.z = phenotypes; // color individuals according to phenotype for (ind in inds) { hue = ((ind.z + SLOPE) / (SLOPE * 2)) * 0.66; ind.color = rgb2color(hsv2rgb(c(hue, 1.0, 1.0))); } // evaluate phenotypic competition i1.evaluate(p1); competition = sapply(inds, "sum(i1.strength(applyValue));"); effects = 1.0 - competition / size(inds); inds.fitnessScaling = inds.fitnessScaling * effects; } 2: first() { // evaluate mate choice in preparation for reproduction i2.evaluate(p1); } mateChoice() { // spatial mate choice return i2.strength(individual); } 1:5001 late() { if (sim.cycle == 1) cat(" cyc mean sd\n"); if (sim.cycle % 100 == 1) { phenotypes = p1.individuals.z; cat(format("%5d ", sim.cycle)); cat(format("%6.2f ", mean(phenotypes))); cat(format("%6.2f\n", sd(phenotypes))); } } ================================================ FILE: SLiMgui/Recipes/Recipe 18.1 - A minimal tree-seq model.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 5000 late() { sim.treeSeqOutput("./overlay.trees"); } // Section 17.2's recipe, which is a Python script that overlays // neutral mutations onto the .trees file saved here, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: SLiMgui/Recipes/Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(simplificationRatio=INF, timeUnit="generations"); initializeMutationRate(0); initializeMutationType("m2", 0.5, "f", 1.0); m2.convertToSubstitution = F; initializeGenomicElementType("g1", m2, 1); initializeGenomicElement(g1, 0, 1e6 - 1); initializeRecombinationRate(3e-10); } 1 late() { sim.addSubpop("p1", 1e5); } 100 late() { sample(p1.haplosomes, 1).addNewDrawnMutation(m2, 5e5); } 100:10000 late() { mut = sim.mutationsOfType(m2); if (mut.size() != 1) stop(sim.cycle + ": LOST"); else if (sum(sim.mutationFrequencies(NULL, mut)) == 1.0) { sim.treeSeqOutput("decap.trees"); sim.simulationFinished(); } } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: SLiMgui/Recipes/Recipe 18.10 - Adding a neutral burn-in after simulation with recapitation II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import tskit, pyslim import numpy as np import matplotlib.pyplot as plt # Load the .trees file ts = tskit.load("decap.trees") # no simplify! # Calculate tree heights, giving uncoalesced sites the maximum time def tree_heights(ts): heights = np.zeros(ts.num_trees + 1) for tree in ts.trees(): if tree.num_roots > 1: # not fully coalesced heights[tree.index] = ts.metadata['SLiM']['tick'] else: children = tree.children(tree.root) real_root = tree.root if len(children) > 1 else children[0] heights[tree.index] = tree.time(real_root) heights[-1] = heights[-2] # repeat the last entry for plotting with step return heights # Plot tree heights before recapitation breakpoints = list(ts.breakpoints()) heights = tree_heights(ts) plt.step(breakpoints, heights, where='post') plt.show() # Recapitate! recap = pyslim.recapitate(ts, ancestral_Ne=1e5, recombination_rate=3e-10, random_seed=1) recap.dump("recap.trees") # Plot the tree heights after recapitation breakpoints = list(recap.breakpoints()) heights = tree_heights(recap) plt.step(breakpoints, heights, where='post') plt.show() ================================================ FILE: SLiMgui/Recipes/Recipe 18.11 - Optimizing tree-sequence simplification.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, simplification initialize() { setSeed(0); initializeTreeSeq(simplificationInterval=1000); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 10000); } 50001 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 18.2 - Overlaying neutral mutations.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording # This is a Python recipe, to be run after the section 17.1 recipe import msprime, tskit ts = tskit.load("./overlay.trees") ts = ts.simplify() for t in ts.trees(): assert t.num_roots == 1, ("not coalesced! on segment {} to {}".format(t.interval[0], t.interval[1])) mutated = msprime.sim_mutations(ts, rate=1e-7, random_seed=1, keep=True) mutated.dump("./overlay_II.trees") ================================================ FILE: SLiMgui/Recipes/Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "g", -0.01, 1.0); // deleterious initializeMutationType("m3", 1.0, "f", 0.05); // introduced initializeGenomicElementType("g1", c(m1, m2), c(0.9, 0.1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m3, 10000); defineConstant("PATH", tempdir() + "slim_" + simID + ".trees"); sim.outputFull(PATH); } 1000:100000 late() { if (sim.countOfMutationsOfType(m3) == 0) { if (sum(sim.substitutions.mutationType == m3) == 1) { cat(simID + ": FIXED\n"); sim.simulationFinished(); } else { cat(simID + ": LOST - RESTARTING\n"); sim.readFromPopulationFile(PATH); } } } ================================================ FILE: SLiMgui/Recipes/Recipe 18.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, conditional sweep initialize() { initializeSLiMOptions(keepPedigrees=T); initializeTreeSeq(); initializeMutationRate(1e-8); initializeMutationType("m2", 0.5, "g", -0.01, 1.0); // deleterious initializeMutationType("m3", 1.0, "f", 0.05); // introduced initializeGenomicElementType("g1", m2, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // assign tag values to be preserved inds = sortBy(sim.subpopulations.individuals, "pedigreeID"); tags = rdunif(size(inds), 0, 100000); inds.tag = tags; // record tag values and pedigree IDs in metadata metadataDict = Dictionary("tags", tags, "ids", inds.pedigreeID); target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m3, 10000); defineConstant("PATH", tempdir() + "slim_" + simID + ".trees"); sim.treeSeqOutput(PATH, metadata=metadataDict); } 1000:100000 late() { if (sim.countOfMutationsOfType(m3) == 0) { if (sum(sim.substitutions.mutationType == m3) == 1) { cat(simID + ": FIXED\n"); sim.treeSeqOutput("slim_" + simID + "_FIXED.trees"); sim.simulationFinished(); } else { cat(simID + ": LOST - RESTARTING\n"); sim.readFromPopulationFile(PATH); metadataDict = treeSeqMetadata(PATH); tags = metadataDict.getValue("tags"); inds = sortBy(sim.subpopulations.individuals, "pedigreeID"); inds.tag = tags; } } } ================================================ FILE: SLiMgui/Recipes/Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { defineConstant("N", 10000); // pop size defineConstant("L", 1e8); // total chromosome length defineConstant("L0", 200e3); // between genes defineConstant("L1", 1e3); // gene length initializeTreeSeq(); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8, L-1); initializeMutationType("m2", 0.5, "g", -(5/N), 1.0); initializeGenomicElementType("g2", m2, 1.0); for (start in seq(from=L0, to=L-(L0+L1), by=(L0+L1))) initializeGenomicElement(g2, start, (start+L1)-1); } 1 early() { sim.addSubpop("p1", N); } 10*N late() { sim.treeSeqOutput("./diversity.trees"); } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: SLiMgui/Recipes/Recipe 18.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording # This is a Python recipe; note that it runs the SLiM model internally, below import subprocess, tskit import matplotlib.pyplot as plt import numpy as np # Run the SLiM model and load the resulting .trees subprocess.check_output(["slim", "-m", "-s", "0", "./diversity.slim"]) ts = tskit.load("./diversity.trees") ts = ts.simplify() # Measure the tree height at each base position height_for_pos = np.zeros(int(ts.sequence_length)) for tree in ts.trees(): mean_height = np.mean([tree.time(root) for root in tree.roots]) left, right = map(int, tree.interval) height_for_pos[left: right] = mean_height # Convert heights along chromosome into heights at distances from gene height_for_pos = height_for_pos - np.min(height_for_pos) L, L0, L1 = int(1e8), int(200e3), int(1e3) # total length, length between genes, gene length gene_starts = np.arange(L0, L - (L0 + L1) + 1, L0 + L1) gene_ends = gene_starts + L1 - 1 max_d = L0 // 4 height_for_left_dist = np.zeros(max_d) height_for_right_dist = np.zeros(max_d) for d in range(max_d): height_for_left_dist[d] = np.mean(height_for_pos[gene_starts - d - 1]) height_for_right_dist[d] = np.mean(height_for_pos[gene_ends + d + 1]) height_for_distance = np.hstack([height_for_left_dist[::-1], height_for_right_dist]) distances = np.hstack([np.arange(-max_d, 0), np.arange(1, max_d + 1)]) # Make a simple plot plt.plot(distances, height_for_distance) plt.show() ================================================ FILE: SLiMgui/Recipes/Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, migration, dispersal initialize() { defineConstant("L", 1e8); initializeTreeSeq(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.1); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); sim.treeSeqRememberIndividuals(sim.subpopulations.individuals); p1.haplosomes.addNewDrawnMutation(m1, asInteger(L * 0.2)); p2.haplosomes.addNewDrawnMutation(m1, asInteger(L * 0.8)); sim.addSubpop("p3", 1000); p3.setMigrationRates(c(p1, p2), c(0.5, 0.5)); } 2 late() { p3.setMigrationRates(c(p1, p2), c(0.0, 0.0)); p1.setSubpopulationSize(0); p2.setSubpopulationSize(0); } 2: late() { if (sim.mutationsOfType(m1).size() == 0) { sim.treeSeqOutput("./admix.trees"); sim.simulationFinished(); } } 10000 late() { stop("Did not reach fixation of beneficial alleles."); } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: SLiMgui/Recipes/Recipe 18.5 - Mapping admixture (analyzing ancestry in Python) II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording # This is a Python recipe; note that it runs the SLiM model internally, below import subprocess, tskit import matplotlib.pyplot as plt import numpy as np # Run the SLiM model and load the resulting .trees file subprocess.check_output(["slim", "-m", "-s", "0", "./admix.slim"]) ts = tskit.load("./admix.trees") # Load the .trees file and assess true local ancestry breaks = np.zeros(ts.num_trees + 1) ancestry = np.zeros(ts.num_trees + 1) for tree in ts.trees(): subpop_sum, subpop_weights = 0, 0 for root in tree.roots: leaves_count = tree.num_samples(root) - 1 # subtract one for the root, which is a sample subpop_sum += tree.population(root) * leaves_count subpop_weights += leaves_count breaks[tree.index] = tree.interval[0] ancestry[tree.index] = subpop_sum / subpop_weights breaks[-1] = ts.sequence_length ancestry[-1] = ancestry[-2] # Make a simple plot plt.plot(breaks, ancestry) plt.show() ================================================ FILE: SLiMgui/Recipes/Recipe 18.6 - Measuring the coalescence time of a model I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(checkCoalescence=T); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1: late() { if (sim.treeSeqCoalesced()) { catn(sim.cycle + ": COALESCED"); sim.simulationFinished(); } } 100000 late() { catn("NO COALESCENCE BY CYCLE 100000"); } ================================================ FILE: SLiMgui/Recipes/Recipe 18.6 - Measuring the coalescence time of a model II.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording, deferred scheduling, variable-length burn-in initialize() { initializeTreeSeq(checkCoalescence=T); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1: late() { if (sim.treeSeqCoalesced()) { catn(community.tick + ": COALESCED"); defineConstant("COALESCE", community.tick); community.deregisterScriptBlock(self); } } COALESCE+100 late() { catn(community.tick + ": FINISHED"); sim.simulationFinished(); } 100000 late() { catn("NO COALESCENCE BY CYCLE 100000"); } ================================================ FILE: SLiMgui/Recipes/Recipe 18.7 - Analyzing selection coefficients in Python with tskit I.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording initialize() { initializeTreeSeq(); initializeMutationRate(1e-10); initializeMutationType("m1", 0.5, "g", 0.1, 0.1); initializeMutationType("m2", 0.5, "g", -0.1, 0.1); initializeGenomicElementType("g1", c(m1, m2), c(1.0, 1.0)); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 20000 late() { sim.treeSeqOutput("./selcoeff.trees"); } // Part II of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: SLiMgui/Recipes/Recipe 18.7 - Analyzing selection coefficients in Python with tskit II.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import tskit ts = tskit.load("selcoeff.trees") # selection coefficients of all selected mutations coeffs = [] for mut in ts.mutations(): md = mut.metadata sel = [x["selection_coeff"] for x in md["mutation_list"]] if any([s != 0 for s in sel]): coeffs += sel b = [x for x in coeffs if x > 0] d = [x for x in coeffs if x < 0] print("Beneficial: " + str(len(b)) + ", mean " + str(sum(b) / len(b))) print("Deleterious: " + str(len(d)) + ", mean " + str(sum(d) / len(d))) ================================================ FILE: SLiMgui/Recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history I.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import msprime, pyslim ts = msprime.sim_ancestry(samples=5000, population_size=5000, sequence_length=1e8, recombination_rate=1e-8) slim_ts = pyslim.annotate(ts, model_type="WF", tick=1) slim_ts.dump("coalasex.trees") ================================================ FILE: SLiMgui/Recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history II.txt ================================================ // Keywords: tree-sequence recording, tree sequence recording // Part I of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ initialize() { initializeTreeSeq(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.1); initializeGenomicElementType("g1", m2, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } 1 late() { sim.readFromPopulationFile("coalasex.trees"); target = sample(sim.subpopulations.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1: late() { if (sim.mutationsOfType(m2).size() == 0) { print(sim.substitutions.size() ? "FIXED" else "LOST"); sim.treeSeqOutput("coalasex_II.trees"); sim.simulationFinished(); } } 2000 early() { sim.simulationFinished(); } // Part III of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ ================================================ FILE: SLiMgui/Recipes/Recipe 18.8 - Starting a hermaphroditic WF model with a coalescent history III.py ================================================ # Keywords: Python, tree-sequence recording, tree sequence recording import tskit ts = tskit.load("coalasex_II.trees").simplify() for tree in ts.trees(): for root in tree.roots: print(tree.time(root)) ================================================ FILE: SLiMgui/Recipes/Recipe 18.9 - Starting a sexual nonWF model with a coalescent history I.py ================================================ # Keywords: Python, nonWF, non-Wright-Fisher, tree-sequence recording, tree sequence recording import msprime, pyslim, random import numpy as np ts = msprime.sim_ancestry(samples=5000, population_size=5000, sequence_length=1e8, recombination_rate=1e-8) tables = ts.dump_tables() pyslim.annotate_tables(tables, model_type="nonWF", tick=1) # add sexes and ages individual_metadata = [ind.metadata for ind in tables.individuals] for md in individual_metadata: md["sex"] = random.choice([pyslim.INDIVIDUAL_TYPE_FEMALE, pyslim.INDIVIDUAL_TYPE_MALE]) md["age"] = random.choice([0, 1, 2, 3, 4]) ims = tables.individuals.metadata_schema tables.individuals.packset_metadata( [ims.validate_and_encode_row(md) for md in individual_metadata]) # add selected mutation mut_ind_id = random.choice(range(tables.individuals.num_rows)) mut_node_id = random.choice(np.where(tables.nodes.individual == mut_ind_id)[0]) mut_node = tables.nodes[mut_node_id] mut_metadata = { "mutation_list": [ { "mutation_type": 2, "selection_coeff": 0.1, "subpopulation": mut_node.population, "slim_time": int(tables.metadata['SLiM']['tick'] - mut_node.time), "nucleotide": -1 } ] } site_num = tables.sites.add_row(position=5000, ancestral_state='') tables.mutations.add_row( node=mut_node_id, site=site_num, derived_state='1', time=mut_node.time, metadata=mut_metadata) slim_ts = tables.tree_sequence() slim_ts.dump("coalsex.trees") ================================================ FILE: SLiMgui/Recipes/Recipe 18.9 - Starting a sexual nonWF model with a coalescent history II.txt ================================================ // Keywords: nonWF, non-Wright-Fisher, sexual, tree-sequence recording, tree sequence recording, reproduction() // Part I of this recipe, which is a Python script, may be found in // the Recipes archive downloadable at https://messerlab.org/slim/ initialize() { initializeSLiMModelType("nonWF"); initializeTreeSeq(); initializeSex(); initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.1); m2.convertToSubstitution=T; initializeGenomicElementType("g1", m2, 1.0); initializeGenomicElement(g1, 0, 1e8-1); initializeRecombinationRate(1e-8); } reproduction(NULL, "F") { subpop.addCrossed(individual, subpop.sampleIndividuals(1, sex="M")); } 1 early() { sim.readFromPopulationFile("coalsex.trees"); } early() { p0.fitnessScaling = 5000 / p0.individualCount; } 1: late() { if (sim.mutationsOfType(m2).size() == 0) { print(sim.substitutions.size() ? "FIXED" else "LOST"); sim.treeSeqOutput("coalsex_II.trees"); sim.simulationFinished(); } } 2000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.1 - A simple neutral nucleotide-based model.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 1e6); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-7)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.10 - Varying the mutation rate along the chromosome in a nucleotide-based model.txt ================================================ // Keywords: nucleotide-based, hot spot, cold spot, variable mutation rate initialize() { defineConstant("L", 1e5); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); m1.color = "black"; initializeGenomicElementType("g1", m1, 1.0, mmKimura(1.8e-07, 6e-08)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); ends = c(sort(sample(0:(L-2), 99)), L-1); multipliers = rlnorm(100, 0.0, 0.75); initializeHotspotMap(multipliers, ends); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.11 - Modeling GC-biased gene conversion (gBGC).txt ================================================ // Keywords: nucleotide-based, gene conversion, GC-bias, GC biased gene conversion, GC content initialize() { defineConstant("L", 1e5); defineConstant("alpha", 2.5e-6); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(alpha)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-5); initializeGeneConversion(0.7, 1500, 0.80, 0.10); } 1 early() { sim.addSubpop("p1", 500); } 1:500001 early() { if (sim.cycle % 1000 == 1) { cat(sim.cycle + ": "); print(nucleotideFrequencies(sim.chromosomes.ancestralNucleotides())); } } ================================================ FILE: SLiMgui/Recipes/Recipe 19.12 - Reading VCF files to create nucleotide-based SNPs.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence, VCF file reading, empirical population, SNPs, 1000 Genomes Project // The input files used here can be downloaded from http://benhaller.com/slim/recipe_19_12_files.zip initialize() { initializeSLiMOptions(nucleotideBased=T); length = initializeAncestralNucleotides("hs37d5_chr22_patched.fa"); defineConstant("L", length); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeMutationTypeNuc("m2", 0.5, "f", 0.0); m2.color = "red"; m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(0.0)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 99); p1.haplosomes.readHaplosomesFromVCF("chr22_filtered.recode.vcf", m1); p1.setSubpopulationSize(1000); } 5 late() { mut = sample(sim.mutations, 1); mut.setMutationType(m2); mut.setSelectionCoeff(0.5); } 1:2000 late() { mut = sim.mutationsOfType(m2); if (mut.size()) { f = sim.mutationFrequencies(p1, mut); catn(sim.cycle + ": " + sim.mutations.size() + ", f = " + f); if (f == 1.0) { catn("\nFIXED in cycle " + sim.cycle); catn(sim.substitutions.size() + " substitutions."); catn(paste(sim.substitutions.nucleotide)); sim.simulationFinished(); } } else { catn(sim.cycle + ": " + sim.mutations.size()); } } ================================================ FILE: SLiMgui/Recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models I.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence, sequence-based mutation rate initialize() { defineConstant("L", 1e5); initializeSLiMOptions(nucleotideBased=T); initializeTreeSeq(); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-6)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-6); } 1 early() { sim.addSubpop("p1", 1000); } 1000 late() { sim.treeSeqOutput("recipe_nucleotides.trees"); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models II.py ================================================ # Keywords: Python, nucleotide-based, nucleotide sequence, sequence-based mutation rate import tskit, pyslim import numpy as np ts = tskit.load("recipe_nucleotides.trees") M = [[0 for _ in pyslim.NUCLEOTIDES] for _ in pyslim.NUCLEOTIDES] for mut in ts.mutations(): mut_list = mut.metadata["mutation_list"] k = np.argmax([u["slim_time"] for u in mut_list]) derived_nuc = mut_list[k]["nucleotide"] if mut.parent == -1: acgt = ts.reference_sequence.data[int(ts.site(mut.site).position)] parent_nuc = pyslim.NUCLEOTIDES.index(acgt) else: parent_mut = ts.mutation(mut.parent) assert(parent_mut.site == mut.site) parent_nuc = parent_mut.metadata["mutation_list"][0]["nucleotide"] M[parent_nuc][derived_nuc] += 1 print("{}\t{}\t{}".format('ancestral', 'derived', 'count')) for j, a in enumerate(pyslim.NUCLEOTIDES): for k, b in enumerate(pyslim.NUCLEOTIDES): print("{}\t{}\t{}".format(a, b, M[j][k])) ================================================ FILE: SLiMgui/Recipes/Recipe 19.13 - Tree-sequence recording and nucleotide-based models III.py ================================================ # Keywords: Python, nucleotide-based, nucleotide sequence, sequence-based mutation rate import tskit, pyslim import numpy as np ts = tskit.load("recipe_nucleotides.trees") slim_gen = ts.metadata["SLiM"]["tick"] M = np.zeros((4,4,4,4), dtype='int') for mut in ts.mutations(): pos = ts.site(mut.site).position # skip mutations at the end of the sequence if pos > 0 and pos < ts.sequence_length - 1: mut_list = mut.metadata["mutation_list"] k = np.argmax([u["slim_time"] for u in mut_list]) derived_nuc = mut_list[k]["nucleotide"] pretime = mut.time + 1.0 left_nuc = pyslim.nucleotide_at(ts, mut.node, pos - 1, time = pretime) right_nuc = pyslim.nucleotide_at(ts, mut.node, pos + 1, time = pretime) parent_nuc = pyslim.nucleotide_at(ts, mut.node, pos, time = pretime) M[left_nuc, parent_nuc, right_nuc, derived_nuc] += 1 print("{}\t{}\t{}".format('ancestral', 'derived', 'count')) for j0, a0 in enumerate(pyslim.NUCLEOTIDES): for j1, a1 in enumerate(pyslim.NUCLEOTIDES): for j2, a2 in enumerate(pyslim.NUCLEOTIDES): for k, b in enumerate(pyslim.NUCLEOTIDES): print("{}{}{}\t{}{}{}\t{}".format(a0, a1, a2, a0, b, a2, M[j0, j1, j2, k])) ================================================ FILE: SLiMgui/Recipes/Recipe 19.14 - Modeling identity by state (IBS) (uniquing mutations with a mutation() callback).txt ================================================ // Keywords: IBS, identity by state, IBD, identity by descent, unique down initialize() { initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(100)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-4 / 3)); initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(1e-3); } 1 early() { sim.addSubpop("p1", 500); } mutation() { m = sim.subsetMutations(position=mut.position, nucleotide=mut.nucleotide); if (m.length()) return m; return T; } 1000 late() { for (pos in 0:99) { muts = sim.subsetMutations(position=pos); nucs = muts.nucleotide; cat(pos + " : " + paste(nucs)); if (size(nucs) != size(unique(nucs))) cat(" DUPLICATES!"); catn(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 19.15 - Modeling identity by state (IBS) (uniquing back-mutations to the ancestral state).txt ================================================ // Keywords: IBS, identity by state, IBD, identity by descent, unique down, back-mutation initialize() { initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(100)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-4 / 3)); initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(1e-3); } 1 early() { sim.addSubpop("p1", 500); } mutation() { m = sim.subsetMutations(position=mut.position, nucleotide=mut.nucleotide); if (m.length()) return m; return T; } late() { // unique new mutations down to the ancestral state muts = sim.mutations; new_muts = muts[muts.originTick == community.tick]; back_muts = NULL; for (mut in new_muts) { pos = mut.position; if (mut.nucleotide == sim.chromosomes.ancestralNucleotides(pos, pos)) back_muts = c(back_muts, mut); } if (size(back_muts)) sim.subpopulations.haplosomes.removeMutations(back_muts); } 1000 late() { for (pos in 0:99) { muts = sim.subsetMutations(position=pos); nucs = muts.nucleotide; ancestral = sim.chromosomes.ancestralNucleotides(pos, pos); cat(pos + " : " + paste(nucs)); if (size(nucs) != size(unique(nucs))) cat(" DUPLICATES!"); if (any(nucs == ancestral)) cat(" BACK-MUTATION (" + ancestral + ")!"); catn(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 19.2 - Reading an ancestral nucleotide sequence from a FASTA file.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence initialize() { initializeSLiMOptions(nucleotideBased=T); defineConstant("L", initializeAncestralNucleotides("FASTA.txt")); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmKimura(1.8e-07, 6e-08)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.3 - Sequence output from nucleotide-based models.txt ================================================ // Keywords: nucleotide-based, nucleotide sequence initialize() { defineConstant("L", 10); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-5)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { c = sim.chromosomes; catn("Ancestral: " + c.ancestralNucleotides()); catn("Ancestral: " + paste(c.ancestralNucleotides(format="char"))); catn("Ancestral: " + paste(c.ancestralNucleotides(format="integer"))); catn("positions: " + paste(0:(L-1))); catn(); sim.addSubpop("p1", 500); } 5000 late() { catn("Fixed: " + paste(sim.substitutions.nucleotide)); catn("Fixed: " + paste(sim.substitutions.nucleotideValue)); catn("positions: " + paste(sim.substitutions.position)); catn(); c = sim.chromosomes; catn("Ancestral: " + c.ancestralNucleotides()); catn("Ancestral: " + paste(c.ancestralNucleotides(format="char"))); catn("Ancestral: " + paste(c.ancestralNucleotides(format="integer"))); catn("positions: " + paste(0:(L-1))); catn(); g = p1.haplosomes[0]; catn("SNPs: " + paste(g.mutations.nucleotide)); catn("SNPs: " + paste(g.mutations.nucleotideValue)); catn("positions: " + paste(g.mutations.position)); catn(); catn("Derived: " + g.nucleotides()); catn("Derived: " + paste(g.nucleotides(format="char"))); catn("Derived: " + paste(g.nucleotides(format="integer"))); catn("positions: " + paste(0:(L-1))); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.4 - Back-mutations, independent mutational lineages, and VCF output.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 10); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-5)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); } 5000 late() { g = p1.sampleIndividuals(5).haplosomes; g.outputHaplosomesToVCF(simplifyNucleotides=F); g.outputHaplosomesToVCF(simplifyNucleotides=T); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.5 - Modeling elevated CpG mutation rates and equilibrium nucleotide frequencies.txt ================================================ // Keywords: nucleotide-based, sequence-based mutation rate initialize() { defineConstant("L", 1e5); defineConstant("mu", 7.5e-6); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); mm = mm16To256(mmJukesCantor(mu / 3)); xcg = c("ACG", "CCG", "GCG", "TCG"); xcg_codons = nucleotidesToCodons(paste0(xcg)); mm[xcg_codons,3] = mm[xcg_codons,3] * 20; // rates to T cgx = c("CGA", "CGC", "CGG", "CGT"); cgx_codons = nucleotidesToCodons(paste0(cgx)); mm[cgx_codons,0] = mm[cgx_codons,0] * 20; // rates to A initializeGenomicElementType("g1", m1, 1.0, mutationMatrix=mm); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 10); } 1:10000000 early() { if (sim.cycle % 10000 == 1) { cat(sim.cycle + ": "); print(nucleotideFrequencies(sim.chromosomes.ancestralNucleotides())); } } ================================================ FILE: SLiMgui/Recipes/Recipe 19.6 - A nucleotide-based model with introduced non-nucleotide-based mutations.txt ================================================ // Keywords: nucleotide-based, non-nucleotide-based, mixed model initialize() { defineConstant("L", 1e5); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.1); m2.convertToSubstitution = F; m2.color = "red"; initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-7)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 late() { sim.addSubpop("p1", 500); sample(p1.haplosomes, 10).addNewDrawnMutation(m2, 20000); } 2000 late() { print(sim.mutationsOfType(m2)); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.7 - Using standard SLiM fitness effects with nucleotides (modeling synonymous sites).txt ================================================ // Keywords: nucleotide-based initialize() { initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(3e5)); mm = mmJukesCantor(2.5e-8); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); // neutral initializeMutationTypeNuc("m2", 0.1, "g", -0.03, 0.2); // deleterious initializeGenomicElementType("g1", c(m1,m2), c(3,3), mm); // pos 1/2 initializeGenomicElementType("g2", c(m1,m2), c(5,1), mm); // pos 3 initializeRecombinationRate(1e-8); types = rep(c(g1,g2), 1e5); starts = repEach(seqLen(1e5) * 3, 2) + rep(c(0,2), 1e5); ends = starts + rep(c(1,0), 1e5); initializeGenomicElement(types, starts, ends); } 1 early() { sim.addSubpop("p1", 500); } 1e6 late() { sub = sim.substitutions; pos3 = (sub.position % 3 == 2); pos12 = !pos3; catn(size(sub) + " substitutions occurred."); catn(mean(sub.mutationType == m1)*100 + "% are neutral."); catn(mean(sub.mutationType == m2)*100 + "% are non-neutral."); catn(); catn(size(sub[pos12]) + " substitutions are at position 1 or 2."); catn(mean(sub[pos12].mutationType == m1)*100 + "% are neutral."); catn(mean(sub[pos12].mutationType == m2)*100 + "% are non-neutral."); catn(); catn(size(sub[pos3]) + " substitutions are at position 3."); catn(mean(sub[pos3].mutationType == m1)*100 + "% are neutral."); catn(mean(sub[pos3].mutationType == m2)*100 + "% are non-neutral."); catn(); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.8 - Defining sequence-based fitness effects at the nucleotide level.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 1e4); defineConstant("EFF", c(1.0, 0.1, 1.5, 3.0)); initializeSLiMOptions(nucleotideBased=T); seq = randomNucleotides(100) + 'A' + randomNucleotides(1e4 - 101); initializeAncestralNucleotides(seq); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-7)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } late() { if (sum(sim.mutations.position == 100) == 0) s1.active = 0; } s1 fitnessEffect() { nuc1 = individual.haploidGenome1.nucleotides(100, 100, format="integer"); nuc2 = individual.haploidGenome2.nucleotides(100, 100, format="integer"); return EFF[nuc1] * EFF[nuc2]; } 10000 late() { subs = sim.substitutions[sim.substitutions.position == 100]; for (sub in subs) catn("Sub to " + sub.nucleotide + " in " + sub.fixationTick); } ================================================ FILE: SLiMgui/Recipes/Recipe 19.9 - Defining sequence-based fitness effects at the amino acid level.txt ================================================ // Keywords: nucleotide-based initialize() { defineConstant("L", 1e4); defineConstant("TAA", nucleotidesToCodons("TAA")); defineConstant("TAG", nucleotidesToCodons("TAG")); defineConstant("TGA", nucleotidesToCodons("TGA")); defineConstant("STOP", c(TAA, TAG, TGA)); defineConstant("NONSTOP", (0:63)[match(0:63, STOP) < 0]); codons = sample(NONSTOP, 194, replace=T); seq1 = randomNucleotides(253); seq2 = paste0(codonsToNucleotides(codons, format="char")[0:417]); seq3 = randomNucleotides(200); seq4 = paste0(codonsToNucleotides(codons, format="char")[418:581]); seq5 = randomNucleotides(L-1035); seq = seq1 + seq2 + seq3 + seq4 + seq5; catn("Initial AA sequence: " + codonsToAminoAcids(codons)); initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(seq); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(2.5e-6)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } fitnessEffect() { for (g in individual.haplosomes) { seq = g.nucleotides(253, 670) + g.nucleotides(871, 1034); codons = nucleotidesToCodons(seq); if (sum(match(codons, STOP) >= 0)) return 0.0; } return 1.0; } 100000 late() { catn(sim.substitutions.size() + " fixed mutations."); as1 = sim.chromosomes.ancestralNucleotides(253, 670, "integer"); as2 = sim.chromosomes.ancestralNucleotides(871, 1034, "integer"); as = c(as1, as2); codons = nucleotidesToCodons(as); catn("Final AA sequence: " + codonsToAminoAcids(codons)); } ================================================ FILE: SLiMgui/Recipes/Recipe 20.1 - A simple multispecies model.txt ================================================ // Keywords: multispecies species sim initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } ticks all 1 early() { sim.addSubpop("p1", 500); } ticks all 2000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 20.2 - A two-species model.txt ================================================ // Keywords: multispecies species fox initialize() { initializeSpecies(tickModulo=3, tickPhase=5, avatar="🦊"); } species mouse initialize() { initializeSpecies(tickModulo=1, tickPhase=1, avatar="🐭"); } ticks all 1 early() { fox.addSubpop("p1", 50); mouse.addSubpop("p2", 500); } ticks all 2000 late() { fox.outputFixedMutations(); mouse.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 20.3 - A deterministic host-parasitoid model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); } ticks all late() { x1 = p1.individualCount / S; // host density x2 = p2.individualCount / S; // parasitoid density x1′ = x1 * exp(R - x1/K - A*x2); // x1[t+1] x2′ = x1 * (1 - exp(-A*x2)); // x2[t+1] p1.setSubpopulationSize(asInteger(round(S * x1′))); p2.setSubpopulationSize(asInteger(round(S * x2′))); } ticks all 250 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 20.4 - An individual-based host-parasitoid model I.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); initializeSLiMModelType("nonWF"); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); // tag values in host indicate survival (0) or death (1) } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); // tag values in parasitoid count successful hunts } ticks all first() { hosts = host.subpopulations.individuals; parasitoids = parasitoid.subpopulations.individuals; // assess densities x1 = hosts.size() / S; // host density x2 = parasitoids.size() / S; // parasitoid density // hunt: each parasitoid counts its successes, and // each host tracks whether it was killed parasitoids.tag = 0; P_parasitized = 1 - exp(-A * x2); killed = rbinom(hosts.size(), 1, P_parasitized); hosts.tag = killed; // 1 means killed hunters = sample(parasitoids, sum(killed), replace=T); for (hunter in hunters) hunter.tag = hunter.tag + 1; survivors = hosts[killed == 0]; // competition: kill a fraction of survivors; note // that this is based on pre-parasitism density P_survives = exp(-x1 / K); survived = rbinom(survivors.size(), 1, P_survives); dead = survivors[survived == 0]; // 1 means survived dead.tag = 1; // mark as dead } species host reproduction() { // only hosts tagged 0 (survived) get to reproduce if (individual.tag != 0) return; // reproduce each host with a mean of exp(r) offspring litterSize = rpois(1, exp(R)); if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); } // non-overlapping generations: parents die, offspring live species host survival() { return (individual.age == 0); } species parasitoid survival() { return (individual.age == 0); } ticks all 250 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 20.4 - An individual-based host-parasitoid model II.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); initializeSLiMModelType("nonWF"); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); // tag values in host are unused } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); // tag values in parasitoid count successful hunts } species host reproduction() { // reproduce each host with a mean of exp(r) offspring litterSize = rpois(1, exp(R)); if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); } ticks all early() { hosts = host.subpopulations.individuals; parasitoids = parasitoid.subpopulations.individuals; // first, kill off the parental generation host_age = hosts.age; hosts[host_age > 0].fitnessScaling = 0.0; parasitoid_age = parasitoids.age; parasitoids[parasitoid_age > 0].fitnessScaling = 0.0; // narrow down to the juveniles and assess densities hosts = hosts[host_age == 0]; parasitoids = parasitoids[parasitoid_age == 0]; x1 = hosts.size() / S; // host density x2 = parasitoids.size() / S; // parasitoid density // next, hunt; each parasitoid counts its successes parasitoids.tag = 0; P_parasitized = 1 - exp(-A * x2); luck = rbinom(hosts.size(), 1, P_parasitized); dead = hosts[luck == 1]; dead.fitnessScaling = 0.0; hunters = sample(parasitoids, dead.size(), replace=T); for (hunter in hunters) hunter.tag = hunter.tag + 1; hosts = hosts[luck == 0]; // finally, competition kills a fraction of survivors // this is based on pre-parasitism density P_survives = exp(-x1 / K); luck = rbinom(hosts.size(), 1, P_survives); dead = hosts[luck == 0]; dead.fitnessScaling = 0.0; } ticks all 250 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 20.5 - A continuous-space host-parasitoid model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("SIDE", 10); // one side length; square root of S defineConstant("S", SIDE*SIDE); defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); defineConstant("S_P", 0.5); // parasitoid hunting/mating kernel width defineConstant("S_H", 0.2); // host competition/mating kernel width defineConstant("D_H", 0.2); // host dispersal kernel width defineConstant("CROSS_SCRIPT", "subpop.addCrossed(individual, mate);"); initializeSLiMModelType("nonWF"); // parasitoids looking for things (long search distance) initializeInteractionType(1, "xy", maxDistance=S_P); i1.setInteractionFunction("l", 1.0); // hosts looking for things (short search distance) initializeInteractionType(2, "xy", maxDistance=S_H); i2.setInteractionFunction("l", 1.0); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); initializeSLiMOptions(dimensionality="xy"); // tag values in host indicate survival (0) or death (1) } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); initializeSLiMOptions(dimensionality="xy"); // tag values in parasitoid count successful hunts } ticks all 2: first() { host_pop = host.subpopulations; hosts = host_pop.individuals; parasitoid_pop = parasitoid.subpopulations; parasitoids = parasitoid_pop.individuals; // assess densities, per individual i1.evaluate(c(host_pop, parasitoid_pop)); i2.evaluate(host_pop); parasitoid_density_byhost = i1.localPopulationDensity(hosts, parasitoid_pop); // hunt: each parasitoid counts its successes, // and remembers the positions of its prey; // each host tracks whether it was killed parasitoids.tag = 0; parasitoids.setValue("PREY_POS", NULL); P_parasitized_byhost = 1 - exp(-A * parasitoid_density_byhost); killed = (runif(hosts.size()) < P_parasitized_byhost); hosts.tag = asInteger(killed); // T/1 means killed preys = hosts[killed]; for (prey in preys) { hunter = i1.drawByStrength(prey, 1, parasitoid_pop); preyPos = prey.spatialPosition; preyPos = c(hunter.getValue("PREY_POS"), preyPos); hunter.tag = hunter.tag + 1; hunter.setValue("PREY_POS", preyPos); } unhunted = hosts[!killed]; // competition: kill a fraction of unhunted; note // that this is based on pre-parasitism density host_density_by_unhunted = i2.localPopulationDensity(unhunted, host_pop); P_survives_by_unhunted = exp(-host_density_by_unhunted / K); survived = (runif(unhunted.size()) < P_survives_by_unhunted); dead = unhunted[!survived]; // T means survived dead.tag = 1; // mark as dead } species host reproduction() { // only hosts tagged 0 (survived) get to reproduce if (individual.tag != 0) return; // reproduce each host with a mean of exp(r) offspring mate = i2.drawByStrength(individual); if (mate.size()) { litterSize = rpois(1, exp(R)); if (litterSize > 0) { offspring = sapply(seqLen(litterSize), CROSS_SCRIPT); // vectorized set of offspring spatial positions, based // on the first parent position plus dispersal D_H positions = rep(individual.spatialPosition, litterSize); positions = positions + rnorm(litterSize * 2, 0, D_H); positions = p1.pointReflected(positions); offspring.setSpatialPosition(positions); } } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = i1.drawByStrength(individual); if (mate.size()) { offspring = sapply(seqLen(litterSize), CROSS_SCRIPT); // vectorized set of offspring positions to prey positions offspring.setSpatialPosition(individual.getValue("PREY_POS")); } } } ticks all 1 early() { host.addSubpop("p1", N0_host); p1.setSpatialBounds(c(0, 0, SIDE, SIDE)); p1.individuals.setSpatialPosition(p1.pointUniform(N0_host)); parasitoid.addSubpop("p2", N0_parasitoid); p2.setSpatialBounds(c(0, 0, SIDE, SIDE)); p2.individuals.setSpatialPosition(p2.pointUniform(N0_parasitoid)); } // non-overlapping generations: parents die, offspring live species host survival() { return (individual.age == 0); } species parasitoid survival() { return (individual.age == 0); } ticks all 250 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 20.6 - A coevolutionary host-parasitoid trait-matching model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("K", 100); defineConstant("R", log(20)); defineConstant("A", 0.015); defineConstant("S", 10^2); // larger is more stable, but slower defineConstant("N0_host", asInteger((135.6217 + 0.01) * S)); defineConstant("N0_parasitoid", asInteger((109.3010 + 0.01) * S)); defineConstant("S_M", 1.0); // parasitoid/host matching width defineConstant("S_S", 2.0); // stabilizing fitness function width initializeSLiMModelType("nonWF"); } species host initialize() { initializeSpecies(avatar="🐛", color="cornflowerblue"); // tag values in host indicate survival (0) or death (1) // tagF values in host are QTL-based additive phenotypes // one short QTL controlling parasitism avoidance initializeMutationType("m1", 0.5, "n", 0.0, 0.1); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 9999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } species parasitoid initialize() { initializeSpecies(avatar="🦟", color="red"); // tag values in parasitoid count successful hunts // tagF values in parasitoid are QTL-based additive phenotypes // one short QTL controlling host trait matching initializeMutationType("m2", 0.5, "n", 0.0, 0.1); initializeGenomicElementType("g2", m2, 1.0); initializeGenomicElement(g2, 0, 9999); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } species host mutationEffect(m1) { return 1.0; } species parasitoid mutationEffect(m2) { return 1.0; } ticks all 2: first() { hosts = host.subpopulations.individuals; parasitoids = parasitoid.subpopulations.individuals; // assess densities x1 = hosts.size() / S; // host density x2 = parasitoids.size() / S; // parasitoid density // assess matches between hosts and the mean parasitoid host_values = hosts.tagF; parasitoid_values = parasitoids.tagF; mean_parasitoid = mean(parasitoid_values); scale = dnorm(0.0, 0.0, S_M); host_match = dnorm(host_values, mean_parasitoid, S_M) / scale; // hunt: each parasitoid counts its successes, and // each host tracks whether it was killed parasitoids.tag = 0; P_parasitized_byhost = 1 - exp(-A * x2 * host_match); killed = rbinom(hosts.size(), 1, P_parasitized_byhost); hosts.tag = killed; // 1 means killed hunters = sapply(hosts[killed == 1], "sample(parasitoids, 1, " + "weights=dnorm(applyValue.tagF - parasitoid_values, 0.0, S_M));"); for (hunter in hunters) hunter.tag = hunter.tag + 1; survivors = hosts[killed == 0]; // competition: kill a fraction of survivors; note // that this is based on pre-parasitism density P_survives = exp(-x1 / K); survived = rbinom(survivors.size(), 1, P_survives); dead = survivors[survived == 0]; // 1 means survived dead.tag = 1; // mark as dead } species host reproduction() { // only hosts tagged 0 (survived) get to reproduce if (individual.tag != 0) return; // reproduce each host with a mean of exp(r) offspring litterSize = rpois(1, exp(R)); if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } species parasitoid reproduction() { // reproduce each parasitoid as many times as it parasitized litterSize = individual.tag; if (litterSize > 0) { mate = subpop.sampleIndividuals(1, exclude=individual); for (i in seqLen(litterSize)) subpop.addCrossed(individual, mate); } } ticks all 1 early() { host.addSubpop("p1", N0_host); parasitoid.addSubpop("p2", N0_parasitoid); log = community.createLogFile("host-parasite log.txt", logInterval=1); log.addTick(); log.addPopulationSize(host); log.addMeanSDColumns("host", "p1.individuals.tagF;"); log.addPopulationSize(parasitoid); log.addMeanSDColumns("parasitoid", "p2.individuals.tagF;"); } ticks all early() { // calculate phenotypes and implement stabilizing selection scale = dnorm(0.0, 0.0, S_S); hosts = host.subpopulations.individuals; phenotypes = hosts.sumOfMutationsOfType(m1); hosts.fitnessScaling = dnorm(phenotypes, 0.0, S_S) / scale; hosts.tagF = phenotypes; parasitoids = parasitoid.subpopulations.individuals; phenotypes = parasitoids.sumOfMutationsOfType(m2); parasitoids.fitnessScaling = dnorm(phenotypes, 0.0, S_S) / scale; parasitoids.tagF = phenotypes; // non-overlapping generations: parents die, offspring live hosts[hosts.age > 0].fitnessScaling = 0.0; parasitoids[parasitoids.age > 0].fitnessScaling = 0.0; } ticks all 10000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 20.7 - A coevolutionary host-parasite matching-allele model.txt ================================================ // Keywords: multispecies, interaction species all initialize() { defineConstant("L", 1); } species host initialize() { initializeSpecies(avatar="🦌"); // one nucleotide controlling infection initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-3)); initializeGenomicElement(g1, 0, L - 1); initializeRecombinationRate(1e-8); } species parasite initialize() { initializeSpecies(avatar="🐛"); // one nucleotide controlling resistance initializeSLiMOptions(nucleotideBased=T); initializeAncestralNucleotides(randomNucleotides(L)); initializeMutationTypeNuc("m2", 0.5, "f", 0.0); initializeGenomicElementType("g2", m2, 1.0, mmJukesCantor(1e-3)); initializeGenomicElement(g2, 0, L - 1); initializeRecombinationRate(1e-8); } function (float$)nucleotideFreq(o$ species, s$ nuc) { nucs = species.subpopulations.individuals.haplosomes.nucleotides(); return mean(nucs == nuc); } ticks all 1 early() { host.addSubpop("p0", 1000); parasite.addSubpop("p1", 1000); log = community.createLogFile("host-parasite log.txt", logInterval=1); log.addTick(); log.addCustomColumn("hA", "nucleotideFreq(host, 'A');"); log.addCustomColumn("hC", "nucleotideFreq(host, 'C');"); log.addCustomColumn("hG", "nucleotideFreq(host, 'G');"); log.addCustomColumn("hT", "nucleotideFreq(host, 'T');"); log.addCustomColumn("pA", "nucleotideFreq(parasite, 'A');"); log.addCustomColumn("pC", "nucleotideFreq(parasite, 'C');"); log.addCustomColumn("pG", "nucleotideFreq(parasite, 'G');"); log.addCustomColumn("pT", "nucleotideFreq(parasite, 'T');"); } ticks all late() { // each parasite will pick a random host and try to infect it parasites = p1.individuals; chosen_hosts = sample(p0.individuals, size(parasites), replace=T); for (p in parasites, h in chosen_hosts) { // infection depends upon a match (diploid matching-allele model) all_nucleotides = c(p,h).haplosomes.nucleotides(); if (size(unique(all_nucleotides, preserveOrder=F)) == 1) { // parasite and host are both homozygous for the same nucleotide(s) // with a match, the parasite's fitness increases, the host's decreases p.fitnessScaling = 1.5 * p.fitnessScaling; h.fitnessScaling = 0.5 * h.fitnessScaling; } } } ticks all 2000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 20.8 - Within-host reproduction in a host-pathogen model.txt ================================================ // Keywords: multispecies, interaction, life history, timescale, parasite, infection, SIR, S-I-R species all initialize() { initializeSLiMModelType("nonWF"); defineConstant("K_MONKEY", 100); // monkey carrying capacity defineConstant("F_MONKEY", 3.0); // monkey fecundity defineConstant("G_MONKEY", 20); // relative generation timescale defineConstant("P_TRANSMISSION", 0.0001); // probability of transmission defineConstant("SUPPRESSION_μ", 30000); // optimal size for suppression defineConstant("SUPPRESSION_σ", 10000); // width for suppression defineConstant("SUPPRESSION_STRENGTH", 0.4); // strength of suppression defineConstant("DEATH", 100000); // level for certain death } species monkey initialize() { initializeSpecies(tickModulo=G_MONKEY, avatar="🐵", color="tan3"); initializeSLiMOptions(keepPedigrees=T); initializeSex(); // pedigree IDs are used to identify host-pathogen matches // colors: blue == uninfected, yellow -> red == infected, black == dead } species pathogen initialize() { initializeSpecies(avatar="🦠", color="chartreuse3"); // pathogen.tag is a counter of the next subpop ID to use // subpopulation.tag is the pedigree ID of the host for the subpop } ticks all 2: first() { if (p1.individualCount == 0) stop(monkey.avatar + " extinct"); if (pathogen.subpopulations.size() == 0) stop(pathogen.avatar + " extinct"); } species monkey reproduction(p1, "F") { // monkeys reproduce sexually, non-monogamously litterSize = rpois(1, F_MONKEY); for (i in seqLen(litterSize)) { mate = subpop.sampleIndividuals(1, sex="M"); subpop.addCrossed(individual, mate); } } species pathogen reproduction() { // the pathogen reproduces clonally subpop.addCloned(individual); } ticks all 1 early() { monkey.addSubpop("p1", K_MONKEY); // choose initial hosts carrying the infection initial_hosts = p1.sampleIndividuals(5, replace=F); pathogen.tag = 3; for (initial_host in initial_hosts) { // make a pathogen subpop for the host pathogen_subpop = pathogen.addSubpop(pathogen.tag, 1); pathogen_subpop.tag = initial_host.pedigreeID; pathogen.tag = pathogen.tag + 1; } // log some basic output logfile = community.createLogFile("host_pathogen_log.csv", logInterval=1); logfile.addTick(); logfile.addSubpopulationSize(p1); logfile.addPopulationSize(pathogen); logfile.addCustomColumn("host_count", "pathogen.subpopulations.size();"); logfile.addMeanSDColumns("monkeyAge", "p1.individuals.age;"); } ticks monkey early() { // monkey population regulation p1.fitnessScaling = K_MONKEY / p1.individualCount; } ticks all early() { // horizontal transmission if (p1.individualCount > 1) { pathogenSubpops = pathogen.subpopulations; allPathogens = pathogenSubpops.individuals; isTransmitted = (rbinom(allPathogens.size(), 1, P_TRANSMISSION) == 1); moving = allPathogens[isTransmitted]; for (ind in moving) { // figure out which host we are in hostID = ind.subpopulation.tag; currentHost = monkey.individualsWithPedigreeIDs(hostID); // choose a different host and get its ID newHost = p1.sampleIndividuals(1, exclude=currentHost); newHostID = newHost.pedigreeID; // find/create a subpop for the new host and move to it newSubpop = pathogenSubpops[pathogenSubpops.tag == newHostID]; if (newSubpop.size() == 0) { newSubpop = pathogen.addSubpop(pathogen.tag, 0); pathogen.tag = pathogen.tag + 1; newSubpop.tag = newHostID; // need to incorporate the new subpop in case it receives another pathogenSubpops = c(pathogenSubpops, newSubpop); } newSubpop.takeMigrants(ind); } } } ticks all early() { // disease outcomes: either suppression or mortality suppress_scale = SUPPRESSION_STRENGTH / dnorm(0.0, 0.0, SUPPRESSION_σ); for (pathogenSubpop in pathogen.subpopulations) { popsize = asFloat(pathogenSubpop.individualCount); P_supp = (dnorm(popsize, SUPPRESSION_μ, SUPPRESSION_σ) * suppress_scale); P_death = popsize / DEATH; if (runif(1) < P_supp) { // the infection is suppressed; host lives, pathogen dies pathogenSubpop.removeSubpopulation(); } else if (runif(1) < P_death) { // the host has died; kill it and its pathogens host = monkey.individualsWithPedigreeIDs(pathogenSubpop.tag); monkey.killIndividuals(host); pathogenSubpop.removeSubpopulation(); } } } ticks all early() { // color monkeys by their infection level pathogenSubpops = pathogen.subpopulations; for (host in p1.individuals) { hostID = host.pedigreeID; pathogenSubpop = pathogenSubpops[pathogenSubpops.tag == hostID]; if (pathogenSubpop.size() == 1) { pathogenCount = pathogenSubpop.individualCount; hue = max(0.0, 1.0 - pathogenCount / DEATH) * 0.15; host.color = rgb2color(hsv2rgb(c(hue, 1, 1))); } else host.color = "cornflowerblue"; } } species monkey survival(p1) { // when a monkey dies, any pathogens in it die if (!surviving) { hostID = individual.pedigreeID; pathogenSubpops = pathogen.subpopulations; pathogenSubpop = pathogenSubpops[pathogenSubpops.tag == hostID]; pathogenSubpop.removeSubpopulation(); } return NULL; } ticks all 2000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 4.1 - A basic neutral simulation.txt ================================================ // Keywords: // set up a simple neutral simulation initialize() { // set the overall mutation rate initializeMutationRate(1e-7); // m1 mutation type: neutral initializeMutationType("m1", 0.5, "f", 0.0); // g1 genomic element type: uses m1 for all mutations initializeGenomicElementType("g1", m1, 1.0); // uniform chromosome of length 100 kb initializeGenomicElement(g1, 0, 99999); // uniform recombination along the chromosome initializeRecombinationRate(1e-8); } // create a population of 500 individuals 1 early() { sim.addSubpop("p1", 500); } // run to tick 10000 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 4.1.10 - Using symbolic constants for model parameters.txt ================================================ // Keywords: parameter, constant, symbol, global, define initialize() { // define some global constants for parameters defineConstant("MU", 1e-7); defineConstant("L", 1e5); defineConstant("R", 1e-8); defineConstant("N", 500); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L - 1); initializeRecombinationRate(R); } 1 early() { sim.addSubpop("p1", N); } 20*N early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 4.2.1 - Basic output, Entire population.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { sim.outputFull(); } ================================================ FILE: SLiMgui/Recipes/Recipe 4.2.2 - Basic output, Random population sample.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 5000 late() { p1.outputSample(10); } 10000 late() { sim.outputFull(); } ================================================ FILE: SLiMgui/Recipes/Recipe 4.2.3 - Basic output, Sampling individuals for output.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.01); initializeGenomicElementType("g1", c(m1,m2), c(1.0,0.01)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); p1.setMigrationRates(p2, 0.01); p2.setMigrationRates(p1, 0.01); } 10000 late() { allIndividuals = sim.subpopulations.individuals; w = asFloat(allIndividuals.countOfMutationsOfType(m2) + 1); sampledIndividuals = sample(allIndividuals, 10, weights=w); sampledIndividuals.haplosomes.outputHaplosomes(); } ================================================ FILE: SLiMgui/Recipes/Recipe 4.2.4 - Basic output, Substitutions.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 5000 late() { p1.outputSample(10); } 10000 late() { sim.outputFull(); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 4.2.5 - Basic output, Automatic logging with LogFile.txt ================================================ // Keywords: migration, dispersal, table output, CSV, TSV, FST initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 999999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); sim.addSubpop("p2", 1000); p1.setMigrationRates(p2, 0.001); p2.setMigrationRates(p1, 0.001); log = community.createLogFile("sim_log.txt", logInterval=10); log.addCycle(); log.addCustomColumn("FST", "calcFST(p1.haplosomes, p2.haplosomes);"); } 20000 late() { } ================================================ FILE: SLiMgui/Recipes/Recipe 4.2.6 - Basic output, Custom output with Eidos.txt ================================================ // Keywords: MS format initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 500); } // custom MS-style output from a multi-subpop sample 2000 late() { // obtain a random sample of haplosomes from the whole population h = sample(sim.subpopulations.haplosomes, 10, T); // get the unique mutations in the sample, sorted by position m = sortBy(unique(h.mutations, preserveOrder=F), "position"); // print the number of segregating sites cat("\n\nsegsites: " + size(m) + "\n"); // print the positions positions = format("%.6f", m.position / sim.chromosomes.lastPosition); cat("positions: " + paste(positions, sep=" ") + "\n"); // print the sampled haplosomes for (haplosome in h) { hasMuts = (match(m, haplosome.mutations) >= 0); cat(paste(asInteger(hasMuts), sep="") + "\n"); } } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.1 - Subpopulation size, Instantaneous changes.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 1000 early() { p1.setSubpopulationSize(100); } 2000 early() { p1.setSubpopulationSize(1000); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth I.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000:1099 early() { newSize = asInteger(p1.individualCount * 1.03); p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth II.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000:1099 early() { newSize = asInteger(round(1.03^(sim.cycle - 999) * 100)); p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth III.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000:2000 early() { if (p1.individualCount < 2000) { newSize = asInteger(round(1.03^(sim.cycle - 999) * 100)); p1.setSubpopulationSize(newSize); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth IV.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000: early() { newSize = round(1.03^(sim.cycle - 999) * 100); if (newSize > 2000) newSize = 2000; p1.setSubpopulationSize(asInteger(newSize)); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.2 - Subpopulation size, Exponential growth V.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } 1000: early() { newSize = asInteger(round(1.03^(sim.cycle - 999) * 100)); if (newSize >= 2000) { newSize = 2000; community.deregisterScriptBlock(self); } p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.4 - Subpopulation size, Cyclical changes.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1500); } early() { newSize = cos((sim.cycle - 1) / 100) * 500 + 1000; p1.setSubpopulationSize(asInteger(newSize)); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.1.5 - Subpopulation size, Context-dependent changes (Muller's Ratchet).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "e", -0.01); m2.convertToSubstitution = F; initializeGenomicElementType("g1", c(m1,m2), c(1,1)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 100); } early() { meanFitness = mean(p1.cachedFitness(NULL)); newSize = asInteger(100 * meanFitness); p1.setSubpopulationSize(newSize); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.2.1 - Population structure, Adding subpopulations.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 100); sim.addSubpop("p3", 1000); p1.setMigrationRates(c(p2,p3), c(0.2,0.1)); p2.setMigrationRates(c(p1,p3), c(0.8,0.01)); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.2.2 - Population structure, Removing subpopulations.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 100 early() { sim.addSubpop("p2", 100); } 100:150 early() { migrationProgress = (sim.cycle - 100) / 50; p1.setMigrationRates(p2, 0.2 * migrationProgress); p2.setMigrationRates(p1, 0.8 * migrationProgress); } 1000 early() { sim.addSubpop("p3", 10); } 1000:1100 early() { p3Progress = (sim.cycle - 1000) / 100; p3.setSubpopulationSize(asInteger(990 * p3Progress + 10)); p1.setMigrationRates(p3, 0.1 * p3Progress); p2.setMigrationRates(p3, 0.01 * p3Progress); } 2000 early() { p2.setSubpopulationSize(0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.2.3 - Population structure, Splitting subpopulations.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 100 early() { sim.addSubpopSplit("p2", 100, p1); } 100:150 early() { migrationProgress = (sim.cycle - 100) / 50; p1.setMigrationRates(p2, 0.2 * migrationProgress); p2.setMigrationRates(p1, 0.8 * migrationProgress); } 1000 early() { sim.addSubpopSplit("p3", 10, p2); } 1000:1100 early() { p3Progress = (sim.cycle - 1000) / 100; p3.setSubpopulationSize(asInteger(990 * p3Progress + 10)); p1.setMigrationRates(p3, 0.1 * p3Progress); p2.setMigrationRates(p3, 0.01 * p3Progress); } 2000 early() { p2.setSubpopulationSize(0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.2.4 - Population structure, Joining subpopulations.txt ================================================ // Keywords: migration, admixture, merging, combining initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); sim.addSubpop("p2", 1000); } 1000 early() { // set up p3 to generate itself entirely from migrants sim.addSubpop("p3", 300); p3.setMigrationRates(c(p1, p2), c(0.75, 0.25)); } 1000 late() { // remove the source subpopulations p3.setMigrationRates(c(p1, p2), c(0.0, 0.0)); p1.setSubpopulationSize(0); p2.setSubpopulationSize(0); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.3.1 - Migration and admixture, A linear stepping-stone model.txt ================================================ // Keywords: migration, dispersal initialize() { defineConstant("COUNT", 10); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { for (i in 0:(COUNT-1)) sim.addSubpop(i, 500); subpops = sim.subpopulations; for (i in 1:(COUNT-1)) subpops[i].setMigrationRates(i-1, 0.2); for (i in 0:(COUNT-2)) subpops[i].setMigrationRates(i+1, 0.05); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.3.2 - Migration and admixture, A non-spatial metapopulation.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { subpopCount = 5; for (i in 1:subpopCount) sim.addSubpop(i, 500); for (i in 1:subpopCount) for (j in 1:subpopCount) if (i != j) sim.subpopulations[i-1].setMigrationRates(j, 0.05); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.3.3 - Migration and admixture, A two-dimensional subpopulation matrix.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { metapopSide = 3; // number of subpops along one side of the grid metapopSize = metapopSide * metapopSide; for (i in 1:metapopSize) sim.addSubpop(i, 500); subpops = sim.subpopulations; for (x in 1:metapopSide) for (y in 1:metapopSide) { destID = (x - 1) + (y - 1) * metapopSide + 1; destSubpop = subpops[destID - 1]; if (x > 1) // left to right destSubpop.setMigrationRates(destID - 1, 0.05); if (x < metapopSide) // right to left destSubpop.setMigrationRates(destID + 1, 0.05); if (y > 1) // top to bottom destSubpop.setMigrationRates(destID - metapopSide, 0.05); if (y < metapopSide) // bottom to top destSubpop.setMigrationRates(destID + metapopSide, 0.05); } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.3.4 - Migration and admixture, A random, sparse spatial metapopulation.txt ================================================ // Keywords: migration, dispersal, SLiMgui initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.3); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 late() { mSide = 10; // number of subpops along one side of the grid for (i in 1:(mSide * mSide)) sim.addSubpop(i, 500); subpops = sim.subpopulations; for (x in 1:mSide) for (y in 1:mSide) { destID = (x - 1) + (y - 1) * mSide + 1; ds = subpops[destID - 1]; if (x > 1) // left to right ds.setMigrationRates(destID - 1, runif(1, 0.0, 0.05)); if (x < mSide) // right to left ds.setMigrationRates(destID + 1, runif(1, 0.0, 0.05)); if (y > 1) // top to bottom ds.setMigrationRates(destID - mSide, runif(1, 0.0, 0.05)); if (y < mSide) // bottom to top ds.setMigrationRates(destID + mSide, runif(1, 0.0, 0.05)); // set up SLiMgui's population visualization nicely xd = ((x - 1) / (mSide - 1)) * 0.9 + 0.05; yd = ((y - 1) / (mSide - 1)) * 0.9 + 0.05; ds.configureDisplay(c(xd, yd), 0.4); } // remove 25% of the subpopulations subpops[sample(0:99, 25)].setSubpopulationSize(0); // introduce a beneficial mutation target_subpop = sample(sim.subpopulations, 1); sample(target_subpop.haplosomes, 10).addNewDrawnMutation(m2, 20000); } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.3.5 - Migration and admixture, Reading a migration matrix from a file.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { for (i in 1:3) sim.addSubpop(i, 1000); subpops = sim.subpopulations; // this file is in the recipe archive at http://benhaller.com/slim/SLiM_Recipes.zip lines = readFile("migration.csv"); lines = lines[substr(lines, 0, 1) != "//"]; for (line in lines) { fields = strsplit(line, ","); i = asInteger(fields[0]); j = asInteger(fields[1]); m = asFloat(fields[2]); if (i != j) { p_i = subpops[subpops.id == i]; p_j = subpops[subpops.id == j]; p_j.setMigrationRates(p_i, m); } } } 10000 late() { sim.outputFixedMutations(); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.4 - The Gravel et al. (2011) model of human evolution I.txt ================================================ // Keywords: migration, dispersal // Model based on Gravel et al. 2011, doi:10.1073/pnas.1019276108 (hereafter "paper") initialize() { initializeMutationRate(2.36e-8); // theta=3813.75 initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 9999); // paper uses 4.04e6, 5007837, etc. initializeRecombinationRate(1e-8); } // INITIALIZE the ancestral African population of size 7310 1 early() { sim.addSubpop("p1", asInteger(round(7310.370867595234))); } // paper rounds to 7310 // END BURN-IN period of 10*N=73104 generations (specific to SLiM recipe); EXPAND the African population // This occurs (5919.131117 generations)*(25 years)=147978 yr ago; paper rounds to 5920 gens (148000 yr) // Thus, simulation should end at generation 1+73104+5919.131117=79024 73105 early() { p1.setSubpopulationSize(asInteger(round(14474.54608753566))); } // paper rounds to 14474 // SPLIT Eurasians (p2) from Africans (p1) and SET UP MIGRATION between them // This occurs 2056.396652 generations (51409.9163 years) ago; paper rounds to 2040 gens (51000 yr) // Relative to beginning, this is generation 79024-2056.396652=76968 76968 early() { sim.addSubpopSplit("p2", asInteger(round(1861.288190027689)), p1); // paper rounds to 1861 p1.setMigrationRates(c(p2), c(15.24422112e-5)); // paper rounds to 15e-5 p2.setMigrationRates(c(p1), c(15.24422112e-5)); // paper rounds to 15e-5 } // SPLIT p2 into European (p2) and East Asian (p3) subpopulations; RESIZE; SET UP MIGRATION between them // This occurs 939.8072428 generations (23495.18107 years) ago; paper rounds to 920 gens (23000 yr) // Relative to beginning, this is generation 79024-939.8072428=78084 78084 early() { sim.addSubpopSplit("p3", asInteger(round(553.8181989)), p2); // paper rounds to 554 p2.setSubpopulationSize(asInteger(round(1032.1046957333444))); // reduce European size; paper rounds to 1032 // Set migration rates for the rest of the simulation p1.setMigrationRates(c(p2, p3), c(2.54332678e-5, 0.7770583877e-5)); // paper rounds to c(2.5e-5, 0.78e-5) p2.setMigrationRates(c(p1, p3), c(2.54332678e-5, 3.115817913e-5)); // paper rounds to c(2.5e-5, 3.11e-5) p3.setMigrationRates(c(p1, p2), c(0.7770583877e-5, 3.115817913e-5)); // paper rounds to c(0.78e-5, 3.11e-5) } // SET UP EXPONENTIAL GROWTH in Europe (p2) and East Asia (p3) // Where N(0) is the base subpopulation size and t = gen - 78084: // N(Europe) should be int(round(N(0) * (1 + 0.003784324268)^t)), i.e., growth is r=0.38% per generation // N(East Asia) should be int(round(N(0) * (1 + 0.004780219543)^t)), i.e., growth is r=0.48% per generation 78084:79024 early() { t = sim.cycle - 78084; p2_size = round(1032.1046957333444 * (1 + 0.003784324268)^t); // paper rounds to N(0)=1032 and r=0.0038 p3_size = round(553.8181989 * (1 + 0.004780219543)^t); // paper rounds to N(0)=554 and r=0.0048 p2.setSubpopulationSize(asInteger(p2_size)); p3.setSubpopulationSize(asInteger(p3_size)); } // OUTPUT AND TERMINATE // Generation 79024 is the present, i.e., 1 initialize + 73104 burn-in + 5919 evolution 79024 late() { p1.outputSample(216); // YRI phase 3 diploid sample of size 108 p2.outputSample(198); // CEU phase 3 diploid sample of size 99 p3.outputSample(206); // CHB phase 3 diploid sample of size 103 } ================================================ FILE: SLiMgui/Recipes/Recipe 5.4 - The Gravel et al. (2011) model of human evolution II.txt ================================================ /// # Gravel Model in SLiM /// #### _(with Jump Menu annotations)_ /// initialize() { initializeMutationRate(2.36e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 9999); initializeRecombinationRate(1e-8); } /// /// **Demography:** 1 early() /* create p1 */ { sim.addSubpop("p1", asInteger(round(7310.370867595234))); } 73105 early() /* end burn-in */ { p1.setSubpopulationSize(asInteger(round(14474.54608753566))); } 76968 early() /* split p2 from p1 */ { sim.addSubpopSplit("p2", asInteger(round(1861.288190027689)), p1); p1.setMigrationRates(c(p2), c(15.24422112e-5)); p2.setMigrationRates(c(p1), c(15.24422112e-5)); } 78084 early() /* split p3 from p2 */ { sim.addSubpopSplit("p3", asInteger(round(553.8181989)), p2); p2.setSubpopulationSize(asInteger(round(1032.1046957333444))); p1.setMigrationRates(c(p2, p3), c(2.54332678e-5, 0.7770583877e-5)); p2.setMigrationRates(c(p1, p3), c(2.54332678e-5, 3.115817913e-5)); p3.setMigrationRates(c(p1, p2), c(0.7770583877e-5, 3.115817913e-5)); } 78084:79024 early() /* exponential growth */ { t = sim.cycle - 78084; p2_size = round(1032.1046957333444 * (1 + 0.003784324268)^t); p3_size = round(553.8181989 * (1 + 0.004780219543)^t); p2.setSubpopulationSize(asInteger(p2_size)); p3.setSubpopulationSize(asInteger(p3_size)); } /***/ /** **Final output:** */ 79024 late() { p1.outputSample(216); p2.outputSample(198); p3.outputSample(206); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.5 - Rescaling population sizes to improve simulation performance I.txt ================================================ // Keywords: rescale initialize() { initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", -0.01); initializeGenomicElementType("g1", c(m1,m2), c(0.8,0.2)); initializeGenomicElement(g1, 0, 9999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 50000 early() { p1.setSubpopulationSize(1000); } 55000 early() { p1.setSubpopulationSize(5000); } 60000 late() { p1.outputSample(10); } ================================================ FILE: SLiMgui/Recipes/Recipe 5.5 - Rescaling population sizes to improve simulation performance II.txt ================================================ // Keywords: rescale initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", -0.1); initializeGenomicElementType("g1", c(m1,m2), c(0.8,0.2)); initializeGenomicElement(g1, 0, 9999); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 5000 early() { p1.setSubpopulationSize(100); } 5500 early() { p1.setSubpopulationSize(500); } 6000 late() { p1.outputSample(10); } ================================================ FILE: SLiMgui/Recipes/Recipe 6.1 - Genomic structure, Part I (Mutation types and fitness effects).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 6.2 - Genomic structure, Part II (Genomic element types).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial initializeGenomicElementType("g1", c(m2,m3,m4), c(2,8,0.1)); // exon initializeGenomicElementType("g2", c(m1,m3), c(9,1)); // intron initializeGenomicElementType("g3", c(m1), 1); // non-coding initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 6.3 - Genomic structure, Part III (Chromosome organization).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial initializeGenomicElementType("g1", c(m2,m3,m4), c(2,8,0.1)); // exon initializeGenomicElementType("g2", c(m1,m3), c(9,1)); // intron initializeGenomicElementType("g3", c(m1), 1); // non-coding // Generate random genes along an approximately 100000-base chromosome base = 0; while (base < 100000) { // make a non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); base = base + nc_length; // make first exon ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; // make additional intron-exon pairs do { in_length = asInteger(rlnorm(1, log(100), log(1.5))) + 10; initializeGenomicElement(g2, base, base + in_length - 1); base = base + in_length; ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; } while (runif(1) < 0.8); // 20% probability of stopping } // final non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); // single recombination rate initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 6.4 - Genomic structure, Part IV (Custom display colors in SLiMgui).txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); // non-coding initializeMutationType("m2", 0.5, "f", 0.0); // synonymous initializeMutationType("m3", 0.1, "g", -0.03, 0.2); // deleterious initializeMutationType("m4", 0.8, "e", 0.1); // beneficial m1.color = "gray40"; m2.color = "gray40"; m3.color = "red"; m4.color = "green"; m1.colorSubstitution = "gray20"; m2.colorSubstitution = "gray20"; m3.colorSubstitution = "#550000"; m4.colorSubstitution = "#005500"; initializeGenomicElementType("g1", c(m2,m3,m4), c(2,8,0.1)); // exon initializeGenomicElementType("g2", c(m1,m3), c(9,1)); // intron initializeGenomicElementType("g3", c(m1), 1); // non-coding g1.color = "cornflowerblue"; g2.color = "#00009F"; g3.color = "black"; // Generate random genes along an approximately 100000-base chromosome base = 0; while (base < 100000) { // make a non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); base = base + nc_length; // make first exon ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; // make additional intron-exon pairs do { in_length = asInteger(rlnorm(1, log(100), log(1.5))) + 10; initializeGenomicElement(g2, base, base + in_length - 1); base = base + in_length; ex_length = asInteger(rlnorm(1, log(50), log(2))) + 1; initializeGenomicElement(g1, base, base + ex_length - 1); base = base + ex_length; } while (runif(1) < 0.8); // 20% probability of stopping } // final non-coding region nc_length = rdunif(1, 100, 5000); initializeGenomicElement(g3, base, base + nc_length - 1); // single recombination rate initializeRecombinationRate(1e-8); } 1 early() { sim.chromosomes.colorSubstitution = ""; sim.addSubpop("p1", 5000); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.1.1 - Reproduction, Enabling separate sexes.txt ================================================ // Keywords: sexual initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.1.2 - Reproduction, Sex ratios I.txt ================================================ // Keywords: sexual, sex ratio initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500, 0.6); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.1.2 - Reproduction, Sex ratios II.txt ================================================ // Keywords: sexual, sex ratio initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1: early() { p1.setSexRatio(runif(1, 0.3, 0.7)); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.1.3 - Reproduction, Selfing in hermaphroditic populations.txt ================================================ // Keywords: selfing initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.setSelfingRate(0.8); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.1.4 - Reproduction, Cloning I.txt ================================================ // Keywords: clonal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.setCloningRate(0.1); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.1.4 - Reproduction, Cloning II.txt ================================================ // Keywords: clonal, sexual initialize() { initializeSex(); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.setCloningRate(c(0.5,0.0)); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.2.1 - Recombination, Making a random recombination map.txt ================================================ // Keywords: recombination rate map, recombination map initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); // 1000 random recombination regions ends = c(sort(sample(0:99998, 999)), 99999); rates = runif(1000, 1e-9, 1e-7); initializeRecombinationRate(rates, ends); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.2.2 - Recombination, Reading a recombination map from a file.txt ================================================ // Keywords: recombination rate map, recombination map initialize() { defineConstant("L", 23011544); initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); // read Drosophila 2L map from Comeron et al. 2012; this is in the // recipe archive at http://benhaller.com/slim/SLiM_Recipes.zip initializeRecombinationRateFromFile("Comeron_100kb_chr2L.txt", L-1); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.2.3 - Recombination, Unlinked loci.txt ================================================ // Keywords: unlinked loci, free recombination, linkage disequilibrium initialize() { initializeMutationRate(1e-5); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(0.5); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.2.4 - Recombination, Gene conversion.txt ================================================ // Keywords: gene conversion initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); initializeGeneConversion(0.2, 500, 1.0); } 1 early() { sim.addSubpop("p1", 500); } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.3.1 - Multiple diploid autosomes.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "g", -0.03, 0.2); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElementType("g2", c(m1, m2), c(1,2)); initializeChromosome(1, 1e5); initializeGenomicElement(g1, 0, 1e5-1); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); initializeChromosome(2, 2e5); initializeGenomicElement(g1, 0, 1e5-1); initializeGenomicElement(g2, 1e5, 1e5+5e4-1); initializeGenomicElement(g1, 1e5+5e4, 2e5-1); initializeMutationRate(2e-7); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 2000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.3.2 - Clonal haploids and chromosome types.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeMutationType("m1", 1.0, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, type="H"); initializeGenomicElement(g1, 0, 1e5-1); initializeMutationRate(1e-7); initializeRecombinationRate(0.0); } 1 early() { sim.addSubpop("p1", 500); p1.setCloningRate(1.0); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.3.3 - Haploids with recombination.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeMutationType("m1", 1.0, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, 1e5, type="H"); initializeGenomicElement(g1); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.3.4 - Sex-chromosome evolution and null haplosomes.txt ================================================ // Keywords: multiple chromosomes initialize() { defineConstant("X_LEN", 156040895); defineConstant("Y_LEN", 57227415); initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeChromosome(1, X_LEN, type="X", symbol="X"); initializeGenomicElement(g1); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); initializeChromosome(2, Y_LEN, type="Y", symbol="Y"); initializeGenomicElement(g1); initializeMutationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 10000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.3.5 - Modeling the full human genome.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); // length data: https://www.ncbi.nlm.nih.gov/grc/human/data, // Human Genome Assembly GRCh38.p14, 2022-02-03 ids = 1:24; symbols = c(1:22, "X", "Y"); lengths = c(248956422, 242193529, 198295559, 190214555, 181538259, 170805979, 159345973, 145138636, 138394717, 133797422, 135086622, 133275309, 114364328, 107043718, 101991189, 90338345, 83257441, 80373285, 58617616, 64444167, 46709983, 50818468, 156040895, 57227415); types = c(rep("A", 22), "X", "Y"); for (id in ids, symbol in symbols, length in lengths, type in types) { initializeChromosome(id, length, type, symbol); initializeMutationRate(1e-7); initializeRecombinationRate(1e-8); // not used for the Y initializeGenomicElement(g1); } } 1 early() { sim.addSubpop("p1", 100); } 1000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.3.6 - A model of bryophytes with UV sex determination.txt ================================================ // Keywords: bryophytes, UV sex chromosomes, UV sex determination, haploid recombination, mosses initialize() { initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); for (id in 1:3, type in c("H", "W", "Y"), symbol in c("A", "U", "V")) { initializeChromosome(id, 1e7, type=type, symbol=symbol); initializeMutationRate(1e-7); initializeGenomicElement(g1); initializeRecombinationRate(1e-8); } } 1 early() { sim.addSubpop("p1", 500); } 20000 late() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 8.3.7 - Output from multiple-chromosome models.txt ================================================ // Keywords: multiple chromosomes initialize() { initializeSex(); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); for (id in 1:3, type in c("A", "X", "Y")) { initializeChromosome(id, 1e6, type=type, symbol=type); initializeGenomicElement(g1); initializeMutationRate(1e-8); initializeRecombinationRate(1e-8); } } 1 early() { sim.addSubpop("p1", 500); } 100 late() { sim.outputFull(); inds = p1.sampleIndividuals(5); inds.outputIndividuals(); inds.outputIndividualsToVCF(); } ================================================ FILE: SLiMgui/Recipes/Recipe 9.1 - Introducing adaptive mutations.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000:100000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); cat(ifelse(fixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.10 - Tracking the fate of background mutations.txt ================================================ // Keywords: initialize() { defineConstant("L", 3e6); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.8, "f", 0.5); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-7); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); defineConstant("BACKGROUND", target.mutations); mut = target.addNewDrawnMutation(m2, asInteger(L/2)); defineConstant("SWEEP", mut); } 1000: late() { if (!SWEEP.isSegregating & !SWEEP.isFixed) stop("LOST"); } 1500 late() { nonSeg = BACKGROUND[!BACKGROUND.isSegregating]; fixed = nonSeg[nonSeg.isFixed]; lost = nonSeg[!nonSeg.isFixed]; writeFile("fixed.txt", paste(fixed.position, sep=", ")); writeFile("lost.txt", paste(lost.position, sep=", ")); } ================================================ FILE: SLiMgui/Recipes/Recipe 9.11 - Effective population size versus census population size.txt ================================================ // Keywords: Ne, parameter estimation initialize() { defineGlobal("N", 1000); defineGlobal("L", 1e7); defineGlobal("MU", 1e-7); defineGlobal("R", 1e-8); defineGlobal("S", 2.0); initializeSLiMOptions(keepPedigrees=T); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); // neutral initializeMutationType("m2", 0.5, "f", S); // sweep m2.convertToSubstitution = F; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(R); } 1 late() { sim.addSubpop("p1", N); p1.setValue("previous_N", p1.individualCount); defineConstant("LOG", community.createLogFile("Ne_log.csv")); LOG.addCycle(); LOG.addCustomColumn("N(t-1)", "p1.getValue('previous_N');"); LOG.addCustomColumn("N(t)", "p1.individualCount;"); LOG.addCustomColumn("freq", "mutTypeFrequency(m2);"); LOG.addCustomColumn("Ne_heterozygosity", "estimateNe_Heterozygosity(p1);"); LOG.addCustomColumn("Ne_inbreeding", "estimateNe_Inbreeding(p1);"); } 2: late() { LOG.logRow(); p1.setValue("previous_N", p1.individualCount); } 10000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, integerDiv(L, 2)); } 20000 late() { sim.simulationFinished(); } function (float)mutTypeFrequency(o$ mutType) { muts = sim.mutationsOfType(mutType); if (muts.size() > 0) return sim.mutationFrequencies(NULL, muts); return NULL; } function (float)estimateNe_Heterozygosity(o$ subpop, [No$ chromosome = NULL]) { if (isNULL(chromosome)) { if (size(sim.chromosomes) == 1) chromosome = sim.chromosomes; else stop("ERROR: in a multi-chrom model, a chromosome must be supplied."); } haplosomes = subpop.haplosomesForChromosomes(chromosome, includeNulls=F); pi = calcHeterozygosity(haplosomes); return pi / (4 * MU); } function (integer)tabulateFecundity(o$ subpop, i$ previous_N) { parentIDs = subpop.individuals.pedigreeParentIDs; rescaledParentIDs = parentIDs - min(parentIDs); return tabulate(rescaledParentIDs, previous_N - 1); } function (float)estimateNe_Inbreeding(o$ subpop) { previous_N = subpop.getValue("previous_N"); k = tabulateFecundity(subpop, previous_N); return (previous_N * mean(k) - 2) / (mean(k) - 1 + var(k) / mean(k)); } ================================================ FILE: SLiMgui/Recipes/Recipe 9.12 - Observing the site frequency spectrum (SFS) during selective sweeps.txt ================================================ // Keywords: site frequency spectrum, SFS, population genetics, statistics, custom plotting initialize() { setSeed(4806519412125461529); defineConstant("N", 1000); defineConstant("L", 1e7); defineConstant("MU", 1e-7); defineConstant("BINCOUNT", 100); initializeMutationRate(MU); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.5); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-8); } initialize() { // Calculate the expected SFS with the same bins as our observed SFS. // This is tricky because we need to calculate it for the number of samples // we have (i.e., number of haplosomes), and then re-bin it; maybe there is // an easier way. Note that 10000000 is just a constant large enough to // avoid rounding artifacts in the final curve; it is basically the number // of "mutations" scattered across the original expected SFS in order to // re-bin it to the final bin count. Not sure this math is exactly correct. // It would be great to have a calcExpectedSFS() function built in; if you // know how to do that exactly correctly, for counts as well as density, // then please volunteer! expected = 1 / (1:(2*N-1)); expected = expected / sum(expected); expected = asInteger(round(expected * 10000000)); bins = asInteger(round(repEach(1:(2*N-1), expected) * (BINCOUNT / (2*N-1)))); tallies = tabulate(bins, maxbin=BINCOUNT-1); defineConstant("EXPECTED_SFS", tallies / sum(tallies)); } 1 early() { // make a subpop and start drifting towards equilibrium sim.addSubpop("p1", N); } 20*N early() { // start generating rare m2 sweep mutations g1.setMutationFractions(c(m1, m2), c(1, 0.000001)); } 25*N early() { // stop generating m2 mutations and allow re-equilibration g1.setMutationFractions(m1, 1); } 1:(35*N) late() { updatePlot(); } function (void)updatePlot(void) { if (exists("slimgui")) { // get the empirical population SFS sfs_all = calcSFS(BINCOUNT); sfs_m2 = calcSFS(BINCOUNT, muts=sim.mutationsOfType(m2)); x = seq(from=0.0, to=1.0, length=BINCOUNT+1) + 0.5/BINCOUNT; x = x[0:(length(x)-2)]; // make a plot of the observed vs. expected SFS plot = slimgui.createPlot("Site Frequency Spectrum", xrange=c(0,1), yrange=c(0,1), xlab="Mutation frequency", ylab="Density (sqrt-transformed)"); plot.lines(x=x, y=sqrt(sfs_all), color="black", lwd=3); plot.lines(x=x, y=sqrt(EXPECTED_SFS), color="chartreuse2", lwd=2); plot.lines(x=x, y=sqrt(sfs_m2), color="red", lwd=1); plot.addLegend("topRight"); plot.legendLineEntry("observed", color="black", lwd=3); plot.legendLineEntry("expected", color="chartreuse2", lwd=2); plot.legendLineEntry("m2 (sweeps)", color="red", lwd=2); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.2 - Making sweeps conditional on fixation.txt ================================================ // Keywords: conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { // save this run's identifier, used to save and restore defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // save the state of the simulation sim.outputFull(tempdir() + "slim_" + simID + ".txt"); // introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000:100000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); if (fixed) { cat(simID + ": FIXED\n"); sim.simulationFinished(); } else { cat(simID + ": LOST - RESTARTING\n"); // go back to tick 1000 sim.readFromPopulationFile(tempdir() + "slim_" + simID + ".txt"); // start a newly seeded run setSeed(rdunif(1, 0, asInteger(2^62) - 1)); // re-introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.3 - Making sweeps conditional on establishment.txt ================================================ // Keywords: conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { // save this run's identifier, used to save and restore defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // save the state of the simulation sim.outputFull(tempdir() + "slim_" + simID + ".txt"); // introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000: late() { mut = sim.mutationsOfType(m2); if (size(mut) == 1) { if (sim.mutationFrequencies(NULL, mut) > 0.1) { cat(simID + ": ESTABLISHED\n"); community.deregisterScriptBlock(self); } } else { cat(simID + ": LOST - RESTARTING\n"); // go back to tick 1000 sim.readFromPopulationFile(tempdir() + "slim_" + simID + ".txt"); // start a newly seeded run setSeed(rdunif(1, 0, asInteger(2^62) - 1)); // re-introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } } 10000 early() { sim.simulationFinished(); } ================================================ FILE: SLiMgui/Recipes/Recipe 9.4 - Partial sweeps.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000:10000 late() { mut = sim.mutationsOfType(m2); if (size(mut) == 0) sim.simulationFinished(); else if (mut.selectionCoeff != 0.0) if (sim.mutationFrequencies(NULL, mut) >= 0.5) mut.setSelectionCoeff(0.0); } ================================================ FILE: SLiMgui/Recipes/Recipe 9.5.1 - A soft sweep from recurrent de novo mutations in a large population.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-5); initializeMutationType("m1", 0.45, "f", 0.5); // sweep mutation m1.convertToSubstitution = F; m1.mutationStackPolicy = "f"; initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 0); initializeRecombinationRate(0); } 1 early() { sim.addSubpop("p1", 100000); } 1:10000 early() { counts = p1.haplosomes.countOfMutationsOfType(m1); freq = mean(counts > 0); if (freq == 1.0) { cat("\nTotal mutations: " + size(sim.mutations) + "\n\n"); for (mut in sortBy(sim.mutations, "originTick")) { mutFreq = mean(p1.haplosomes.containsMutations(mut)); cat("Origin " + mut.originTick + ": " + mutFreq + "\n"); } sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.5.2 - A soft sweep with a fixed de novo mutation schedule.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.45, "f", 0.5); // sweep mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); p1.tag = 0; // indicate that a mutation has not yet been seen } 1000:1100 late() { if (sim.cycle % 10 == 0) { target = sample(p1.haplosomes, 1); if (target.countOfMutationsOfType(m2) == 0) target.addNewDrawnMutation(m2, 10000); } } 1:10000 late() { if (p1.tag != sim.countOfMutationsOfType(m2)) { if (any(sim.substitutions.mutationType == m2)) { cat("Hard sweep ended in cycle " + sim.cycle + "\n"); sim.simulationFinished(); } else { p1.tag = sim.countOfMutationsOfType(m2); cat("Cycle " + sim.cycle + ": " + p1.tag + " lineage(s)\n"); if ((p1.tag == 0) & (sim.cycle > 1100)) { cat("Sweep failed to establish.\n"); sim.simulationFinished(); } } } if (all(p1.haplosomes.countOfMutationsOfType(m2) > 0)) { cat("Soft sweep ended in cycle " + sim.cycle + "\n"); cat("Frequencies:\n"); print(sim.mutationFrequencies(p1, sim.mutationsOfType(m2))); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.5.3 - A soft sweep with a random de novo mutation schedule.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.1, "f", 0.5); // sweep mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); gens = cumSum(rpois(10, 10)); // make a vector of start gens gens = gens + (1000 - min(gens)); // align to start at 1000 defineConstant("Z", max(gens)); // remember the last gen defineConstant("ADD_GENS", gens); // schedule the add events } ADD_GENS late() { target = sample(p1.haplosomes, 1); mut = sim.mutationsOfType(m2); if (mut.size() > 0) target.addMutations(mut); else target.addNewDrawnMutation(m2, 10000); } 1:10000 late() { if (any(sim.substitutions.mutationType == m2)) { catn("Sweep completed in cycle " + sim.cycle + "."); sim.simulationFinished(); } else if ((sim.countOfMutationsOfType(m2) == 0) & (sim.cycle > Z)) { catn("Soft sweep failed to establish."); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.6.1 - A sweep from standing variation at a random locus.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 1.0, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { muts = sim.mutations; muts = muts[sim.mutationFrequencies(p1, muts) > 0.1]; if (size(muts)) { mut = sample(muts, 1); mut.setSelectionCoeff(0.5); } else { cat("No contender of sufficient frequency found.\n"); } } 1000:10000 late() { if (sum(sim.mutations.selectionCoeff) == 0.0) { if (sum(sim.substitutions.selectionCoeff) == 0.0) cat("Sweep mutation lost in cycle " + sim.cycle + "\n"); else cat("Sweep mutation reached fixation.\n"); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.6.2 - A sweep from standing variation at a predetermined locus.txt ================================================ // Keywords: conditional sweep initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.0); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { // save this run's identifier, used to save and restore defineConstant("simID", getSeed()); sim.addSubpop("p1", 500); } 1000 late() { // save the state of the simulation sim.outputFull(tempdir() + "slim_" + simID + ".txt"); // introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 1000: late() { mut = sim.mutationsOfType(m2); if (size(mut) == 1) { if (sim.mutationFrequencies(NULL, mut) > 0.1) { cat(simID + ": ESTABLISHED - CONVERTING TO BENEFICIAL\n"); mut.setSelectionCoeff(0.5); community.deregisterScriptBlock(self); } } else { cat(simID + ": LOST BEFORE ESTABLISHMENT - RESTARTING\n"); // go back to tick 1000 sim.readFromPopulationFile(tempdir() + "slim_" + simID + ".txt"); // start a newly seeded run setSeed(rdunif(1, 0, asInteger(2^62) - 1)); // re-introduce the sweep mutation target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } } 1000:10000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); cat(simID + ifelse(fixed, ": FIXED\n", ": LOST\n")); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.7 - Adaptive introgression.txt ================================================ // Keywords: migration, dispersal initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { subpopCount = 10; for (i in 1:subpopCount) sim.addSubpop(i, 500); for (i in 2:subpopCount) sim.subpopulations[i-1].setMigrationRates(i-1, 0.01); for (i in 1:(subpopCount-1)) sim.subpopulations[i-1].setMigrationRates(i+1, 0.2); } 100 late() { target = sample(p1.haplosomes, 1); target.addNewDrawnMutation(m2, 10000); } 100:100000 late() { if (sim.countOfMutationsOfType(m2) == 0) { fixed = (sum(sim.substitutions.mutationType == m2) == 1); cat(ifelse(fixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/Recipe 9.8 - Fixation probabilities under Hill-Robertson interference.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "f", 0.05); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 1000); } 6000 late() { // Calculate the fixation probability for a beneficial mutation s = 0.05; N = 1000; p_fix = (1 - exp(-2 * s)) / (1 - exp(-4 * N * s)); // Calculate the expected number of fixed mutations n_gens = 1000; // first 5000 ticks were burn-in mu = 1e-6; locus_size = 100000; expected = mu * locus_size * n_gens * 2 * N * p_fix; // Figure out the actual number of fixations after burn-in subs = sim.substitutions; actual = sum(subs.fixationTick >= 5000); // Print a summary of our findings cat("P(fix) = " + p_fix + "\n"); cat("Expected fixations: " + expected + "\n"); cat("Actual fixations: " + actual + "\n"); cat("Ratio, actual/expected: " + (actual/expected) + "\n"); } ================================================ FILE: SLiMgui/Recipes/Recipe 9.9 - Keeping a reference to a sweep mutation.txt ================================================ // Keywords: initialize() { initializeMutationRate(1e-7); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 1.0, "f", 0.5); // introduced mutation initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1000 late() { target = sample(p1.haplosomes, 1); mut = target.addNewDrawnMutation(m2, 10000); defineConstant("SWEEP", mut); } 1000:100000 late() { if (!SWEEP.isSegregating) { cat(ifelse(SWEEP.isFixed, "FIXED\n", "LOST\n")); sim.simulationFinished(); } } ================================================ FILE: SLiMgui/Recipes/_README.txt ================================================ These recipes are all taken directly from SLiM's manual: Haller, B.C., and Messer, P.W. (2016). SLiM: An Evolutionary Simulation Framework. SLiM's manual contains full explanations of each recipe, in the corresponding section; please refer to it for further information. These recipes are provided as separate files for convenience, particularly since copy-paste from the PDF manual often works poorly (but note that SLiMgui now allows you to open these recipes directly from the File menu, also, via the Open Recipe submenu). -------------------------------------------------------------- These recipes were all created by Ben Haller, except recipe 5.4, created by Aaron Sams and Chase W. Nelson. All recipes are copyright (c) 2016-2025 Benjamin C. Haller. All rights reserved. They are products of the Messer Lab, http://messerlab.org/slim/ All of these recipes are a part of SLiM. SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with SLiM. If not, see . -------------------------------------------------------------- The file Comeron_100kb_chr2L.txt provided here is part of the supplemental data from the paper: Comeron, J.M., Ratnappan, R., and Bailin, S. (2012). The many landscapes of recombination in Drosophila melanogaster. PLoS Genetics 8(10), e1002905. This data file is used by recipe 6.1.2. -------------------------------------------------------------- For further information, please feel free to contact us: http://messerlab.org/slim/ Ben Haller, bhaller@benhaller.com Philipp Messer, philipp.messer@gmail.com ================================================ FILE: SLiMgui/SLiMDocument.h ================================================ // // SLiMDocument.h // SLiM // // Created by Ben Haller on 1/31/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import @class SLiMWindowController; @interface SLiMDocument : NSDocument { // This is our model, effectively. We don't really follow MVC very closely at all. When we are getting created, we have to use // this ivar to save our model state temporarily, because a SLiMWindowController is not yet created for us to stuff the model // into. That is the only time we use this ivar, though; once the controller is created, it keeps the model for us. Not the // most elegant design; probably a bunch of stuff should move from SLiMWindowController over to here. NSString *documentScriptString; // Change count tracking relative to our last recycle (which is change count 0) int slimChangeCount; // Transient document support BOOL transient; } @property (retain, nonatomic) NSString *recipeName; + (NSString *)defaultWFScriptString; + (NSString *)defaultNonWFScriptString; - (NSString *)documentScriptString; - (void)setDocumentScriptString:(NSString *)newString; - (SLiMWindowController *)slimWindowController; - (BOOL)changedSinceRecycle; - (void)resetSLiMChangeCount; // Transient documents - (BOOL)isTransient; - (void)setTransient:(BOOL)flag; - (BOOL)isTransientAndCanBeReplaced; @end ================================================ FILE: SLiMgui/SLiMDocument.mm ================================================ // // SLiMDocument.m // SLiM // // Created by Ben Haller on 1/31/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "SLiMDocument.h" #import "SLiMWindowController.h" #import "SLiMDocumentController.h" @implementation SLiMDocument + (NSString *)defaultWFScriptString { static NSString *str = @"// set up a simple neutral simulation\n" "initialize() {\n" " initializeMutationRate(1e-7);\n" " \n" " // m1 mutation type: neutral\n" " initializeMutationType(\"m1\", 0.5, \"f\", 0.0);\n" " \n" " // g1 genomic element type: uses m1 for all mutations\n" " initializeGenomicElementType(\"g1\", m1, 1.0);\n" " \n" " // uniform chromosome of length 100 kb with uniform recombination\n" " initializeGenomicElement(g1, 0, 99999);\n" " initializeRecombinationRate(1e-8);\n" "}\n" "\n" "// create a population of 500 individuals\n" "1 early() {\n" " sim.addSubpop(\"p1\", 500);\n" "}\n" "\n" "// output samples of 10 haplosomes periodically, all fixed mutations at end\n" "1000 late() { p1.outputSample(10); }\n" "2000 late() { p1.outputSample(10); }\n" "2000 late() { sim.outputFixedMutations(); }\n"; return str; } + (NSString *)defaultNonWFScriptString { static NSString *str = @"// set up a simple neutral nonWF simulation\n" "initialize() {\n" " initializeSLiMModelType(\"nonWF\");\n" " defineConstant(\"K\", 500); // carrying capacity\n" " \n" " // neutral mutations, which are allowed to fix\n" " initializeMutationType(\"m1\", 0.5, \"f\", 0.0);\n" " m1.convertToSubstitution = T;\n" " \n" " initializeGenomicElementType(\"g1\", m1, 1.0);\n" " initializeGenomicElement(g1, 0, 99999);\n" " initializeMutationRate(1e-7);\n" " initializeRecombinationRate(1e-8);\n" "}\n" "\n" "// each individual reproduces itself once\n" "reproduction() {\n" " subpop.addCrossed(individual, subpop.sampleIndividuals(1));\n" "}\n" "\n" "// create an initial population of 10 individuals\n" "1 early() {\n" " sim.addSubpop(\"p1\", 10);\n" "}\n" "\n" "// provide density-dependent selection\n" "early() {\n" " p1.fitnessScaling = K / p1.individualCount;\n" "}\n" "\n" "// output all fixed mutations at end\n" "2000 late() { sim.outputFixedMutations(); }\n"; return str; } - (instancetype)init { if (self = [super init]) { //[[self undoManager] disableUndoRegistration]; if ([(SLiMDocumentController *)[NSDocumentController sharedDocumentController] creatingNonWFDocument]) documentScriptString = [[SLiMDocument defaultNonWFScriptString] retain]; else documentScriptString = [[SLiMDocument defaultWFScriptString] retain]; //[[self undoManager] enableUndoRegistration]; } return self; } - (void)dealloc { //NSLog(@"[SLiMDocument dealloc]"); [documentScriptString release]; documentScriptString = nil; [self setRecipeName:nil]; [super dealloc]; } - (NSString *)documentScriptString { return documentScriptString; } - (SLiMWindowController *)slimWindowController { NSArray *controllers = [self windowControllers]; if ([controllers count] > 0) { NSWindowController *mainController = [controllers objectAtIndex:0]; if ([mainController isKindOfClass:[SLiMWindowController class]]) return (SLiMWindowController *)mainController; } return nil; } - (void)makeWindowControllers { NSArray *myControllers = [self windowControllers]; // If this document displaced a transient document, it will already have been assigned // a window controller. If that is not the case, create one. if ([myControllers count] == 0) { SLiMWindowController *controller = [[[SLiMWindowController alloc] init] autorelease]; [controller setShouldCloseDocument:YES]; [self addWindowController:controller]; } } - (void)windowControllerDidLoadNib:(NSWindowController *)aController { // Note this method is not called, because we override -makeWindowControllers [super windowControllerDidLoadNib:aController]; } - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { if ([typeName isEqualToString:@"edu.messerlab.slim"]) { SLiMWindowController *controller = [[self windowControllers] objectAtIndex:0]; NSString *curScriptString = [controller->scriptTextView string]; NSData *data = [curScriptString dataUsingEncoding:NSUTF8StringEncoding]; if (!data && outError) *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteInapplicableStringEncodingError userInfo:@{ NSLocalizedDescriptionKey : @"The script could not be converted to UTF-8 encoding."}]; [controller->scriptTextView breakUndoCoalescing]; return data; } if (outError) *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:nil]; return nil; } - (void)setDocumentScriptString:(NSString *)newString { [newString retain]; [documentScriptString release]; documentScriptString = newString; SLiMWindowController *mainController = [self slimWindowController]; if (mainController) { [mainController->scriptTextView setString:newString]; [mainController->scriptTextView recolorAfterChanges]; [mainController recycle:nil]; } } - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { if ([typeName isEqualToString:@"edu.messerlab.slim"]) { NSString *scriptString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; BOOL lossy = false; if (!scriptString) [NSString stringEncodingForData:data encodingOptions:@{NSStringEncodingDetectionSuggestedEncodingsKey: @[[NSNumber numberWithUnsignedInt:NSUTF8StringEncoding], [NSNumber numberWithUnsignedInt:NSUTF16StringEncoding], [NSNumber numberWithUnsignedInt:NSASCIIStringEncoding], [NSNumber numberWithUnsignedInt:NSISOLatin1StringEncoding]]} convertedString:&scriptString usedLossyConversion:&lossy]; [self setDocumentScriptString:scriptString]; return YES; } else if ([typeName isEqualToString:@"public.plain-text"]) { NSString *scriptString = nil; BOOL lossy = false; [NSString stringEncodingForData:data encodingOptions:@{NSStringEncodingDetectionSuggestedEncodingsKey: @[[NSNumber numberWithUnsignedInt:NSUTF8StringEncoding], [NSNumber numberWithUnsignedInt:NSUTF16StringEncoding], [NSNumber numberWithUnsignedInt:NSASCIIStringEncoding], [NSNumber numberWithUnsignedInt:NSISOLatin1StringEncoding]]} convertedString:&scriptString usedLossyConversion:&lossy]; [self setDocumentScriptString:scriptString]; return YES; } if (outError) { *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:nil]; } return NO; } - (BOOL)prepareSavePanel:(NSSavePanel *)savePanel { [savePanel setTitle:@"Save Script"]; [savePanel setMessage:@"Save the simulation script to a file:"]; [savePanel setExtensionHidden:NO]; [savePanel setCanSelectHiddenExtension:NO]; return YES; } // Do our own tracking of the change count. We do this so that we know whether the script is in // the same state it was in when we last recycled, or has been changed. If it has been changed, // we add a highlight under the recycle button to suggest to the user that they might want to // recycle to bring their changes into force. - (void)updateChangeCount:(NSDocumentChangeType)change { // When a document is changed, it ceases to be transient. [self setTransient:NO]; [super updateChangeCount:change]; // Mask off flags in the high bits. Apple is not explicit about this, but NSChangeDiscardable // is 256, and acts as a flag bit, so it seems reasonable to assume this for future compatibility. NSDocumentChangeType maskedChange = (NSDocumentChangeType)(change & 0x00FF); if ((maskedChange == NSChangeDone) || (maskedChange == NSChangeRedone)) slimChangeCount++; else if (maskedChange == NSChangeUndone) slimChangeCount--; [[self slimWindowController] updateRecycleHighlightForChangeCount:slimChangeCount]; } - (BOOL)changedSinceRecycle { return !(slimChangeCount == 0); } - (void)resetSLiMChangeCount { slimChangeCount = 0; [[self slimWindowController] updateRecycleHighlightForChangeCount:slimChangeCount]; } // Transient document support: adapted from TextEdit. A transient document is an untitled document that // was opened automatically. If a real document is opened before the transient document is edited, the // real document should replace the transient. If a transient document is edited, it ceases to be transient. - (BOOL)isTransient { return transient; } - (void)setTransient:(BOOL)flag { transient = flag; } - (BOOL)isTransientAndCanBeReplaced { if (![self isTransient]) return NO; // We can't replace transient document that have sheets on them. for (NSWindowController *controller in [self windowControllers]) if ([[controller window] attachedSheet]) return NO; return YES; } // Opt out of all of Apple's autosaving, versioning, etc. Partly because I dislike those features, // partly because I don't want a user's model to be saved until they explicitly save since // modeling often involves a lot of trial and error, partly because I'm not sure how versioning // in OS X interacts with sharing files with Linux. + (BOOL)autosavesInPlace { return NO; } + (BOOL)autosavesDrafts { return NO; } + (BOOL)preservesVersions { return NO; } @end ================================================ FILE: SLiMgui/SLiMDocumentController.h ================================================ // // SLiMDocumentController.h // SLiM // // Created by Ben Haller on 2/2/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import // This class provides some additional functionality to support transient documents. It is instantiated in MainMenu.xib, // and thus becomes the shared document controller for the app. @class SLiMDocument; @interface SLiMDocumentController : NSDocumentController { } @property (nonatomic) BOOL creatingNonWFDocument; // a flag set across newNonWFDocument: to signal that the new document is a nonWF document, not a WF document - (SLiMDocument *)transientDocumentToReplace; - (void)replaceTransientDocument:(NSArray *)documents; - (void)openRecipeWithFilename:(NSString *)filename; - (IBAction)newNonWFDocument:(id)sender; - (IBAction)findRecipe:(id)sender; - (IBAction)openRecipe:(id)sender; @end ================================================ FILE: SLiMgui/SLiMDocumentController.mm ================================================ // // SLiMDocumentController.m // SLiM // // Created by Ben Haller on 2/2/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "SLiMDocumentController.h" #import "SLiMDocument.h" #import "SLiMPDFDocument.h" #import "FindRecipeController.h" #import "CocoaExtra.h" @implementation SLiMDocumentController - (SLiMDocument *)transientDocumentToReplace { NSArray *documents = [self documents]; if ([documents count] == 1) { NSDocument *firstDoc = [documents objectAtIndex:0]; if ([firstDoc isKindOfClass:[SLiMDocument class]]) { SLiMDocument *slimDoc = (SLiMDocument *)firstDoc; if ([slimDoc isTransientAndCanBeReplaced]) return slimDoc; } } return nil; } - (void)replaceTransientDocument:(NSArray *)documents { SLiMDocument *transientDoc = [documents objectAtIndex:0], *doc = [documents objectAtIndex:1]; NSArray *controllersToTransfer = [[transientDoc windowControllers] copy]; NSEnumerator *controllerEnum = [controllersToTransfer objectEnumerator]; NSWindowController *controller; [controllersToTransfer makeObjectsPerformSelector:@selector(retain)]; while (controller = [controllerEnum nextObject]) { [doc addWindowController:controller]; [transientDoc removeWindowController:controller]; } [transientDoc close]; [controllersToTransfer makeObjectsPerformSelector:@selector(release)]; [controllersToTransfer release]; // Push the model into the new doc again, so that it gets flushed out to the window controller that really manages it [doc setDocumentScriptString:[doc documentScriptString]]; } // When a document is opened, check to see whether there is a document that is already open, and whether it is transient. // If so, transfer the document's window controllers and close the transient document. - (void)openDocumentWithContentsOfURL:(NSURL *)absoluteURL display:(BOOL)displayDocument completionHandler:(nonnull void (^)(NSDocument * _Nullable, BOOL, NSError * _Nullable))completionHandler { SLiMDocument *transientDoc = [self transientDocumentToReplace]; if (transientDoc) { // Defer display so we can replace the transient document first. We have to have our own handler to intercept the result, // process it, and then call our caller's completion handler. [super openDocumentWithContentsOfURL:absoluteURL display:NO completionHandler:(^ void (NSDocument *typelessDoc, BOOL already_open, NSError *error) { if (typelessDoc) { if ([typelessDoc isKindOfClass:[SLiMDocument class]]) { SLiMDocument *doc = (SLiMDocument *)typelessDoc; [self replaceTransientDocument:[NSArray arrayWithObjects:transientDoc, doc, nil]]; } if (displayDocument) { [typelessDoc makeWindowControllers]; [typelessDoc showWindows]; } } completionHandler(typelessDoc, NO, error); })]; } else { [super openDocumentWithContentsOfURL:absoluteURL display:displayDocument completionHandler:completionHandler]; } } - (IBAction)newNonWFDocument:(id)sender { @try { _creatingNonWFDocument = YES; [self newDocument:sender]; } @finally { _creatingNonWFDocument = NO; } } - (IBAction)findRecipe:(id)sender { [FindRecipeController runFindRecipesPanel]; } - (void)openRecipeWithFilename:(NSString *)filename { if ([[filename pathExtension] isEqualToString:@"py"]) { NSBundle *bundle = [NSBundle mainBundle]; NSURL *urlForRecipe = [bundle URLForResource:[filename stringByDeletingPathExtension] withExtension:[filename pathExtension] subdirectory:@"Recipes"]; if (urlForRecipe) { // Duplicate the file into /tmp; there seems to be no protection against the user modifying files inside the app bundle! // Setting the stationery bit wouldn't work either, I guess; I guess it would create a duplicate file inside the app bundle :-O // This all seems pretty broken; maybe it's because I'm running under Xcode, but I want it to behave right even in that case. // This at least prevents the user from seeing/modifying the file inside the app bundle, but it will have a weird name/path. NSFileManager *fm = [NSFileManager defaultManager]; NSString *tempPath = [NSString slimPathForTemporaryFileWithPrefix:[filename stringByDeletingPathExtension]]; NSString *tempFile = [tempPath stringByAppendingPathExtension:@"py"]; [fm copyItemAtPath:[urlForRecipe path] toPath:tempFile error:NULL]; // Then open Python files in whatever app the user has set up for that [[NSWorkspace sharedWorkspace] openFile:tempFile]; } } else if ([[filename pathExtension] isEqualToString:@"txt"]) { NSBundle *bundle = [NSBundle mainBundle]; NSURL *urlForRecipe = [bundle URLForResource:[filename stringByDeletingPathExtension] withExtension:[filename pathExtension] subdirectory:@"Recipes"]; if (urlForRecipe) { NSString *scriptString = [NSString stringWithContentsOfURL:urlForRecipe usedEncoding:NULL error:NULL]; if (scriptString) { SLiMDocument *transientDoc = [self transientDocumentToReplace]; NSDocument *typelessDoc = [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:NO error:NULL]; if (typelessDoc && [typelessDoc isKindOfClass:[SLiMDocument class]]) { SLiMDocument *doc = (SLiMDocument *)typelessDoc; if (transientDoc) [self replaceTransientDocument:[NSArray arrayWithObjects:transientDoc, doc, nil]]; [doc makeWindowControllers]; [doc showWindows]; [doc setDocumentScriptString:scriptString]; NSString *recipeName = [filename stringByDeletingPathExtension]; if ([recipeName hasPrefix:@"Recipe "]) recipeName = [recipeName substringFromIndex:7]; [doc setRecipeName:recipeName]; // tell the doc it's a recipe doc, so it can manage its title properly [[doc slimWindowController] synchronizeWindowTitleWithDocumentName]; // remake the window title } } } } else { NSLog(@"Unrecognized recipe extension %@", [filename pathExtension]); NSBeep(); } } - (IBAction)openRecipe:(id)sender { NSMenuItem *senderMenuItem = (NSMenuItem *)sender; NSString *recipeName = [senderMenuItem title]; if ([recipeName hasSuffix:@".py 🐍"]) { NSString *trimmedRecipeName = [recipeName stringByReplacingOccurrencesOfString:@" 🐍" withString:@""]; NSString *fullRecipeName = [NSString stringWithFormat:@"Recipe %@", trimmedRecipeName]; [self openRecipeWithFilename:fullRecipeName]; } else { NSString *fullRecipeName = [[NSString stringWithFormat:@"Recipe %@", recipeName] stringByAppendingPathExtension:@"txt"]; [self openRecipeWithFilename:fullRecipeName]; } } // When a second document is added, the first document's transient status is cleared. // This happens when the user selects "New" when a transient document already exists. - (void)addDocument:(NSDocument *)newDoc { NSArray *documents = [self documents]; if ([documents count] == 1) { NSDocument *firstDoc = [documents objectAtIndex:0]; if ([firstDoc isKindOfClass:[SLiMDocument class]]) { SLiMDocument *slimDoc = (SLiMDocument *)firstDoc; if ([slimDoc isTransient]) [slimDoc setTransient:NO]; } } [super addDocument:newDoc]; } - (void)noteNewRecentDocument:(NSDocument *)document { // prevent PDF documents from being added to the recent documents list if ([document isKindOfClass:[SLiMPDFDocument class]]) return; [super noteNewRecentDocument:document]; } @end ================================================ FILE: SLiMgui/SLiMHelpCallbacks.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2709 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Italic;\f1\fnil\fcharset0 Menlo-Italic;\f2\fswiss\fcharset0 Optima-Regular; \f3\fnil\fcharset0 Menlo-Regular;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red28\green0\blue207;\red63\green110\blue116; \red20\green0\blue196;\red181\green0\blue19;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c15223\c16313\c84844;\cssrgb\c30795\c50445\c52970; \cssrgb\c10980\c0\c81176;\cssrgb\c76863\c10196\c8627;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab720 \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i\fs22 \cf0 5.13.0 ITEM: 1. \f1\fs18 initialize() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 Before a SLiM simulation can be run, the various classes underlying the simulation need to be set up with an initial configuration. Simulation configuration in SLiM is done in \f1\i\fs18 initialize() \f0\fs22 callbacks \f2\i0 that run prior to the beginning of simulation execution. For our present purposes, the idea is very simple; in your input file, you can write something like this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 initialize()\uc0\u8232 \{\u8232 ...\u8232 \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 initialize() \f2\fs22 declaration specifies that the script block is to be executed as an \f3\fs18 initialize() \f2\fs22 callback before the simulation starts. The script between the braces \f3\fs18 \{\} \f2\fs22 would set up various aspects of the simulation by calling \f0\i initialization functions \f2\i0 . These are SLiM functions that may be called only in an \f3\fs18 initialize() \f2\fs22 callback, and their names begin with \f3\fs18 initialize \f2\fs22 to mark them clearly as such. You may also use other Eidos functionality in these callbacks; for example, you might automate generating a complex genetic structure containing many genes by using a \f3\fs18 for \f2\fs22 loop.\ In general, it is required for a species to set up its genetic structure in an \f3\fs18 initialize() \f2\fs22 callback with calls to \f3\fs18 initializeMutationRate() \f2\fs22 , \f3\fs18 initializeRecombinationRate() \f2\fs22 , \f3\fs18 initializeMutationType() \f2\fs22 , \f3\fs18 initializeGenomicElementType() \f2\fs22 , and \f3\fs18 initializeGenomicElement() \f2\fs22 ; species must call all of these, setting up at least one mutation type, at least one genomic element type, and at least one genomic element. The exception to this general rule is for species that have no genetics at all \'96 species that are modeled purely on an ecological/behavioral level. Such species may be defined by calling \f0\i none \f2\i0 of those initialization functions; in this case, SLiM will default to a zero-length chromosome with mutation and recombination rates of zero. A middle ground between these two configuration paths is not allowed; either a species has no genetics, or it fully defines its genetics.\ One thing worth mentioning is that in the context of an \f3\fs18 initialize() \f2\fs22 callback, the \f3\fs18 sim \f2\fs22 global representing the species being simulated is not defined. This is because the state of the simulation is not yet constructed fully, and accessing partially constructed state would not be safe. (Similarly, in multispecies models, the \f3\fs18 community \f2\fs22 object and the objects representing individual species are not yet defined.)\ The above \f3\fs18 initialize() \f2\fs22 callback syntax \f0\i implicitly \f2\i0 declares a single species, with the default name of \f3\fs18 sim \f2\fs22 , and therefore sets up a single-species model. It is also possible to \f0\i explicitly \f2\i0 declare a species, which is done with this extended syntax (using a species name of \f3\fs18 fox \f2\fs22 as an example):\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 species fox initialize() \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 This sets up a multispecies model (although it might, in fact, declare only a single species, \f3\fs18 fox \f2\fs22 ; the term \'93multispecies\'94, in SLiM parlance, really means \'93explicitly declared species\'94, but multispecies models almost always \f0\i do \f2\i0 contain multiple species, so the distinction is unimportant). In most respects multispecies models work identically to single-species models, so we will tend to focus on the single-species case in the reference documentation, with a species name of \f3\fs18 sim \f2\fs22 , for simplicity and clarity.\ In single-species models all initialization can be done in a single \f3\fs18 initialize() \f2\fs22 callback (or you can have more than one, if you wish). In multispecies models, each species must be initialized with its own callback(s), as shown above. In addition, multispecies models also support an optional community-level initialization callback that is declared as follows:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 species all initialize() \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 These callbacks, technically called \f0\i non-species-specific \f1\fs18 initialize() \f0\fs22 callbacks \f2\i0 , provide a place for community-level initialization to occur. They are run before any species-specific \f3\fs18 initialize() \f2\fs22 callbacks are run, so you might wish to set up all of your model parameters in one, providing a single location for all parameters. In multispecies models, the \f3\fs18 initializeModelType() \f2\fs22 and \f3\fs18 initializeInteractionType() \f2\fs22 functions may only be called from a non-species-specific \f3\fs18 initialize() \f2\fs22 callback, since those aspects of model configuration span the entire community. In single-species models, these functions may be called from an ordinary \f3\fs18 initialize() \f2\fs22 callback for simplicity and backward compatibility.\ Once all \f3\fs18 initialize() \f2\fs22 callbacks have executed, in the order in which they are specified in the SLiM input file, the simulation will begin. The tick number at which it starts is determined by the Eidos events you have defined; the first tick in which an Eidos event is scheduled to execute is the tick at which the simulation starts. Similarly, the simulation will terminate after the last tick for which a script block (either an event or a callback) is registered to execute, unless the \f3\fs18 stop() \f2\fs22 function or the \f3\fs18 simulationFinished() \f2\fs22 method of \f3\fs18 Community \f2\fs22 or \f3\fs18 Species \f2\fs22 are called to end the simulation earlier.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 5.13.1 ITEM: 2. Eidos events\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 An Eidos event is a block of Eidos code that is executed every tick, within a tick range, to perform a desired task. The syntax of an Eidos event declaration looks like one of these:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] first() \{ ... \}\uc0\u8232 [id] [t1 [: t2]] early() \{ ... \}\u8232 [id] [t1 [: t2]] late() \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The first declaration declares a \f3\fs18 first() \f2\fs22 event that executes first thing in the tick cycle. The second declaration declares an \f3\fs18 early() \f2\fs22 event that executes relatively early in the tick cycle. The third declaration declares a \f3\fs18 late() \f2\fs22 event that executes near the end of the tick cycle. Exactly when these events run depends upon whether the model is a WF model or a nonWF model; see the tick cycle diagrams for those model types.\ The \f3\fs18 id \f2\fs22 is an optional identifier like \f3\fs18 s1 \f2\fs22 (or more generally, \f3\fs18 sX \f2\fs22 , where \f3\fs18 X \f2\fs22 is an integer greater than or equal to \f3\fs18 0 \f2\fs22 ) that defines an identifier that can be used to refer to the script block. In most situations it can be omitted, in which case the \f3\fs18 id \f2\fs22 is implicitly defined as \f3\fs18 -1 \f2\fs22 , a placeholder value that essentially represents the lack of an identifier value. Supplying an \f3\fs18 id \f2\fs22 is only useful if you wish to manipulate your script blocks programmatically.\ Then comes a tick or a range of ticks, and then a block of Eidos code enclosed in braces to form a compound statement. A trivial example might look like this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf3 1000\cf2 :\cf3 5000\cf2 early() \{\uc0\u8232 catn(\cf4 community\cf2 .tick);\uc0\u8232 \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 This would print the tick number in every tick in the specified range, which is obviously not very exciting. The broader point is that the Eidos code in the braces \f3\fs18 \{\} \f2\fs22 is executed early in every tick within the specified range of ticks. In this case, the tick range is \f3\fs18 1000 \f2\fs22 to \f3\fs18 5000 \f2\fs22 , and so the Eidos event will be executed 4001 times (not 4000!). A range of ticks can be given, as in the example above, or a single tick can be given with a single integer:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf5 100\cf2 late() \{\uc0\u8232 print(\cf6 "Finished tick 100!"\cf2 );\uc0\u8232 \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The tick range may also be incompletely specified, with a somewhat idiosyncratic syntax. A range of \f3\fs18 1000: \f2\fs22 would specify that the event should run in tick \f3\fs18 1000 \f2\fs22 and every subsequent tick until the model finishes; a range of \f3\fs18 :1000 \f2\fs22 would similarly specify that the event should run in the first tick executed, and every subsequent tick, up to and including tick \f3\fs18 1000 \f2\fs22 .\ In fact, you can omit specifying a tick altogether, in which case the Eidos event runs every tick. Since it takes a little time to set up the Eidos interpreter and interpret a script, it is advisable to use the narrowest range of ticks possible; however, that is more of a concern with callbacks, since they might be called many time in every tick, whereas \f3\fs18 first() \f2\fs22 , \f3\fs18 early() \f2\fs22 , and \f3\fs18 late() \f2\fs22 events will just be called once per tick.\ The ticks specified for a Eidos event block can be any positive integer. All blocks that apply to a given time point will be run in \f0\i definition order \f2\i0 ; blocks specified higher in the input file will run before those specified lower. Sometimes it is desirable to have a script block execute in a tick which is not fixed, but instead depends upon some parameter, defined constant, or calculation; this may be achieved by rescheduling the script block with the \f3\fs18 Community \f2\fs22 method \f3\fs18 rescheduleScriptBlock() \f2\fs22 .\ In multispecies models, one can optionally provide a \f3\fs18 ticks \f2\fs22 specifier before the definition of an Eidos event, specifying that the event should only run in ticks in which a particular species is active. That extended syntax looks like this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [ticks species_name] [id] [t1 [: t2]] first() \{ ... \}\uc0\u8232 [ticks species_name] [id] [t1 [: t2]] early() \{ ... \}\u8232 [ticks species_name] [id] [t1 [: t2]] late() \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 species_name \f2\fs22 should be the name of a species that was explicitly declared in the multispecies model. If the \f3\fs18 ticks \f2\fs22 specifier is omitted, the event will run in every tick (within the specified tick range).\ When Eidos events are executed, several global variables are defined by SLiM for use by the Eidos code. Here is a summary of those SLiM globals:\ \pard\tx1980\tx2880\pardeftab720\li1890\fi-1343\sa60\partightenfactor0 \f3\fs18 \cf2 community \f2\fs22 The \f3\fs18 Community \f2\fs22 object for the overall simulation\ \f3\fs18 sim \f2\fs22 A \f3\fs18 Species \f2\fs22 object for the simulated species (in single-species simulations)\ \f3\fs18 g1 \f2\fs22 , \f3\fs18 ... \f2\fs22 \f3\fs18 GenomicElementType \f2\fs22 objects for defined genomic element types\ \f3\fs18 i1 \f2\fs22 , \f3\fs18 ... \f2\fs22 \f3\fs18 InteractionType \f2\fs22 objects for defined interaction types\ \f3\fs18 m1 \f2\fs22 , \f3\fs18 ... \f2\fs22 \f3\fs18 MutationType \f2\fs22 objects representing defined mutation types\ \f3\fs18 p1 \f2\fs22 , \f3\fs18 ... \f2\fs22 \f3\fs18 Subpopulation \f2\fs22 objects for existing subpopulations\ \f3\fs18 s1 \f2\fs22 , \f3\fs18 ... \f2\fs22 \f3\fs18 SLiMEidosBlock \f2\fs22 objects for named events and callbacks\ \pard\tx1980\tx2880\pardeftab720\li1890\fi-1343\sa180\partightenfactor0 \f3\fs18 \cf2 self \f2\fs22 A \f3\fs18 SLiMEidosBlock \f2\fs22 object for the script block currently executing\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 In multispecies models, symbols for each species will be defined instead of \f3\fs18 sim \f2\fs22 . Note that species symbols such as \f3\fs18 sim \f2\fs22 are \f0\i not \f2\i0 available in \f3\fs18 initialize() \f2\fs22 callbacks, since the species objects have not yet been initialized. Similarly, the globals for subpopulations, mutation types, and genomic element types are only available after the point at which those objects have been defined by an \f3\fs18 initialize() \f2\fs22 callback.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 \kerning1\expnd0\expndtw0 5.13.2 ITEM: 3. \f1\fs18 mutationEffect() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 An Eidos callback is a block of Eidos code that is called by SLiM in specific circumstances, to allow the customization of particular actions taken by SLiM while running the simulation of a species. Nine types of callbacks are presently supported (in addition to \f3\fs18 initialize() \f2\fs22 callbacks): \f3\fs18 mutationEffect() \f2\fs22 callbacks, discussed here, and \f3\fs18 fitnessEffect() \f2\fs22 , \f3\fs18 mateChoice() \f2\fs22 , \f3\fs18 modifyChild() \f2\fs22 , \f3\fs18 recombination() \f2\fs22 , \f3\fs18 interaction() \f2\fs22 , \f3\fs18 reproduction() \f2\fs22 , \f3\fs18 mutation() \f2\fs22 , and \f3\fs18 survival() \f2\fs22 callbacks.\ A \f3\fs18 mutationEffect() \f2\fs22 callback is called by SLiM when it is determining the fitness effect of a mutation carried by an individual. Normally, the fitness effect of a mutation is determined by the selection coefficient \f0\i s \f2\i0 of the mutation and the dominance coefficient \f0\i h \f2\i0 of the mutation (the latter used only if the individual is heterozygous for the mutation). More specifically, the standard calculation for the fitness effect of a mutation takes one of two forms. If the individual is homozygous, then the fitness effect is (1+ \f0\i s \f2\i0 ), or:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f0\i \cf2 w \f2\i0 = \f0\i w \f2\i0 * (1.0 + selectionCoefficient),\ \pard\pardeftab397\ri720\sb40\sa40\partightenfactor0 \cf2 where \f0\i w \f2\i0 is the relative fitness of the individual carrying the mutation. If the individual is heterozygous, then the dominance coefficient enters the picture, and the fitness effect is (1+ \f0\i hs \f2\i0 ) or:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f0\i \cf2 w \f2\i0 = \f0\i w \f2\i0 * (1.0 + dominanceCoeff * selectionCoeff).\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 The dominance coefficient usually comes from the \f3\fs18 dominanceCoeff \f2\fs22 property of the mutation\'92s \f3\fs18 MutationType \f2\fs22 ; if the focal individual has only one non-null haplosome, however, such that the mutation is paired with a null haplosome (i.e., is actually hemizygous, not heterozygous), the \f3\fs18 hemizygousDominanceCoeff \f2\fs22 property of the \f3\fs18 MutationType \f2\fs22 is used instead.\ That is the standard behavior of SLiM, reviewed here to provide a conceptual baseline. Supplying a \f3\fs18 mutationEffect() \f2\fs22 callback allows you to substitute any calculation you wish for the relative fitness effect of a mutation; the new relative fitness effect computation becomes:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f0\i \cf2 w \f2\i0 = \f0\i w \f2\i0 * \f3\fs18 mutationEffect() \f2\fs22 \ \pard\pardeftab397\ri720\sb40\sa40\partightenfactor0 \cf2 where \f3\fs18 mutationEffect() \f2\fs22 is the value returned by your callback. This value is a multiplicative fitness effect, so \f3\fs18 1.0 \f2\fs22 is neutral, unlike the selection coefficient scale where \f3\fs18 0.0 \f2\fs22 is neutral; be careful with this distinction!\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 Like Eidos events, \f3\fs18 mutationEffect() \f2\fs22 callbacks are defined as script blocks in the input file, but they use a variation of the syntax for defining an Eidos event:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] mutationEffect( [, ]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 For example, if the callback were defined as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 1000:2000 mutationEffect(m2, p3) \{ 1.0; \}\ \pard\pardeftab397\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 then a relative fitness of \f3\fs18 1.0 \f2\fs22 (i.e. neutral) would be used for all mutations of mutation type \f3\fs18 m2 \f2\fs22 in subpopulation \f3\fs18 p3 \f2\fs22 from tick \f3\fs18 1000 \f2\fs22 to tick \f3\fs18 2000 \f2\fs22 . The very same mutations, if also present in individuals in other subpopulations, would preserve their normal selection coefficient and dominance coefficient in those other subpopulations; this callback would therefore establish spatial heterogeneity in selection, in which mutation type \f3\fs18 m2 \f2\fs22 was neutral in subpopulation \f3\fs18 p3 \f2\fs22 but under selection in other subpopulations, for the range of ticks given.\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 In multispecies models, callbacks must be defined with a \f3\fs18 species \f2\fs22 specifier that states the species with which species the callback is associated. Such a definition looks like this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 species species_name [id] [t1 [: t2]] mutationEffect(...) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 It is the same syntax, in other words, except for the \f3\fs18 species \f2\fs22 specifier at the beginning, with the name of the species that the callback will modify. As with the \f3\fs18 ticks \f2\fs22 specifier for events, this means the callback will only be called in ticks when the species is active; but the \f3\fs18 species \f2\fs22 specifier goes further, making that species the focal species for the callback.\ In addition to the standard SLiM globals, a \f3\fs18 mutationEffect() \f2\fs22 callback is supplied with some additional information passed through \'93pseudo-parameters\'94, variables that are defined by SLiM within the context of the callback\'92s code to supply the callback with relevant information:\ \pard\tx1890\tx2880\pardeftab720\li1886\fi-1339\partightenfactor0 \f3\fs18 \cf2 mut \f2\fs22 A \f3\fs18 Mutation \f2\fs22 object, the mutation whose relative fitness is being evaluated\ \f3\fs18 homozygous \f2\fs22 A value of \f3\fs18 T \f2\fs22 (the mutation is homozygous), \f3\fs18 F \f2\fs22 (heterozygous), or \f3\fs18 NULL \f2\fs22 (it is\uc0\u8232 paired with a null haplosome, and is thus hemizygous or haploid)\ \f3\fs18 effect \f2\fs22 The default relative fitness value calculated by SLiM\ \f3\fs18 individual \f2\fs22 The individual carrying this mutation (an object of class \f3\fs18 Individual \f2\fs22 )\ \pard\tx1890\tx2880\pardeftab720\li1890\fi-1343\sa180\partightenfactor0 \f3\fs18 \cf2 subpop \f2\fs22 The subpopulation in which that individual lives\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 These may be used in the \f3\fs18 mutationEffect() \f2\fs22 callback to compute a fitness value. To implement the standard fitness functions used by SLiM for an autosomal simulation with no null haplosomes involved, for example, you could do something like this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 mutationEffect(m1) \{\uc0\u8232 if (homozygous)\u8232 return 1.0 + mut.selectionCoeff;\u8232 else\u8232 return 1.0 + mut.mutationType.dominanceCoeff * mut.selectionCoeff;\u8232 \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 As mentioned above, a relative fitness of \f3\fs18 1.0 \f2\fs22 is neutral (whereas a selection coefficient of \f3\fs18 0.0 \f2\fs22 is neutral); the \f3\fs18 1.0 + \f2\fs22 in these calculations converts between the selection coefficient scale and the relative fitness scale, and is therefore essential. However, the \f3\fs18 effect \f2\fs22 global variable mentioned above would already contain this value, precomputed by SLiM, so you could simply return \f3\fs18 effect \f2\fs22 to get that behavior when you want it:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 mutationEffect(m1) \{\uc0\u8232 if ()\u8232 ;\u8232 else\u8232 return effect;\u8232 \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 This would return a modified fitness value in certain conditions, but would return the standard fitness value otherwise.\ More than one \f3\fs18 mutationEffect() \f2\fs22 callback may be defined to operate in the same tick. As with Eidos events, multiple callbacks will be called in the order in which they were defined in the input file. Furthermore, each callback will be given the \f3\fs18 effect \f2\fs22 value returned by the previous callback \'96 so the value of \f3\fs18 effect \f2\fs22 is not necessarily the default value, in fact, but is the result of all previous \f3\fs18 mutationEffect() \f2\fs22 callbacks for that individual in that tick. In this way, the effects of multiple callbacks can \'93stack\'94.\ One caveat to be aware of in WF models is that \f3\fs18 mutationEffect() \f2\fs22 callbacks are called at the end of the tick, just before the next tick begins. If you have a \f3\fs18 mutationEffect() \f2\fs22 callback defined for tick \f3\fs18 10 \f2\fs22 , for example, it will actually be called at the very end of tick \f3\fs18 10 \f2\fs22 , after child generation has finished, after the new children have been promoted to be the next parental generation, and after \f3\fs18 late() \f2\fs22 events have been executed. The fitness values calculated will thus be used during tick \f3\fs18 11 \f2\fs22 ; the fitness values used in tick \f3\fs18 10 \f2\fs22 were calculated at the end of tick \f3\fs18 9 \f2\fs22 . (This is primarily so that SLiMgui, which refreshes its display in between ticks, has computed fitness values at hand that it can use to display the new parental individuals in the proper colors.) This is not an issue in nonWF models, since fitness values are used in the same tick in which they are calculated.\ If the \f3\fs18 randomizeCallbacks \f2\fs22 parameter to \f3\fs18 initializeSLiMOptions() \f2\fs22 is \f3\fs18 T \f2\fs22 (the default), the order in which the fitness of individuals is evaluated will be randomized within each subpopulation. This partially mitigates order-dependency issues, although such issues can still arise whenever the effects of a \f3\fs18 mutationEffect() \f2\fs22 callback are not independent. If \f3\fs18 randomizeCallbacks \f2\fs22 is \f3\fs18 F \f2\fs22 , the fitness of individuals will be evaluated in sequential order within each subpopulation, greatly increasing the risk of order-dependency problems.\ Many other possibilities can be implemented with \f3\fs18 mutationEffect() \f2\fs22 callbacks. However, since \f3\fs18 mutationEffect() \f2\fs22 callbacks involve Eidos code being executed for the evaluation of fitness of every mutation of every individual (within the tick range, mutation type, and subpopulation specified), they can slow down a simulation considerably, so use them as sparingly as possible.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 5.13.3 ITEM: 4. \f1\fs18 fitnessEffect() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 We have already seen \f3\fs18 mutationEffect() \f2\fs22 callbacks, which modify the effect of a given mutation in a focal individual. Sometimes it is desirable to model effects upon individual fitness that are not governed by particular mutations (or not directly, at least); fitness effects due to spatial position, or resource acquisition, or behavior such as competitive or altruistic interactions, for example. Another situation of this type is when fitness depends upon the overall phenotype of an individual \'96 the height of a tree, say \'96 which might be influenced by genetics, but also by environmental effects, climate, and so forth. For these sorts of situations, SLiM provides \f3\fs18 fitnessEffect() \f2\fs22 callbacks.\ A \f3\fs18 fitnessEffect() \f2\fs22 callback is called by SLiM when it is determining the fitness of an individual \'96 typically, but not always, once per tick during the fitness calculation tick cycle stage. Normally, the fitness of a given individual is determined by multiplying together the fitness effects of all mutations possessed by that individual. Supplying a \f3\fs18 fitnessEffect() \f2\fs22 callback allows you to add another multiplicative fitness effect into that calculation. As with \f3\fs18 mutationEffect() \f2\fs22 callbacks, the value returned by \f3\fs18 fitnessEffect() \f2\fs22 callbacks is a fitness effect, so \f3\fs18 1.0 \f2\fs22 is neutral.\ The syntax for declaring \f3\fs18 fitnessEffect() \f2\fs22 callbacks is similar to that for \f3\fs18 mutationEffect() \f2\fs22 callbacks, but simpler since no mutation type is needed:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] fitnessEffect([]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 (In multispecies models, the definition must be preceded by a \f3\fs18 species \f2\fs22 specification as usual.)\ For example, if the callback were defined as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 1000:2000 fitnessEffect(p3) \{ 0.75; \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 then a fitness effect of \f3\fs18 0.75 \f2\fs22 would be multiplied into the fitness values of all individuals in subpopulation \f3\fs18 p3 \f2\fs22 from tick \f3\fs18 1000 \f2\fs22 to tick \f3\fs18 2000 \f2\fs22 .\ Much more interesting, of course, are \f3\fs18 fitnessEffect() \f2\fs22 callbacks that return different fitness effects for different individuals, depending upon their state! In addition to the standard SLiM globals, a \f3\fs18 fitnessEffect() \f2\fs22 callback is supplied with some additional information passed through \'93pseudo-parameters\'94, variables that are defined by SLiM within the context of the callback\'92s code to supply the callback with relevant information:\ \pard\tx1890\tx2880\pardeftab720\li1886\fi-1339\partightenfactor0 \f3\fs18 \cf2 individual \f2\fs22 The focal individual (an object of class \f3\fs18 Individual \f2\fs22 )\ \pard\tx1890\tx2880\pardeftab720\li1890\fi-1343\sa180\partightenfactor0 \f3\fs18 \cf2 subpop \f2\fs22 The subpopulation in which that individual lives\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 These may be used in the \f3\fs18 fitnessEffect() \f2\fs22 callback to compute a fitness effect that depends upon the state of the focal individual. The fitness effect for the callback is simply returned as a singleton \f3\fs18 float \f2\fs22 value, as usual.\ More than one \f3\fs18 fitnessEffect() \f2\fs22 callback may be defined to operate in the same tick. Each such callback will provide an independent fitness effect for the focal individual; the results of each \f3\fs18 fitnessEffect() \f2\fs22 callback will be multiplied in to the individual\'92s fitness. These callbacks will generally be called once per individual in each tick, in an order that is formally undefined.\ Beginning in SLiM 3.0, it is also possible to set the \f3\fs18 fitnessScaling \f2\fs22 property on a subpopulation to scale the fitness values of every individual in the subpopulation by the same constant amount, or to set the \f3\fs18 fitnessScaling \f2\fs22 property on an individual to scale the fitness value of that specific individual. These scaling factors are multiplied together with all other fitness effects for an individual to produce the individual\'92s final fitness value. The \f3\fs18 fitnessScaling \f2\fs22 properties of \f3\fs18 Subpopulation \f2\fs22 and \f3\fs18 Individual \f2\fs22 can often provide similar functionality to \f3\fs18 fitnessEffect() \f2\fs22 callbacks with greater efficiency and simplicity. They are reset to \f3\fs18 1.0 \f2\fs22 in every tick for which a given species is active, immediately after fitness values are calculated, so they only need to be set when a value other than \f3\fs18 1.0 \f2\fs22 is desired.\ As with \f3\fs18 mutationEffect() \f2\fs22 callbacks, \f3\fs18 fitnessEffect() \f2\fs22 callbacks are called at the end of the tick, just before the next tick begins. Also, as with \f3\fs18 mutationEffect() \f2\fs22 callbacks, the order in which \f3\fs18 fitnessEffect() \f2\fs22 callbacks are called will be shuffled when \f3\fs18 randomizeCallbacks \f2\fs22 is enabled, as it is by default, partially mitigating order-dependency issues.\ The \f3\fs18 fitnessEffect() \f2\fs22 callback mechanism is quite flexible and useful, although it has been considerably eclipsed by the modern modern and efficient \f3\fs18 fitnessScaling \f2\fs22 property mentioned above. When efficiency is not at a premium, it remains a clear and expressive paradigm for modeling individual-level fitness effects. The performance penalty paid is often not large, since these callbacks are called only once per individual per tick, whereas a \f3\fs18 mutationEffect() \f2\fs22 for a type of mutation that is common in the simulation might be called thousands of times per individual per tick (once per mutation of that type possessed by the focal individual). The performance penalty typically becomes severe only when the \f3\fs18 fitnessEffect() \f2\fs22 callback needs to perform calculations, once per focal individual, that would vectorize well if performed across a whole vector of individuals. In such cases, \f3\fs18 fitnessScaling \f2\fs22 should be used.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 5.13.4 ITEM: 5. \f1\fs18 mateChoice() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 Normally, WF models in SLiM regulate mate choice according to fitness; individuals of higher fitness are more likely to be chosen as mates. However, one might wish to simulate more complex mate-choice dynamics such as assortative or disassortative mating, mate search algorithms, and so forth. Such dynamics can be handled in WF models with the \f3\fs18 mateChoice() \f2\fs22 callback mechanism. (In nonWF models mating is arranged by the script, so there is no need for a callback.)\ A \f3\fs18 mateChoice() \f2\fs22 callback is established in the input file with a syntax very similar to that of \f3\fs18 fitnessEffect() \f2\fs22 callbacks:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] mateChoice([]) \{ ... \}\ \pard\pardeftab397\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 (In multispecies models, the definition must be preceded by a \f3\fs18 species \f2\fs22 specification as usual.)\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 Note that if a subpopulation is given to which the \f3\fs18 mateChoice() \f2\fs22 callback is to apply, the callback is used for all matings that will generate a \f0\i child \f2\i0 in the stated subpopulation (as opposed to all matings of \f0\i parents \f2\i0 in the stated subpopulation); this distinction is important when migration causes children in one subpopulation to be generated by matings of parents in a different subpopulation.\ When a \f3\fs18 mateChoice() \f2\fs22 callback is defined, the first parent in a mating is still chosen proportionally according to fitness (if you wish to influence that choice, you can use a \f3\fs18 mutationEffect() \f2\fs22 or \f3\fs18 fitnessEffect() \f2\fs22 callback). In a sexual (rather than hermaphroditic) simulation, this will be the female parent; SLiM does not currently support males as the choosy sex. The second parent \'96 the male parent, in a sexual simulation \'96 will then be chosen based upon the results of the \f3\fs18 mateChoice() \f2\fs22 callback.\ More specifically, the callback must return a vector of weights, one for each individual in the subpopulation; SLiM will then choose a parent with probability proportional to weight. The \f3\fs18 mateChoice() \f2\fs22 callback could therefore modify or replace the standard fitness-based weights depending upon some other criterion such as assortativeness. A singleton object of type \f3\fs18 Individual \f2\fs22 may be returned instead of a weights vector to indicate that that specific individual has been chosen as the mate (beginning in SLiM 2.3); this could also be achieved by returned a vector of weights in which the chosen mate has a non-zero weight and all other weights are zero, but returning the chosen individual directly is much more efficient. A zero-length return vector \'96 as generated by \f3\fs18 float(0) \f2\fs22 , for example \'96 indicates that a suitable mate was not found; in that event, a new first parent will be drawn from the subpopulation. Finally, if the callback returns \f3\fs18 NULL \f2\fs22 , that signifies that SLiM should use the standard fitness-based weights to choose a mate; the \f3\fs18 mateChoice() \f2\fs22 callback did not wish to alter the standard behavior for the current mating (this is equivalent to returning the unmodified vector of weights, but returning \f3\fs18 NULL \f2\fs22 is much faster since it allows SLiM to drop into an optimized case). Apart from the special cases described above \'96 a singleton \f3\fs18 Individual \f2\fs22 , \f3\fs18 float(0) \f2\fs22 , and \f3\fs18 NULL \f2\fs22 \'96 the returned vector of weights must contain the same number of values as the size of the subpopulation, and all weights must be non-negative. Note that the vector of weights is not required to sum to \f3\fs18 1 \f2\fs22 , however; SLiM will convert relative weights on any scale to probabilities for you.\ If the sum of the returned weights vector is zero, SLiM treats it as meaning the same thing as a return of \f3\fs18 float(0) \f2\fs22 \'96 a suitable mate could not be found, and a new first parent will thus be drawn. (This is a change in policy beginning in SLiM 2.3; prior to that, returning a vector of sum zero was considered a runtime error.) There is a subtle difference in semantics between this and a return of \f3\fs18 float(0) \f2\fs22 : returning \f3\fs18 float(0) \f2\fs22 immediately short-circuits mate choice for the current first parent, whereas returning a vector of zeros allows further applicable \f3\fs18 mateChoice() \f2\fs22 callbacks to be called, one of which might \'93rescue\'94 the first parent by returning a non-zero weights vector or an individual. In most models this distinction is irrelevant, since chaining \f3\fs18 mateChoice() \f2\fs22 callbacks is uncommon. When the choice is otherwise unimportant, returning \f3\fs18 float(0) \f2\fs22 will be handled more quickly by SLiM.\ In addition to the standard SLiM globals, a \f3\fs18 mateChoice() \f2\fs22 callback is supplied with some additional information passed through \'93pseudo-parameters\'94:\ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\partightenfactor0 \f3\fs18 \cf2 individual \f2\fs22 The parent already chosen (the female, in sexual simulations)\ \f3\fs18 subpop \f2\fs22 The subpopulation into which the offspring will be placed \f3\fs18 \ sourceSubpop \f2\fs22 The subpopulation from which the parents are being chosen \f3\fs18 \ \pard\tx2070\tx2880\pardeftab720\li2070\fi-1523\sa180\partightenfactor0 \cf2 weights \f2\fs22 The standard fitness-based weights for all individuals\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 If sex is enabled, the \f3\fs18 mateChoice() \f2\fs22 callback must ensure that the appropriate weights are zero and nonzero to guarantee that all eligible mates are male (since the first parent chosen is always female, as explained above). In other words, weights for females must be \f3\fs18 0 \f2\fs22 . The \f3\fs18 weights \f2\fs22 vector given to the callback is guaranteed to satisfy this constraint. If sex is not enabled \'96 in a hermaphroditic simulation, in other words \'96 this constraint does not apply.\ For example, a simple \f3\fs18 mateChoice() \f2\fs22 callback might look like this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 1000:2000 mateChoice(p2) \{\uc0\u8232 return weights ^ 2;\u8232 \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 This defines a \f3\fs18 mateChoice() \f2\fs22 callback for ticks \f3\fs18 1000 \f2\fs22 to \f3\fs18 2000 \f2\fs22 for subpopulation \f3\fs18 p2 \f2\fs22 . The callback simply transforms the standard fitness-based probabilities by squaring them. Code like this could represent a situation in which fitness and mate choice proceed normally in one subpopulation ( \f3\fs18 p1 \f2\fs22 , here, presumably), but are altered by the effects of a social dominance hierarchy or male-male competition in another subpopulation ( \f3\fs18 p2 \f2\fs22 , here), such that the highest-fitness individuals tend to be chosen as mates more often than their (perhaps survival-based) fitness values would otherwise suggest. Note that by basing the returned weights on the \f3\fs18 weights \f2\fs22 vector supplied by SLiM, the requirement that females be given weights of \f3\fs18 0 \f2\fs22 is finessed; in other situations, care would need to be taken to ensure that.\ More than one \f3\fs18 mateChoice() \f2\fs22 callback may be defined to operate in the same tick. As with Eidos events, multiple callbacks will be called in the order in which they were defined. Furthermore, each callback will be given the \f3\fs18 weights \f2\fs22 vector returned by the previous callback \'96 so the value of \f3\fs18 weights \f2\fs22 is not necessarily the default fitness-based weights, in fact, but is the result of all previous \f3\fs18 weights() \f2\fs22 callbacks for the current mate-choice event. In this way, the effects of multiple callbacks can \'93stack\'94. If any \f3\fs18 mateChoice() \f2\fs22 callback returns \f3\fs18 float(0) \f2\fs22 , however \'96 indicating that no eligible mates exist, as described above \'96 then the remainder of the callback chain will be short-circuited and a new first parent will immediately be chosen.\ Note that matings in SLiM do not proceed in random order. Offspring are generated for each subpopulation in turn, and within each subpopulation the order of offspring generation is also non-random with respect to both the source subpopulation and the sex of the offspring. It is important, therefore, that \f3\fs18 mateChoice() \f2\fs22 callbacks are not in any way biased by the offspring generation order; they should not treat matings early in the process any differently than matings late in the process. Any failure to guarantee such invariance could lead to large biases in the simulation outcome. In particular, it is usually dangerous to activate or deactivate \f3\fs18 mateChoice() \f2\fs22 callbacks while offspring generation is in progress.\ A wide variety of mate choice algorithms can easily be implemented with \f3\fs18 mateChoice() \f2\fs22 callbacks. However, \f3\fs18 mateChoice() \f2\fs22 callbacks can be particularly slow since they are called for every proposed mating, and the vector of mating weights can be large and slow to process.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 \kerning1\expnd0\expndtw0 5.13.5 ITEM: 6. \f1\fs18 modifyChild() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 Normally, a SLiM simulation defines child generation with its rules regarding selfing versus crossing, recombination, mutation, and so forth. However, one might wish to modify these rules in particular circumstances \'96 by preventing particular children from being generated, by modifying the generated children in particular ways, or by generating children oneself. All of these dynamics can be handled in SLiM with the \f3\fs18 modifyChild() \f2\fs22 callback mechanism.\ A \f3\fs18 modifyChild() \f2\fs22 callback is established in the input file with a syntax very similar to that of other callbacks:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] modifyChild([]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 modifyChild() \f2\fs22 callback may optionally be restricted to the children generated to occupy a specified subpopulation. (In multispecies models, the definition must be preceded by a \f3\fs18 species \f2\fs22 specification as usual.)\ When a \f3\fs18 modifyChild() \f2\fs22 callback is called, a parent or parents have already been chosen, and a candidate child has already been generated. The parent or parents are provided to the callback, as is the generated child. The callback may accept the generated child, modify it, substitute completely different genetic information for it, or reject it (causing a new parent or parents to be selected and a new child to be generated, which will again be passed to the callback).\ In addition to the standard SLiM globals, a \f3\fs18 modifyChild() \f2\fs22 callback is supplied with additional information passed through \'93pseudo-parameters\'94:\ \pard\tx2520\pardeftab720\li1080\fi-533\partightenfactor0 \f3\fs18 \cf2 child \f2\fs22 The generated child (an object of class \f3\fs18 Individual \f2\fs22 )\ \f3\fs18 isCloning \f2\fs22 \f3\fs18 T \f2\fs22 if the child is the result of cloning\ \f3\fs18 isSelfing \f2\fs22 \f3\fs18 T \f2\fs22 if the child is the result of selfing (but see note below)\ \f3\fs18 parent1 \f2\fs22 The first parent (an object of class \f3\fs18 Individual \f2\fs22 )\ \f3\fs18 parent2 \f2\fs22 The second parent (an object of class \f3\fs18 Individual \f2\fs22 )\ \f3\fs18 subpop \f2\fs22 The subpopulation in which the child will live\ \pard\tx2520\pardeftab720\li1080\fi-533\sa180\partightenfactor0 \f3\fs18 \cf2 sourceSubpop \f2\fs22 The subpopulation of the parents ( \f3\fs18 ==subpop \f2\fs22 if not a migration mating)\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 These may be used in the \f3\fs18 modifyChild() \f2\fs22 callback to decide upon a course of action. The haplosomes of child (available as \f3\fs18 child.haplosomes \f2\fs22 ) may be modified by the callback; whatever mutations they contain on exit will be used for the new child. Alternatively, they may be left unmodified (to accept the generated child as is). The child\'92s haplosomes may be thought of as the two gametes that will fuse to produce the fertilized egg that results in a new offspring; for a biparental cross involving diploid autosomes, \f3\fs18 child.haploidGenome1 \f2\fs22 is the gamete contributed by the first parent (the female, if sex is turned on), and \f3\fs18 child.haploidGenome2 \f2\fs22 is the gamete contributed by the second parent (the male, if sex is turned on). The \f3\fs18 child \f2\fs22 object itself may also be modified \'96 for example, to set the spatial position of the child.\ Importantly, a \f3\fs18 logical \f2\fs22 singleton return value is required from \f3\fs18 modifyChild() \f2\fs22 callbacks. Normally this should be \f3\fs18 T \f2\fs22 , indicating that generation of the child may proceed (with whatever modifications might have been made to the child\'92s haplosomes). A return value of \f3\fs18 F \f2\fs22 indicates that generation of this child should not continue; this will cause new parent(s) to be drawn, a new child to be generated, and a new call to the \f3\fs18 modifyChild() \f2\fs22 callback. A \f3\fs18 modifyChild() \f2\fs22 callback that always returns \f3\fs18 F \f2\fs22 can cause SLiM to hang, so be careful that it is guaranteed that your callback has a nonzero probability of returning \f3\fs18 T \f2\fs22 for every state your simulation can reach.\ Note that \f3\fs18 isSelfing \f2\fs22 is \f3\fs18 T \f2\fs22 only when a mating was explicitly set up to be a selfing event by SLiM; an individual may also mate with itself by chance (by drawing itself as a mate) even when SLiM did not explicitly set up a selfing event, which one might term \f0\i incidental \f2\i0 selfing. If you need to know whether a mating event was an incidental selfing event, you can compare the parents; self-fertilization will always entail \f3\fs18 parent1==parent2 \f2\fs22 , even when \f3\fs18 isSelfing \f2\fs22 is \f3\fs18 F \f2\fs22 . Since selfing is enabled only in non-sexual simulations, \f3\fs18 isSelfing \f2\fs22 will always be \f3\fs18 F \f2\fs22 in sexual simulations (and incidental selfing is also impossible in sexual simulations).\ Note that matings in SLiM do not proceed in random order. Offspring are generated for each subpopulation in turn, and within each subpopulation the order of offspring generation is also non-random with respect to the source subpopulation, the sex of the offspring, and the reproductive mode (selfing, cloning, or autogamy). It is important, therefore, that \f3\fs18 modifyChild() \f2\fs22 callbacks are not in any way biased by the offspring generation order; they should not treat offspring generated early in the process any differently than offspring generated late in the process. Similar to \f3\fs18 mateChoice() \f2\fs22 callbacks, any failure to guarantee such invariance could lead to large biases in the simulation outcome. In particular, it is usually dangerous to activate or deactivate \f3\fs18 modifyChild() \f2\fs22 callbacks while offspring generation is in progress. When SLiM sees that \f3\fs18 mateChoice() \f2\fs22 or \f3\fs18 modifyChild() \f2\fs22 callbacks are defined, it randomizes the order of child generation within each subpopulation, so this issue is mitigated somewhat. However, offspring are still generated for each subpopulation in turn. Furthermore, in ticks without active callbacks offspring generation order will not be randomized (making the order of parents nonrandom in the next generation), with possible side effects. In short, order-dependency issues are possible and must be handled very carefully.\ As with the other callback types, multiple \f3\fs18 modifyChild() \f2\fs22 callbacks may be registered and active. In this case, all registered and active callbacks will be called for each child generated, in the order that the callbacks were registered. If a \f3\fs18 modifyChild() \f2\fs22 callback returns \f3\fs18 F \f2\fs22 , however, indicating that the child should not be generated, the remaining callbacks in the chain will not be called.\ There are many different ways in which a \f3\fs18 modifyChild() \f2\fs22 callback could be used in a simulation. In nonWF models, \f3\fs18 modifyChild() \f2\fs22 callbacks are often unnecessary since each generated child is available to the script in the models\'92 \f3\fs18 reproduction() \f2\fs22 callback anyway; but they may be used if desired.\cf0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 5.13.6 ITEM: 7. \f1\fs18 recombination() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 Typically, a simulation sets up a recombination map at the beginning of the run with \f3\fs18 initializeRecombinationRate() \f2\fs22 , and that map is used for the duration of the run. Less commonly, the recombination map is changed dynamically from tick to tick, with \f3\fs18 Chromosome \f2\fs22 \'92s method \f3\fs18 setRecombinationRate() \f2\fs22 ; but still, a single recombination map applies for all individuals of a species in a given tick. However, in unusual circumstances a simulation may need to modify the way that recombination works on an individual basis; for this, the \f3\fs18 recombination() \f2\fs22 callback mechanism is provided. This can be useful for models involving chromosomal inversions that prevent recombination within a region for some individuals, for example, or for models of the evolution of recombination.\ A \f3\fs18 recombination() \f2\fs22 callback is defined with a syntax much like that of other callbacks:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] recombination([ [, ]]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 recombination() \f2\fs22 callback will be called during the generation of every gamete during the tick(s) in which it is active. It may optionally be restricted to apply only to gametes generated by parents in a specified subpopulation, using the \f3\fs18 \f2\fs22 specifier. In addition, in multi-chromosome models it may optionally be restricted to apply only to a specified chromosome, using the \f3\fs18 \f2\fs22 specifier, which may be either the \f3\fs18 id \f2\fs22 or the \f3\fs18 symbol \f2\fs22 of a chromosome defined in the species. (In multispecies models, the definition must be preceded by a \f3\fs18 species \f2\fs22 specification as usual.)\ When a \f3\fs18 recombination() \f2\fs22 callback is called, a parent has already been chosen to generate a gamete, and candidate recombination breakpoints for use in recombining the parental haplosomes have been drawn. The relevant haplosomes of the focal parent are provided to the callback, as is the focal parent itself (as an \f3\fs18 Individual \f2\fs22 object) and the subpopulation in which it resides. Furthermore, the proposed breakpoints are provided to the callback. The callback may modify these breakpoints in order to change the breakpoints used, in which case it must return \f3\fs18 T \f2\fs22 to indicate that changes were made, or it may leave the proposed breakpoints unmodified, in which case it must return \f3\fs18 F \f2\fs22 . (The behavior of SLiM is undefined if the callback returns the wrong \f3\fs18 logical \f2\fs22 value.)\ In addition to the standard SLiM globals, then, a \f3\fs18 recombination() \f2\fs22 callback is supplied with additional information passed through \'93pseudo-parameters\'94:\ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\partightenfactor0 \f3\fs18 \cf2 individual \f2\fs22 The focal parent that is generating a gamete\ \f3\fs18 haplosome1 \f2\fs22 One haplosome of the focal parent; this is the initial copy strand\ \f3\fs18 haplosome2 \f2\fs22 The other haplosome of the focal parent \f3\fs18 \ subpop \f2\fs22 The subpopulation to which the focal parent belongs \f3\fs18 \ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\sa180\partightenfactor0 \cf2 breakpoints \f2\fs22 An \f3\fs18 integer \f2\fs22 vector of crossover breakpoints \f3\fs18 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 These may be used in the \f3\fs18 recombination() \f2\fs22 callback to determine the final recombination breakpoints used by SLiM. If values are set into \f3\fs18 breakpoints \f2\fs22 , the new values must be of type \f3\fs18 integer \f2\fs22 . If \f3\fs18 breakpoints \f2\fs22 is modified by the callback, \f3\fs18 T \f2\fs22 should be returned, otherwise \f3\fs18 F \f2\fs22 should be returned (this is a speed optimization, so that SLiM does not have to spend time checking for changes when no changes have been made).\ The positions specified in \f3\fs18 breakpoints \f2\fs22 mean that a crossover will occur immediately \f0\i before \f2\i0 the specified base position (between the preceding base and the specified base, in other words). The haplosome specified by \f3\fs18 haplosome1 \f2\fs22 will be used as the initial copy strand when SLiM executes the recombination; this cannot presently be changed by the callback. (Note that \f3\fs18 haplosome1 \f2\fs22 and \f3\fs18 haplosome2 \f2\fs22 will be haplosomes from \f3\fs18 individual \f2\fs22 , but their order may be swapped, depending on which is the initial copy strand!)\ In this design, the recombination callback does not specify a custom recombination map. Instead, the callback can add or remove breakpoints at specific locations. To implement a chromosomal inversion, for example, if the parent is heterozygous for the inversion mutation then crossovers within the inversion region are removed by the callback. As another example, to implement a model of the evolution of the overall recombination rate, a model could (1) set the global recombination rate to the highest rate attainable in the simulation, (2) for each individual, within the \f3\fs18 recombination() \f2\fs22 callback, calculate the fraction of that maximum rate that the focal individual would experience based upon its genetics, and (3) probabilistically remove proposed crossover points based upon random uniform draws compared to that threshold fraction, thus achieving the individual effective recombination rate desired. Other similar treatments could actually vary the effective recombination map, not just the overall rate, by removing proposed crossovers with probabilities that depend upon their position, allowing for the evolution of localized recombination hot-spots and cold-spots. Crossovers may also be added, not just removed, by \f3\fs18 recombination() \f2\fs22 callbacks.\ In SLiM 3.3 the recombination model in SLiM was redesigned. This required a corresponding redesign of \f3\fs18 recombination() \f2\fs22 callbacks. In particular, the \f3\fs18 gcStarts \f2\fs22 and \f3\fs18 gcEnds \f2\fs22 pseudo-parameters to \f3\fs18 recombination() \f2\fs22 callbacks were removed. In the present design, the callback receives \'93crossover breakpoints\'94 information only, in the \f3\fs18 breakpoints \f2\fs22 pseudo-parameter; it receives no information about gene conversion. However, \f3\fs18 recombination() \f2\fs22 callbacks can still be used with the \'93DSB\'94 recombination model; at the point when the callback is called, the pattern of gene conversion tracts will have been simplified down to a vector of crossover breakpoints. \'93Complex\'94 gene conversion tracts, however, involving heteroduplex mismatch repair, are not compatible with \f3\fs18 recombination() \f2\fs22 callbacks, since there is presently no way for them to be specified to the callback.\ Note that the positions in \f3\fs18 breakpoints \f2\fs22 are not, in the general case, guaranteed to be sorted or uniqued; in other words, positions may appear out of order, and the same position may appear more than once. After all \f3\fs18 recombination() \f2\fs22 callbacks have completed, the positions from \f3\fs18 breakpoints \f2\fs22 will be sorted, uniqued, and used as the crossover points in generating the prospective gamete haplosome. The essential point here is that if the same position occurs more than once, across \f3\fs18 breakpoints \f2\fs22 , the multiple occurrences of the position do not cancel; SLiM does not cross over and then \'93cross back over\'94 given a pair of identical positions. Instead, the multiple occurrences of the position will simply be uniqued down to a single occurrence.\ As with the other callback types, multiple \f3\fs18 recombination() \f2\fs22 callbacks may be registered and active. In this case, all registered and active callbacks will be called for each gamete generated, in the order that the callbacks were registered.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 \kerning1\expnd0\expndtw0 5.13.7 ITEM: 8. \f1\fs18 interaction() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 The \f3\fs18 InteractionType \f2\fs22 class provides various built-in interaction functions that translate from distances to interaction strengths. However, it may sometimes be useful to define a custom function for that purpose; for that reason, SLiM allows \f3\fs18 interaction() \f2\fs22 callbacks to be defined that modify the standard interaction strength calculated by \f3\fs18 InteractionType \f2\fs22 . In particular, this mechanism allows the strength of interactions to depend upon not only the distance between individuals, but also the genetics and other state of the individuals, the spatial position of the individuals, and other environmental variables.\ An \f3\fs18 interaction() \f2\fs22 callback is called by SLiM when it is determining the strength of the interaction between one individual (the \f0\i receiver \f2\i0 of the interaction) and another individual (the \f0\i exerter \f2\i0 of the interaction). This generally occurs when an interaction query is made to \f3\fs18 InteractionType \f2\fs22 , as a side effect of serving that query. This means that \f3\fs18 interaction() \f2\fs22 callbacks may be called at a variety of points in the tick cycle, unlike the other callback types in SLiM, which are each called at a specific point. If you write an \f3\fs18 interaction() \f2\fs22 callback, you need to take this into account; assuming that the tick cycle is at a particular stage, or even that the tick or cycle is the same as it was when \f3\fs18 evaluate() \f2\fs22 was called, may be dangerous.\ When an interaction strength is needed, the first thing SLiM does is calculate the default interaction strength using the interaction function that has been defined for the \f3\fs18 InteractionType \f2\fs22 . If the receiver is the same as the exerter, the interaction strength is always zero; and in spatial simulations if the distance between the receiver and the exerter is greater than the maximum distance set for the \f3\fs18 InteractionType \f2\fs22 , the interaction strength is also always zero. In these cases, \f3\fs18 interaction() \f2\fs22 callbacks will not be called, and there is no way to redefine these interaction strengths.\ Otherwise, SLiM will then call \f3\fs18 interaction() \f2\fs22 callbacks that apply to the interaction type and exerter subpopulation for the interaction being evaluated. An \f3\fs18 interaction() \f2\fs22 callback is defined with a variation of the syntax used for other callbacks:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] interaction( [, ]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 For example, if the callback were defined as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 1000:2000 interaction(i2, p3) \{ 1.0; \}\ \pard\pardeftab397\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 then an interaction strength of \f3\fs18 1.0 \f2\fs22 would be used for all interactions of interaction type \f3\fs18 i2 \f2\fs22 , for exerters in subpopulation \f3\fs18 p3 \f2\fs22 , from tick \f3\fs18 1000 \f2\fs22 to tick \f3\fs18 2000 \f2\fs22 .\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 Beginning in SLiM 4, the receiver and exerter may be in different subpopulations from each other \'96 or even, in multispecies models, in different species altogether. For the subpopulation id in the \f3\fs18 interaction() \f2\fs22 callback declaration, it does not matter which subpopulation the receiver is in; if the exerter is in \f3\fs18 p3 \f2\fs22 , for the above example, then the \f3\fs18 interaction() \f2\fs22 callback will be called regardless of the receiver\'92s subpopulation (assuming other preconditions are also met, such as the tick range and the interaction type id). This means that \f3\fs18 interaction() \f2\fs22 callbacks are not species-specific, unlike other callback types; even if an \f3\fs18 interaction() \f2\fs22 callback is declared to be specific to exerters in \f3\fs18 p3 \f2\fs22 , as above, receivers can still be in a different species. With no subpopulation id specified, \f3\fs18 interaction() \f2\fs22 callbacks are even more general: the \f3\fs18 InteractionType \f2\fs22 can then be evaluated and queried for receivers and exerters belonging to any species. For this reason, in multispecies models \f3\fs18 interaction() \f2\fs22 callbacks must be declared using a species specifier of \f3\fs18 species all \f2\fs22 , unlike all other SLiM callback types; it is not legal to declare an \f3\fs18 interaction() \f2\fs22 callback as species-specific. Note that there is no way to declare an \f3\fs18 interaction() \f2\fs22 callback as applying only to receivers in a given subpopulation; if that functionality is desired, you can test \f3\fs18 receiver.subpopulation \f2\fs22 in your callback code and act accordingly.\ In addition to the standard SLiM globals, an \f3\fs18 interaction() \f2\fs22 callback is supplied with some additional information passed through \'93pseudo-parameters\'94:\ \pard\tx1890\tx2880\pardeftab720\li1886\fi-1339\partightenfactor0 \f3\fs18 \cf2 distance \f2\fs22 The distance from receiver to exerter, in spatial simulations; \f3\fs18 NAN \f2\fs22 otherwise\ \f3\fs18 strength \f2\fs22 The default interaction strength calculated by the interaction function\ \f3\fs18 receiver \f2\fs22 The individual receiving the interaction (an object of class \f3\fs18 Individual \f2\fs22 )\ \pard\tx1890\tx2880\pardeftab720\li1890\fi-1343\sa180\partightenfactor0 \f3\fs18 \cf2 exerter \f2\fs22 The individual exerting the interaction (an object of class \f3\fs18 Individual \f2\fs22 )\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 These may be used in the \f3\fs18 interaction() \f2\fs22 callback to compute an interaction strength. To simply use the default interaction strength that SLiM would use if a callback had not been defined for interaction type \f3\fs18 i1 \f2\fs22 , for example, you could do this:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 interaction(i1) \{\uc0\u8232 return strength;\u8232 \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 Usually an \f3\fs18 interaction() \f2\fs22 callback will modify that default strength based upon factors such as the genetics of the receiver and/or the exerter, the spatial positions of the two individuals, or some other simulation state. Any finite \f3\fs18 float \f2\fs22 value greater than or equal to \f3\fs18 0.0 \f2\fs22 may be returned. The value returned will be not be cached by SLiM; if the interaction strength between the same two individuals is needed again later, the \f3\fs18 interaction() \f2\fs22 callback will be called again (something to keep in mind if the interaction strength includes a stochastic component). Note that the provided \f3\fs18 distance \f2\fs22 and \f3\fs18 strength \f2\fs22 values are based upon the spatial positions of the exerter and receiver when \f3\fs18 evaluate() \f2\fs22 was called, not their current spatial positions, if they have moved since the interaction was evaluated.\ More than one \f3\fs18 interaction() \f2\fs22 callback may be defined to operate in the same tick. As with other callbacks, multiple callbacks will be called in the order in which they were defined in the input file. Furthermore, each callback will be given the \f3\fs18 strength \f2\fs22 value returned by the previous callback \'96 so the value of \f3\fs18 strength \f2\fs22 is not necessarily the default value, in fact, but is the result of all previous \f3\fs18 interaction() \f2\fs22 callbacks for the interaction in question. In this way, the effects of multiple callbacks can \'93stack\'94.\ The \f3\fs18 interaction() \f2\fs22 callback mechanism is extremely powerful and flexible, allowing any sort of user-defined interactions whatsoever to be queried dynamically using the methods of \f3\fs18 InteractionType \f2\fs22 . However, in the general case a simulation may call for the evaluation of the interaction strength between each individual and every other individual, making the computation of the full interaction network an O( \f0\i N \f2\i0\fs14\fsmilli7333 \super 2 \fs22 \nosupersub ) problem. Since \f3\fs18 interaction() \f2\fs22 callbacks may be called for each of those \f0\i N \f2\i0\fs14\fsmilli7333 \super 2 \fs22 \nosupersub interaction evaluations, they can slow down a simulation considerably, so it is recommended that they be used sparingly. This is the reason that the various interaction functions of \f3\fs18 InteractionType \f2\fs22 were provided; when an interaction does not depend upon individual state, the intention is to avoid the necessity of an \f3\fs18 interaction() \f2\fs22 callback altogether. Furthermore, constraining the number of cases in which interaction strengths need to be calculated \'96 using a short maximum interaction distance, querying the nearest neighbors of the focal individual rather than querying all possible interactions with that individual, and specifying the reciprocality and sex segregation of the \f3\fs18 InteractionType \f2\fs22 , for example \'96 may greatly decrease the computational overhead of interaction evaluation.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 \kerning1\expnd0\expndtw0 5.13.8 ITEM: 9. \f1\fs18 reproduction() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 In WF models (the default model type in SLiM), the SLiM core manages the reproduction of individuals in each tick. In nonWF models, however, reproduction is managed by the model script, in \f3\fs18 reproduction() \f2\fs22 callbacks. These callbacks may only be defined in nonWF models.\ A \f3\fs18 reproduction() \f2\fs22 callback is defined with a syntax much like that of other callbacks:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] reproduction([ [, ]]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 reproduction() \f2\fs22 callback will be called once for each individual during the tick(s) in which it is active. It may optionally be restricted to apply only to individuals in a specified subpopulation, using the \f3\fs18 \f2\fs22 specifier; this may be a subpopulation specifier such as \f3\fs18 p1 \f2\fs22 , or \f3\fs18 NULL \f2\fs22 indicating no restriction. It may also optionally be restricted to apply only to individuals of a specified sex (in sexual models), using the \f3\fs18 \f2\fs22 specifier; this may be \f3\fs18 "M" \f2\fs22 or \f3\fs18 "F" \f2\fs22 , or \f3\fs18 NULL \f2\fs22 indicating no restriction. (In multispecies models, the definition must be preceded by a \f3\fs18 species \f2\fs22 specification as usual.)\ When a \f3\fs18 reproduction() \f2\fs22 callback is called, SLiM\'92s expectation is that the callback will trigger the reproduction of a focal individual by making method calls to add new offspring individuals. Typically the offspring added are the offspring of the focal individual, and typically they are added to the subpopulation to which the focal individual belongs, but neither of these is required; a \f3\fs18 reproduction() \f2\fs22 callback may add offspring generated by any parent(s), to any subpopulation in the focal species. The focal individual is provided to the callback (as an \f3\fs18 Individual \f2\fs22 object), as is the subpopulation in which it resides.\ A common alternative pattern is for a \f3\fs18 reproduction() \f2\fs22 callback to ignore the focal individual and generate all of the offspring for a species for the current tick, from all parents. The callback then sets \f3\fs18 self.active \f2\fs22 to \f3\fs18 0 \f2\fs22 , preventing itself from being called again in the current tick; this callback design therefore executes once per tick. This can be useful if individuals influence each other\'92s offspring generation (as in a monogamous-mating model, for example); it can also simply be more efficient when producing offspring in bulk.\ In addition to the usual SLiM globals, then, a \f3\fs18 reproduction() \f2\fs22 callback is supplied with additional information passed through global variables:\ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\partightenfactor0 \f3\fs18 \cf2 individual \f2\fs22 The focal individual that is expected to reproduce\ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\sa180\partightenfactor0 \f3\fs18 \cf2 subpop \f2\fs22 The subpopulation to which the focal individual belongs \f3\fs18 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 At present, the return value from \f3\fs18 reproduction() \f2\fs22 callbacks is not used, and must be \f3\fs18 void \f2\fs22 (i.e., a value may not be returned). It is possible that other return values will be defined in future.\ It is possible, of course, to do actions unrelated to reproduction inside \f3\fs18 reproduction() \f2\fs22 callbacks, but it is not recommended. The \f3\fs18 first() \f2\fs22 event phase of the current tick provides an opportunity for actions immediately before reproduction, and the \f3\fs18 early() \f2\fs22 event phase of the current tick provides an opportunity for actions immediately after reproduction, so only actions that are intertwined with reproduction itself should occur in \f3\fs18 reproduction() \f2\fs22 callbacks. Besides providing conceptual clarity, following this design principle will also decrease the probability of bugs, since actions that are unrelated to reproduction should usually not influence or be influenced by the dynamics of reproduction.\ If the \f3\fs18 randomizeCallbacks \f2\fs22 parameter to \f3\fs18 initializeSLiMOptions() \f2\fs22 is \f3\fs18 T \f2\fs22 (the default), the order in which individuals are given an opportunity to reproduce with a call to \f3\fs18 reproduction() \f2\fs22 callbacks will be randomized within each subpopulation. This partially mitigates order-dependency issues, although such issues can still arise whenever the effects of a \f3\fs18 reproduction() \f2\fs22 callback are not independent. If \f3\fs18 randomizeCallbacks \f2\fs22 is \f3\fs18 F \f2\fs22 , individuals will be given their opportunity to reproduce in sequential order within each subpopulation, greatly increasing the risk of order-dependency problems.\ As with the other callback types, multiple \f3\fs18 reproduction() \f2\fs22 callbacks may be registered and active. In this case, all registered and active callbacks will be called for each individual, in the order that the callbacks were registered.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 \kerning1\expnd0\expndtw0 5.13.9 ITEM: 10. \f1\fs18 mutation() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 SLiM auto-generates new mutations according to the current mutation rate (or rate map) and the genetic structure defined by genomic elements, their genomic element types, the mutation types those genomic element types draw from, and the distribution of fitness effects defined by those mutation types. In nucleotide-based models, the nucleotide sequence and the mutation matrix also play a role in determining both the rate of mutation and the nucleotide mutated to. In some models it can be desirable to modify these dynamics in some way \'96 altering the selection coefficients of new mutations in some way, changing the mutation type used, dictating the nucleotide to be used, replacing the proposed mutation with a pre-existing mutation at the same position, or even suppressing the proposed mutation altogether. To achieve this, one may define a \f3\fs18 mutation() \f2\fs22 callback.\ A \f3\fs18 mutation() \f2\fs22 callback is defined as:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] mutation([ [, ]]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 mutation() \f2\fs22 callback will be called once for each new auto-generated mutation during the tick(s) in which the callback is active. It may optionally be restricted to apply only to mutations of a particular mutation type, using the \f3\fs18 \f2\fs22 specifier; this may be a mutation type specifier such as \f3\fs18 m1 \f2\fs22 , or \f3\fs18 NULL \f2\fs22 indicating no restriction. It may also optionally be restricted to individuals generated by a specified subpopulation (usually \'96 see below for discussion), using the \f3\fs18 \f2\fs22 specifier; this should be a subpopulation specifier such as \f3\fs18 p1 \f2\fs22 . (In multispecies models, the definition must be preceded by a \f3\fs18 species \f2\fs22 specification as usual.)\ When a \f3\fs18 mutation() \f2\fs22 callback is called, a focal mutation (provided to the callback as an object of type \f3\fs18 Mutation \f2\fs22 ) has just been created by SLiM, referencing a particular position in a parental haplosome (also provided, as an object of type \f3\fs18 Haplosome \f2\fs22 ). The mutation will not be added to that parental haplosome; rather, the parental haplosome is being copied, during reproduction, to make a gamete or an offspring haplosome, and the mutation is, conceptually, a copying error made during that process. It will be added to the offspring haplosome that is the end result of the copying process (which may also involve recombination with another haplosome). At the point that the \f3\fs18 mutation() \f2\fs22 callback is called, the offspring haplosome is not yet created, however, and so it cannot be accessed from within the \f3\fs18 mutation() \f2\fs22 callback; the \f3\fs18 mutation() \f2\fs22 callback can affect only the mutation itself, not the haplosome to which the mutation will be added.\ In addition to the standard SLiM globals, then, a \f3\fs18 mutation() \f2\fs22 callback is supplied with additional information passed through global variables:\ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\partightenfactor0 \f3\fs18 \cf2 mut \f2\fs22 The focal mutation that is being modified or reviewed\ \f3\fs18 haplosome \f2\fs22 The parental haplosome that is being copied\ \f3\fs18 element \f2\fs22 The genomic element that controls the mutation site\ \f3\fs18 originalNuc \f2\fs22 The nucleotide ( \f3\fs18 0 \f2\fs22 / \f3\fs18 1 \f2\fs22 / \f3\fs18 2 \f2\fs22 / \f3\fs18 3 \f2\fs22 for \f3\fs18 A \f2\fs22 / \f3\fs18 C \f2\fs22 / \f3\fs18 G \f2\fs22 / \f3\fs18 T \f2\fs22 ) originally at the mutating position\ \f3\fs18 parent \f2\fs22 The parent which is generating the offspring haplosome \f3\fs18 \ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\sa180\partightenfactor0 \cf2 subpop \f2\fs22 The subpopulation to which the parent belongs \f3\fs18 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 mutation() \f2\fs22 callback has three possible returns: \f3\fs18 T \f2\fs22 , \f3\fs18 F \f2\fs22 , or (beginning in SLiM 3.5) a singleton object of type \f3\fs18 Mutation \f2\fs22 . A return of \f3\fs18 T \f2\fs22 indicates that the proposed mutation should be used in generating the offspring haplosome (perhaps with modifications made by the callback). Conversely, a return of \f3\fs18 F \f2\fs22 indicates that the proposed mutation should be suppressed. If a proposed mutation is suppressed, SLiM will not try again; one fewer mutations will be generated during reproduction than would otherwise have been true. Returning \f3\fs18 F \f2\fs22 will therefore mean that the realized mutation rate in the model will be lower than the expected mutation rate. Finally, a return of an object of type \f3\fs18 Mutation \f2\fs22 replaces the proposed mutation ( \f3\fs18 mut \f2\fs22 ) with the mutation returned; the offspring haplosomes being generated will contain the returned mutation. The position of the returned mutation must match that of the proposed mutation. This provides a mechanism for a \f3\fs18 mutation() \f2\fs22 callback to make SLiM re-use existing mutations instead of generating new mutations, which can be useful.\ The callback may perform a variety of actions related to the generated mutation. The selection coefficient of the mutation can be changed with \f3\fs18 setSelectionCoefficient() \f2\fs22 , and the mutation type of the mutation can be changed with \f3\fs18 setMutationType() \f2\fs22 ; the \f3\fs18 drawSelectionCoefficient() \f2\fs22 method of \f3\fs18 MutationType \f2\fs22 may also be useful here. A \f3\fs18 tag \f2\fs22 property value may be set for the mutation, and named values may be attached to the mutation with \f3\fs18 setValue() \f2\fs22 . In nucleotide-based models, the \f3\fs18 nucleotide \f2\fs22 (or \f3\fs18 nucleotideValue \f2\fs22 ) property of the mutation may also be changed; note that the original nucleotide at the focal position in the parental haplosome is provided through \f3\fs18 originalNuc \f2\fs22 (it could be retrieved with \f3\fs18 haplosome.nucleotides() \f2\fs22 , but SLiM already has it at hand anyway). All of these modifications to the new mutation may be based upon the state of the parent, including its genetic state, or upon any other model state.\ It is possible, of course, to do actions unrelated to mutation inside \f3\fs18 mutation() \f2\fs22 callbacks, but it is not recommended; \f3\fs18 first() \f2\fs22 , \f3\fs18 early() \f2\fs22 , and \f3\fs18 late() \f2\fs22 events should be used for general-purpose scripting. Besides providing conceptual clarity, following this design principle will also decrease the probability of bugs, since actions that are unrelated to mutation should not influence or be influenced by the dynamics of mutation.\ The proposed mutation will not appear in the \f3\fs18 sim.mutations \f2\fs22 vector of segregating mutations until it has been added to a haplosome; it will therefore not be visible in that vector within its own \f3\fs18 mutation() \f2\fs22 callback invocation, and indeed, may not be visible in subsequent callbacks during the reproduction tick cycle stage until such time as the offspring individual being generated has been completed. If that offspring is ultimately rejected, in particular by a \f3\fs18 modifyChild() \f2\fs22 callback, the proposed mutation may not be used by SLiM at all. It may therefore be unwise to assume, in a \f3\fs18 mutation() \f2\fs22 callback, that the focal mutation will ultimately be added to the simulation, depending upon the rest of the model\'92s script.\ There is one subtlety to be mentioned here, having to do with subpopulations. The \f3\fs18 subpop \f2\fs22 pseudo-parameter discussed above is always the subpopulation of the parent which possesses the haplosome that is being copied and is mutating; there is no ambiguity about that whatsoever. The \f3\fs18 \f2\fs22 specified in the \f3\fs18 mutation() \f2\fs22 callback declaration, however, is a bit more subtle; above it was said that it restricts the callback \'93to individuals generated by a specified subpopulation\'94, and that is usually true but requires some explanation. In WF models, recall that migrants are generated in a source subpopulation and placed in a target subpopulation, as a model of juvenile migration; in that context, the \f3\fs18 \f2\fs22 specifies the \f0\i source \f2\i0 subpopulation to which the \f3\fs18 mutation() \f2\fs22 callback will be restricted. In nonWF models, offspring are generated by the \f3\fs18 add...() \f2\fs22 family of \f3\fs18 Subpopulation \f2\fs22 methods, which can cross individuals from two different subpopulations and place the result in a third target subpopulation; in that context, in general, the \f3\fs18 \f2\fs22 specifies the source subpopulation that is generating the particular \f0\i gamete \f2\i0 that is sustaining a mutation during its production. The exception to this rule is \f3\fs18 addRecombinant() \f2\fs22 and \f3\fs18 addMultiRecombinant() \f2\fs22 ; since there are four different source subpopulations potentially in play there per mutation, it was deemed simpler in that case for the \f3\fs18 \f2\fs22 to specify the \f0\i target \f2\i0 subpopulation to which the \f3\fs18 mutation() \f2\fs22 callback will be restricted. If restriction to the source subpopulation is needed with \f3\fs18 addRecombinant() \f2\fs22 or \f3\fs18 addMultiRecombinant() \f2\fs22 , the \f3\fs18 subpop \f2\fs22 pseudo-parameter may be consulted rather than using \f3\fs18 \f2\fs22 .\ Note that \f3\fs18 mutation() \f2\fs22 callbacks are only called for mutations that are auto-generated by SLiM, as a consequence of the mutation rate and the genetic structure defined. Mutations that are created in script, using \f3\fs18 addNewMutation() \f2\fs22 or \f3\fs18 addNewDrawnMutation() \f2\fs22 , will not trigger \f3\fs18 mutation() \f2\fs22 callbacks; but of course the script may modify or tailor such added mutations in whatever way is desired, so there is no need for callbacks in that situation.\ As with the other callback types, multiple \f3\fs18 mutation() \f2\fs22 callbacks may be registered and active. In this case, all registered and active callbacks will be called for each generated mutation to which they apply, in the order that the callbacks were registered.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f0\i \cf0 \kerning1\expnd0\expndtw0 5.13.10 ITEM: 11. \f1\fs18 survival() \f0\fs22 callbacks\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\i0 \cf2 In nonWF models, a selection phase in the tick cycle results in mortality; individuals survive or die based upon their fitness. In most cases this standard behavior is sufficient; but occasionally it can be useful to observe the survival decisions SLiM makes (to log out information about dying individuals, for example), to modify those decisions (influencing which individuals live and which die, perhaps based upon factors other than genetics), or even to short-circuit mortality completely (moving dead individuals into a \'93cold storage\'94 subpopulation for later use, perhaps). To accomplish such goals, one can the \f3\fs18 survival() \f2\fs22 callback mechanism to override SLiM\'92s default behavior. Note that in WF models, since they always model non-overlapping generations, the entire parental generation dies in each tick regardless of fitness; \f3\fs18 survival() \f2\fs22 callbacks therefore apply only to nonWF models.\ A \f3\fs18 survival() \f2\fs22 callback is defined with a syntax much like that of other callbacks:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sb180\sa180\partightenfactor0 \f3\fs18 \cf2 [id] [t1 [: t2]] survival([]) \{ ... \}\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 The \f3\fs18 survival() \f2\fs22 callback will be called during the selection phase of the tick cycle of nonWF models, during the tick(s) in which it is active. By default it will be called once per individual in the entire population (whether slated for survival or not); it may optionally be restricted to apply only to individuals in a specified subpopulation, using the \f3\fs18 \f2\fs22 specifier. (In multispecies models, the definition must be preceded by a \f3\fs18 species \f2\fs22 specification as usual.)\ When a \f3\fs18 survival() \f2\fs22 callback is called, a focal individual has already been evaluated by SLiM regarding its survival; a final fitness value for the individual has been calculated, and a random uniform draw in \f3\fs18 [0,1] \f2\fs22 has been generated that determines whether the individual is to survive (a draw less than the individual\'92s fitness) or die (a draw greater than or equal to the individual\'92s fitness). The focal individual is provided to the callback, as is the subpopulation in which it resides. Furthermore, the preliminary decision (whether the focal individual will survive or not), the focal individual\'92s fitness, and the random draw made by SLiM to determine survival are also provided to the callback. The callback may return \f3\fs18 NULL \f2\fs22 to accept SLiM\'92s decision, or may return \f3\fs18 T \f2\fs22 to indicate that the individual should survive, or \f3\fs18 F \f2\fs22 to indicate that it should die, regardless of its fitness and the random deviate drawn. The callback may also return a singleton \f3\fs18 Subpopulation \f2\fs22 object to indicate the individual should remain alive but should be moved to that subpopulation (note that calling \f3\fs18 takeMigrants() \f2\fs22 during the survival phase is illegal, because SLiM is busy modifying the population\'92s internal state).\ In addition to the standard SLiM globals, then, a \f3\fs18 survival() \f2\fs22 callback is supplied with additional information passed through \'93pseudo-parameters\'94:\ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\partightenfactor0 \f3\fs18 \cf2 individual \f2\fs22 The focal individual that will live or die\ \f3\fs18 subpop \f2\fs22 The subpopulation to which the focal individual belongs\ \f3\fs18 surviving \f2\fs22 A \f3\fs18 logical \f2\fs22 value indicating SLiM\'92s preliminary decision ( \f3\fs18 T \f2\fs22 == survival)\ \f3\fs18 fitness \f2\fs22 The focal individual\'92s fitness\ \pard\tx2070\tx2880\pardeftab720\li2073\fi-1526\sa180\partightenfactor0 \f3\fs18 \cf2 draw \f2\fs22 SLiM\'92s random uniform deviate, which determined the preliminary decision \f3\fs18 \ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f2\fs22 \cf2 These may be used in the \f3\fs18 survival() \f2\fs22 callback to determine the final decision.\ While \f3\fs18 survival() \f2\fs22 callbacks are still being called, no decisions are put into effect; no individuals actually die, and none are moved to a new \f3\fs18 Subpopulation \f2\fs22 if that was requested. In effect, SLiM pre-plans the fate of every individual completely without modifying the model state at all. After all \f3\fs18 survival() \f2\fs22 callbacks have completed for every individual, the planned fates for every individual will then be executed, without any opportunity for further intervention through callbacks. It is therefore legal to inspect subpopulations and individuals inside a \f3\fs18 survival() \f2\fs22 callback, but it should be understood that previously made decisions about the fates of other individuals will not yet have any visible effect. It is generally a good idea for the decisions rendered by \f3\fs18 survival() \f2\fs22 callbacks to be independent anyway, to avoid biases due to order-dependency. If the \f3\fs18 randomizeCallbacks \f2\fs22 parameter to \f3\fs18 initializeSLiMOptions() \f2\fs22 is \f3\fs18 T \f2\fs22 (the default), the order in which \f3\fs18 survival() \f2\fs22 callbacks are called on individuals will be randomized within each subpopulation; nevertheless, order-dependency issues can occur if callback effects are not independent. If \f3\fs18 randomizeCallbacks \f2\fs22 is \f3\fs18 F \f2\fs22 , the order in which individuals are evaluated within each subpopulation is not guaranteed to be random, and order-dependency problems are thus even more likely.\ It is worth noting that if \f3\fs18 survival() \f2\fs22 callbacks are used, \'93fitness\'94 in the model is then no longer really fitness; the model is making its own decisions about which individuals live and die, and those decisions are the true determinant of fitness in the biological sense. A \f3\fs18 survival() \f2\fs22 callback that makes its own decisions regarding survival with no regard for SLiM\'92s calculated fitness values can completely alter the pattern of selection in a population, rendering all of SLiM\'92s fitness machinery \'96 selection and dominance coefficients, \f3\fs18 fitnessScaling \f2\fs22 values, etc. \'96 completely irrelevant. To avoid highly counterintuitive and confusing effects, it is thus generally a good idea to use of \f3\fs18 survival() \f2\fs22 callbacks only when it is strictly necessary to achieve a desired outcome.\ As with the other callback types, multiple \f3\fs18 survival() \f2\fs22 callbacks may be registered and active. In this case, all registered and active callbacks will be called for each individual evaluated, in the order that the callbacks were registered.\ \pard\pardeftab543\fi274\ri720\sb40\sa40\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 \ } ================================================ FILE: SLiMgui/SLiMHelpClasses.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2761 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Bold;\f1\fswiss\fcharset0 Optima-Italic;\f2\fnil\fcharset0 Menlo-Italic; \f3\fnil\fcharset0 Menlo-Regular;\f4\fswiss\fcharset0 Optima-Regular;\f5\froman\fcharset0 TimesNewRomanPSMT; \f6\froman\fcharset0 TimesNewRomanPS-ItalicMT;\f7\fnil\fcharset0 LucidaGrande;\f8\fswiss\fcharset0 Helvetica-Oblique; \f9\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red0\green0\blue255;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c0\c0\c100000;} \margl1440\margr1440\vieww9000\viewh19740\viewkind0 \deftab720 \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.2 Class Chromosome\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.2.1 \f2\fs18 Chromosome \f1\fs22 properties\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 colorSubstitution <\'96> (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The color used to display substitutions in SLiMgui when both mutations and substitutions are being displayed in the chromosome view. Outside of SLiMgui, this property still exists, but is not used by SLiM. Colors may be specified by name, or with hexadecimal RGB values of the form \f3\fs18 "#RRGGBB" \f4\fs20 . If \f3\fs18 colorSubstitution \f4\fs20 is the empty string, \f3\fs18 "" \f4\fs20 , SLiMgui will defer to the color scheme of each \f3\fs18 MutationType \f4\fs20 , just as it does when only substitutions are being displayed. The default, \f3\fs18 "3333FF" \f4\fs20 , causes all substitutions to be shown as dark blue when displayed in conjunction with mutations, to prevent the view from becoming too noisy. Note that when substitutions are displayed without mutations also being displayed, this value is ignored by SLiMgui and the substitutions use the color scheme of each \f3\fs18 MutationType \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 geneConversionEnabled => (logical$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 When gene conversion has been enabled by calling \f3\fs18 initializeGeneConversion() \f4\fs20 , switching to the DSB recombination model, this property is \f3\fs18 T \f4\fs20 ; otherwise, when using the crossover breakpoints model, it is \f3\fs18 F \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 geneConversionGCBias => (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The gene conversion bias coefficient, which expresses a bias in the resolution of heteroduplex mismatches in complex gene conversion tracts. When gene conversion has not been enabled by calling \f3\fs18 initializeGeneConversion() \f4\fs20 , this property will be unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 geneConversionNonCrossoverFraction => (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The fraction of double-stranded breaks that result in non-crossover events. When gene conversion has not been enabled by calling \f3\fs18 initializeGeneConversion() \f4\fs20 , this property will be unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 geneConversionMeanLength => (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The mean length of a gene conversion tract (in base positions). When gene conversion has not been enabled by calling \f3\fs18 initializeGeneConversion() \f4\fs20 , this property will be unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 geneConversionSimpleConversionFraction => (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The fraction of gene conversion tracts that are \'93simple\'94 (i.e., not involving resolution of heteroduplex mismatches); the remainder will be \'93complex\'94. When gene conversion has not been enabled by calling \f3\fs18 initializeGeneConversion() \f4\fs20 , this property will be unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 genomicElements => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 All of the \f3\fs18 GenomicElement \f4\fs20 objects that comprise the chromosome\cf2 , in sorted order (not necessarily in the order in which they were defined). \f5 \cf0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 hotspotEndPositions => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The end positions for hotspot map regions along the chromosome. Each hotspot map region is assumed to start at the position following the end of the previous hotspot map region; in other words, the regions are assumed to be contiguous. When using sex-specific hotspot maps, this property will unavailable; see \f3\fs18 hotspotEndPositionsF \f4\fs20 and \f3\fs18 hotspotEndPositionsM \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 hotspotEndPositionsF => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The end positions for hotspot map regions for females, when using sex-specific hotspot maps; unavailable otherwise. See \f3\fs18 hotspotEndPositions \f4\fs20 for further explanation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 hotspotEndPositionsM => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The end positions for hotspot map regions for males, when using sex-specific hotspot maps; unavailable otherwise. See \f3\fs18 hotspotEndPositions \f4\fs20 for further explanation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 hotspotMultipliers => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The hotspot multiplier for each of the hotspot map regions specified by \f3\fs18 hotspotEndPositions \f4\fs20 . When using sex-specific hotspot maps, this property will be unavailable; see \f3\fs18 hotspotMultipliersF \f4\fs20 and \f3\fs18 hotspotMultipliersM \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 hotspotMultipliersF => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The hotspot multiplier for each of the hotspot map regions specified by \f3\fs18 hotspotEndPositionsF \f4\fs20 , when using sex-specific hotspot maps; unavailable otherwise.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 hotspotMultipliersM => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The hotspot multiplier for each of the hotspot map regions specified by \f3\fs18 hotspotEndPositionsM \f4\fs20 , when using sex-specific hotspot maps; unavailable otherwise.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The id for the chromosome, as given to \f3\fs18 initializeChromosome() \f4\fs20 . For an implicitly defined chromosome, the \f3\fs18 id \f4\fs20 will be \f3\fs18 1 \f4\fs20 . The \f3\fs18 id \f4\fs20 can be used to refer to the chromosome; see also \f3\fs18 symbol \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 intrinsicPloidy => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The intrinsic ploidy of the chromosome, meaning the number of haplosome objects that are allocated in each individual, associated with the chromosome (even if some of those haplosomes are null haplosomes acting as placeholders). This is a consequence of the chromosome\'92s type. Chromosome types \f3\fs18 "A" \f4\fs20 , \f3\fs18 "X" \f4\fs20 , and \f3\fs18 "Z" \f4\fs20 are intrinsically diploid (and thus this property would have the value \f3\fs18 2 \f4\fs20 ), as are the backwards-compatibility chromosome types \f3\fs18 "H-" \f4\fs20 and \f3\fs18 "-Y" \f4\fs20 . All other chromosome types are intrinsically haploid (and thus this property would have the value \f3\fs18 1 \f4\fs20 ).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 isSexChromosome => (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Indicates whether the chromosome is a sex chromosome (T) or not (F). This is a consequence of the chromosome\'92s type. Chromosome types \f3\fs18 "X" \f4\fs20 , \f3\fs18 "Y" \f4\fs20 , \f3\fs18 "Z" \f4\fs20 , and \f3\fs18 "W" \f4\fs20 are considered sex chromosomes, as is the backwards-compatibility type \f3\fs18 "-Y" \f4\fs20 ; all other chromosome types are not. See also the \f3\fs18 sexChromosomes \f4\fs20 property of \f3\fs18 Species \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 lastPosition => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The last valid position in the chromosome; equal to \f3\fs18 length-1 \f4\fs20 , where \f3\fs18 length \f4\fs20 is the length as given to \f3\fs18 initializeChromosome() \f4\fs20 . For an implicitly defined chromosome, the chromosome\'92s last position is determined by the \f1\i maximum \f4\i0 of the end of the last genomic element, the end of the last recombination region, and the end of the last mutation map region (or hotspot map region). See also \f3\fs18 length \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 length => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The length of the chromosome (meaning the number of valid base positions it contains), as given to \f3\fs18 initializeChromosome() \f4\fs20 . The length is simply equal to the last position plus \f3\fs18 1 \f4\fs20 , since the chromosome always starts at \f3\fs18 0 \f4\fs20 . See also \f3\fs18 lastPosition \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 mutationEndPositions => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The end positions for mutation rate regions along the chromosome. Each mutation rate region is assumed to start at the position following the end of the previous mutation rate region; in other words, the regions are assumed to be contiguous. When using sex-specific mutation rate maps, this property will unavailable; see \f3\fs18 mutationEndPositionsF \f4\fs20 and \f3\fs18 mutationEndPositionsM \f4\fs20 .\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 mutationEndPositionsF => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The end positions for mutation rate regions for females, when using sex-specific mutation rate maps; unavailable otherwise. See \f3\fs18 mutationEndPositions \f4\fs20 for further explanation.\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 mutationEndPositionsM => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The end positions for mutation rate regions for males, when using sex-specific mutation rate maps; unavailable otherwise. See \f3\fs18 mutationEndPositions \f4\fs20 for further explanation.\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 mutationRates => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The mutation rate for each of the mutation rate regions specified by \f3\fs18 mutationEndPositions \f4\fs20 . When using sex-specific mutation rate maps, this property will be unavailable; see \f3\fs18 mutationRatesF \f4\fs20 and \f3\fs18 mutationRatesM \f4\fs20 .\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 mutationRatesF => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The mutation rate for each of the mutation rate regions specified by \f3\fs18 mutationEndPositionsF \f4\fs20 , when using sex-specific mutation rate maps; unavailable otherwise.\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 mutationRatesM => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The mutation rate for each of the mutation rate regions specified by \f3\fs18 mutationEndPositionsM \f4\fs20 , when using sex-specific mutation rate maps; unavailable otherwise.\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 name <\'96> (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The name of the chromosome, as given to \f3\fs18 initializeChromosome() \f4\fs20 . The chromosome name is not used by SLiM, and may be whatever you wish.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 overallMutationRate => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The overall mutation rate across the whole chromosome determining the overall number of mutation events that will occur anywhere in the chromosome, as calculated from the individual mutation ranges and rates as well as the coverage of the chromosome by genomic elements (since mutations are only generated within genomic elements, regardless of the mutation rate map). When using sex-specific mutation rate maps, this property will unavailable; see \f3\fs18 overallMutationRateF \f4\fs20 and \f3\fs18 overallMutationRateM \f4\fs20 .\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 overallMutationRateF => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The overall mutation rate for females, when using sex-specific mutation rate maps; unavailable otherwise. See \f3\fs18 overallMutationRate \f4\fs20 for further explanation.\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 overallMutationRateM => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The overall mutation rate for males, when using sex-specific mutation rate maps; unavailable otherwise. See \f3\fs18 overallMutationRate \f4\fs20 for further explanation.\ This property is unavailable in nucleotide-based models.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 overallRecombinationRate => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The overall recombination rate across the whole chromosome determining the overall number of recombination events that will occur anywhere in the chromosome, as calculated from the individual recombination ranges and rates. When using sex-specific recombination maps, this property will unavailable; see \f3\fs18 overallRecombinationRateF \f4\fs20 and \f3\fs18 overallRecombinationRateM \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 overallRecombinationRateF => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The overall recombination rate for females, when using sex-specific recombination maps; unavailable otherwise. See \f3\fs18 overallRecombinationRate \f4\fs20 for further explanation.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 overallRecombinationRateM => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The overall recombination rate for males, when using sex-specific recombination maps; unavailable otherwise. See \f3\fs18 overallRecombinationRate \f4\fs20 for further explanation.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 recombinationEndPositions => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The end positions for recombination regions along the chromosome. Each recombination region is assumed to start at the position following the end of the previous recombination region; in other words, the regions are assumed to be contiguous. When using sex-specific recombination maps, this property will unavailable; see \f3\fs18 recombinationEndPositionsF \f4\fs20 and \f3\fs18 recombinationEndPositionsM \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 recombinationEndPositionsF => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The end positions for recombination regions for females, when using sex-specific recombination maps; unavailable otherwise. See \f3\fs18 recombinationEndPositions \f4\fs20 for further explanation.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 recombinationEndPositionsM => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The end positions for recombination regions for males, when using sex-specific recombination maps; unavailable otherwise. See \f3\fs18 recombinationEndPositions \f4\fs20 for further explanation.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 recombinationRates => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The recombination rate for each of the recombination regions specified by \f3\fs18 recombinationEndPositions \f4\fs20 . When using sex-specific recombination maps, this property will unavailable; see \f3\fs18 recombinationRatesF \f4\fs20 and \f3\fs18 recombinationRatesM \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 recombinationRatesF => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The recombination rate for each of the recombination regions specified by \f3\fs18 recombinationEndPositionsF \f4\fs20 , when using sex-specific recombination maps; unavailable otherwise.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 recombinationRatesM => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The recombination rate for each of the recombination regions specified by \f3\fs18 recombinationEndPositionsM \f4\fs20 , when using sex-specific recombination maps; unavailable otherwise.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 species => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The species to which the target object belongs.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 symbol => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The symbol for the chromosome, as given to \f3\fs18 initializeChromosome() \f4\fs20 ; see the documentation for that function. For an implicitly defined chromosome, the symbol is \f3\fs18 "A" \f4\fs20 for non-sexual models, and for sexual models (for historical reasons) is determined by the model type passed to \f3\fs18 initializeSex() \f4\fs20 as documented there. The \f3\fs18 symbol \f4\fs20 can be used to refer to the chromosome; see also \f3\fs18 id \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 type => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The type of the chromosome, as given to \f3\fs18 initializeChromosome() \f4\fs20 ; see the documentation for that function for a list of the supported chromosome types. For an implicitly defined chromosome, the type is \f3\fs18 "A" \f4\fs20 for non-sexual models, and for sexual models (for historical reasons) is determined by the model type passed to \f3\fs18 initializeSex() \f4\fs20 as documented there. \f5 \cf0 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.2.2 \f2\fs18 Chromosome \f1\fs22 methods\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(is)ancestralNucleotides([Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL], [string$\'a0format\'a0=\'a0"string"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the ancestral nucleotide sequence originally supplied to \f3\fs18 initializeAncestralNucleotides() \f4\fs20 , including any sequence changes due to nucleotide mutations that have fixed and substituted. This nucleotide sequence is the reference sequence for positions in a haplosome that do not contain a nucleotide-based mutation. The range of the returned sequence may be constrained by a start position given in \f3\fs18 start \f4\fs20 and/or an end position given in \f3\fs18 end \f4\fs20 ; nucleotides will be returned from \f3\fs18 start \f4\fs20 to \f3\fs18 end \f4\fs20 , inclusive. The default value of \f3\fs18 NULL \f4\fs20 for \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 represent the first and last base positions of the chromosome, respectively.\ The format of the returned sequence is controlled by the \f3\fs18 format \f4\fs20 parameter. A format of \f3\fs18 "string" \f4\fs20 will return the sequence as a singleton \f3\fs18 string \f4\fs20 (e.g., \f3\fs18 "TATA" \f4\fs20 ). A format of \f3\fs18 "char" \f4\fs20 will return a \f3\fs18 string \f4\fs20 vector with one element per nucleotide (e.g., \f3\fs18 "T" \f4\fs20 , \f3\fs18 "A" \f4\fs20 , \f3\fs18 "T" \f4\fs20 , \f3\fs18 "A" \f4\fs20 ). A format of \f3\fs18 "integer" \f4\fs20 will return an \f3\fs18 integer \f4\fs20 vector with values A= \f3\fs18 0 \f4\fs20 , C= \f3\fs18 1 \f4\fs20 , G= \f3\fs18 2 \f4\fs20 , T= \f3\fs18 3 \f4\fs20 (e.g., \f3\fs18 3 \f4\fs20 , \f3\fs18 0 \f4\fs20 , \f3\fs18 3 \f4\fs20 , \f3\fs18 0 \f4\fs20 ). If the sequence returned is likely to be long, the \f3\fs18 "string" \f4\fs20 format will be the most memory-efficient, and may also be the fastest (but may be harder to work with).\ For purposes related to interpreting the nucleotide sequence as a coding sequence, a format of \f3\fs18 "codon" \f4\fs20 is also supported. This format will return an \f3\fs18 integer \f4\fs20 vector with values from \f3\fs18 0 \f4\fs20 to \f3\fs18 63 \f4\fs20 , based upon successive nucleotide triplets in the sequence (which, for this format, must have a length that is a multiple of three). The codon value for a given nucleotide triplet XYZ is 16X\'a0+\'a04Y\'a0+\'a0Z, where X, Y, and Z have the usual values A= \f3\fs18 0 \f4\fs20 , C= \f3\fs18 1 \f4\fs20 , G= \f3\fs18 2 \f4\fs20 , T= \f3\fs18 3 \f4\fs20 . For example, the triplet AAA has a codon value of \f3\fs18 0 \f4\fs20 , AAC is \f3\fs18 1 \f4\fs20 , AAG is \f3\fs18 2 \f4\fs20 , AAT is \f3\fs18 3 \f4\fs20 , ACA is \f3\fs18 4 \f4\fs20 , and on upward to TTT which is \f3\fs18 63 \f4\fs20 . If the nucleotide sequence AACACATTT is requested in codon format, the codon vector \f3\fs18 1 4 63 \f4\fs20 will therefore be returned. These codon values can be useful in themselves; they can also be passed to \f3\fs18 codonToAminoAcid() \f4\fs20 to translate them into the corresponding amino acid sequence if desired.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0\cf2 \expnd0\expndtw0\kerning0 (integer)drawBreakpoints([No$\'a0parent\'a0=\'a0NULL], [Ni$\'a0n\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Draw recombination breakpoints, using the chromosome\'92s recombination rate map, the current gene conversion parameters, and (in some cases \'96 see below) any active and applicable \f3\fs18 recombination() \f4\fs20 callbacks. The number of breakpoints to generate, \f3\fs18 n \f4\fs20 , may be supplied; if it is \f3\fs18 NULL \f4\fs20 (the default), the number of breakpoints will be drawn based upon the overall recombination rate and the chromosome length (following the standard procedure in SLiM). Note that if the double-stranded breaks model has been chosen, the number of breakpoints generated will probably not be equal to the number requested, because most breakpoints will entail gene conversion tracts, which entail additional crossover breakpoints.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 It is generally recommended that the parent individual be supplied to this method, but \f3\fs18 parent \f4\fs20 is \f3\fs18 NULL \f4\fs20 by default. The individual supplied in \f3\fs18 parent \f4\fs20 is used for two purposes. First, in sexual models that define separate recombination rate maps for males versus females, the sex of \f3\fs18 parent \f4\fs20 will be used to determine which map is used; in this case, a non- \f3\fs18 NULL \f4\fs20 value \f1\i must \f4\i0 be supplied for \f3\fs18 parent \f4\fs20 , since the choice of recombination rate map must be determined. Second, in models that define \f3\fs18 recombination() \f4\fs20 callbacks, \f3\fs18 parent \f4\fs20 is used to determine the various pseudo-parameters that are passed to \f3\fs18 recombination() \f4\fs20 callbacks ( \f3\fs18 individual \f4\fs20 , \f3\fs18 haplosome1 \f4\fs20 , \f3\fs18 haplosome2 \f4\fs20 , \f3\fs18 subpop \f4\fs20 ), and the subpopulation to which \f3\fs18 parent \f4\fs20 belongs is used to select which \f3\fs18 recombination() \f4\fs20 callbacks are applicable; given the necessity of this information, \f3\fs18 recombination() \f4\fs20 callbacks will not be called as a side effect of this method if \f3\fs18 parent \f4\fs20 is \f3\fs18 NULL \f4\fs20 . Apart from these two uses, \f3\fs18 parent \f4\fs20 is not used, and the caller does not guarantee that the generated breakpoints will actually be used to recombine the haplosomes of \f3\fs18 parent \f4\fs20 in particular. If a \f3\fs18 recombination() \f4\fs20 callback is called, \f3\fs18 haplosome1 \f4\fs20 for that callback will always be the first haplosome of \f3\fs18 parent \f4\fs20 for the chromosome; in other words, \f3\fs18 drawBreakpoints() \f4\fs20 will always treat the first haplosome of a homologous pair as the initial copy strand. If the caller wishes to randomly choose an initial copy strand (which is usually desirable), they should do that themselves (note that the \f3\fs18 addRecombinant() \f4\fs20 and \f3\fs18 addMultiRecombinant() \f4\fs20 methods have a flag to facilitate this).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)genomicElementForPosition(integer\'a0positions)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of \f3\fs18 GenomicElement \f4\fs20 objects corresponding to the given vector \f3\fs18 positions \f4\fs20 , which contains base positions along the chromosome. If every position lies within a defined genomic element, the returned vector will have the same length as \f3\fs18 positions \f4\fs20 , and will correspond one-to-one with it. However, if a position in \f3\fs18 positions \f4\fs20 is not within a genomic element, no \f3\fs18 GenomicElement \f4\fs20 object will be present for it in the returned vector, and so the returned vector will no longer have the same length as \f3\fs18 positions \f4\fs20 , and will no longer correspond one-to-one with it. The method \f3\fs18 hasGenomicElementForPosition() \f4\fs20 can be used to detect this circumstance.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(logical)hasGenomicElementForPosition(integer\'a0positions)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a \f3\fs18 logical \f4\fs20 vector corresponding to the given vector \f3\fs18 positions \f4\fs20 , which contains base positions along the chromosome. The returned vector will have the same length as \f3\fs18 positions \f4\fs20 , and will correspond one-to-one with it, containing \f3\fs18 T \f4\fs20 if the corresponding position lies inside a genomic element, or \f3\fs18 F \f4\fs20 if it does not. The method \f3\fs18 genomicElementForPosition() \f4\fs20 can be used to look up the \f3\fs18 GenomicElement \f4\fs20 objects themselves.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(integer$)setAncestralNucleotides(is\'a0sequence)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This method, which may be called only in nucleotide-based models, replaces the ancestral nucleotide sequence for the model. The \f3\fs18 sequence \f4\fs20 parameter is interpreted exactly as it is in the \f3\fs18 initializeAncestralSequence() \f4\fs20 function; see that documentation for details. The length of the ancestral sequence is returned.\ It is unusual to replace the ancestral sequence in a running simulation, since the nucleotide states of segregating and fixed mutations will depend upon the original ancestral sequence. It can be useful when loading a new population state with \f3\fs18 readHaplosomesFromMS() \f4\fs20 or \f3\fs18 readHaplosomesFromVCF() \f4\fs20 , such as when resetting the simulation state to an earlier state in a conditional simulation; however, that is more commonly done using \f3\fs18 readFromPopulationFile() \f4\fs20 with a SLiM or \f3\fs18 .trees \f4\fs20 file. \f1\i \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(void)setGeneConversion(numeric$\'a0nonCrossoverFraction, numeric$\'a0meanLength, numeric$\'a0simpleConversionFraction, [numeric$\'a0bias\'a0=\'a00])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This method switches the recombination model to the \'93double-stranded break (DSB)\'94 model (if it is not already set to that), and configures the details of the gene conversion tracts that will therefore be modeled. The meanings and effects of the parameters exactly mirror the \f3\fs18 initializeGeneConversion() \f4\fs20 function.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setHotspotMap(numeric\'a0multipliers, [Ni\'a0ends\'a0=\'a0NULL], [string$\'a0sex\'a0=\'a0"*"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 In nucleotide-based models, set the mutation rate \f1\i multiplier \f4\i0 along the chromosome. There are two ways to call this method. If the optional \f3\fs18 ends \f4\fs20 parameter is \f3\fs18 NULL \f4\fs20 (the default), then \f3\fs18 multipliers \f4\fs20 must be a singleton value that specifies a single multiplier to be used along the entire chromosome. If, on the other hand, \f3\fs18 ends \f4\fs20 is supplied, then \f3\fs18 multipliers \f4\fs20 and \f3\fs18 ends \f4\fs20 must be the same length, and the values in \f3\fs18 ends \f4\fs20 must be specified in ascending order. In that case, \f3\fs18 multipliers \f4\fs20 and \f3\fs18 ends \f4\fs20 taken together specify the multipliers to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in \f3\fs18 ends \f4\fs20 should extend to the end of the chromosome (as previously determined, during simulation initialization). See the \f3\fs18 initializeHotspotMap() \f4\fs20 function for further discussion of precisely how these multipliers and positions are interpreted.\ If the optional \f3\fs18 sex \f4\fs20 parameter is \f3\fs18 "*" \f4\fs20 (the default), then the supplied hotspot map will be used for both sexes (which is the only option for hermaphroditic simulations). In sexual simulations \f3\fs18 sex \f4\fs20 may be \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 instead, in which case the supplied hotspot map is used only for that sex. Note that whether sex-specific hotspot maps will be used is set by the way that the simulation is initially configured with \f3\fs18 initializeHotspot() \f4\fs20 , and cannot be changed with this method; so if the simulation was set up to use sex-specific hotspot maps then sex must be \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 here, whereas if it was set up not to, then sex must be \f3\fs18 "*" \f4\fs20 or unsupplied here. If a simulation needs sex-specific hotspot maps only some of the time, the male and female maps can simply be set to be identical the rest of the time.\ The hotspot map is normally constant in simulations, so be sure you know what you are doing.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0(void)setMutationRate(numeric\'a0rates, [Ni\'a0ends\'a0=\'a0NULL], [string$\'a0sex\'a0=\'a0"*"]) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the mutation rate per base position per \cf2 \expnd0\expndtw0\kerning0 gamete\cf0 \kerning1\expnd0\expndtw0 . There are two ways to call this method. If the optional \f3\fs18 ends \f4\fs20 parameter is \f3\fs18 NULL \f4\fs20 (the default), then \f3\fs18 rates \f4\fs20 must be a singleton value that specifies a single mutation rate to be used along the entire chromosome. If, on the other hand, \f3\fs18 ends \f4\fs20 is supplied, then \f3\fs18 rates \f4\fs20 and \f3\fs18 ends \f4\fs20 must be the same length, and the values in \f3\fs18 ends \f4\fs20 must be specified in ascending order. In that case, \f3\fs18 rates \f4\fs20 and \f3\fs18 ends \f4\fs20 taken together specify the mutation rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in \f3\fs18 ends \f4\fs20 should extend to the end of the chromosome (as previously determined, during simulation initialization). See the \f3\fs18 initializeMutationRate() \f4\fs20 function for further discussion of precisely how these rates and positions are interpreted.\ If the optional \f3\fs18 sex \f4\fs20 parameter is \f3\fs18 "*" \f4\fs20 (the default), then the supplied mutation rate map will be used for both sexes (which is the only option for hermaphroditic simulations). In sexual simulations \f3\fs18 sex \f4\fs20 may be \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 instead, in which case the supplied mutation rate map is used only for that sex. Note that whether sex-specific mutation rate maps will be used is set by the way that the simulation is initially configured with \f3\fs18 initializeMutationRate() \f4\fs20 , and cannot be changed with this method; so if the simulation was set up to use sex-specific mutation rate maps then sex must be \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 here, whereas if it was set up not to, then sex must be \f3\fs18 "*" \f4\fs20 or unsupplied here. If a simulation needs sex-specific mutation rate maps only some of the time, the male and female maps can simply be set to be identical the rest of the time.\ The mutation rate intervals are normally a constant in simulations, so be sure you know what you are doing.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In nucleotide-based models, \f3\fs18 setMutationRate() \f4\fs20 may not be called. If variation in the mutation rate along the chromosome is desired, \f3\fs18 setHotspotMap() \f4\fs20 should be used.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0(void)setRecombinationRate(numeric\'a0rates, [Ni\'a0ends\'a0=\'a0NULL], [string$\'a0sex\'a0=\'a0"*"]) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the recombination rate per base position per \cf2 \expnd0\expndtw0\kerning0 gamete\cf0 \kerning1\expnd0\expndtw0 . \cf2 \expnd0\expndtw0\kerning0 All rates must be in the interval [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 0.5 \f4\fs20 ]. \cf0 \kerning1\expnd0\expndtw0 There are two ways to call this method. If the optional \f3\fs18 ends \f4\fs20 parameter is \f3\fs18 NULL \f4\fs20 (the default), then \f3\fs18 rates \f4\fs20 must be a singleton value that specifies a single recombination rate to be used along the entire chromosome. If, on the other hand, \f3\fs18 ends \f4\fs20 is supplied, then \f3\fs18 rates \f4\fs20 and \f3\fs18 ends \f4\fs20 must be the same length, and the values in \f3\fs18 ends \f4\fs20 must be specified in ascending order. In that case, \f3\fs18 rates \f4\fs20 and \f3\fs18 ends \f4\fs20 taken together specify the recombination rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in \f3\fs18 ends \f4\fs20 should extend to the end of the chromosome (as previously determined, during simulation initialization). See the \f3\fs18 initializeRecombinationRate() \f4\fs20 function for further discussion of precisely how these rates and positions are interpreted.\ If the optional \f3\fs18 sex \f4\fs20 parameter is \f3\fs18 "*" \f4\fs20 (the default), then the supplied recombination rate map will be used for both sexes (which is the only option for hermaphroditic simulations). In sexual simulations \f3\fs18 sex \f4\fs20 may be \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 instead, in which case the supplied recombination map is used only for that sex. Note that whether sex-specific recombination maps will be used is set by the way that the simulation is initially configured with \f3\fs18 initializeRecombinationRate() \f4\fs20 , and cannot be changed with this method; so if the simulation was set up to use sex-specific recombination maps then sex must be \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 here, whereas if it was set up not to, then sex must be \f3\fs18 "*" \f4\fs20 or unsupplied here. If a simulation needs sex-specific recombination maps only some of the time, the male and female maps can simply be set to be identical the rest of the time.\ The recombination intervals are normally a constant in simulations, so be sure you know what you are doing.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.3 Class Community\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.3.1 \f2\fs18 Community \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 allGenomicElementTypes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the \f3\fs18 GenomicElementType \f4\fs20 objects defined in the simulation. These are guaranteed to be in sorted order, by their \f3\fs18 id \f4\fs20 property.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 allInteractionTypes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the \f3\fs18 InteractionType \f4\fs20 objects defined in the simulation. These are guaranteed to be in sorted order, by their \f3\fs18 id \f4\fs20 property.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 allMutationTypes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the \f3\fs18 MutationType \f4\fs20 objects defined in the simulation. These are guaranteed to be in sorted order, by their \f3\fs18 id \f4\fs20 property.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 allScriptBlocks => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects in the simulation. These are guaranteed to be in sorted order, by their \f3\fs18 id \f4\fs20 property.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 allSpecies => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the \f3\fs18 Species \f4\fs20 objects defined in the simulation (in species declaration order).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 allSubpopulations => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the \f3\fs18 Subpopulation \f4\fs20 objects defined in the simulation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 cycleStage => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The current cycle stage, as a \f3\fs18 string \f4\fs20 . The values of this property essentially mirror the cycle stages of WF and nonWF models. Common values include \f3\fs18 "first" \f4\fs20 (during execution of \f3\fs18 first() \f4\fs20 events), \f3\fs18 "early" \f4\fs20 (during execution of \f3\fs18 early() \f4\fs20 events), \f3\fs18 "reproduction" \f4\fs20 (during offspring generation), \f3\fs18 "fitness" \f4\fs20 (during fitness evaluation), \f3\fs18 "survival" \f4\fs20 (while applying selection and mortality in nonWF models), and \f3\fs18 "late" \f4\fs20 (during execution of \f3\fs18 late() \f4\fs20 events).\ Other possible values include \f3\fs18 "begin" \f4\fs20 (during internal setup before each cycle), \f3\fs18 "tally" \f4\fs20 (while tallying mutation reference counts and removing fixed mutations), \f3\fs18 "swap" \f4\fs20 (while swapping the offspring generation into the parental generation in WF models), \f3\fs18 "end" \f4\fs20 (during internal bookkeeping after each cycle), and \f3\fs18 "console" \f4\fs20 (during the in-between-ticks state in which commands in SLiMgui\'92s Eidos console are executed). It would probably be a good idea not to use this latter set of values; they are probably not user-visible during ordinary model execution anyway.\ During execution of \f3\fs18 initialize() \f4\fs20 callbacks, no \f3\fs18 Community \f4\fs20 object yet exists and so this property cannot be accessed. To detect this state, use \f3\fs18 exists("community") \f4\fs20 ; if that is \f3\fs18 F \f4\fs20 , \f3\fs18 community \f4\fs20 does not exist, and therefore your code is executing during \f3\fs18 initialize() \f4\fs20 callbacks (or outside of SLiM entirely, in some other Eidos-based context).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 logFiles => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 LogFile \f4\fs20 objects being used in the simulation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 modelType => (string$)\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The type of model being simulated, as specified in \f3\fs18 initializeSLiMModelType() \f4\fs20 . This will be \f3\fs18 "WF" \f4\fs20 for WF models (Wright-Fisher models, the default), or \f3\fs18 "nonWF" \f4\fs20 for nonWF models (non-Wright-Fisher models).\kerning1\expnd0\expndtw0 This must be the same for all species in the community; it is therefore a property on \f3\fs18 Community \f4\fs20 , not \f3\fs18 Species \f4\fs20 .\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to the simulation. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tick <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The current tick number. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 verbosity <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The verbosity level, for SLiM\'92s logging of information about the simulation. This is \f3\fs18 1 \f4\fs20 by default, but can be changed at the command line with the \f3\fs18 -l[ong] \f4\fs20 option. It is provided here so that scripts can consult it to govern the level of verbosity of their own output, or set the verbosity level for particular sections of their code. A verbosity level of 0 suppresses most of SLiM\'92s optional output; 2 adds some extra output beyond SLiM\'92s standard output.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.3.2 \f2\fs18 Community \f1\fs22 methods\ \pard\pardeftab529\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(object$)createLogFile(string$\'a0filePath, [Ns\'a0initialContents\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F], [logical$\'a0compress\'a0=\'a0F], [string$\'a0sep\'a0=\'a0","], [Ni$\'a0logInterval\'a0=\'a0NULL], [Ni$\'a0flushInterval\'a0=\'a0NULL], [logical$\'a0header\'a0=\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Creates and returns a new \f3\fs18 LogFile \f4\fs20 object that logs data from the simulation (see the documentation for the \f3\fs18 LogFile \f4\fs20 class for details). Logged data will be written to the file at \f3\fs18 filePath \f4\fs20 , overwriting any existing file at that path by default, or appending to it instead if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 (successive rows of the log table will always be appended to the previously written content, of course). Before the header line for the log is written out, any \f3\fs18 string \f4\fs20 elements in \f3\fs18 initialContents \f4\fs20 will be written first, separated by newlines, allowing for a user-defined file header. If \f3\fs18 compress \f4\fs20 is \f3\fs18 T \f4\fs20 , the contents will be compressed with \f3\fs18 zlib \f4\fs20 as they are written, and the standard \f3\fs18 .gz \f4\fs20 extension for gzip-compressed files will be appended to the filename in \f3\fs18 filePath \f4\fs20 if it is not already present.\ The \f3\fs18 sep \f4\fs20 parameter specifies the separator between data values within a row. The default of \f3\fs18 "," \f4\fs20 will generate a \'93comma-separated value\'94 (CSV) file, while passing \f3\fs18 sep="\\t" \f4\fs20 will use a tab separator instead to generate a \'93tab-separated value\'94 (TSV) file. Other values for \f3\fs18 sep \f4\fs20 may also be used, but are less standard.\ LogTable supports periodic automatic logging of a new row of data, enabled by supplying a non- \f3\fs18 NULL \f4\fs20 value for \f3\fs18 logInterval \f4\fs20 . In this case, a new row will be logged (as if \f3\fs18 logRow() \f4\fs20 were called on the \f3\fs18 LogFile \f4\fs20 ) at the end of every \f3\fs18 logInterval \f4\fs20 ticks (just before the tick counter increments, in both WF and nonWF models), starting at the end of the tick in which the \f3\fs18 LogFile \f4\fs20 was created. A \f3\fs18 logInterval \f4\fs20 of \f3\fs18 1 \f4\fs20 will cause automatic logging at the end of every tick, whereas a \f3\fs18 logInterval \f4\fs20 of \f3\fs18 NULL \f4\fs20 disables automatic logging. Automatic logging can always be disabled or reconfigured later with the \f3\fs18 LogFile \f4\fs20 method \f3\fs18 setLogInterval() \f4\fs20 , or logging can be triggered manually by calling \f3\fs18 logRow() \f4\fs20 .\ When compression is enabled, \f3\fs18 LogFile \f4\fs20 flushes new data lazily by default, for performance reasons, buffering data for multiple rows before writing to disk. Passing a non- \f3\fs18 NULL \f4\fs20 value for \f3\fs18 flushInterval \f4\fs20 requests a flush every \f3\fs18 flushInterval \f4\fs20 rows (with a value of \f3\fs18 1 \f4\fs20 providing unbuffered operation). Note that flushing very frequently will likely result in both lower performance and a larger final file size (in one simple test, \f3\fs18 48943 \f4\fs20 bytes instead of \f3\fs18 4280 \f4\fs20 bytes, or more than a 10\'d7 increase in size). Alternatively, passing a very large value for \f3\fs18 flushInterval \f4\fs20 will effectively disable automatic flushing, except at the end of the simulation (but be aware that this may use a large amount of memory for large log files). In any case, the log file will be created immediately, with its requested initial contents; the initial write is not buffered. When compression is not enabled, the \f3\fs18 flushInterval \f4\fs20 setting is ignored.\ The \f3\fs18 header \f4\fs20 parameter controls whether a header line is written out at the beginning of logging. If it is \f3\fs18 T \f4\fs20 (the default), a header line is written out; if \f3\fs18 F \f4\fs20 , no header is written. Suppressing the header output can be useful if you are using \f3\fs18 LogFile \f4\fs20 to append data to an existing file that already has a header line.\ The \f3\fs18 LogFile \f4\fs20 documentation discusses how to configure and use \f3\fs18 LogFile \f4\fs20 to write out the data you are interested in from your simulation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(integer$)estimatedLastTick(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns SLiM\'92s current estimate of the last tick in which the model will execute. Because script blocks can be added, removed, and rescheduled, and because the simulation may end prematurely (due to a call to \f3\fs18 simulationFinished() \f4\fs20 , for example), this is only an estimate, and may change over time.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)deregisterScriptBlock(io\'a0scriptBlocks)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 All \f3\fs18 SLiMEidosBlock \f4\fs20 objects specified by \f3\fs18 scriptBlocks \f4\fs20 (either with \f3\fs18 SLiMEidosBlock \f4\fs20 objects or with \f3\fs18 integer \f4\fs20 identifiers) will be scheduled for deregistration. The deregistered blocks remain valid, and may even still be executed in the current stage of the current tick; the blocks are not actually deregistered and deallocated until sometime after the currently executing script block has completed. To immediately prevent a script block from executing, even when it is scheduled to execute in the current stage of the current tick, use the \f3\fs18 active \f4\fs20 property of the script block. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)genomicElementTypesWithIDs(integer\'a0ids)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Find and return the \f3\fs18 GenomicElementType \f4\fs20 objects with \f3\fs18 id \f4\fs20 values matching the values in \f3\fs18 ids \f4\fs20 . If no matching \f3\fs18 GenomicElementType \f4\fs20 object can be found with a given \f3\fs18 id \f4\fs20 , an error results.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)interactionTypesWithIDs(integer\'a0ids)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Find and return the \f3\fs18 InteractionType \f4\fs20 objects with \f3\fs18 id \f4\fs20 values matching the values in \f3\fs18 ids \f4\fs20 . If no matching \f3\fs18 InteractionType \f4\fs20 object can be found with a given \f3\fs18 id \f4\fs20 , an error results.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)mutationTypesWithIDs(integer\'a0ids)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Find and return the \f3\fs18 MutationType \f4\fs20 objects with \f3\fs18 id \f4\fs20 values matching the values in \f3\fs18 ids \f4\fs20 . If no matching \f3\fs18 MutationType \f4\fs20 object can be found with a given \f3\fs18 id \f4\fs20 , an error results.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(void)outputUsage(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Output the current memory usage of the simulation to Eidos\'92s output stream. The specifics of what is printed, and in what format, should not be relied upon as they may change from version to version of SLiM. This method is primarily useful for understanding where the memory usage of a simulation predominantly resides, for debugging or optimization. Note that it does not capture \f1\i all \f4\i0 memory usage by the process; rather, it summarizes the memory usage by SLiM and Eidos in directly allocated objects and buffers. To get the same memory usage reported by \f3\fs18 outputUsage() \f4\fs20 , but as a \f3\fs18 float$ \f4\fs20 value, use the \f3\fs18 Community \f4\fs20 method \f3\fs18 usage() \f4\fs20 . To get the \f1\i total \f4\i0 memory usage of the running process (either current or peak), use the Eidos function \f3\fs18 usage() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerEarlyEvent(Nis$\'a0id, string$\'a0source, [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL]\cf2 , [No$\'a0ticksSpec\'a0=\'a0NULL]\cf0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 early() \f4\fs20 event in the current simulation, with optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks\cf2 (and, for multispecies models, optional ticks specifier \f3\fs18 ticksSpec \f4\fs20 )\cf0 limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered event is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)registerFirstEvent(Nis$\'a0id, string$\'a0source, [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL], [No$\'a0ticksSpec\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 first() \f4\fs20 event in the current simulation, with optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks (and, for multispecies models, optional ticks specifier \f3\fs18 ticksSpec \f4\fs20 ) limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered event is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerInteractionCallback(Nis$\'a0id, string$\'a0source, io$\'a0intType, [Nio$\'a0subpop \f5 \'a0 \f3 =\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 interaction() \f4\fs20 callback in the current simulation\cf2 (global to the community)\cf0 , with a required interaction type \f3\fs18 intType \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 identifier), optional exerter subpopulation \f3\fs18 subpop \f4\fs20 (which may also be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations), and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it will be eligible to execute the next time an \f3\fs18 InteractionType \f4\fs20 is evaluated. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerLateEvent(Nis$\'a0id, string$\'a0source, [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL]\cf2 , [No$\'a0ticksSpec\'a0=\'a0NULL]\cf0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 late() \f4\fs20 event in the current simulation, with optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks\cf2 (and, for multispecies models, optional ticks specifier \f3\fs18 ticksSpec \f4\fs20 )\cf0 limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered event is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)rescheduleScriptBlock(io$\'a0block, [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL], [Ni\'a0ticks\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Reschedule the target script block given by \f3\fs18 block \f4\fs20 to execute in a specified set of ticks. The \f3\fs18 block \f4\fs20 parameter may be either an \f3\fs18 integer \f4\fs20 representing the ID of the desired script block, or a \f3\fs18 SLiMScriptBlock \f4\fs20 specified directly. The target script block, \f3\fs18 block \f4\fs20 , is returned.\ The first way to specify the tick set is with \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 parameter values; \f3\fs18 block \f4\fs20 will then execute from \f3\fs18 start \f4\fs20 to \f3\fs18 end \f4\fs20 , inclusive.\ The second way to specify the tick set is using the \f3\fs18 ticks \f4\fs20 parameter, specifying each tick in which the block should execute. The vector supplied for \f3\fs18 ticks \f4\fs20 does not need to be in sorted order, but it must not contain any duplicates.\ It can sometimes be better to handle script block scheduling in other ways. If an \f3\fs18 early() \f4\fs20 event needs to execute every tenth tick over the whole duration of a long model run, for example, it might not be advisable to use a call like \f3\fs18 community.rescheduleScriptBlock(s1, ticks=seq(10, 100000, 10)) \f4\fs20 for that purpose, since that would make things complicated for SLiM\'92s scheduler. Instead, it might be preferable to add a test such as \f3\fs18 if (community.tick % 10 != 0) return; \f4\fs20 at the beginning of the event. It is legal to reschedule a script block while the block is executing; a call like \f3\fs18 community.rescheduleScriptBlock(self, community.tick + 10, community.tick + 10); \f4\fs20 made inside a given block would therefore also cause the block to execute every tenth tick, although this sort of self-rescheduling code is probably harder to read, maintain, and debug.\ Whichever way of specifying the tick set is used, \f3\fs18 block \f4\fs20 may continue to be executed during the current tick cycle stage even after it has been rescheduled, unless it is made inactive using its \f3\fs18 active \f4\fs20 property, and similarly, the block may not execute during the current tick cycle stage if it was not already scheduled to do so. Rescheduling script blocks during the tick and tick cycle stage in which they are executing, or in which they are intended to execute, should be avoided. Also, note that script blocks which are open-ended (i.e., with no specified end tick), are not used in determining whether the end of the simulation has been reached (because then the simulation would run forever).\ Note that new script blocks can also be created and scheduled using the \f3\fs18 register...() \f4\fs20 methods of \f3\fs18 Community \f4\fs20 and \f3\fs18 Species \f4\fs20 ; by using the same source as a template script block, the template can be duplicated and scheduled for different ticks, perhaps with modifications or variations. In multispecies models, note that blocks may not run due to their \f3\fs18 species \f4\fs20 or \f3\fs18 ticks \f4\fs20 specifier, even in ticks in which they are scheduled to run.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)scriptBlocksWithIDs(integer\'a0ids)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Find and return the \f3\fs18 SLiMEidosBlock \f4\fs20 objects with \f3\fs18 id \f4\fs20 values matching the values in \f3\fs18 ids \f4\fs20 . If no matching \f3\fs18 SLiMEidosBlock \f4\fs20 object can be found with a given \f3\fs18 id \f4\fs20 , an error results.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)simulationFinished(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Declare the current simulation finished. Normally SLiM ends a simulation when, at the end of a tick, there are no script events or callbacks registered for any future tick (excluding scripts with no declared end tick). If you wish to end a simulation before this condition is met, a call to \f3\fs18 simulationFinished() \f4\fs20 will cause the current simulation to end at the end of the current tick. For example, a simulation might self-terminate if a test for a dynamic equilibrium condition is satisfied. Note that the current tick will finish executing; if you want the simulation to stop immediately, you can use the Eidos method \f3\fs18 stop() \f4\fs20 , which raises an error condition. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)speciesWithIDs(integer\'a0ids)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Find and return the \f3\fs18 Species \f4\fs20 objects with \f3\fs18 id \f4\fs20 values matching the values in \f3\fs18 ids \f4\fs20 . If no matching \f3\fs18 Species \f4\fs20 object can be found with a given \f3\fs18 id \f4\fs20 , an error results.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)subpopulationsWithIDs(integer\'a0ids)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Find and return the \f3\fs18 Subpopulation \f4\fs20 objects with \f3\fs18 id \f4\fs20 values matching the values in \f3\fs18 ids \f4\fs20 . If no matching \f3\fs18 Subpopulation \f4\fs20 object can be found with a given \f3\fs18 id \f4\fs20 , an error results.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)subpopulationsWithNames(string\'a0names)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Find and return the \f3\fs18 Subpopulation \f4\fs20 objects with \f3\fs18 name \f4\fs20 values matching the values in \f3\fs18 names \f4\fs20 . If no matching \f3\fs18 Subpopulation \f4\fs20 object can be found with a given name, an error results.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float$)usage(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Return the current memory usage of the simulation. The specifics of what is totalled up should not be relied upon as it may change from version to version of SLiM. This method is primarily useful for understanding where the memory usage of a simulation predominantly resides, for debugging or optimization. Note that it does not capture \f1\i all \f4\i0 memory usage by the process; rather, it summarizes the memory usage by SLiM and Eidos in directly allocated objects and buffers. To see details of this internal memory usage, use the \f3\fs18 Community \f4\fs20 method \f3\fs18 outputUsage() \f4\fs20 . To get the \f1\i total \f4\i0 memory usage of the running process (either current or peak), use the Eidos function \f3\fs18 usage() \f4\fs20 .\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.4 Class Haplosome\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.4.1 \f2\fs18 Haplosome \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 chromosome => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 Chromosome \f4\fs20 object which this haplosome represents; for example, if this haplosome represents the X sex chromosome in a particular individual, then this property provides the \f3\fs18 Chromosome \f4\fs20 object that defines the genetic structure of the X sex chromosome in that individual\'92s species.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 chromosomeSubposition => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The position index of the haplosome, within the set of haplosomes associated with the \f3\fs18 Chromosome \f4\fs20 object which this haplosome represents. For example, if an individual in a multi-chromosome model has two haplosomes that represent a given chromosome within its \f3\fs18 haplosomes \f4\fs20 vector, the first of those haplosomes will have a \f3\fs18 chromosomeSubposition \f4\fs20 value of \f3\fs18 0 \f4\fs20 , the second will have a \f3\fs18 chromosomeSubposition \f4\fs20 value of \f3\fs18 1 \f4\fs20 . For an intrinsically diploid chromosome in individuals generated by a standard biparental cross, the first haplosome (at subposition \f3\fs18 0 \f4\fs20 ) came from its first parent (the female parent, in sexual models), and the second haplosome (at subposition \f3\fs18 1 \f4\fs20 ) came from its second parent (the male parent, in sexual models).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 haplosomePedigreeID => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 or tree-sequence recording is turned on with \f3\fs18 initializeTreeSeq() \f4\fs20 , \f3\fs18 haplosomePedigreeID \f4\fs20 is a \'93semi-unique\'94 non-negative identifier for each haplosome in a simulation, never re-used throughout the duration of the simulation run. The \f3\fs18 haplosomePedigreeID \f4\fs20 of a given haplosome will be equal to either \f3\fs18 (2*pedigreeID) \f4\fs20 or \f3\fs18 (2*pedigreeID + 1) \f4\fs20 of the individual that the haplosome belongs to (the former for a first haplosome of the individual, the latter for a second haplosome of the individual if one exists); this invariant relationship is guaranteed.\ This value is \'93semi-unique\'94 in the sense that it is shared by \f1\i all \f4\i0 of the first haplosomes of an individual, or by \f1\i all \f4\i0 of the second haplosomes of an individual. In a single-chromosome model, a given individual will have just one first haplosome, and perhaps (depending on the chromosome type) one second haplosome, and so the value of \f3\fs18 haplosomePedigreeID \f4\fs20 for each of those haplosomes will be truly unique. In a multi-chromosome model, however, an individual has a first haplosome for each chromosome, and perhaps (depending on the chromosome types) a second haplosome for each chromosome. In that case, the value of \f3\fs18 haplosomePedigreeID \f4\fs20 is unique in the sense that it is different for each individual, but it is \f1\i not \f4\i0 unique in the sense that it will be shared by other haplosomes within the same individual \'96 shared by all the first haplosomes, or shared by all the second haplosomes. This \'93semi-uniqueness\'94 is intentional; it allows \f3\fs18 haplosomePedigreeID \f4\fs20 to be used as a \'93key\'94 that associates the haplosomes of an individual across disparate datasets, such as across the different tree sequences for each chromosome that are produced by tree-sequence recording in a multi-chromosome model. See sections 1.5.1 and 8.3 for further discussion of multi-chromosome models.\ If neither pedigree tracking nor tree-sequence recording is enabled, this property is unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 individual => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 Individual \f4\fs20 object to which this haplosome belongs.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 isNullHaplosome => (logical$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 T \f4\fs20 if the haplosome is a \'93null\'94 haplosome, \f3\fs18 F \f4\fs20 if it is an ordinary haplosome object. Null haplosomes are used as placeholders when a real haplosome doesn\'92t exist in a particular slot, allowing SLiM\'92s code to operate in the same way across a wide variety of genomic configurations. For example, when simulating chromosome type \f3\fs18 "X" \f4\fs20 (an X chromosome), the second haplosome for that chromosome in males will be a null haplosome, preserving a constant number of haplosomes for all individuals even though males have one X while females have two. Null haplosomes should not be accessed or manipulated.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 mutationCount => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The number of \f3\fs18 Mutation \f4\fs20 objects present in this haplosome. This property is provided as a convenience, and for efficiency; the result of \f3\fs18 haplosome.mutationCount \f4\fs20 will be equal to \f3\fs18 size(haplosome.mutations) \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 mutations => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the \f3\fs18 Mutation \f4\fs20 objects present in this haplosome. If you only need the number of mutations present, use the \f3\fs18 mutationCount \f4\fs20 property.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. Note that the \f3\fs18 Haplosome \f4\fs20 objects used by SLiM are new with every new individual, so the \f3\fs18 tag \f4\fs20 value of each new offspring haplosome generated in each tick will be initially undefined.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.4.2 \f2\fs18 Haplosome \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 +\'a0(void)addMutations(object\'a0mutations) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Add the existing mutations in \f3\fs18 mutations \f4\fs20 to the target haplosomes, if they are not already present (if they are already present, they will be ignored), and if the addition is not prevented by the mutation stacking policy (see the \f3\fs18 mutationStackPolicy \f4\fs20 property of \f3\fs18 MutationType \f4\fs20 ). All target haplosomes and all mutations in \f3\fs18 mutations \f4\fs20 must be associated with the same \f3\fs18 Chromosome \f4\fs20 object; attempting to add a mutation to a haplosome associated with a different chromosome will raise an error.\ Calling this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ Note that in nonWF models that use tree-sequence recording, mutations cannot be added to an individual after the tick in which the individual is created (i.e., when the \f3\fs18 age \f4\fs20 of the individual is greater than \f3\fs18 0 \f4\fs20 ), to prevent the possibility of inconsistencies in the recorded tree sequence.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 +\'a0(object)addNewDrawnMutation(io\'a0mutationType, integer\'a0position, [Nio\'a0originSubpop\'a0=\'a0NULL]\cf2 \expnd0\expndtw0\kerning0 , [Nis\'a0nucleotide\'a0=\'a0NULL]\cf0 \kerning1\expnd0\expndtw0 ) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Add new mutations to the target haplosomes with the specified \f3\fs18 mutationType \f4\fs20 (specified by the \f3\fs18 MutationType \f4\fs20 object or by \f3\fs18 integer \f4\fs20 identifier), \f3\fs18 position \f4\fs20 , \f3\fs18 originTick \f4\fs20 (which may be \f3\fs18 NULL \f4\fs20 , the default, to specify the current tick; otherwise, beginning in SLiM 3.5, it must be equal to the current tick anyway, as other uses of this property have been deprecated), and \f3\fs18 originSubpop \f4\fs20 (specified by the \f3\fs18 Subpopulation \f4\fs20 object or by \f3\fs18 integer \f4\fs20 identifier, or by \f3\fs18 NULL \f4\fs20 , the default, to specify the subpopulation to which the first target haplosome belongs). If \f3\fs18 originSubpop \f4\fs20 is supplied as an \f3\fs18 integer \f4\fs20 , it is intentionally not checked for validity; you may use arbitrary values of \f3\fs18 originSubpop \f4\fs20 to \'93tag\'94 the mutations that you create. The selection coefficients of the mutations are drawn from their mutation types; \f3\fs18 addNewMutation() \f4\fs20 may be used instead if you wish to specify selection coefficients. All of the target haplosomes must be associated with the same \f3\fs18 Chromosome \f4\fs20 object, since each new mutation is added to all of the target haplosomes.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In non-nucleotide-based models, \f3\fs18 mutationType \f4\fs20 will always be a non-nucleotide-based mutation type, and so \f3\fs18 nucleotide \f4\fs20 must be \f3\fs18 NULL \f4\fs20 (the default). In a nucleotide-based model, \f3\fs18 mutationType \f4\fs20 might still be non-nucleotide-based (in which case \f3\fs18 nucleotide \f4\fs20 must still be \f3\fs18 NULL \f4\fs20 ), or \f3\fs18 mutationType \f4\fs20 might be nucleotide-based, in which case a non- \f3\fs18 NULL \f4\fs20 value must be supplied for \f3\fs18 nucleotide \f4\fs20 , specifying the nucleotide(s) to be associated with the new mutation(s). Nucleotides may be specified with string values ( \f3\fs18 "A" \f4\fs20 , \f3\fs18 "C" \f4\fs20 , \f3\fs18 "G" \f4\fs20 , or \f3\fs18 "T" \f4\fs20 ), or with integer values (A= \f3\fs18 0 \f4\fs20 , C= \f3\fs18 1 \f4\fs20 , G= \f3\fs18 2 \f4\fs20 , T= \f3\fs18 3 \f4\fs20 ). If a nucleotide mutation already exists at the mutating position, it is replaced automatically in accordance with the stacking policy for nucleotide-based mutation types. No check is performed that a new mutation\'92s nucleotide differs from the ancestral sequence, or that its selection coefficient is consistent with other mutations that may already exist at the given position with the same nucleotide; model consistency is the responsibility of the model.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf0 \kerning1\expnd0\expndtw0 Beginning in SLiM 2.5 this method is vectorized, so all of these parameters may be singletons (in which case that single value is used for all mutations created by the call) or non-singleton vectors (in which case one element is used for each corresponding mutation created). Non-singleton parameters must match in length, since their elements need to be matched up one-to-one.\ The new mutations created by this method are returned, even if their actual addition is prevented by the mutation stacking policy (see the \f3\fs18 mutationStackPolicy \f4\fs20 property of \f3\fs18 MutationType \f4\fs20 ). However, the order of the mutations in the returned vector is not guaranteed to be the same as the order in which the values are specified in parameter vectors, unless the \f3\fs18 position \f4\fs20 parameter is specified in ascending order. In other words, pre-sorting the parameters to this method into ascending order by position, using \f3\fs18 order() \f4\fs20 and subsetting, will guarantee that the order of the returned vector of mutations corresponds to the order of elements in the parameters to this method; otherwise, no such guarantee exists.\ Beginning in SLiM 2.1, this is a class method, not an instance method. This means that it does not get multiplexed out to all of the elements of the receiver (which would add a different new mutation to each element); instead, it is performed as a single operation, adding the same new mutation objects to all of the elements of the receiver. Before SLiM 2.1, to add the same mutations to multiple haplosomes, it was necessary to call \f3\fs18 addNewDrawnMutation() \f4\fs20 on one of the haplosomes, and then add the returned \f3\fs18 Mutation \f4\fs20 object to all of the other haplosomes using \f3\fs18 addMutations() \f5\fs20 . \f4 That is not necessary in SLiM 2.1 and later, because of this change (although doing it the old way does no harm and produces identical behavior). Pre-2.1 code that actually relied upon the old multiplexing behavior will no longer work correctly (but this is expected to be an extremely rare pattern of usage).\ \pard\pardeftab720\li547\ri720\sa60\partightenfactor0 \cf2 Before SLiM 4, this method also took a \f3\fs18 originGeneration \f4\fs20 parameter. This was deprecated (the origin generation was then required to be equal to the current generation, for internal consistency), and was removed in SLiM 4.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf0 Calling this will normally affect the fitness values calculated at the end of the current tick (but not sooner); if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 Note that in nonWF models that use tree-sequence recording, mutations cannot be added to an individual after the tick in which the individual is created (i.e., when the \f3\fs18 age \f4\fs20 of the individual is greater than \f3\fs18 0 \f4\fs20 ), to prevent the possibility of inconsistencies in the recorded tree sequence.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 +\'a0(object)addNewMutation(io\'a0mutationType, numeric\'a0selectionCoeff, integer\'a0position, [Nio\'a0originSubpop\'a0=\'a0NULL]\cf2 \expnd0\expndtw0\kerning0 , [Nis\'a0nucleotide\'a0=\'a0NULL]\cf0 \kerning1\expnd0\expndtw0 ) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Add new mutations to the target haplosomes with the specified \f3\fs18 mutationType \f4\fs20 (specified by the \f3\fs18 MutationType \f4\fs20 object or by \f3\fs18 integer \f4\fs20 identifier), \f3\fs18 selectionCoeff \f4\fs20 , \f3\fs18 position \f4\fs20 , \f3\fs18 originTick \f4\fs20 (which may be \f3\fs18 NULL \f4\fs20 , the default, to specify the current tick; otherwise, beginning in SLiM 3.5, it must be equal to the current tick anyway, as other uses of this property have been deprecated), and \f3\fs18 originSubpop \f4\fs20 (specified by the \f3\fs18 Subpopulation \f4\fs20 object or by \f3\fs18 integer \f4\fs20 identifier, or by \f3\fs18 NULL \f4\fs20 , the default, to specify the subpopulation to which the first target haplosome belongs). If \f3\fs18 originSubpop \f4\fs20 is supplied as an \f3\fs18 integer \f4\fs20 , it is intentionally not checked for validity; you may use arbitrary values of \f3\fs18 originSubpop \f4\fs20 to \'93tag\'94 the mutations that you create. The \f3\fs18 addNewDrawnMutation() \f4\fs20 method may be used instead if you wish selection coefficients to be drawn from the mutation types of the mutations. All of the target haplosomes must be associated with the same \f3\fs18 Chromosome \f4\fs20 object, since each new mutation is added to all of the target haplosomes.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In non-nucleotide-based models, \f3\fs18 mutationType \f4\fs20 will always be a non-nucleotide-based mutation type, and so \f3\fs18 nucleotide \f4\fs20 must be \f3\fs18 NULL \f4\fs20 (the default). In a nucleotide-based model, \f3\fs18 mutationType \f4\fs20 might still be non-nucleotide-based (in which case \f3\fs18 nucleotide \f4\fs20 must still be \f3\fs18 NULL \f4\fs20 ), or \f3\fs18 mutationType \f4\fs20 might be nucleotide-based, in which case a non- \f3\fs18 NULL \f4\fs20 value must be supplied for \f3\fs18 nucleotide \f4\fs20 , specifying the nucleotide(s) to be associated with the new mutation(s). Nucleotides may be specified with string values ( \f3\fs18 "A" \f4\fs20 , \f3\fs18 "C" \f4\fs20 , \f3\fs18 "G" \f4\fs20 , or \f3\fs18 "T" \f4\fs20 ), or with integer values (A= \f3\fs18 0 \f4\fs20 , C= \f3\fs18 1 \f4\fs20 , G= \f3\fs18 2 \f4\fs20 , T= \f3\fs18 3 \f4\fs20 ). If a nucleotide mutation already exists at the mutating position, it is replaced automatically in accordance with the stacking policy for nucleotide-based mutation types. No check is performed that a new mutation\'92s nucleotide differs from the ancestral sequence, or that its selection coefficient is consistent with other mutations that may already exist at the given position with the same nucleotide; model consistency is the responsibility of the model.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf0 \kerning1\expnd0\expndtw0 The new mutations created by this method are returned, even if their actual addition is prevented by the mutation stacking policy (see the \f3\fs18 mutationStackPolicy \f4\fs20 property of \f3\fs18 MutationType \f4\fs20 ). However, the order of the mutations in the returned vector is not guaranteed to be the same as the order in which the values are specified in parameter vectors, unless the \f3\fs18 position \f4\fs20 parameter is specified in ascending order. In other words, pre-sorting the parameters to this method into ascending order by position, using \f3\fs18 order() \f4\fs20 and subsetting, will guarantee that the order of the returned vector of mutations corresponds to the order of elements in the parameters to this method; otherwise, no such guarantee exists.\ Beginning in SLiM 2.1, this is a class method, not an instance method. This means that it does not get multiplexed out to all of the elements of the receiver (which would add a different new mutation to each element); instead, it is performed as a single operation, adding the same new mutation object to all of the elements of the receiver. Before SLiM 2.1, to add the same mutation to multiple haplosomes, it was necessary to call \f3\fs18 addNewMutation() \f4\fs20 on one of the haplosomes, and then add the returned \f3\fs18 Mutation \f4\fs20 object to all of the other haplosomes using \f3\fs18 addMutations() \f5\fs20 . \f4 That is not necessary in SLiM 2.1 and later, because of this change (although doing it the old way does no harm and produces identical behavior). Pre-2.1 code that actually relied upon the old multiplexing behavior will no longer work correctly (but this is expected to be an extremely rare pattern of usage).\ \pard\pardeftab720\li547\ri720\sa60\partightenfactor0 \cf2 Before SLiM 4, this method also took a \f3\fs18 originGeneration \f4\fs20 parameter. This was deprecated (the origin generation was then required to be equal to the current generation, for internal consistency), and was removed in SLiM 4.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf0 Calling this will normally affect the fitness values calculated at the end of the current tick (but not sooner); if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 Note that in nonWF models that use tree-sequence recording, mutations cannot be added to an individual after the tick in which the individual is created (i.e., when the \f3\fs18 age \f4\fs20 of the individual is greater than \f3\fs18 0 \f4\fs20 ), to prevent the possibility of inconsistencies in the recorded tree sequence.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (\cf2 \expnd0\expndtw0\kerning0 Nlo$\cf0 \kerning1\expnd0\expndtw0 )containsMarkerMutation(io$\'a0mutType, integer$ \f5 \'a0 \f3 position\cf2 \expnd0\expndtw0\kerning0 , [logical$\'a0returnMutation\'a0=\'a0F]\cf0 \kerning1\expnd0\expndtw0 ) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns \f3\fs18 T \f4\fs20 if the haplosome contains a mutation of type \f3\fs18 mutType \f4\fs20 at \f3\fs18 position \f4\fs20 , \f3\fs18 F \f4\fs20 otherwise\cf2 \expnd0\expndtw0\kerning0 (if \f3\fs18 returnMutation \f4\fs20 has its default value of \f3\fs18 F \f4\fs20 ; see below)\cf0 \kerning1\expnd0\expndtw0 . This method is, as its name suggests, intended for checking for \'93marker mutations\'94: mutations of a special mutation type that are not literally mutations in the usual sense, but instead are added in to particular haplosomes to mark them as possessing some property. Marker mutations are not typically added by SLiM\'92s mutation-generating machinery; instead they are added explicitly with \f3\fs18 addNewMutation() \f4\fs20 or \f3\fs18 addNewDrawnMutation() \f4\fs20 at a known, constant position in the haplosome. This method provides a check for whether a marker mutation of a given type exists in a particular haplosome; because the position to check is known in advance, that check can be done much faster than the equivalent check with \f3\fs18 containsMutations() \f4\fs20 or \f3\fs18 countOfMutationsOfType() \f4\fs20 , using a binary search of the haplosome. \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4 \cf2 \expnd0\expndtw0\kerning0 If \f3\fs18 returnMutation \f4\fs20 is \f3\fs18 T \f4\fs20 (an option added in SLiM 3), this method returns the actual mutation found, rather than just \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 . More specifically, the \f1\i first \f4\i0 mutation found of \f3\fs18 mutType \f4\fs20 at \f3\fs18 position \f4\fs20 will be returned; if more than one such mutation exists in the target haplosome, which one is returned is not defined. If \f3\fs18 returnMutation \f4\fs20 is T and no mutation of \f3\fs18 mutType \f4\fs20 is found at \f3\fs18 position \f4\fs20 , \f3\fs18 NULL \f4\fs20 will be returned.\ \pard\pardeftab529\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0(logical)containsMutations(object\'a0mutations) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a \f3\fs18 logical \f4\fs20 vector indicating whether each of the mutations in \f3\fs18 mutations \f4\fs20 is present in the target haplosome; each element in the returned vector indicates whether the corresponding mutation is present ( \f3\fs18 T \f4\fs20 ) or absent ( \f3\fs18 F \f4\fs20 ). This method is provided for speed; it is much faster than the corresponding Eidos code.\ Note that the mutations must be associated with the same chromosome as the target haplosome, otherwise an error is raised. The \f3\fs18 containsMutations() \f4\fs20 method of \f3\fs18 Individual \f4\fs20 does not have this restriction, since it checks for mutations across all of the haplosomes of the target individual. This restriction is intended to find logic errors, since it seems to make little sense to check for a mutation in a haplosome for the wrong chromosome; but if this restriction proves inconvenient in common situations, it could be relaxed.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (integer$)countOfMutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns the number of mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations in the haplosome. If you need a vector of the matching \f3\fs18 Mutation \f4\fs20 objects, rather than just a count, use \f3\fs18 -mutationsOfType() \f5\fs20 . \f4 This method is provided for speed; it is much faster than the corresponding Eidos code. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(integer)mutationCountsInHaplosomes([No\'a0mutations\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Return an \f3\fs18 integer \f4\fs20 vector with the frequency counts of all of the \f3\fs18 Mutation \f4\fs20 objects passed in \f3\fs18 mutations \f4\fs20 , within the target \f3\fs18 Haplosome \f4\fs20 vector. If the optional \f3\fs18 mutations \f4\fs20 argument is \f3\fs18 NULL \f4\fs20 (the default), frequency counts will be returned for all of the active \f3\fs18 Mutation \f4\fs20 objects in the species \'96 the same \f3\fs18 Mutation \f4\fs20 objects, and in the same order, as would be returned by the \f3\fs18 mutations \f4\fs20 property of \f3\fs18 sim \f4\fs20 , in other words.\ In multi-chromosome models, you might often wish to obtain counts only for mutations associated with one particular chromosome. In that case, you would probably want to pass a vector of the mutations associated with that specific chromosome, as obtained from the \f3\fs18 subsetMutations() \f4\fs20 method of \f3\fs18 Species \f4\fs20 , rather than passing \f3\fs18 NULL \f4\fs20 . (Passing \f3\fs18 NULL \f4\fs20 in that scenario would give you counts of \f3\fs18 0 \f4\fs20 for all of the mutations associated with other chromosomes in the model.)\ See the \f3\fs18 +mutationFrequenciesInHaplosomes() \f4\fs20 method to obtain \f3\fs18 float \f4\fs20 frequencies instead of \f3\fs18 integer \f4\fs20 counts. See also the \f3\fs18 Species \f4\fs20 methods \f3\fs18 mutationCounts() \f4\fs20 and \f3\fs18 mutationFrequencies() \f4\fs20 , which might be more efficient for getting counts/frequencies for whole subpopulations or for the whole species.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(float)mutationFrequenciesInHaplosomes([No\'a0mutations\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Return a \f3\fs18 float \f4\fs20 vector with the frequencies of all of the \f3\fs18 Mutation \f4\fs20 objects passed in \f3\fs18 mutations \f4\fs20 , within the target \f3\fs18 Haplosome \f4\fs20 vector. If the optional \f3\fs18 mutations \f4\fs20 argument is \f3\fs18 NULL \f4\fs20 (the default), frequencies will be returned for all of the active \f3\fs18 Mutation \f4\fs20 objects in the species \'96 the same \f3\fs18 Mutation \f4\fs20 objects, and in the same order, as would be returned by the \f3\fs18 mutations \f4\fs20 property of \f3\fs18 sim \f4\fs20 , in other words.\ In multi-chromosome models, the frequency of each mutation is assessed within the subset of target haplosomes that are associated with the same chromosome. In other words, if a mutation is associated with chromosome 1, and the target haplosomes are associated with both chromosomes 1 and 2, the frequency of the mutation will be calculated only within the haplosomes for chromosome 1 (as you would expect). However, you might often wish to obtain frequencies only for mutations associated with one particular chromosome. In that case, you would probably want to pass a vector of the mutations associated with that specific chromosome, as obtained from the \f3\fs18 subsetMutations() \f4\fs20 method of \f3\fs18 Species \f4\fs20 , rather than passing \f3\fs18 NULL \f4\fs20 . (Passing \f3\fs18 NULL \f4\fs20 in that scenario would give you frequencies of \f3\fs18 0 \f4\fs20 for all of the mutations associated with other chromosomes in the model.)\ See the \f3\fs18 +mutationCountsInHaplosomes() \f4\fs20 method to obtain \f3\fs18 integer \f4\fs20 counts instead of \f3\fs18 float \f4\fs20 frequencies. See also the \f3\fs18 Species \f4\fs20 methods \f3\fs18 mutationCounts() \f4\fs20 and \f3\fs18 mutationFrequencies() \f4\fs20 , which might be more efficient for getting counts/frequencies for whole subpopulations or for the whole species.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (object)mutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns an \f3\fs18 object \f4\fs20 vector of all the mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations in the haplosome. If you just need a count of the matching \f3\fs18 Mutation \f4\fs20 objects, rather than a vector of the matches, use \f3\fs18 -countOfMutationsOfType() \f4\fs20 ; if you need just the positions of matching \f3\fs18 Mutation \f4\fs20 objects, use \f3\fs18 -positionsOfMutationsOfType() \f4\fs20 ; and if you are aiming for a sum of the selection coefficients of matching \f3\fs18 Mutation \f4\fs20 objects, use \f3\fs18 -sumOfMutationsOfType() \f5\fs20 . \f4 This method is provided for speed; it is much faster than the corresponding Eidos code.\cf2 See also \f3\fs18 substitutionsOfType() \f4\fs20 . \f5 \cf0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(is)nucleotides([Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL], [string$\'a0format\'a0=\'a0"string"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the nucleotide sequence for the haplosome. This is the current ancestral sequence, as would be returned by the \f3\fs18 Chromosome \f4\fs20 method \f3\fs18 ancestralNucleotides() \f4\fs20 , with the nucleotides for any nucleotide-based mutations in the haplosome overlaid. The range of the returned sequence may be constrained by a start position given in \f3\fs18 start \f4\fs20 and/or an end position given in \f3\fs18 end \f4\fs20 ; nucleotides will be returned from \f3\fs18 start \f4\fs20 to \f3\fs18 end \f4\fs20 , inclusive. The default value of \f3\fs18 NULL \f4\fs20 for \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 represent the first and last base positions of the chromosome, respectively.\ The format of the returned sequence is controlled by the \f3\fs18 format \f4\fs20 parameter. A format of \f3\fs18 "string" \f4\fs20 will return the sequence as a singleton \f3\fs18 string \f4\fs20 (e.g., \f3\fs18 "TATA" \f4\fs20 ). A format of \f3\fs18 "char" \f4\fs20 will return a \f3\fs18 string \f4\fs20 vector with one element per nucleotide (e.g., \f3\fs18 "T" \f4\fs20 , \f3\fs18 "A" \f4\fs20 , \f3\fs18 "T" \f4\fs20 , \f3\fs18 "A" \f4\fs20 ). A format of \f3\fs18 "integer" \f4\fs20 will return an \f3\fs18 integer \f4\fs20 vector with values A= \f3\fs18 0 \f4\fs20 , C= \f3\fs18 1 \f4\fs20 , G= \f3\fs18 2 \f4\fs20 , T= \f3\fs18 3 \f4\fs20 (e.g., \f3\fs18 3 \f4\fs20 , \f3\fs18 0 \f4\fs20 , \f3\fs18 3 \f4\fs20 , \f3\fs18 0 \f4\fs20 ). A format of \f3\fs18 "codon" \f4\fs20 will return an \f3\fs18 integer \f4\fs20 vector with values from \f3\fs18 0 \f4\fs20 to \f3\fs18 63 \f4\fs20 , based upon successive nucleotide triplets in the sequence (which, for this format, must have a length that is a multiple of three); see the \f3\fs18 ancestralNucleotides() \f4\fs20 documentation for details. If the sequence returned is likely to be long, the \f3\fs18 "string" \f4\fs20 format will be the most memory-efficient, and may also be the fastest (but may be harder to work with).\ Several helper functions are provided for working with sequences, such as \f3\fs18 nucleotideCounts() \f4\fs20 to get the counts of A/C/G/T nucleotides in a sequence, \f3\fs18 nucleotideFrequencies() \f4\fs20 to get the same information as frequencies, and \f3\fs18 codonsToAminoAcids() \f4\fs20 to convert a codon sequence (such as provided by the codon format described above) to an amino acid sequence.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 +\'a0(void)outputHaplosomes([Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F]\cf2 , [logical$\'a0objectTags\'a0=\'a0F]\cf0 ) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output the target haplosomes in SLiM\'92s native format. This low-level output method may be used to output any sample of \f3\fs18 Haplosome \f4\fs20 objects associated with a single chromosome. The Eidos function \f3\fs18 sample() \f4\fs20 may be useful for constructing custom samples, as may the SLiM class \f3\fs18 Individual \f4\fs20 . For output of a sample from a single \f3\fs18 Subpopulation \f4\fs20 , the \f3\fs18 outputSample() \f4\fs20 method of \f3\fs18 Subpopulation \f4\fs20 may be more straightforward to use. If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output is directed to SLiM\'92s standard output. Otherwise, the output is sent to the file specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 .\ The \f3\fs18 objectTags \f4\fs20 parameter may be used to request that tag values for objects be written out. This option is turned off ( \f3\fs18 F \f4\fs20 ) by default, for brevity; if it turned on ( \f3\fs18 T \f4\fs20 ), the \f3\fs18 tag \f4\fs20 property values of all haplosomes and mutations in the output will be written. If there is other state that you wish you persist, such as tags on objects of other classes, values attached to objects with \f3\fs18 setValue() \f4\fs20 , and so forth, you should persist that state in separate files using calls such as \f3\fs18 writeFile() \f4\fs20 .\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 See \f3\fs18 output\cf2 HaplosomesTo\cf0 MS() \f4\fs20 and \f3\fs18 output\cf2 HaplosomesTo\cf0 VCF() \f4\fs20 for other output formats. Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 +\'a0(void)output\cf2 HaplosomesTo\cf0 MS([Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F]\cf2 \expnd0\expndtw0\kerning0 , [logical$\'a0filterMonomorphic\'a0=\'a0F]\cf0 \kerning1\expnd0\expndtw0 ) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output the target haplosomes in MS format. This low-level output method may be used to output any sample of \f3\fs18 Haplosome \f4\fs20 objects associated with a single chromosome. The Eidos function \f3\fs18 sample() \f4\fs20 may be useful for constructing custom samples, as may the SLiM class \f3\fs18 Individual \f4\fs20 . For output of a sample from a single \f3\fs18 Subpopulation \f4\fs20 , the \f3\fs18 outputMSSample() \f4\fs20 of \f3\fs18 Subpopulation \f4\fs20 may be more straightforward to use. If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output is directed to SLiM\'92s standard output. Otherwise, the output is sent to the file specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 . Positions in the output will span the interval [0,1].\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 If \f3\fs18 filterMonomorphic \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), all mutations that are present in the sample will be included in the output. This means that some mutations may be included that are actually monomorphic within the sample (i.e., that exist in \f1\i every \f4\i0 sampled haplosome, and are thus apparently fixed). These may be filtered out with \f3\fs18 filterMonomorphic = T \f4\fs20 if desired; note that this option means that some mutations that do exist in the sampled haplosomes might not be included in the output, simply because they exist in every sampled haplosome.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 \kerning1\expnd0\expndtw0 See \f3\fs18 outputHaplosomes() \f4\fs20 and \f3\fs18 output\cf2 HaplosomesTo\cf0 VCF() \f4\fs20 for other output formats. Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 +\'a0(void)output\cf2 HaplosomesTo\cf0 VCF([Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0outputMultiallelics\'a0=\'a0T], [logical$\'a0append\'a0=\'a0F]\cf2 \expnd0\expndtw0\kerning0 , [logical$\'a0simplifyNucleotides\'a0=\'a0F], [logical$\'a0outputNonnucleotides\'a0=\'a0T]\kerning1\expnd0\expndtw0 , [logical$\'a0groupAsIndividuals\'a0=\'a0T]\cf0 ) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output the target haplosomes in VCF format. This low-level output method may be used to output any sample of \f3\fs18 Haplosome \f4\fs20 objects associated with a single chromosome. The Eidos function \f3\fs18 sample() \f4\fs20 may be useful for constructing custom samples, as may the SLiM class \f3\fs18 Individual \f4\fs20 . For output of a sample from a single \f3\fs18 Subpopulation \f4\fs20 , the \f3\fs18 outputVCFSample() \f4\fs20 method of \f3\fs18 Subpopulation \f4\fs20 may be more straightforward to use. If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output is directed to SLiM\'92s standard output. Otherwise, the output is sent to the file specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 .\ The parameters \f3\fs18 outputMultiallelics \f4\fs20 , \f3\fs18 simplifyNucleotides \f4\fs20 , and \f3\fs18 outputNonnucleotides \f4\fs20 affect the format of the output produced.\ With \f3\fs18 groupAsIndividuals \f4\fs20 being \f3\fs18 T \f4\fs20 (the default), the target haplosome vector should be structured as if it represents all of the haplosomes for some set of individuals, for a single focal chromosome. All haplosomes for the focal chromosome should be present, including null haplosomes. It should provide all of the haplosomes for the first individual (for the chosen chromosome); then for the second individual; and so forth. The haplosomes in the target haplosome vector do not, in fact, need to belong to individuals in SLiM following this pattern; they just need to specify well-formed individuals in the VCF output. For an intrinsically haploid chromosome, the target haplosome for a given output individual is used to generate a haploid call ( \f3\fs18 0 \f4\fs20 or \f3\fs18 1 \f4\fs20 ) for that individual; if the haplosome is a null haplosome, the call will be \f3\fs18 ~ \f4\fs20 (an ASCII tilde). For example, calls for (non-null) Y haplosomes in males will be emitted as \f3\fs18 0 \f4\fs20 or \f3\fs18 1 \f4\fs20 , whereas calls for the (null) Y haplosomes in females will be emitted as \f3\fs18 ~ \f4\fs20 . For an intrinsically diploid chromosome, the pair of target haplosomes for a given individual is used to generate a call for that individual, but null haplosomes are allowed (in the patterns expected by SLiM given the chromosome type). For example, a pair of non-null haplosomes for an X chromosome will be emitted as a diploid call (such as \f3\fs18 1|0 \f4\fs20 ) for a female (XX), but if the second haplosome of the pair is a null haplosome, the pair will be emitted as a haploid call ( \f3\fs18 0 \f4\fs20 or \f3\fs18 1 \f4\fs20 ) for a male (X). If the first haplosome of the pair were a null haplosome for an X chromosome, an error would be raised, since that is not an allowed pattern in SLiM (as discussed in the documentation for the \f3\fs18 Chromosome \f4\fs20 class). For a diploid autosome of type \f3\fs18 "A" \f4\fs20 , however, any pattern is legal, but the VCF format cannot distinguish between a non-null haplosome first and a null haplosome second, versus a null haplosome first and a non-null haplosome second; both will be emitted as a haploid call ( \f3\fs18 0 \f4\fs20 or \f3\fs18 1 \f4\fs20 ). For a diploid autosome of type \f3\fs18 "A" \f4\fs20 , if both haplosomes are null the call will be \f3\fs18 ~ \f4\fs20 . The VCF specification does not actually seem to discuss sex chromosomes, but this design is intended to follow standard usage.\ With \f3\fs18 groupAsIndividuals \f4\fs20 being \f3\fs18 F \f4\fs20 , the focal chromosome is treated as being intrinsically haploid whether it is or not; each haplosome will be called as a haploid sample whether the chromosome type is diploid or haploid. This provides more detailed and accurate information; the exact state of each haplosome will be represented with either \f3\fs18 0 \f4\fs20 , \f3\fs18 1 \f4\fs20 , or (for null haplosomes) \f3\fs18 ~ \f4\fs20 , rather than the state of a pair of haplosomes being represented as a single call in a way that can sometimes be ambiguous, as discussed above. However, the resulting output might confuse some VCF parsers that expect diploid calls for individuals, and it will not be as obvious which calls in the output belong to a given diploid individual.\ See \f3\fs18 outputHaplosomesToMS() \f4\fs20 and \f3\fs18 outputHaplosomes() \f4\fs20 for other output formats. Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(integer)positionsOfMutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns the positions of mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations in the haplosome. If you need a vector of the matching \f3\fs18 Mutation \f4\fs20 objects, rather than just positions, use \f3\fs18 -mutationsOfType() \f4\fs20 . This method is provided for speed; it is much faster than the corresponding Eidos code. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 +\'a0(\cf0 \kerning1\expnd0\expndtw0 object\cf2 \expnd0\expndtw0\kerning0 )readHaplosomesFromMS(string$\'a0filePath, io$\'a0mutationType)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Read new mutations from the MS format file at \f3\fs18 filePath \f4\fs20 and add them to the target haplosomes. The number of target haplosomes must match the number of haplosomes represented in the MS file, and all target haplosomes must be associated with the same chromosome, and must not be null haplosomes. The target haplosomes correspond, in order, to the call lines in the MS file. To read into all of the non-null haplosomes in a given subpopulation \f3\fs18 pN \f4\fs20 in a single-chromosome model, simply call \f3\fs18 pN.haplosomesNonNull.\expnd0\expndtw0\kerning0 readHaplosomesFromMS\kerning1\expnd0\expndtw0 () \f4\fs20 , assuming the subpopulation\'92s size matches that of the MS file. A vector containing all of the mutations created by \f3\fs18 \expnd0\expndtw0\kerning0 readHaplosomesFromMS\kerning1\expnd0\expndtw0 () \f4\fs20 is returned.\ Each mutation is created at the position specified in the file, using the mutation type given by \f3\fs18 mutationType \f4\fs20 . Positions are expected to be in [0,1], and are scaled to the length of the chromosome by multiplying by the last valid base position of the chromosome (i.e., one less than the chromosome length). Selection coefficients are drawn from the mutation type. The population of origin for each mutation is set to \f3\fs18 -1 \f4\fs20 , and the tick of origin is set to the current tick. In a nucleotide-based model, if \f3\fs18 mutationType \f4\fs20 is nucleotide-based, a random nucleotide different from the ancestral nucleotide at the position will be chosen with equal probability.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 +\'a0(\cf0 \kerning1\expnd0\expndtw0 object\cf2 \expnd0\expndtw0\kerning0 )readHaplosomesFromVCF(string$\'a0filePath, [Nio$\'a0mutationType\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Read new mutations from the VCF format file at \f3\fs18 filePath \f4\fs20 and add them to the target haplosomes. The number of target haplosomes must match the number of haplosomes represented in the VCF file (i.e., two times the number of diploid samples plus one times the number of haploid samples). To read into all of the haplosomes in a given subpopulation \f3\fs18 pN \f4\fs20 in a single-chromosome model, simply call \f3\fs18 pN.haplosomes.readHaplosomesFromVCF() \f4\fs20 , assuming the subpopulation\'92s size matches that of the VCF file taking ploidy into account. A vector containing all of the mutations created by \f3\fs18 readHaplosomesFromVCF() \f4\fs20 is returned.\ This method and the \f3\fs18 readIndividualsFromVCF() \f4\fs20 method of \f3\fs18 Individual \f4\fs20 provide two alternative ways of reading VCF data, focused in the perspective of either haplosomes (this method) or individuals (the \f3\fs18 Individual \f4\fs20 method). See the documentation of \f3\fs18 readIndividualsFromVCF() \f4\fs20 for discussion of the pros and cons of each approach; that discussion will not be duplicated here.\ SLiM\'92s VCF parsing is quite primitive. The header is parsed only inasmuch as SLiM looks to see whether SLiM-specific VCF fields are defined or not; the rest of the header information is ignored. Call lines are assumed to follow the format:\ \pard\tx990\tx1260\tx1530\tx1800\tx2070\tx2340\tx2610\tx2880\tx3150\tx3420\pardeftab720\li547\ri720\sl264\slmult1\sa180\partightenfactor0 \f3\fs18 \cf2 #CHROM POS ID REF ALT QUAL FILTER INFO FORMAT i0...iN\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 CHROM \f4\fs20 field is largely ignored, but \f3\fs18 readHaplosomesFromVCF() \f4\fs20 does check that its value is identical across all call lines, to prevent the genetic data for more than one chromosome from being glommed together nonsensically; the input VCF file must contain data for just a single chromosome. In single-chromosome models the \f3\fs18 CHROM \f4\fs20 field is not otherwise checked or validated. In multi-chromosome models, \f3\fs18 readHaplosomesFromVCF() \f4\fs20 imposes some additional restrictions. First, all haplosomes in the target vector must be associated with the same single focal chromosome. Second, the \f3\fs18 CHROM \f4\fs20 field for every call line in the VCF file must match the \f3\fs18 symbol \f4\fs20 property of that focal chromosome; the VCF file must indicate that it specifically matches the focal chromosome associated with the target haplosomes. These restrictions boil down to the fact that \f3\fs18 readHaplosomesFromVCF() \f4\fs20 only reads data for a single chromosome. If you wish to read multi-chromosome VCF data into a multi-chromosome SLiM model, the \f3\fs18 readIndividualsFromVCF() \f4\fs20 method provided by the \f3\fs18 Individual \f4\fs20 class supports that functionality (because it can work at the level of individuals, rather than haplosomes, making it possible to match calls to the corresponding haplosomes in a reasonable way). Alternatively, you can call \f3\fs18 readHaplosomesFromVCF() \f4\fs20 multiple times to read data for different chromosomes one by one.\ The \f3\fs18 ID \f4\fs20 , \f3\fs18 QUAL \f4\fs20 , \f3\fs18 FILTER \f4\fs20 , and \f3\fs18 FORMAT \f4\fs20 fields are ignored, and information in the genotype fields beyond the \f3\fs18 GT \f4\fs20 genotype subfield are also ignored. SLiM\'92s own VCF annotations are honored; in particular, mutations will be created using the given values of \f3\fs18 MID \f4\fs20 , \f3\fs18 S \f4\fs20 , \f3\fs18 PO \f4\fs20 , \f3\fs18 TO \f4\fs20 , and \f3\fs18 MT \f4\fs20 if those subfields are present, and \f3\fs18 DOM \f4\fs20 , if it is present, must match the dominance coefficient of the mutation type. The parameter \f3\fs18 mutationType \f4\fs20 (a \f3\fs18 MutationType \f4\fs20 object or id) will be used for any mutations that have no supplied mutation type id in the \f3\fs18 MT \f4\fs20 subfield; if \f3\fs18 mutationType \f4\fs20 would be used but is \f3\fs18 NULL \f4\fs20 an error will result. Mutation IDs supplied in \f3\fs18 MID \f4\fs20 will be used if no mutation IDs have been used in the simulation so far; if any have been used, it is difficult for SLiM to guarantee that there are no conflicts, so a warning will be emitted and the \f3\fs18 MID \f4\fs20 values will be ignored. If selection coefficients are not supplied with the \f3\fs18 S \f4\fs20 subfield, they will be drawn from the mutation type used for the mutation. If a population of origin is not supplied with the \f3\fs18 PO \f4\fs20 subfield, \f3\fs18 -1 \f4\fs20 will be used. If a tick of origin is not supplied with the \f3\fs18 TO \f4\fs20 subfield (or a generation of origin \f3\fs18 GO \f4\fs20 field, which was the SLiM convention before SLiM 4), the current tick will be used.\ \f3\fs18 REF \f4\fs20 and \f3\fs18 ALT \f4\fs20 must always be comprised of simple nucleotides ( \f3\fs18 A \f4\fs20 / \f3\fs18 C \f4\fs20 / \f3\fs18 G \f4\fs20 / \f3\fs18 T \f4\fs20 ) rather than values representing indels or other complex states. Beyond this, the handling of the \f3\fs18 REF \f4\fs20 and \f3\fs18 ALT \f4\fs20 fields depends upon several factors. In non-nucleotide-based models, we have the first case: (1)\'a0These fields are ignored, although they are still checked for conformance. In nucleotide-based models, when a header definition for SLiM\'92s \f3\fs18 NONNUC \f4\fs20 tag is present (as when nucleotide-based output is generated by SLiM) there are two further possibilities, given as (2) and (3) here: (2)\'a0If a \f3\fs18 NONNUC \f4\fs20 field is present in the \f3\fs18 INFO \f4\fs20 field the call line is taken to represent a non-nucleotide-based mutation, and \f3\fs18 REF \f4\fs20 and \f3\fs18 ALT \f4\fs20 are again ignored; in this case the mutation type used must be non-nucleotide-based. (3)\'a0If a \f3\fs18 NONNUC \f4\fs20 field is \f1\i not \f4\i0 present the call line is taken to represent a nucleotide-based mutation; in this case, the mutation type used must be nucleotide-based, and the specified reference nucleotide must match the existing ancestral nucleotide at the given position. Finally, in nucleotide-based models, when a header definition for SLiM\'92s \f3\fs18 NONNUC \f4\fs20 tag is \f1\i not \f4\i0 present (as when loading a non-SLiM-generated VCF file), there is a remaining possibility: (4)\'a0The mutation type used will govern the way nucleotides are handled. In this case, if the mutation type used for a mutation is nucleotide-based, the nucleotide provided in the VCF file for that allele will be used, whereas if the mutation type is non-nucleotide-based, the nucleotide provided will be ignored.\ If multiple alleles using the same nucleotide at the same position are specified in the VCF file, a separate mutation will be created for each, mirroring SLiM\'92s behavior with independent mutational lineages when writing VCF. The \f3\fs18 MULTIALLELIC \f4\fs20 flag is ignored by \f3\fs18 readHaplosomesFromVCF() \f4\fs20 ; call lines for mutations at the same base position in the same haplosome will result in stacked mutations whether or not \f3\fs18 MULTIALLELIC \f4\fs20 is present.\ The target haplosomes correspond, in order, to the haploid or diploid calls provided for \f3\fs18 i0 \f4\fs20 \'85 \f3\fs18 iN \f4\fs20 (the sample IDs) in the VCF file. Null haplosomes in the target vector will be skipped, and will not be used to correspond to any of the calls for \f3\fs18 i0 \f4\fs20 \'85 \f3\fs18 iN \f4\fs20 ; however, care should be taken in this case that the haplosomes in the VCF file correspond to the target haplosomes in the manner desired.\ A call of \f3\fs18 ~ \f4\fs20 (an ASCII tilde character) for an individual \f3\fs18 i0 \f4\fs20 \'85 \f3\fs18 iN \f4\fs20 is taken to indicate that that individual possesses no genetic information for the chromosome; it lacks that chromosome entirely. For example, if the VCF file represents Y-chromosome data, female individuals should have calls of \f3\fs18 ~ \f4\fs20 . This is treated differently than a call of \f3\fs18 0 \f4\fs20 or \f3\fs18 0|0 \f4\fs20 ; a call of \f3\fs18 0 \f4\fs20 matches that call to a non-null target haplosome (but does not add the called mutation to that haplosome), and a call of \f3\fs18 0|0 \f4\fs20 matches two non-null target haplosomes (but does not add the called mutation to either), whereas a call of \f3\fs18 ~ \f4\fs20 is simply skipped, without matching to any haplosome in the target vector, mirroring the fact that \f3\fs18 readHaplosomesFromVCF() \f4\fs20 skips over null haplosomes in the target haplosome vector. (When reading Y-chromosome data, a female\'92s null Y haplosome could be omitted from the target haplosome vector, or it could be present since it would be skipped anyway \'96 as stated above, all null haplosomes are skipped.) Note that these semantics using \f3\fs18 ~ \f4\fs20 are non-standard; the VCF standard does not seem to say anything about how sex chromosomes should be represented (or anything about other types of chromosomes that might be absent from some individuals), so the usage of \f3\fs18 ~ \f4\fs20 was invented for SLiM. This is an area where standardization is very much needed.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 +\'a0(void)removeMutations([No\'a0mutations\'a0=\'a0NULL], [logical$\'a0substitute\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Remove the mutations in \f3\fs18 mutations \f4\fs20 from the target haplosomes, if they are present (if they are not present, they will be ignored). If \f3\fs18 NULL \f4\fs20 is passed for \f3\fs18 mutations \f4\fs20 (which is the default), then all mutations will be removed from the target haplosomes; in this case, \f3\fs18 substitute \f4\fs20 must be \f3\fs18 F \f4\fs20 (a specific vector of mutations to be substituted is required). Note that the \f3\fs18 Mutation \f4\fs20 objects removed remain valid, and will still be in the simulation\'92s mutation registry (i.e., will be returned by the \f3\fs18 Species \f4\fs20 property \f3\fs18 mutations \f4\fs20 ), until the next tick. All target haplosomes and all mutations in \f3\fs18 mutations \f4\fs20 must be associated with the same \f3\fs18 Chromosome \f4\fs20 object; attempting to remove a mutation from a haplosome associated with a different chromosome will raise an error.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 Removing mutations will normally affect the fitness values calculated at the end of the current tick; if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ The optional parameter \f3\fs18 substitute \f4\fs20 was added in SLiM 2.2, with a default of \f3\fs18 F \f4\fs20 for backward compatibility. If \f3\fs18 substitute \f4\fs20 is \f3\fs18 T \f4\fs20 , \f3\fs18 Substitution \f4\fs20 objects will be created for all of the removed mutations so that they are recorded in the simulation as having fixed, just as if they had reached fixation and been removed by SLiM\'92s own internal machinery. This will occur regardless of whether the mutations have in fact fixed, regardless of the \f3\fs18 convertToSubstitution \f4\fs20 property of the relevant mutation types, and regardless of whether all copies of the mutations have even been removed from the simulation (making it possible to create \f3\fs18 Substitution \f4\fs20 objects for mutations that are still segregating). It is up to the caller to perform whatever checks are necessary to preserve the integrity of the simulation\'92s records. Typically \f3\fs18 substitute \f4\fs20 will only be set to \f3\fs18 T \f4\fs20 in the context of calls like \f3\fs18 sim.subpopulations.haplosomes.removeMutations(muts, T) \f4\fs20 , such that the substituted mutations are guaranteed to be entirely removed from circulation. As mentioned above, \f3\fs18 substitute \f4\fs20 may not be \f3\fs18 T \f4\fs20 if \f3\fs18 mutations \f4\fs20 is \f3\fs18 NULL \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96 \f5 \'a0 \f3 (float$)sumOfMutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns the sum of the selection coefficients of all mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations in the haplosome. This is often useful in models that use a particular mutation type to represent QTLs with additive effects; in that context, \f3\fs18 sumOfMutationsOfType() \f4\fs20 will provide the sum of the additive effects of the QTLs for the given mutation type. This method is provided for speed; it is much faster than the corresponding Eidos code. Note that this method also exists on \f3\fs18 Individual \f4\fs20 , for cases in which the sum across both haplosomes of an individual is desired. \f5 \ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.5 Class GenomicElement\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.5.1 \f2\fs18 GenomicElement \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 endPosition => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The last position in the chromosome contained by this genomic element. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 genomicElementType => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 GenomicElementType \f4\fs20 object that defines the behavior of this genomic element. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 startPosition => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The first position in the chromosome contained by this genomic element.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. \f5 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.5.2 \f2\fs18 GenomicElement \f1\fs22 methods\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 \'96\'a0(void)setGenomicElementType(io$\'a0genomicElementType) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the genomic element type used for a genomic element. The genomicElementType parameter should supply the new genomic element type for the element, either as a \f3\fs18 GenomicElementType \f4\fs20 object or as an \f3\fs18 integer \f4\fs20 identifier. The genomic element type for a genomic element is normally a constant in simulations, so be sure you know what you are doing. \f5 \ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.6 Class GenomicElementType\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.6.1 \f2\fs18 GenomicElementType \f1\fs22 properties\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 color <\'96> (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The color used to display genomic elements of this type in SLiMgui. Outside of SLiMgui, this property still exists, but is not used by SLiM. Colors may be specified by name, or with hexadecimal RGB values of the form \f3\fs18 "#RRGGBB" \f4\fs20 . If \f3\fs18 color \f4\fs20 is the empty string, \f3\fs18 "" \f4\fs20 , SLiMgui\'92s default color scheme is used; this is the default for new \f3\fs18 GenomicElementType \f4\fs20 objects.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier for this genomic element type; for genomic element type \f3\fs18 g3 \f4\fs20 , for example, this is \f3\fs18 3 \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 mutationFractions => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 For each \f3\fs18 MutationType \f4\fs20 represented in this genomic element type, this property has the corresponding fraction of all mutations that will be drawn from that \f3\fs18 MutationType \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 mutationMatrix => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The nucleotide mutation matrix used for this genomic element type, set up by \f3\fs18 initializeGenomicElementType() \f4\fs20 and \f3\fs18 setMutationMatrix() \f4\fs20 . This property is only defined in nucleotide-based models; it is unavailable otherwise.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 mutationTypes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 MutationType \f4\fs20 instances used by this genomic element type.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 species => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The species to which the target object belongs.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to genomic element types. \f5 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.6.2 \f2\fs18 GenomicElementType \f1\fs22 methods\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 \'96\'a0(void)setMutationFractions(io\'a0mutationTypes, numeric\'a0proportions) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the mutation type fractions contributing to a genomic element type. The \f3\fs18 mutationTypes \f4\fs20 vector should supply the mutation types used by the genomic element (either as \f3\fs18 MutationType \f4\fs20 objects or as \f3\fs18 integer \f4\fs20 identifiers), and the \f3\fs18 proportions \f4\fs20 vector should be of equal length, specifying the relative proportion of mutations that will be drawn from each corresponding type. This is normally a constant in simulations, so be sure you know what you are doing. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(void)setMutationMatrix(float\'a0mutationMatrix)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Sets a new nucleotide mutation matrix for the genomic element type. This replaces the mutation matrix originally set by \f3\fs18 initializeGenomicElementType() \f4\fs20 . This method may only be called in nucleotide-based models.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 \kerning1\expnd0\expndtw0 5.7 Class Individual\ \pard\pardeftab397\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.7.1 \f2\fs18 Individual \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 age \cf2 \expnd0\expndtw0\kerning0 <\'96>\cf0 \kerning1\expnd0\expndtw0 (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The age of the individual, measured in cycles. A newly generated offspring individual will have an age of \f3\fs18 0 \f4\fs20 in the same tick in which it was created. The age of every individual is incremented by one at the same point that its species cycle counter is incremented, at the end of the tick cycle, \f1\i if and only if \f4\i0 its species was active in that tick. The age of individuals may be changed; usually this only makes sense when setting up the initial state of a model, however.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 color <\'96> (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The color used to display the individual in SLiMgui. Outside of SLiMgui, this property still exists, but is not used by SLiM. Colors may be specified by name, or with hexadecimal RGB values of the form \f3\fs18 "#RRGGBB" \f4\fs20 (see the Eidos manual). If \f3\fs18 color \f4\fs20 is the empty string, \f3\fs18 "" \f4\fs20 , SLiMgui\'92s default (fitness-based) color scheme is used; this is the default for new \f3\fs18 Individual \f4\fs20 objects. Note that named colors will be converted to RGB internally, so the value of this property will always be a hexadecimal RGB color string (or \f3\fs18 "" \f4\fs20 ).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 fitnessScaling <\'96> (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A \f3\fs18 float \f4\fs20 scaling factor applied to the individual\'92s fitness (i.e., the fitness value computed for the individual will be multiplied by this value). This provides a simple, fast way to modify the fitness of an individual; conceptually it is similar to returning a fitness effect for the individual from a \f3\fs18 fitnessEffect() \f4\fs20 callback, but without the complexity and performance overhead of implementing such a callback. To scale the fitness of all individuals in a subpopulation by the same factor, see the \f3\fs18 fitnessScaling \f4\fs20 property of \f3\fs18 Subpopulation \f4\fs20 .\ The value of \f3\fs18 fitnessScaling \f4\fs20 is reset to \f3\fs18 1.0 \f4\fs20 every tick, so that any scaling factor set lasts for only a single tick. This reset occurs immediately after fitness values are calculated, in both WF and nonWF models.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 haploidGenome1 => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A vector of all \f3\fs18 Haplosome \f4\fs20 objects associated with this individual that are attributed to its first parent (the female parent, in sexual models). This method assumes the individual was generated by the typical method for each chromosome type, as explained below; it does not trace back the true ancestry of each haplosome. The semantics of this are more obvious for some chromosome types than others, depending on the inheritance pattern of the chromosome as described in \f3\fs18 initializeChromosome() \f4\fs20 . For chromosomes with two associated haplosomes (types \f3\fs18 "A" \f4\fs20 , \f3\fs18 "X" \f4\fs20 , \f3\fs18 "Z" \f4\fs20 , \f3\fs18 "H-" \f4\fs20 , and \f3\fs18 "-Y" \f4\fs20 ), the first haplosome is assumed to be from the first parent, and is thus included, whereas the second haplosome is assumed to be from the second parent and is thus not included. For chromosomes with one associated haplosome that is inherited from the female/first parent in one way or another (types \f3\fs18 "W" \f4\fs20 , \f3\fs18 "HF" \f4\fs20 , and \f3\fs18 "FL" \f4\fs20 ), that haplosome is always included. For type \f3\fs18 "H" \f4\fs20 , the single haplosome is assumed to have come from the first parent (since clonal inheritance is the common case), and so is included. Other chromosome types ( \f3\fs18 "Y" \f4\fs20 , \f3\fs18 "HM" \f4\fs20 , \f3\fs18 "ML" \f4\fs20 ) are never included. See also the \f3\fs18 haploidGenome1NonNull \f4\fs20 property and the \f3\fs18 haplosomesForChromosomes() \f4\fs20 method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 haploidGenome1NonNull => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This provides the same vector of haplosomes as the \f3\fs18 haploidGenome1 \f4\fs20 property, except that null haplosomes are not included in this property. This is a convenience shorthand, sometimes useful in models that involve null haplosomes. See also the \f3\fs18 haplosomesForChromosomes() \f4\fs20 method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 haploidGenome2 => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A vector of all \f3\fs18 Haplosome \f4\fs20 objects associated with this individual that are attributed to its second parent (the male parent, in sexual models). This method assumes the individual was generated by the typical method for each chromosome type, as explained below; it does not trace back the true ancestry of each haplosome. The semantics of this are more obvious for some chromosome types than others, depending on the inheritance pattern of the chromosome as described in \f3\fs18 initializeChromosome() \f4\fs20 . For chromosomes with two associated haplosomes (types \f3\fs18 "A" \f4\fs20 , \f3\fs18 "X" \f4\fs20 , \f3\fs18 "Z" \f4\fs20 , \f3\fs18 "H-" \f4\fs20 , and \f3\fs18 "-Y" \f4\fs20 ), the second haplosome is assumed to be from the second parent, and is thus included, whereas the first haplosome is assumed to be from the first parent and is thus not included. For chromosomes with one associated haplosome that is inherited from the male/second parent in one way or another (types \f3\fs18 "Y" \f4\fs20 , \f3\fs18 "HM" \f4\fs20 , and \f3\fs18 "ML" \f4\fs20 ), that haplosome is always included. For type \f3\fs18 "H" \f4\fs20 , the single haplosome is assumed to have come from the first parent (since clonal inheritance is the common case), and so is not included. Other chromosome types ( \f3\fs18 "W" \f4\fs20 , \f3\fs18 "HF" \f4\fs20 , \f3\fs18 "FL" \f4\fs20 ) are never included. See also the \f3\fs18 haploidGenome2NonNull \f4\fs20 property and the \f3\fs18 haplosomesForChromosomes() \f4\fs20 method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 haploidGenome2NonNull => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This provides the same vector of haplosomes as the \f3\fs18 haploidGenome2 \f4\fs20 property, except that null haplosomes are not included in this property. This is a convenience shorthand, sometimes useful in models that involve null haplosomes. See also the \f3\fs18 haplosomesForChromosomes() \f4\fs20 method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 haplosomes => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A vector of all \f3\fs18 Haplosome \f4\fs20 objects associated with this individual, in the order in which the chromosomes were defined for the species. See also the \f3\fs18 haplosomesNonNull \f4\fs20 , \f3\fs18 haploidGenome1 \f4\fs20 , \f3\fs18 haploidGenome1NonNull \f4\fs20 , \f3\fs18 haploidGenome2 \f4\fs20 , and \f3\fs18 haploidGenome2NonNull \f4\fs20 properties and the \f3\fs18 haplosomesForChromosomes() \f4\fs20 method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 haplosomesNonNull => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A vector of all \f3\fs18 Haplosome \f4\fs20 objects associated with this individual, in the order in which the chromosomes were defined for the species (as with the \f3\fs18 haplosomes \f4\fs20 property), but excluding any null haplosomes from the returned vector. This is a convenience shorthand, sometimes useful in models that involve null haplosomes.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 index => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The index of the individual in the \f3\fs18 individuals \f4\fs20 vector of its \f3\fs18 Subpopulation \f5\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 meanParentAge => (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The average age of the parents of this individual, measured in cycles. Parentless individuals will have a \f3\fs18 meanParentAge \f4\fs20 of \f3\fs18 0.0 \f4\fs20 . The mean parent age is determined when a new offspring is generated, from the \f3\fs18 age \f4\fs20 property of the parent or parents involved in generating the offspring. For \f3\fs18 addRecombinant() \f4\fs20 and \f3\fs18 addMultiRecombinant() \f4\fs20 that is somewhat complex; see those methods for details.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 migrant => (logical$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Set to \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 T \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 if the individual is a recent migrant, \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 F \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 otherwise. The definition of \'93recent\'94 depends upon the model type (WF or nonWF).\ In WF models, this flag is set at the point when a new child is generated if it is a migrant (i.e., if its source subpopulation is not the same as its subpopulation), and remains valid, with the same value, for the rest of the individual\'92s lifetime.\ In nonWF models, this flag is \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 F \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 for all new individuals, is set to \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 F \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 in all individuals at the end of the reproduction tick cycle stage, and is set to \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 T \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 on all individuals moved to a new subpopulation by \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 takeMigrants() \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 or a \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 survival() \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 callback; the \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 T \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 value set by \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 takeMigrants() \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 or \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 survival() \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 will remain until it is reset at the end of the next reproduction tick cycle stage.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 pedigreeID => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 or tree-sequence recording is turned on with \f3\fs18 initializeTreeSeq() \f4\fs20 , \f3\fs18 pedigreeID \f4\fs20 is a unique non-negative identifier for each individual in a simulation, never re-used throughout the duration of the simulation run. If neither pedigree tracking nor tree-sequence recording is enabled, this property is unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 pedigreeParentIDs => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , \f3\fs18 pedigreeParentIDs \f4\fs20 contains the values of \f3\fs18 pedigreeID \f4\fs20 that were possessed by the parents of an individual; it is thus a vector of two values. If pedigree tracking is not enabled, this property is unavailable. Parental values may be \f3\fs18 -1 \f4\fs20 if insufficient ticks have elapsed for that information to be available (because the simulation just started, or because a subpopulation is new).\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 pedigreeGrandparentIDs => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , \f3\fs18 pedigreeGrandparentIDs \f4\fs20 contains the values of \f3\fs18 pedigreeID \f4\fs20 that were possessed by the grandparents of an individual; it is thus a vector of four values. If pedigree tracking is not enabled, this property is unavailable. Grandparental values may be \f3\fs18 -1 \f4\fs20 if insufficient ticks have elapsed for that information to be available (because the simulation just started, or because a subpopulation is new).\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 reproductiveOutput => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , \f3\fs18 reproductiveOutput \f4\fs20 contains the number of offspring for which this individual has been a parent. If pedigree tracking is not enabled, this property is unavailable. If an individual is a parent by cloning or selfing, or as \f1\i both \f4\i0 parents for a biparental mating, this value is incremented by two. Involvement of an individual as a parent for an \f3\fs18 addRecombinant() \f4\fs20 or \f3\fs18 addMultiRecombinant() \f4\fs20 call does not change this property\'92s value, since the reproductive contribution in that case is unclear; one must conduct separate bookkeeping for that case if necessary, or use tree-sequence recording to infer it from the inheritance record.\ See also the \f3\fs18 Subpopulation \f4\fs20 property \f3\fs18 lifetimeReproductiveOutput \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 sex => (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The sex of the individual. This will be \f3\fs18 "H" \f4\fs20 if sex is not enabled in the simulation (i.e., for hermaphrodites), otherwise \f3\fs18 "F" \f4\fs20 or \f3\fs18 "M" \f4\fs20 as appropriate. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 spatialPosition => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The spatial position of the individual. The length of the \f3\fs18 spatialPosition \f4\fs20 property (the number of coordinates in the spatial position of an individual) depends upon the spatial dimensionality declared with \f3\fs18 initializeSLiMOptions() \f4\fs20 . If the spatial dimensionality is zero (as it is by default), it is an error to access this property. The elements of this property are identical to the values of the \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 z \f4\fs20 properties (if those properties are encompassed by the spatial dimensionality of the simulation). In other words, if the declared dimensionality is \f3\fs18 "xy" \f5\fs20 , \f4 the \f3\fs18 individual.spatialPosition \f4\fs20 property is equivalent to \f3\fs18 c(individual.x,\'a0individual.y) \f4\fs20 ; \f3\fs18 individual.z \f4\fs20 is not used since it is not encompassed by the simulation\'92s dimensionality. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 subpopulation => (object$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 Subpopulation \f4\fs20 object to which the individual belongs. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value (as opposed to \f3\fs18 tagF \f4\fs20 , which is of type \f3\fs18 float \f4\fs20 ). The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to individuals. \cf2 Note that the \f3\fs18 Individual \f4\fs20 objects used by SLiM are new for every new offspring, so the \f3\fs18 tag \f4\fs20 value of each new offspring generated in each tick will be initially undefined. \f5 \cf0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tagF <\'96> (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 float \f4\fs20 value (as opposed to \f3\fs18 tag \f4\fs20 , which is of type \f3\fs18 integer \f4\fs20 ). The value of \f3\fs18 tagF \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tagF \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to individuals.\ Note that at present, although many classes in SLiM have an \f3\fs18 integer \f4\fs20 -type \f3\fs18 tag \f4\fs20 property, only \f3\fs18 Individual \f4\fs20 has a \f3\fs18 float \f4\fs20 -type \f3\fs18 tagF \f4\fs20 property, because attaching model state to individuals seems to be particularly common and useful. If a \f3\fs18 tagF \f4\fs20 property would be helpful on another class, it would be easy to add.\ See the description of the \f3\fs18 tag \f4\fs20 property above for additional comments. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 tagL0 <\'96> (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 logical \f4\fs20 value (see also \f3\fs18 tag \f4\fs20 and \f3\fs18 tagF \f4\fs20 ). The value of \f3\fs18 tagL0 \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tagL0 \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual), for another way of attaching state to individuals.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 tagL1 <\'96> (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 logical \f4\fs20 value (see also \f3\fs18 tag \f4\fs20 and \f3\fs18 tagF \f4\fs20 ). The value of \f3\fs18 tagL1 \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tagL1 \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual), for another way of attaching state to individuals.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 tagL2 <\'96> (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 logical \f4\fs20 value (see also \f3\fs18 tag \f4\fs20 and \f3\fs18 tagF \f4\fs20 ). The value of \f3\fs18 tagL2 \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tagL2 \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual), for another way of attaching state to individuals.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 tagL3 <\'96> (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 logical \f4\fs20 value (see also \f3\fs18 tag \f4\fs20 and \f3\fs18 tagF \f4\fs20 ). The value of \f3\fs18 tagL3 \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tagL3 \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual), for another way of attaching state to individuals.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 tagL4 <\'96> (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 logical \f4\fs20 value (see also \f3\fs18 tag \f4\fs20 and \f3\fs18 tagF \f4\fs20 ). The value of \f3\fs18 tagL4 \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tagL4 \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual), for another way of attaching state to individuals.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 uniqueMutations => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the \f3\fs18 Mutation \f4\fs20 objects present in this individual. Mutations present in homologous haplosomes will occur only once in this property, and the mutations for a given chromosome will be given in sorted order by \f3\fs18 position \f4\fs20 , so in single-chromosome simulations this property is similar to \f3\fs18 sortBy(unique(individual.haplosomes.mutations), "position") \f4\fs20 . (Even with a single chromosome it is not identical to that call, since if multiple mutations exist at the exact same position, they might be sorted differently by this method than they would be by \f3\fs18 sortBy() \f4\fs20 .) This method is provided primarily for speed; it executes much faster than the Eidos equivalent above. Indeed, it is faster than just \f3\fs18 individual.haplosomes.mutations \f4\fs20 , and gives uniquing and sorting on top of that, so it is advantageous unless duplicate entries for homozygous mutations are actually needed. For more flexibility, see the method \f3\fs18 mutationsFromHaplosomes() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 x <\'96> (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 float \f4\fs20 value. The value of \f3\fs18 x \f4\fs20 is initially undefined (i.e., has an effectively random value that could be different every time you run your model); if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 x \f4\fs20 is not used by SLiM unless the optional \'93continuous space\'94 facility is enabled with the \f3\fs18 dimensionality \f4\fs20 parameter to \f3\fs18 initializeSLiMOptions() \f4\fs20 , in which case \f3\fs18 x \f4\fs20 will be understood to represent the \f1\i x \f4\i0 coordinate of the individual in space. If continuous space is not enabled, you may use \f3\fs18 x \f4\fs20 as an additional tag value of type \f3\fs18 float \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 xy => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This property provides joint read-only access to the \f3\fs18 x \f4\fs20 and \f3\fs18 y \f4\fs20 properties; they are returned as a two-element \f3\fs18 float \f4\fs20 vector. This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model. See the documentation for the separate properties \f3\fs18 x \f4\fs20 and \f3\fs18 y \f4\fs20 for further comments.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 xyz => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This property provides joint read-only access to the \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 z \f4\fs20 properties; they are returned as a three-element \f3\fs18 float \f4\fs20 vector. This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model. See the documentation for the separate properties \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 z \f4\fs20 for further comments.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 xz => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This property provides joint read-only access to the \f3\fs18 x \f4\fs20 and \f3\fs18 z \f4\fs20 properties; they are returned as a two-element \f3\fs18 float \f4\fs20 vector. This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model. See the documentation for the separate properties \f3\fs18 x \f4\fs20 and \f3\fs18 z \f4\fs20 for further comments.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 y <\'96> (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 float \f4\fs20 value. The value of \f3\fs18 y \f4\fs20 is initially undefined (i.e., has an effectively random value that could be different every time you run your model); if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 y \f4\fs20 is not used by SLiM unless the optional \'93continuous space\'94 facility is enabled with the \f3\fs18 dimensionality \f4\fs20 parameter to \f3\fs18 initializeSLiMOptions() \f4\fs20 , in which case \f3\fs18 y \f4\fs20 will be understood to represent the \f1\i y \f4\i0 coordinate of the individual in space (if the dimensionality is \f3\fs18 "xy" \f4\fs20 or \f3\fs18 "xyz" \f4\fs20 ). If continuous space is not enabled, or the dimensionality is not \f3\fs18 "xy" \f4\fs20 or \f3\fs18 "xyz" \f4\fs20 , you may use \f3\fs18 y \f4\fs20 as an additional tag value of type \f3\fs18 float \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 yz => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This property provides joint read-only access to the \f3\fs18 y \f4\fs20 and \f3\fs18 z \f4\fs20 properties; they are returned as a two-element \f3\fs18 float \f4\fs20 vector. This can be useful in complex spatial models in which the spatiality of interactions/maps differs from the overall dimensionality of the model. See the documentation for the separate properties \f3\fs18 y \f4\fs20 and \f3\fs18 z \f4\fs20 for further comments.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 z <\'96> (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 float \f4\fs20 value. The value of \f3\fs18 z \f4\fs20 is initially undefined (i.e., has an effectively random value that could be different every time you run your model); if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 z \f4\fs20 is not used by SLiM unless the optional \'93continuous space\'94 facility is enabled with the \f3\fs18 dimensionality \f4\fs20 parameter to \f3\fs18 initializeSLiMOptions() \f4\fs20 , in which case \f3\fs18 z \f4\fs20 will be understood to represent the \f1\i z \f4\i0 coordinate of the individual in space (if the dimensionality is \f3\fs18 "xyz" \f4\fs20 ). If continuous space is not enabled, or the dimensionality is not \f3\fs18 "xyz" \f4\fs20 , you may use \f3\fs18 z \f4\fs20 as an additional tag value of type \f3\fs18 float \f4\fs20 .\ \pard\pardeftab397\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.7.2 \f2\fs18 Individual \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 \'96\'a0(logical)containsMutations(object\'a0mutations) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a \f3\fs18 logical \f4\fs20 vector indicating whether each of the mutations in \f3\fs18 mutations \f4\fs20 is present in the individual (in any of its haplosomes); each element in the returned vector indicates whether the corresponding mutation is present ( \f3\fs18 T \f4\fs20 ) or absent ( \f3\fs18 F \f4\fs20 ). This method is provided for speed; it is much faster than the corresponding Eidos code.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (integer$)countOfMutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the number of mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations in the individual (in all of its haplosomes; a mutation that is present in both homologous haplosomes counts twice). If you need a vector of the matching \f3\fs18 Mutation \f4\fs20 objects, rather than just a count, you should probably use \f3\fs18 mutationsFromHaplosomes() \f4\fs20 . This method is provided for speed; it is much faster than the corresponding Eidos code.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)haplosomesForChromosomes([Niso\'a0chromosomes\'a0=\'a0NULL], [Ni$\'a0index\'a0=\'a0NULL], [logical$\'a0includeNulls\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing the haplosomes of the target individual that correspond to the chromosomes passed in \f3\fs18 chromosomes \f4\fs20 (following the order of the \f3\fs18 chromosomes \f4\fs20 property of \f3\fs18 Individual \f4\fs20 ). Chromosomes can be specified by id ( \f3\fs18 integer \f4\fs20 ), by symbol ( \f3\fs18 string \f4\fs20 ) or by the \f3\fs18 Chromosome \f4\fs20 objects themselves; if \f3\fs18 NULL \f4\fs20 is passed (the default), all chromosomes defined for the species are used, in the order in which they were defined.\ For chromosomes that are intrinsically diploid (types \f3\fs18 "A" \f4\fs20 , \f3\fs18 "X" \f4\fs20 , and \f3\fs18 "Z" \f4\fs20 , as well as the \f3\fs18 "H-" \f4\fs20 and \f3\fs18 "-Y" \f4\fs20 backward-compatibility chromosome types), \f3\fs18 index \f4\fs20 can be \f3\fs18 0 \f4\fs20 or \f3\fs18 1 \f4\fs20 , requesting only the first or second haplosome, respectively, for that chromosome; for other chromosome types, \f3\fs18 index \f4\fs20 is ignored. If \f3\fs18 includeNulls \f4\fs20 is \f3\fs18 T \f4\fs20 (the default), any null haplosomes corresponding to the specified chromosomes are included in the result; if it is \f3\fs18 F \f4\fs20 , null haplosomes are excluded. See also the properties \f3\fs18 haplosomes \f4\fs20 , \f3\fs18 haplosomesNonNull \f4\fs20 , \f3\fs18 haploidGenome1 \f4\fs20 , \f3\fs18 haploidGenome1NonNull \f4\fs20 , \f3\fs18 haploidGenome2 \f4\fs20 , and \f3\fs18 haploidGenome2NonNull \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)mutationsFromHaplosomes(string$\'a0category, [Nio$\'a0mutType\'a0=\'a0NULL], [Niso\'a0chromosomes\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of mutations from the haplosomes of the target individual. Several options are provided that filter which mutations are returned.\ The \f3\fs18 category \f4\fs20 parameter must be one of five supported values: \f3\fs18 "unique" \f4\fs20 , \f3\fs18 "homozygous" \f4\fs20 , \f3\fs18 "heterozygous" \f4\fs20 , \f3\fs18 "hemizygous" \f4\fs20 , or \f3\fs18 "all" \f4\fs20 . If \f3\fs18 category \f4\fs20 is \f3\fs18 "unique" \f4\fs20 , a given mutation will be returned only once, whether it is present homozygously or heterozygously (or hemizygously, for that matter); this mode of operation is similar to the \f3\fs18 uniqueMutations \f4\fs20 property, but provides more control due to the other options provided by this method. If \f3\fs18 category \f4\fs20 is \f3\fs18 "homozygous" \f4\fs20 , a given mutation will be returned only if it is present homozygously (in both of the homologous haplosomes for a given chromosome). If \f3\fs18 category \f4\fs20 is \f3\fs18 "heterozygous" \f4\fs20 , a given mutation will be returned only if it is present heterozygously (in only one of the two homologous non-null haplosomes for a given chromosome). If \f3\fs18 category \f4\fs20 is \f3\fs18 "hemizygous" \f4\fs20 , a given mutation will be returned only if it is present hemizygously (in one haplosome for an intrinsically diploid chromosome, when the other haplosome is a null haplosome). If \f3\fs18 category \f4\fs20 is \f3\fs18 "all" \f4\fs20 , a given mutation will be returned each time that it occurs in the haplosomes of the individual; in other words, it will be present in the returned vector \f1\i twice \f4\i0 if it is homozygous, \f1\i once \f4\i0 if it is heterozygous or hemizygous. Mutations in the single haplosome of an intrinsically haploid chromosome will be returned for \f3\fs18 category \f4\fs20 values of \f3\fs18 "unique" \f4\fs20 , \f3\fs18 "homozygous" \f4\fs20 , and \f3\fs18 "all" \f4\fs20 .\ The \f3\fs18 mutType \f4\fs20 parameter may be \f3\fs18 NULL \f4\fs20 , or may specify a mutation type by its \f3\fs18 integer \f4\fs20 id or with the \f3\fs18 MutationType \f4\fs20 object itself. If \f3\fs18 mutType \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), mutations of every mutation type are returned; no filtering by mutation type is done. Otherwise, only mutations of the specified mutation type will be returned.\ The \f3\fs18 chromosomes \f4\fs20 parameter may be \f3\fs18 NULL \f4\fs20 , or may provide a vector of chromosomes specified by their \f3\fs18 integer \f4\fs20 id, \f3\fs18 string \f4\fs20 symbol, or with the \f3\fs18 Chromosome \f4\fs20 object itself. If \f3\fs18 chromosomes \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), mutations associated with every chromosome are returned; no filtering by chromosome is done. Otherwise, only mutations associated with the specified chromosomes will be returned.\ The returned vector will contain tranches of mutations, one tranche per chromosome, in the order that the chromosomes were specified (if \f3\fs18 chromosomes \f4\fs20 is non- \f3\fs18 NULL \f4\fs20 ) or the order the chromosomes were defined in the model (if \f3\fs18 chromosomes \f4\fs20 is \f3\fs18 NULL \f4\fs20 ). Within a given tranche, the mutations for that chromosome will be returned in sorted order by \f3\fs18 position \f4\fs20 . (If more than one mutation associated with a given chromosome exists at the same position, the order in which those mutations are returned is undefined.)\ This method replaces the deprecated method \f3\fs18 uniqueMutationsOfType() \f4\fs20 , while providing additional useful options. It is particularly useful for efficient, vectorized assessment of the homozygous versus heterozygous state of the mutations contained by an individual, which is otherwise difficult to assess efficiently.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(void)outputIndividuals([Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F], [Niso$\'a0chromosome\'a0=\'a0NULL], [logical$\'a0spatialPositions\'a0=\'a0T], [logical$\'a0ages\'a0=\'a0T], [logical$\'a0ancestralNucleotides\'a0=\'a0F], [logical$\'a0pedigreeIDs\'a0=\'a0F], [logical$\'a0objectTags\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output the state of the target vector of individuals in SLiM's own format. If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 . This method is quite similar to the \f3\fs18 Species \f4\fs20 method \f3\fs18 outputFull() \f4\fs20 , but (1)\'a0it can produce output for any vector of individuals, not always for the entire population; (2)\'a0it does not support output in a binary format; (3)\'a0it can produce output regarding the genetics for all chromosomes or for just one focal chromosome; and (4)\'a0there is no corresponding read method, as r \f3\fs18 eadFromPopulationFile() \f4\fs20 can read the data saved by \f3\fs18 outputFull() \f4\fs20 .\ The \f3\fs18 chromosome \f4\fs20 parameter specifies a focal chromosome for which the genetics of the target individuals will be output. If \f3\fs18 chromosome \f4\fs20 is \f3\fs18 NULL \f4\fs20 , all chromosomes will be output; otherwise, \f3\fs18 chromosome \f4\fs20 may specify the focal chromosome with an \f3\fs18 integer \f4\fs20 chromosome id, a \f3\fs18 string \f4\fs20 chromosome symbol, or a \f3\fs18 Chromosome \f4\fs20 object.\ The \f3\fs18 spatialPositions \f4\fs20 parameter may be used to control the output of the spatial positions of individuals in species for which continuous space has been enabled using the \f3\fs18 dimensionality \f4\fs20 option of \f3\fs18 initializeSLiMOptions() \f4\fs20 . If \f3\fs18 spatialPositions \f4\fs20 is \f3\fs18 F \f4\fs20 , the output will not contain spatial positions. If \f3\fs18 spatialPositions \f4\fs20 is \f3\fs18 T \f4\fs20 , spatial position information will be output if it is available. If the species does not have continuous space enabled, the \f3\fs18 spatialPositions \f4\fs20 parameter will be ignored.\ The \f3\fs18 ages \f4\fs20 parameter may be used to control the output of the ages of individuals in nonWF simulations. If \f3\fs18 ages \f4\fs20 is \f3\fs18 F \f4\fs20 , the output will not contain ages. If \f3\fs18 ages \f4\fs20 is \f3\fs18 T \f4\fs20 , ages will be output for nonWF models. In WF simulations, the \f3\fs18 ages \f4\fs20 parameter will be ignored.\ The \f3\fs18 ancestralNucleotides \f4\fs20 parameter may be used to control the output of the ancestral nucleotide sequence in nucleotide-based models. If \f3\fs18 ancestralNucleotides \f4\fs20 is \f3\fs18 F \f4\fs20 , the output will not contain ancestral nucleotide information. This option is provided because the ancestral sequence may be quite large, for models with a long chromosome. If the model is not nucleotide-based (as enabled with the \f3\fs18 nucleotideBased \f4\fs20 parameter to \f3\fs18 initializeSLiMOptions() \f4\fs20 ), the \f3\fs18 ancestralNucleotides \f4\fs20 parameter will be ignored. Note that in nucleotide-based models the output format will \f1\i always \f4\i0 include the nucleotides associated with any nucleotide-based mutations; the \f3\fs18 ancestralNucleotides \f4\fs20 flag governs only the ancestral sequence.\ The \f3\fs18 pedigreeIDs \f4\fs20 parameter may be used to request that pedigree IDs be written out. This option is turned off ( \f3\fs18 F \f4\fs20 ) by default, for brevity. This option may only be used if SLiM\'92s optional pedigree tracking has been enabled with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 .\ Finally, the \f3\fs18 objectTags \f4\fs20 parameter may be used to request that tag values for objects be written out. This option is turned off ( \f3\fs18 F \f4\fs20 ) by default, for brevity; if it turned on ( \f3\fs18 T \f4\fs20 ), the values of all tags for all objects of supported classes ( \f3\fs18 Chromosome \f4\fs20 , \f3\fs18 Individual \f4\fs20 , \f3\fs18 Haplosome \f4\fs20 , \f3\fs18 Mutation \f4\fs20 ) will be written. For individuals, the \f3\fs18 tag \f4\fs20 , \f3\fs18 tagF \f4\fs20 , \f3\fs18 tagL0 \f4\fs20 , \f3\fs18 tagL1 \f4\fs20 , \f3\fs18 tagL2 \f4\fs20 , \f3\fs18 tagL3 \f4\fs20 , and \f3\fs18 tagL4 \f4\fs20 properties will be written; for chromosomes, haplosomes, and mutations, the \f3\fs18 tag \f4\fs20 property will be written. If there is other state that you wish you persist, such as tags on objects of other classes, values attached to objects with \f3\fs18 setValue() \f4\fs20 , and so forth, you should persist that state in separate files using calls such as \f3\fs18 writeFile() \f4\fs20 .\ Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(void)outputIndividualsToVCF([Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F], [Niso$\'a0chromosome\'a0=\'a0NULL], [logical$\'a0outputMultiallelics\'a0=\'a0T], [logical$\'a0simplifyNucleotides\'a0=\'a0F], [logical$\'a0outputNonnucleotides\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output the state of the target vector of individuals in VCF format. If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 . This method is quite similar to the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 outputVCFSample() \f4\fs20 , but (1)\'a0it can produce output for any vector of individuals, rather than sampling from a single population; (2)\'a0it can produce output regarding the genetics for all chromosomes or for just one focal chromosome, whereas \f3\fs18 outputVCFSample() \f4\fs20 can only output data for a single chromosome; and (3)\'a0because it can output genetic information for more than one chromosome, the \f3\fs18 groupAsIndividuals \f4\fs20 option provided by \f3\fs18 outputVCFSample() \f4\fs20 is not available for \f3\fs18 outputIndividualsToVCF() \f4\fs20 ; each VCF sample has to correspond directly to one individual.\ The \f3\fs18 chromosome \f4\fs20 parameter specifies a focal chromosome for which the genetics of the target individuals will be output. If \f3\fs18 chromosome \f4\fs20 is \f3\fs18 NULL \f4\fs20 , all chromosomes will be output (distinguished in the VCF output by the chromosome symbol output in the \f3\fs18 CHROM \f4\fs20 column); otherwise, \f3\fs18 chromosome \f4\fs20 may specify the focal chromosome with an \f3\fs18 integer \f4\fs20 chromosome id, a \f3\fs18 string \f4\fs20 chromosome symbol, or a \f3\fs18 Chromosome \f4\fs20 object.\ The parameters \f3\fs18 outputMultiallelics \f4\fs20 , \f3\fs18 simplifyNucleotides \f4\fs20 , and \f3\fs18 outputNonnucleotides \f4\fs20 affect the format of the output produced.\ Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(object)readIndividualsFromVCF(string$\'a0filePath, [Nio$\'a0mutationType\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Read new mutations from the VCF format file at \f3\fs18 filePath \f4\fs20 and add them to the target individuals. The number of target individuals must match the number of samples represented in the VCF file; each sample will be associated with a corresponding target individual, in the order that the samples and the target individuals are provided. To read into all of the individuals in a given subpopulation \f3\fs18 pN \f4\fs20 , simply call \f3\fs18 pN.individuals.readIndividualsFromVCF() \f4\fs20 , assuming the subpopulation\'92s size matches the number of samples in the VCF file. A vector containing all of the mutations created by \f3\fs18 readIndividualsFromVCF() \f4\fs20 is returned (not necessarily in the order of the corresponding VCF call lines).\ This method and the \f3\fs18 readHaplosomesFromVCF() \f4\fs20 method of \f3\fs18 Haplosome \f4\fs20 provide two alternative ways of reading VCF data, focused on the perspective of either individuals (this method) or haplosomes (the \f3\fs18 Haplosome \f4\fs20 method). As described above, this method draws a correspondence between VCF samples and individuals, whereas the \f3\fs18 Haplosome \f4\fs20 method draws a correspondence between VCF calls and haplosomes. For example, if a VCF call line contained a series of calls like \'93 \f3\fs18 1|1 0|1 1 0 1|0 \f4\fs20 \'94 this method would see that as calls for five individuals, three of which are diploid for the chromosome being called, and two of which are haploid. That would make sense if, for example, the chromosome being called is an X chromosome; the diploid individuals would be females, the haploid individuals would be males. The vector of target individuals would need to contain two females, then two males, and then a female, so that the structure of the calls matched the haplosome structure of the individuals, or an error would result. The \f3\fs18 readHaplosomesFromVCF() \f4\fs20 method of \f3\fs18 Haplosome \f4\fs20 , on the other hand, would see that same series of calls as corresponding to eight haplosomes, and would assign each call to the corresponding non-null target haplosome, without regard to whether the VCF\'92s grouping into diploid and haploid calls corresponded to any coherent structure of individuals in the SLiM model. Each approach has advantages and disadvantages. This method provides much more error-checking and safety when your intention is to read individual-level data from VCF; it can check that the ploidy in the VCF data matches the ploidy of each target individual, and that diploid calls like \f3\fs18 1|1 \f4\fs20 get assigned to the two haplosomes of a single individual correctly. The \f3\fs18 readHaplosomesFromVCF() \f4\fs20 method of \f3\fs18 Haplosome \f4\fs20 does not perform such checks, and can push VCF data into any arbitrary set of haplosomes, so it provides more power and flexibility, but less error-checking and less intelligence.\ Because this method works at the level of individuals, it can read VCF data associated with multiple chromosomes into a multi-chromosome SLiM model and place mutations into the correct haplosomes of each individual based upon the \f3\fs18 CHROM \f4\fs20 column of the VCF file (which \f3\fs18 readHaplosomesFromVCF() \f4\fs20 cannot do). For this to work, the values in the \f3\fs18 CHROM \f4\fs20 column of the call lines must correspond exactly to chromosome symbols in the SLiM model, as provided to \f3\fs18 initializeChromosome() \f4\fs20 . The call lines in the input file may be in any order (they do not have to be sorted by \f3\fs18 CHROM \f4\fs20 value, or any other such requirement). The vector of mutations returned will contain all of the mutations created; when reading multi-chromosome data, that returned vector will therefore contain a mix of mutations with different associated chromosomes. Alternatively, it would work equally well to make a separate call to \f3\fs18 readIndividualsFromVCF() \f4\fs20 for each chromosome, providing each call with a separate VCF file that contains only the mutations associated with one chromosome; in that case, each call would return only the mutations added to the chromosome associated with that call, which might be more convenient if post-processing of the returned mutations is necessary.\ As in \f3\fs18 readHaplosomesFromVCF() \f4\fs20 , a call of \f3\fs18 ~ \f4\fs20 represents the fact that an individual has no genetic information for the chromosome being called; for example, a call line for a mutation on a Y chromosome should have haploid calls for male individuals (they have or do not have the called mutation), and calls of \f3\fs18 ~ \f4\fs20 for female individuals (they have no Y haplosome at all). This convention was invented for SLiM, since the VCF standard does not seem to say anything about how sex chromosomes should be represented (or anything about other types of chromosomes that might be absent from some individuals).\ The \f3\fs18 readHaplosomesFromVCF() \f4\fs20 method\'92s documentation provides many important details on how SLiM treats various VCF fields during input; those details will not be repeated here, for brevity.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)relatedness(object\'a0individuals, [Niso$\'a0chromosome\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing the degrees of relatedness between the receiver and each of the individuals in \f3\fs18 individuals \f4\fs20 . The relatedness between \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 is always \f3\fs18 1.0 \f4\fs20 if \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 are actually the same individual; this facility works even if SLiM\'92s optional pedigree tracking is not enabled (in which case all other relatedness values will be \f3\fs18 0.0 \f4\fs20 ). Otherwise, if pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , this method will use the pedigree information to construct a relatedness estimate. The relatedness is calculated based upon the type of the chromosome specified by \f3\fs18 chromosome \f4\fs20 (as an \f3\fs18 integer \f4\fs20 id, a \f3\fs18 string \f4\fs20 symbol, or a \f3\fs18 Chromosome \f4\fs20 object); if \f3\fs18 chromosome \f4\fs20 is \f3\fs18 NULL \f4\fs20 , it is assumed to be the single chromosome present in the model, or if more than one chromosome is present, an error results and the chromosome must be explicitly given.\ More specifically, this method uses all available pedigree information from the grandparental and parental pedigree records of \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 to compute an estimate of the degree of consanguinity between \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 . When considering a diploid autosome, siblings have a relatedness of \f3\fs18 0.5 \f4\fs20 , as do parents to their children and vice versa; cousins have a relatedness of \f3\fs18 0.125 \f4\fs20 ; and so forth. If, according to the pedigree information available, \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 have no blood relationship, the value returned is \f3\fs18 0.0 \f4\fs20 . Note that the value returned by \f3\fs18 relatedness() \f4\fs20 is what is called the \'93coefficient of relationship\'94 between the two individuals (Wright, 1922; {\field{\*\fldinst{HYPERLINK "https://doi.org/10.1086/279872"}}{\fldrslt \cf3 \ul \ulc3 https://doi.org/10.1086/279872}}), and ranges from \f3\fs18 0.0 \f4\fs20 to \f3\fs18 1.0 \f4\fs20 .\ There is another commonly used metric of relatedness, called the \'93kinship coefficient\'94, that reflects the probability of identity by descent between two individuals \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 . In general, it is approximately equal to one-half of the coefficient of relationship; if an approximate estimate of the kinship coefficient is acceptable, especially in models in which individuals are expected to be outbred, you can simply divide \f3\fs18 relatedness() \f4\fs20 by two. However, it should be noted that Wright\'92s coefficient of relationship is \f1\i not \f4\i0 a measure of the probability of identity by descent, and so it is not exactly double the kinship coefficient; they actually measure different things. More precisely, the relationship between them is \f6\i r \f5\i0 \'a0=\'a02 \f6\i \uc0\u966 \f5\i0 /sqrt((1+ \f6\i f \f5\i0\fs13\fsmilli6667 \sub A \fs20 \nosupersub )(1+ \f6\i f \f5\i0\fs13\fsmilli6667 \sub B \fs20 \nosupersub )) \f4 , where \f6\i r \f4\i0 is Wright\'92s coefficient of relatedness, \f6\i \uc0\u966 \f4\i0 is the kinship coefficient, and \f6\i f \f5\i0\fs13\fsmilli6667 \sub A \f4\fs20 \nosupersub and \f6\i f \f5\i0\fs13\fsmilli6667 \sub B \f4\fs20 \nosupersub are the inbreeding coefficients of \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 respectively.\ Note that this relatedness is simply pedigree-based relatedness, and does not necessarily correspond to genetic relatedness, because of the effects of factors like assortment and recombination. If a metric of actual genetic relatedness is desired, tree-sequence recording can be used after simulation is complete, to compute the exact genetic relatedness between individuals based upon the complete ancestry tree (a topic which is beyond the scope of this manual). Actual genetic relatedness cannot presently be calculated during a simulation run; the information is implicitly contained in the recorded tree-sequence tables, but calculating it is too computationally expensive to be reasonable.\ This method assumes that the grandparents (or the parents, if grandparental information is not available) are themselves unrelated and that they are not inbred; this assumption is necessary because we have no information about their parentage, since SLiM\'92s pedigree tracking information only goes back two generations. Be aware that in a model where inbreeding or selfing occurs at all (including \'93incidental selfing\'94, where a hermaphroditic individual happens to choose itself as a mate), some level of \'93background relatedness\'94 will be present and this assumption will be violated. In such circumstances, \f3\fs18 relatedness() \f4\fs20 will therefore tend to underestimate the degree of relatedness between individuals, and the greater the degree of inbreeding, the greater the underestimation will be. If inbreeding is allowed in a model \'96 and particularly if it is common \'96 the results of \f3\fs18 relatedness() \f4\fs20 should therefore not be taken as an estimate of \f1\i absolute \f4\i0 relatedness, but can still be useful as an estimate of \f1\i relative \f4\i0 relatedness (indicating that, say, A appears from the information available to be more closely related to B than it is to C).\ See also \f3\fs18 sharedParentCount() \f4\fs20 for a different metric of relatedness.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 +\'a0(void)setSpatialPosition(float\'a0position)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Sets the spatial position of the individual (as accessed through the \f3\fs18 spatialPosition \f4\fs20 property). The length of \f3\fs18 position \f4\fs20 (the number of coordinates in the spatial position of an individual) depends upon the spatial dimensionality declared with \f3\fs18 initializeSLiMOptions() \f4\fs20 . If the spatial dimensionality is zero (as it is by default), it is an error to call this method. The elements of \f3\fs18 position \f4\fs20 are set into the values of the \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 z \f4\fs20 properties (if those properties are encompassed by the spatial dimensionality of the simulation). In other words, if the declared dimensionality is \f3\fs18 "xy" \f4\fs20 , calling \f3\fs18 individual.setSpatialPosition(c(1.0, 0.5)) \f4\fs20 property is equivalent to \f3\fs18 individual.x\'a0=\'a01.0; individual.y\'a0=\'a00.5 \f4\fs20 ; \f3\fs18 individual.z \f4\fs20 is not set (even if a third value is supplied in \f3\fs18 position \f4\fs20 ) since it is not encompassed by the simulation\'92s dimensionality in this example.\ Note that this is an Eidos class method, somewhat unusually, which allows it to work in a special way when called on a vector of individuals. When the target vector of individuals is non-singleton, this method can do one of two things. If \f3\fs18 position \f4\fs20 contains just a single point (i.e., is equal in length to the spatial dimensionality of the model), the spatial position of all of the target individuals will be set to the given point. Alternatively, if \f3\fs18 position \f4\fs20 contains one point per target individual (i.e., is equal in length to the number of individuals multiplied by the spatial dimensionality of the model), the spatial position of each target individual will be set to the corresponding point from \f3\fs18 position \f4\fs20 (where the point data is concatenated, not interleaved, just as it would be returned by accessing the \f3\fs18 spatialPosition \f4\fs20 property on the vector of target individuals). Calling this method with a \f3\fs18 position \f4\fs20 vector of any other length is an error.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 \'96\'a0(integer)sharedParentCount(object\'a0individuals)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing the number of parents shared between the receiver and each of the individuals in \f3\fs18 individuals \f4\fs20 . The number of shared parents between \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 is always \f3\fs18 2 \f4\fs20 if \f3\fs18 A \f4\fs20 and \f3\fs18 B \f4\fs20 are actually the same individual; this facility works even if SLiM\'92s optional pedigree tracking is not enabled (in which case all other relatedness values will be \f3\fs18 0 \f4\fs20 ). Otherwise, if pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , this method will use the pedigree information to construct a relatedness estimate.\ More specifically, this method uses the parental pedigree IDs from the pedigree records of a pair of individuals to count the number of shared parents between them, such that full siblings (with all of the same parents) have a count of \f3\fs18 2 \f4\fs20 , and half siblings (with half of the same parents) have a count of \f3\fs18 1 \f4\fs20 . If possible parents of the two individuals are \f3\fs18 A \f4\fs20 , \f3\fs18 B \f4\fs20 , \f3\fs18 C \f4\fs20 , and \f3\fs18 D \f4\fs20 , then the shared parent count is as follows, for some illustrative examples. The first column showing the two parents of the first individual, the second column showing the two parents of the second individual; note that the two parents of an individual can be the same due to cloning or selfing:\ \pard\pardeftab720\li547\sa60\partightenfactor0 \cf2 \f3\fs18 AB \f4\fs20 \f3\fs18 CD \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 0 \f4\fs20 (no shared parents)\ \f3\fs18 AB \f4\fs20 \f3\fs18 CC \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 0 \f4\fs20 (no shared parents)\ \f3\fs18 AB \f4\fs20 \f3\fs18 AC \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 1 \f4\fs20 (half siblings)\ \f3\fs18 AB \f4\fs20 \f3\fs18 AA \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 1 \f4\fs20 (half siblings)\ \f3\fs18 AA \f4\fs20 \f3\fs18 AB \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 1 \f4\fs20 (half siblings)\ \f3\fs18 AB \f4\fs20 \f3\fs18 AB \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 2 \f4\fs20 (full siblings)\ \f3\fs18 AB \f4\fs20 \f3\fs18 BA \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 2 \f4\fs20 (full siblings)\ \f3\fs18 AA \f4\fs20 \f3\fs18 AA \f4\fs20 \f7 \uc0\u8594 \f4 \f3\fs18 2 \f4\fs20 (full siblings)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 This method does not estimate consanguinity. For example, if one individual is itself a parent of the other individual, that is irrelevant for this method. Similarly, in simulations of sex chromosomes, the sexes of the parents are irrelevant, even if no genetic material would have been inherited from a given parent. See \f3\fs18 relatedness() \f4\fs20 for an assessment of pedigree-based relatedness that does estimate the consanguinity of individuals. The \f3\fs18 sharedParentCount() \f4\fs20 method is preferable if your exact question is simply whether individuals are full siblings, half siblings, or non-siblings; in other cases, \f3\fs18 relatedness() \f4\fs20 is probably more useful.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (float$)sumOfMutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns the sum of the selection coefficients of all mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations in the haplosomes of the individual. This is often useful in models that use a particular mutation type to represent QTLs with additive effects; in that context, \f3\fs18 sumOfMutationsOfType() \f4\fs20 will provide the sum of the additive effects of the QTLs for the given mutation type. This method is provided for speed; it is much faster than the corresponding Eidos code. Note that this method also exists on \f3\fs18 Haplosome \f4\fs20 , for cases in which the sum for just one haplosome is desired. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (object)uniqueMutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 This method has been deprecated, and may be removed in a future release of SLiM. \f4\b0 Its functionality was replaced by \f3\fs18 mutationsFromHaplosomes() \f4\fs20 in SLiM 5.0.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 Returns an \f3\fs18 object \f4\fs20 vector of all the mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations in the individual. Mutations present in both homologous haplosomes will occur only once in the result of this method, and the mutations for a given chromosomes will be given in sorted order by \f3\fs18 position \f4\fs20 , so in single-chromosome simulations this method is similar to \f3\fs18 sortBy(unique(individual.haplosomes.mutationsOfType(mutType)), "position") \f4\fs20 . (Even with a single chromosome it is not identical to that call, since if multiple mutations exist at the exact same position, they may be sorted differently by this method than they would be by \f3\fs18 sortBy() \f4\fs20 .) If you just need a count of the matching \f3\fs18 Mutation \f4\fs20 objects, rather than a vector of the matches, use \f3\fs18 -countOfMutationsOfType() \f4\fs20 . This method is provided for speed; it is much faster than the corresponding Eidos code. Indeed, it is faster than just \f3\fs18 individual.haplosomes.mutationsOfType(mutType) \f4\fs20 , and gives uniquing and sorting on top of that, so it is advantageous unless duplicate entries for homozygous mutations are actually needed.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 +\'a0(integer)zygosityOfMutations([No\'a0mutations\'a0=\'a0NULL], [integer$\'a0hemizygousValue\'a0=\'a01], [integer$\'a0haploidValue\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns an \f3\fs18 integer \f4\fs20 matrix with the target individuals\'92 zygosity for all of the \f3\fs18 Mutation \f4\fs20 objects passed in \f3\fs18 mutations \f4\fs20 . If the optional \f3\fs18 mutations \f4\fs20 argument is \f3\fs18 NULL \f4\fs20 (the default), zygosity values will be returned for all of the active \f3\fs18 Mutation \f4\fs20 objects in the species \'96 the same \f3\fs18 Mutation \f4\fs20 objects, and in the same order, as would be returned by the \f3\fs18 mutations \f4\fs20 property of \f3\fs18 sim \f4\fs20 , in other words. The returned matrix has one column for each individual and one row for each mutation.\ For a mutation on a diploid chromosome, the zygosity will be either \f3\fs18 0 \f4\fs20 (absent), \f3\fs18 1 \f4\fs20 (heterozygous), or \f3\fs18 2 \f4\fs20 (homozygous). This is the straightforward \'93base case\'94 that is usually meant by the term \'93zygosity\'94.\ If one of the two haplosomes of an intrinsically diploid chromosome is a null haplosome (as would be the case for an X chromosome in a male individual, for example), mutations present in the non-null haplosome are called \'93hemizygous\'94, and the zygosity returned for them is configurable using the \f3\fs18 hemizygousValue \f4\fs20 parameter. By default, the zygosity returned for hemizygous mutations is \f3\fs18 1 \f4\fs20 .\ Finally, although the term \'93zygosity\'94 is not usually used in the context of haploidy, this method nevertheless supports intrinsically haploid chromosomes; the zygosity returned for mutations present on an intrinsically haploid chromosome is configurable using the \f3\fs18 haploidValue \f4\fs20 parameter. By default, the zygosity returned for haploid mutations is \f3\fs18 1 \f4\fs20 .\ For a large number of mutations \'96 and especially for a \f3\fs18 mutations \f4\fs20 value of \f3\fs18 NULL \f4\fs20 , representing all mutations in the species \'96 this method should be much more efficient than using methods such as \f3\fs18 containsMutations() \f4\fs20 to assess zygosity. Nevertheless, calculating fitness effects in script based upon zygosity will generally be slower than SLiM\'92s internal fitness calculations. Note that for just one or a few mutations \'96 especially if \f3\fs18 mutations \f4\fs20 contains just a small fraction of all of the mutations in the species \'96 this method will probably be much slower than alternative approaches; this method is optimized for the bulk case.\ See also the method \f3\fs18 mutationsFromHaplosomes() \f4\fs20 , which provides an alternative approach for assessing the zygosity of mutations in an individual.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.8 Class InteractionType\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.8.1 \f2\fs18 InteractionType \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier for this interaction type; for interaction type \f3\fs18 i3 \f4\fs20 , for example, this is \f3\fs18 3 \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 maxDistance <\'96> (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The maximum distance over which this interaction will be evaluated. For inter-individual distances greater than \f3\fs18 maxDistance \f5\fs20 , \f4 the interaction strength will be zero. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 reciprocal => (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The reciprocality of the interaction, as specified in \f3\fs18 initializeInteractionType() \f4\fs20 . This will be \f3\fs18 T \f4\fs20 for reciprocal interactions (those for which the interaction strength of B upon A is equal to the interaction strength of A upon B), and \f3\fs18 F \f4\fs20 otherwise.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 sexSegregation => (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The sex-segregation of the interaction, as specified in \f3\fs18 initializeInteractionType() \f4\fs20 or with \f3\fs18 setConstraints() \f4\fs20 . For non-sexual simulations, this will be \f3\fs18 "**" \f4\fs20 . For sexual simulations, this \f3\fs18 string \f4\fs20 value indicates the sex of individuals feeling the interaction, and the sex of individuals exerting the interaction; see \f3\fs18 initializeInteractionType() \f4\fs20 for details.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 spatiality => (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The spatial dimensions used by the interaction, as specified in \f3\fs18 initializeInteractionType() \f4\fs20 . This will be \f3\fs18 "" \f4\fs20 (the empty string) for non-spatial interactions, or \f3\fs18 "x" \f4\fs20 , \f3\fs18 "y" \f4\fs20 , \f3\fs18 "z" \f4\fs20 , \f3\fs18 "xy" \f4\fs20 , \f3\fs18 "xz" \f4\fs20 , \f3\fs18 "yz" \f4\fs20 , or \f3\fs18 "xyz" \f4\fs20 , for interactions using those spatial dimensions respectively. The specified dimensions are used to calculate the distances between individuals for this interaction. The value of this property is always the same as the value given to \f3\fs18 initializeInteractionType() \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to interaction types. \f5 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.8.2 \f2\fs18 InteractionType \f1\fs22 methods\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(float)clippedIntegral(No\'a0receivers)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing the integral of the interaction function as experienced by each of the individuals in \f3\fs18 receivers \f4\fs20 . For each given individual, the interaction function is clipped to the edges of the spatial bounds of the subpopulation that individual inhabits; the individual\'92s spatial position must be within bounds or an error is raised. A periodic boundary will, correctly, not clip the interaction function. The interaction function is also clipped to the interaction\'92s maximum distance; that distance must be less than half of the extent of the spatial bounds in each dimension (so that, for a given dimension, the interaction function is clipped by the spatial bounds on only one side), otherwise an error is raised. Note that receiver constraints are not applied; an individual might not actually receive any interactions because of those constraints, but it is still considered to have the same interaction function integral. If \f3\fs18 receivers \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the maximal integral is returned, as would be experienced by an individual farther than the maximum distance from any edge. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver subpopulation, and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ The computed value of the integral is not exact; it is calculated by an approximate numerical method designed to be fast, but the error should be fairly small (typically less than 1% from the true value). A large amount of computation will occur the first time this method is called (perhaps taking more than a second, depending upon hardware), but subsequent calls should be very fast. This method does not invoke \f3\fs18 interaction() \f4\fs20 callbacks; the calculated integrals are only for the interaction function itself, and so will not be accurate if \f3\fs18 interaction() \f4\fs20 callbacks modify the relationship between distance and interaction strength. For this reason, the overhead of the first call will \f1\i not \f4\i0 reoccur when individuals move or when the interaction is re-evaluated; for typical models, the initial overhead will be incurred only once. The initial overhead will reoccur, however, if the interaction function itself, or the maximum interaction distance, are changed; frequent change of those parameters may render the performance of this method unacceptable.\ The integral values returned by \f3\fs18 clippedIntegral() \f4\fs20 can be useful for computing interaction metrics that are scaled by the amount of \'93interaction field\'94 (to coin a term) that is present for a given individual, producing metrics of interaction \f1\i density \f4\i0 . Notably, the \f3\fs18 localPopulationDensity() \f4\fs20 method automatically incorporates the mechanics of \f3\fs18 clippedIntegral() \f4\fs20 into the calculations it performs; see that method\'92s documentation for further discussion of this concept. This approach can also be useful with the \f3\fs18 interactingNeighborCount() \f4\fs20 method, provided that the interaction function is of type \f3\fs18 "f" \f4\fs20 (since the neighbor count does not depend upon interaction strength).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)distance(object$\'a0receiver, [No\'a0exerters\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing distances between \f3\fs18 receiver \f4\fs20 and the individuals in \f3\fs18 exerters \f4\fs20 . If \f3\fs18 exerters \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), then a vector of the distances from \f3\fs18 receiver \f4\fs20 to all individuals in its subpopulation (including itself) is returned; this case may be handled differently internally, for greater speed, so supplying \f3\fs18 NULL \f4\fs20 is preferable to supplying the vector of all individuals in the subpopulation explicitly. Otherwise, all individuals in \f3\fs18 exerters \f4\fs20 must belong to a single subpopulation (but not necessarily the same subpopulation as \f3\fs18 receiver \f4\fs20 ). The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ Importantly, distances are calculated according to the spatiality of the \f3\fs18 InteractionType \f4\fs20 (as declared in \f3\fs18 initializeInteractionType() \f4\fs20 ), not the dimensionality of the model as a whole (as declared in \f3\fs18 initializeSLiMOptions() \f4\fs20 ). The distances returned are therefore the distances that would be used to calculate interaction strengths. However, \f3\fs18 distance() \f4\fs20 will return finite distances for all pairs of individuals, even if the individuals are non-interacting due to the maximum interaction distance or the interaction constraints; the \f3\fs18 distance() \f4\fs20 between an individual and itself will thus be \f3\fs18 0 \f4\fs20 . See \f3\fs18 interactionDistance() \f4\fs20 for an alternative distance definition.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)distanceFromPoint(float\'a0point, object\'a0exerters)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing distances between the point given by the spatial coordinates in \f3\fs18 point \f4\fs20 , which may be thought of as the \'93receiver\'94, and individuals in \f3\fs18 exerters \f4\fs20 . The \f3\fs18 point \f4\fs20 vector is interpreted as providing coordinates precisely as specified by the spatiality of the interaction type; if the interaction type\'92s spatiality is \f3\fs18 "xz" \f4\fs20 , for example, then \f3\fs18 point[0] \f4\fs20 is assumed to be an \f1\i x \f4\i0 value, and \f3\fs18 point[1] \f4\fs20 is assumed to be a \f1\i z \f4\i0 value, and \f3\fs18 point \f4\fs20 must be exactly two elements in length. Be careful; this means that in general it is not safe to pass an individual\'92s \f3\fs18 spatialPosition \f4\fs20 property for \f3\fs18 point \f4\fs20 , for example (although it is safe if the spatiality of the interaction matches the dimensionality of the simulation); other properties on \f3\fs18 Individual \f4\fs20 exist for getting the individual\'92s coordinates in a particular spatiality, such as the \f3\fs18 xz \f4\fs20 property for this example. A coordinate for a periodic spatial dimension must be within the spatial bounds for that dimension, since coordinates outside of periodic bounds are meaningless ( \f3\fs18 pointPeriodic() \f4\fs20 may be used to ensure this); coordinates for non-periodic spatial dimensions are not restricted.\ All individuals in \f3\fs18 exerters \f4\fs20 must belong to a single subpopulation; the \f3\fs18 evaluate() \f4\fs20 method must have been previously called for that subpopulation, and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ Importantly, distances are calculated according to the spatiality of the \f3\fs18 InteractionType \f4\fs20 (as declared in \f3\fs18 initializeInteractionType() \f4\fs20 ) not the dimensionality of the model as a whole (as declared in \f3\fs18 initializeSLiMOptions() \f4\fs20 ). The distances are therefore interaction distances: the distances that are used to calculate interaction strengths. However, the maximum interaction distance and interaction constraints are not used.\ This method replaces the \f3\fs18 distanceToPoint() \f4\fs20 method that existed prior to SLiM 4.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)drawByStrength(object\'a0receiver, [integer$\'a0count\'a0=\'a01], [No$\'a0exerterSubpop\'a0=\'a0NULL], [logical$\'a0returnDict\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns an \f3\fs18 object \f4\fs20 vector containing up to \f3\fs18 count \f4\fs20 individuals drawn from \f3\fs18 exerterSubpop \f4\fs20 , or if that is \f3\fs18 NULL \f4\fs20 (the default), then from the subpopulation of \f3\fs18 receiver \f4\fs20 , which must be singleton in the default mode of operation (but see below). The probability of drawing particular individuals is proportional to the strength of interaction they exert upon \f3\fs18 receiver \f4\fs20 (which is zero for \f3\fs18 receiver \f4\fs20 itself). All exerters must belong to a single subpopulation (but not necessarily the same subpopulation as \f3\fs18 receiver \f4\fs20 ). The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.\ This method may be used with either spatial or non-spatial interactions, but will be more efficient with spatial interactions that set a short maximum interaction distance. Draws are done with replacement, so the same individual may be drawn more than once; sometimes using \f3\fs18 unique() \f4\fs20 on the result of this call is therefore desirable. If more than one draw will be needed, it is much more efficient to use a single call to \f3\fs18 drawByStrength() \f4\fs20 , rather than drawing individuals one at a time. Note that if no individuals exert a non-zero interaction strength upon \f3\fs18 receiver \f4\fs20 , the vector returned will be zero-length; it is important to consider this possibility.\ Beginning in SLiM 4.1, this method has a vectorized mode of operation in which the \f3\fs18 receiver \f4\fs20 parameter may be non-singleton. To switch the method to this mode, pass \f3\fs18 T \f4\fs20 for \f3\fs18 returnDict \f4\fs20 , rather than the default of \f3\fs18 F \f4\fs20 (the operation of which is described above). In this mode, the return value is a \f3\fs18 Dictionary \f4\fs20 object instead of a vector of \f3\fs18 Individual \f4\fs20 objects. This dictionary uses \f3\fs18 integer \f4\fs20 keys that range from \f3\fs18 0 \f4\fs20 to \f3\fs18 N-1 \f4\fs20 , where \f3\fs18 N \f4\fs20 is the number of individuals passed in \f3\fs18 receiver \f4\fs20 ; these keys thus correspond directly to the indices of the individuals in \f3\fs18 receiver \f4\fs20 , and there is one entry in the dictionary for each receiver. The value in the dictionary, for a given \f3\fs18 integer \f4\fs20 key, is an \f3\fs18 object \f4\fs20 vector with the individuals drawn for the corresponding receiver, exactly as described above for the non-vectorized case. The results for each receiver can therefore be obtained from the returned dictionary with \f3\fs18 getValue() \f4\fs20 , passing the index of the receiver. The speed of this mode of operation will probably be similar to the speed of making \f3\fs18 N \f4\fs20 separate non-vectorized calls to \f3\fs18 drawByStrength() \f4\fs20 , when running single-threaded. When running multi-threaded, however, a substantial performance improvement may be realized by using the vectorized version of this method, since the queries can then be executed in parallel. In this mode of operation, all receivers must belong to the same subpopulation.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)evaluate(io\'a0subpops) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Snapshots model state in preparation for the use of the interaction, for the receiver and exerter subpopulations specified by \f3\fs18 subpops \f4\fs20 . The subpopulations may be supplied either as \f3\fs18 integer \f4\fs20 IDs, or as \f3\fs18 Subpopulation \f4\fs20 objects. This method will discard all previously cached data for the subpopulation(s), and will cache the current spatial positions of all individuals they contain (so that the spatial positions of those individuals may then change without disturbing the state of the interaction at the moment of evaluation). It will also cache which individuals in the subpopulation are eligible to act as exerters, according to the configured exerter constraints, but it will \f1\i not \f4\i0 cache such eligibility information for receiver constraints (which are applied at the time a spatial query is made). Particular interaction distances and strengths are not computed by \f3\fs18 evaluate() \f4\fs20 , and \f3\fs18 interaction() \f4\fs20 callbacks will not be called in response to this method; that work is deferred until required to satisfy a query (at which point the tick and cycle counters may have advanced, so be careful with the tick ranges used in defining \f3\fs18 interaction() \f4\fs20 callbacks).\ You must explicitly call \f3\fs18 evaluate() \f4\fs20 at an appropriate time in the tick cycle before the interaction is used, but after any relevant changes have been made to the population. SLiM will invalidate any existing interactions after any portion of the tick cycle in which new individuals have been born or existing individuals have died. In a WF model, this occurs just before \f3\fs18 late() \f4\fs20 events execute (see the WF tick cycle diagram), so \f3\fs18 late() \f4\fs20 events are often the appropriate place to put \f3\fs18 evaluate() \f4\fs20 calls, but \f3\fs18 first() \f4\fs20 or \f3\fs18 early() \f4\fs20 events can work too if the interaction is not needed until that point in the tick cycle anyway. In nonWF models, on the other hand, new offspring are produced just before \f3\fs18 early() \f4\fs20 events and then individuals die just before \f3\fs18 late() \f4\fs20 events (see the nonWF tick cycle diagram), so interactions will be invalidated twice during each tick cycle. This means that in a nonWF model, an interaction that influences reproduction should usually be evaluated in a \f3\fs18 first() \f4\fs20 event, while an interaction that influences fitness or mortality should usually be evaluated in an \f3\fs18 early() \f4\fs20 event (and an interaction that affects both may need to be evaluated at both times).\ If an interaction is never evaluated for a given subpopulation, it is guaranteed that there will be essentially no memory or computational overhead associated with the interaction for that subpopulation. Furthermore, attempting to query an interaction for a receiver or exerter in a subpopulation that has not been evaluated is guaranteed to raise an error.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(integer)interactingNeighborCount(object\'a0receivers, [No$\'a0exerterSubpop\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the number of interacting individuals for each individual in \f3\fs18 receivers \f4\fs20 , within the maximum interaction distance according to the distance metric of the \f3\fs18 InteractionType \f4\fs20 , from among the exerters in \f3\fs18 exerterSubpop \f4\fs20 (or, if that is \f3\fs18 NULL \f4\fs20 , then from among all individuals in the receiver\'92s subpopulation). More specifically, this method counts the number of individuals which can exert an interaction upon each receiver (which does not include the receiver itself). All of the receivers must belong to a single subpopulation, and all of the exerters must belong to a single subpopulation, but those two subpopulations do not need to be the same. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.\ This method is similar to \f3\fs18 nearestInteractingNeighbors() \f4\fs20 (when passed a large count so as to guarantee that all interacting individuals are returned), but this method returns only a count of the interacting individuals, not a vector containing the individuals.\ Note that this method uses interaction eligibility as a criterion; it will not count neighbors that do not exert an interaction upon a given receiver (due to the configured receiver or exerter constraints). (It also does not count a receiver as a neighbor of itself.) If a count of all neighbors is desired, rather than just interacting neighbors, use \f3\fs18 neighborCount() \f4\fs20 . If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(float)interactionDistance(object$\'a0receiver, [No\'a0exerters\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Returns a vector containing interaction-dependent distances between \f3\fs18 receiver \f4\fs20 and individuals in \f3\fs18 exerters \f4\fs20 . If \f3\fs18 exerters \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), then a vector of the interaction-dependent distances from \f3\fs18 receiver \f4\fs20 to all individuals in its subpopulation (including \f3\fs18 receiver \f4\fs20 itself) is returned; this case may be handled much more efficiently than if a vector of all individuals in the subpopulation is explicitly provided. Otherwise, all individuals in \f3\fs18 exerters \f4\fs20 must belong to a single subpopulation (but not necessarily the same subpopulation as \f3\fs18 receiver \f4\fs20 ). The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ Importantly, distances are calculated according to the spatiality of the \f3\fs18 InteractionType \f4\fs20 (as declared in \f3\fs18 initializeInteractionType() \f4\fs20 ), not the dimensionality of the model as a whole (as declared in \f3\fs18 initializeSLiMOptions() \f4\fs20 ). The distances returned are therefore the distances that would be used to calculate interaction strengths. In addition, \f3\fs18 interactionDistance() \f4\fs20 will return \f3\fs18 INF \f4\fs20 as the distance between \f3\fs18 receiver \f4\fs20 and any individual which does not exert an interaction upon \f3\fs18 receiver \f4\fs20 ; the \f3\fs18 interactionDistance() \f4\fs20 between an individual and itself will thus be \f3\fs18 INF \f4\fs20 , and likewise for pairs excluded from interacting by receiver constraints, exerter constraints, or the maximum interaction distance of the interaction type. See \f3\fs18 distance() \f4\fs20 for an alternative distance definition.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)localPopulationDensity(object\'a0receivers, [No$\'a0exerterSubpop\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of the local population density present at the location of each individual in \f3\fs18 receivers \f4\fs20 , which does not need to be a singleton; indeed, it can be a vector of all of the individuals in a given subpopulation. However, all receivers must be in the same subpopulation. The local population density is computed from exerters in \f3\fs18 exerterSubpop \f4\fs20 , or if that is \f3\fs18 NULL \f4\fs20 (the default), then from the receiver\'92s subpopulation. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.\ Population density is estimated using interaction strengths, effectively doing a kernel density estimate using the interaction function as the kernel. What is returned is computed as the total interaction strength present at a given point, divided by the integral of the interaction function around that point after clipping by the spatial bounds of the exerter subpopulation (what one might think of as the amount of \'93interaction field\'94 around the point). This provides an estimate of local population density, in units of individuals per unit area, as a weighted average over the area covered by the interaction function, where the weight of each exerter in the average is the value of the interaction function at that exerter\'92s position. This can also be thought of as a measure of the amount of interaction happening per unit of interaction field in the space surrounding the point.\ To calculate the clipped integral of the interaction function, this method uses the same numerical estimator used by the \f3\fs18 clippedIntegral() \f4\fs20 method of \f3\fs18 InteractionType \f4\fs20 , and all of the caveats described for that method apply here also; notably, all individuals must be within spatial bounds, the maximum interaction distance must be less than half the spatial extent of the subpopulation, and \f3\fs18 interaction() \f4\fs20 callbacks are not used (and so, for this method, are not allowed to be active). See the documentation for \f3\fs18 clippedIntegral() \f4\fs20 for further discussion of the details of these calculations.\ To calculate the total interaction strength around the position of a receiver, this method uses the same machinery as the \f3\fs18 totalOfNeighborStrengths() \f4\fs20 method of \f3\fs18 InteractionType \f4\fs20 , \f1\i except \f4\i0 that \'96 in contrast to other \f3\fs18 InteractionType \f4\fs20 methods \'96 the interaction strength exerted by the receiver itself is included in the total (if the exerter subpopulation is the receiver\'92s own subpopulation). This is because population density at the location of an individual includes the individual itself. If this is not desirable, the \f3\fs18 totalOfNeighborStrengths() \f4\fs20 method should probably be used.\ To see the point of this method, consider a receiver located near the edge of the spatial bounds of its subpopulation. Some portion of the interaction function that surrounds that receiver falls outside the spatial bounds of its subpopulation, and will therefore never contain an interacting exerter. If, for example, interaction strengths are used as a measure of competition, this receiver will therefore have an advantage, because it will never feel any competition from the portion of its range that falls outside spatial bounds. However, that portion of its range is presumably also not available to the receiver itself, for foraging or hunting, in which case this advantage is not biologically realistic, but is instead just an undesirable \'93edge effect\'94 artifact. Dividing by the integral of the interaction function, clipped to the spatial bounds, provides a way to compensate for this edge effect. A nice side effect of using local population densities instead of total interaction strengths is that the maximum interaction strength passed to \f3\fs18 setInteractionFunction() \f4\fs20 no longer matters; it cancels out when the total interaction strength is divided by the receiver\'92s clipped integral. However, the \f1\i shape \f4\i0 of the interaction function does still matter; it determines the relative weights used for exerters at different distances from the position of the receiver.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)nearestInteractingNeighbors(object\'a0receiver, [integer$\'a0count\'a0=\'a01], [No$\'a0exerterSubpop\'a0=\'a0NULL], [logical$\'a0returnDict\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns an \f3\fs18 object \f4\fs20 vector containing up to \f3\fs18 count \f4\fs20 interacting individuals that are spatially closest to \f3\fs18 receiver \f4\fs20 , according to the distance metric of the \f3\fs18 InteractionType \f4\fs20 , from among the exerters in \f3\fs18 exerterSubpop \f4\fs20 (or, if that is \f3\fs18 NULL \f4\fs20 , then from among all individuals in the receiver\'92s subpopulation). More specifically, this method returns only individuals which can exert an interaction upon \f3\fs18 receiver \f4\fs20 , which must be singleton in the default mode of operation (but see below). To obtain all of the interacting individuals within the maximum interaction distance of \f3\fs18 receiver \f4\fs20 , simply pass a value for \f3\fs18 count \f4\fs20 that is greater than or equal to the size of the exerter subpopulation. Note that if fewer than \f3\fs18 count \f4\fs20 interacting individuals are within the maximum interaction distance, the vector returned may be shorter than \f3\fs18 count \f4\fs20 , or even zero-length; it is important to check for this possibility even when requesting a single neighbor. If only the number of interacting individuals is needed, use \f3\fs18 interactingNeighborCount() \f4\fs20 instead. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ Note that this method uses interaction eligibility as a criterion; it will not return neighbors that cannot exert an interaction upon the receiver (due to the configured receiver or exerter constraints). (It will also never return the receiver as a neighbor of itself.) To find all neighbors of a receiver, whether they can interact with it or not, use \f3\fs18 nearestNeighbors() \f4\fs20 .\ Beginning in SLiM 4.1, this method has a vectorized mode of operation in which the \f3\fs18 receiver \f4\fs20 parameter may be non-singleton. To switch the method to this mode, pass \f3\fs18 T \f4\fs20 for \f3\fs18 returnDict \f4\fs20 , rather than the default of \f3\fs18 F \f4\fs20 (the operation of which is described above). In this mode, the return value is a \f3\fs18 Dictionary \f4\fs20 object instead of a vector of \f3\fs18 Individual \f4\fs20 objects. This dictionary uses \f3\fs18 integer \f4\fs20 keys that range from \f3\fs18 0 \f4\fs20 to \f3\fs18 N-1 \f4\fs20 , where \f3\fs18 N \f4\fs20 is the number of individuals passed in \f3\fs18 receiver \f4\fs20 ; these keys thus correspond directly to the indices of the individuals in \f3\fs18 receiver \f4\fs20 , and there is one entry in the dictionary for each receiver. The value in the dictionary, for a given \f3\fs18 integer \f4\fs20 key, is an \f3\fs18 object \f4\fs20 vector with the interacting neighbors found for the corresponding receiver, exactly as described above for the non-vectorized case. The results for each receiver can therefore be obtained from the returned dictionary with \f3\fs18 getValue() \f4\fs20 , passing the index of the receiver. The speed of this mode of operation will probably be similar to the speed of making \f3\fs18 N \f4\fs20 separate non-vectorized calls to \f3\fs18 nearestInteractingNeighbors() \f4\fs20 , when running single-threaded. When running multi-threaded, however, a substantial performance improvement may be realized by using the vectorized version of this method, since the queries can then be executed in parallel. In this mode of operation, all receivers must belong to the same subpopulation.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)nearestNeighbors(object\'a0receiver, [integer$\'a0count\'a0=\'a01], [No$\'a0exerterSubpop\'a0=\'a0NULL], [logical$\'a0returnDict\'a0=\'a0F])\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns an \f3\fs18 object \f4\fs20 vector containing up to \f3\fs18 count \f4\fs20 individuals that are spatially closest to \f3\fs18 receiver \f4\fs20 , according to the distance metric of the \f3\fs18 InteractionType \f4\fs20 , from among the exerters in \f3\fs18 exerterSubpop \f4\fs20 (or, if that is \f3\fs18 NULL \f4\fs20 , then from among all individuals in the receiver\'92s subpopulation). In the default mode of operation, \f3\fs18 receiver \f4\fs20 must be singleton (but see below). To obtain all of the individuals within the maximum interaction distance of \f3\fs18 receiver \f4\fs20 , simply pass a value for \f3\fs18 count \f4\fs20 that is greater than or equal to the size of \f3\fs18 individual \f4\fs20 \'92s subpopulation. Note that if fewer than \f3\fs18 count \f4\fs20 individuals are within the maximum interaction distance, the vector returned may be shorter than \f3\fs18 count \f4\fs20 , or even zero-length; it is important to check for this possibility even when requesting a single neighbor. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 Note that this method does not use interaction eligibility as a criterion; it will return neighbors that could not interact with the receiver due to the configured receiver or exerter constraints. (It will never return the receiver as a neighbor of itself, however.) To find only neighbors that are eligible to exert an interaction upon the receiver, use \f3\fs18 nearestInteractingNeighbors() \f4\fs20 .\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 Beginning in SLiM 4.1, this method has a vectorized mode of operation in which the \f3\fs18 receiver \f4\fs20 parameter may be non-singleton. To switch the method to this mode, pass \f3\fs18 T \f4\fs20 for \f3\fs18 returnDict \f4\fs20 , rather than the default of \f3\fs18 F \f4\fs20 (the operation of which is described above). In this mode, the return value is a \f3\fs18 Dictionary \f4\fs20 object instead of a vector of \f3\fs18 Individual \f4\fs20 objects. This dictionary uses \f3\fs18 integer \f4\fs20 keys that range from \f3\fs18 0 \f4\fs20 to \f3\fs18 N-1 \f4\fs20 , where \f3\fs18 N \f4\fs20 is the number of individuals passed in \f3\fs18 receiver \f4\fs20 ; these keys thus correspond directly to the indices of the individuals in \f3\fs18 receiver \f4\fs20 , and there is one entry in the dictionary for each receiver. The value in the dictionary, for a given \f3\fs18 integer \f4\fs20 key, is an \f3\fs18 object \f4\fs20 vector with the neighbors found for the corresponding receiver, exactly as described above for the non-vectorized case. The results for each receiver can therefore be obtained from the returned dictionary with \f3\fs18 getValue() \f4\fs20 , passing the index of the receiver. The speed of this mode of operation will probably be similar to the speed of making \f3\fs18 N \f4\fs20 separate non-vectorized calls to \f3\fs18 nearestNeighbors() \f4\fs20 , when running single-threaded. When running multi-threaded, however, a substantial performance improvement may be realized by using the vectorized version of this method, since the queries can then be executed in parallel. In this mode of operation, all receivers must belong to the same subpopulation.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)nearestNeighborsOfPoint(float\'a0point, io$\'a0exerterSubpop, [integer$\'a0count\'a0=\'a01])\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns up to \f3\fs18 count \f4\fs20 individuals in \f3\fs18 exerterSubpop \f4\fs20 that are spatially closest to the point given by the spatial coordinates in \f3\fs18 point \f4\fs20 , which may be thought of as the \'93receiver\'94, according to the distance metric of the \f3\fs18 InteractionType \f4\fs20 . The \f3\fs18 point \f4\fs20 vector is interpreted as providing coordinates precisely as specified by the spatiality of the interaction type; if the interaction type\'92s spatiality is \f3\fs18 "xz" \f4\fs20 , for example, then \f3\fs18 point[0] \f4\fs20 is assumed to be an \f1\i x \f4\i0 value, and \f3\fs18 point[1] \f4\fs20 is assumed to be a \f1\i z \f4\i0 value, and \f3\fs18 point \f4\fs20 must be exactly two elements in length. Be careful; this means that in general it is not safe to pass an individual\'92s \f3\fs18 spatialPosition \f4\fs20 property for \f3\fs18 point \f4\fs20 , for example (although it is safe if the spatiality of the interaction matches the dimensionality of the simulation); other properties on \f3\fs18 Individual \f4\fs20 exist for getting the individual\'92s coordinates in a particular spatiality, such as the \f3\fs18 xz \f4\fs20 property for this example. A coordinate for a periodic spatial dimension must be within the spatial bounds for that dimension, since coordinates outside of periodic bounds are meaningless ( \f3\fs18 pointPeriodic() \f4\fs20 may be used to ensure this); coordinates for non-periodic spatial dimensions are not restricted.\ The subpopulation may be supplied either as an \f3\fs18 integer \f4\fs20 ID, or as a \f3\fs18 Subpopulation \f4\fs20 object. To obtain all of the individuals within the maximum interaction distance of \f3\fs18 point \f4\fs20 , simply pass a value for \f3\fs18 count \f4\fs20 that is greater than or equal to the size of \f3\fs18 exerterSubpop \f4\fs20 . Note that if fewer than \f3\fs18 count \f4\fs20 individuals are within the maximum interaction distance, the vector returned may be shorter than \f3\fs18 count \f4\fs20 , or even zero-length; it is important to check for this possibility even when requesting a single neighbor. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for \f3\fs18 exerterSubpop \f4\fs20 , and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(integer)neighborCount(object\'a0receivers, [No$\'a0exerterSubpop\'a0=\'a0NULL])\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the number of neighbors for each individual in \f3\fs18 receivers \f4\fs20 , within the maximum interaction distance according to the distance metric of the \f3\fs18 InteractionType \f4\fs20 , from among the individuals in \f3\fs18 exerterSubpop \f4\fs20 (or, if that is \f3\fs18 NULL \f4\fs20 , then from among all individuals in the receiver\'92s subpopulation). All of the receivers must belong to a single subpopulation, and all of the exerters must belong to a single subpopulation, but those two subpopulations do not need to be the same. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 This method is similar to \f3\fs18 nearestNeighbors() \f4\fs20 (when passed a large count so as to guarantee that all neighbors are returned), but this method returns only a count of the individuals, not a vector containing the individuals.\ Note that this method does not use interaction eligibility as a criterion; it will count neighbors that cannot exert an interaction upon a receiver (due to the configured receiver or exerter constraints). (It still does not count a receiver as a neighbor of itself, however.) If a count of only interacting neighbors is desired, use \f3\fs18 interactingNeighborCount() \f4\fs20 . If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(integer$)neighborCountOfPoint(float\'a0point, io$\'a0exerterSubpop)\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the number of individuals in \f3\fs18 exerterSubpop \f4\fs20 that are within the maximum interaction distance of the point given by the spatial coordinates in \f3\fs18 point \f4\fs20 , which may be thought of as the \'93receiver\'94, according to the distance metric of the \f3\fs18 InteractionType \f4\fs20 . The \f3\fs18 point \f4\fs20 vector is interpreted as providing coordinates precisely as specified by the spatiality of the interaction type; if the interaction type\'92s spatiality is \f3\fs18 "xz" \f4\fs20 , for example, then \f3\fs18 point[0] \f4\fs20 is assumed to be an \f1\i x \f4\i0 value, and \f3\fs18 point[1] \f4\fs20 is assumed to be a \f1\i z \f4\i0 value, and \f3\fs18 point \f4\fs20 must be exactly two elements in length. Be careful; this means that in general it is not safe to pass an individual\'92s \f3\fs18 spatialPosition \f4\fs20 property for \f3\fs18 point \f4\fs20 , for example (although it is safe if the spatiality of the interaction matches the dimensionality of the simulation); other properties on \f3\fs18 Individual \f4\fs20 exist for getting the individual\'92s coordinates in a particular spatiality, such as the \f3\fs18 xz \f4\fs20 property for this example. A coordinate for a periodic spatial dimension must be within the spatial bounds for that dimension, since coordinates outside of periodic bounds are meaningless ( \f3\fs18 pointPeriodic() \f4\fs20 may be used to ensure this); coordinates for non-periodic spatial dimensions are not restricted.\ The subpopulation may be supplied either as an \f3\fs18 integer \f4\fs20 ID, or as a \f3\fs18 Subpopulation \f4\fs20 object. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for \f3\fs18 exerterSubpop \f4\fs20 , and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ This method is similar to \f3\fs18 nearestNeighborsOfPoint() \f4\fs20 (when passed a large count so as to guarantee that all neighbors are returned), but this method returns only a count of the individuals, not a vector containing the individuals.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setConstraints(string$\'a0who, [Ns$\'a0sex\'a0=\'a0NULL], [Ni$\'a0tag\'a0=\'a0NULL], [Ni$\'a0minAge\'a0=\'a0NULL], [Ni$\'a0maxAge\'a0=\'a0NULL], [Nl$\'a0migrant\'a0=\'a0NULL], [Nl$\'a0tagL0\'a0=\'a0NULL], [Nl$\'a0tagL1\'a0=\'a0NULL], [Nl$\'a0tagL2\'a0=\'a0NULL], [Nl$\'a0tagL3\'a0=\'a0NULL], [Nl$\'a0tagL4\'a0=\'a0NULL])\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Sets constraints upon which individuals can be receivers and/or exerters, making the target \f3\fs18 InteractionType \f4\fs20 measure interactions between only subsets of the population. The parameter \f3\fs18 who \f4\fs20 specifies upon whom the specified constraints apply; it may be \f3\fs18 "exerter" \f4\fs20 to set constraints upon exerters, \f3\fs18 "receiver" \f4\fs20 to set constraints upon receivers, or \f3\fs18 "both" \f4\fs20 to set constraints upon both. If \f3\fs18 "both" \f4\fs20 is used, the \f1\i same \f4\i0 constraints are set for both exerters and receivers; \f1\i different \f4\i0 constraints can be set for exerters versus receivers by making a separate call to \f3\fs18 setConstraints() \f4\fs20 for each. Constraints only affect queries that involve the concept of interaction; for example, they will affect the result of \f3\fs18 nearestInteractingNeighbors() \f4\fs20 , but not the result of \f3\fs18 nearestNeighbors() \f4\fs20 . The constraints specified by a given call to \f3\fs18 setConstraints() \f4\fs20 override all previously set constraints for the category specified (receivers, exerters, or both).\ There is a general policy for the remaining arguments: they are \f3\fs18 NULL \f4\fs20 by default, and if \f3\fs18 NULL \f4\fs20 is used, it specifies \'93no constraint\'94 for that property (removing any currently existing constraint for that property). The \f3\fs18 sex \f4\fs20 parameter constrains the sex of individuals; it may be \f3\fs18 "M" \f4\fs20 or \f3\fs18 \'93F" \f4\fs20 (or \f3\fs18 "*" \f4\fs20 as another way of specifying no constraint, for historical reasons). If \f3\fs18 sex \f4\fs20 is \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 , the individuals to which the constraint is applied (potential receivers/exerters) must belong to a sexual species. The \f3\fs18 tag \f4\fs20 parameter constrains the \f3\fs18 tag \f4\fs20 property of individuals; if this set, the individuals to which the constraint is applied must have defined \f3\fs18 tag \f4\fs20 values. The \f3\fs18 minAge \f4\fs20 and \f3\fs18 maxAge \f4\fs20 properties constrain the \f3\fs18 age \f4\fs20 property of individuals to the given minimum and/or maximum values; these constraints can only be used in nonWF models. The \f3\fs18 migrant \f4\fs20 property constraints the \f3\fs18 migrant \f4\fs20 property of individuals ( \f3\fs18 T \f4\fs20 constrains to only migrants, \f3\fs18 F \f4\fs20 to only non-migrants). Finally, the \f3\fs18 tagL0 \f4\fs20 , \f3\fs18 tagL1 \f4\fs20 , \f3\fs18 tagL2 \f4\fs20 , \f3\fs18 tagL3 \f4\fs20 , and \f3\fs18 tagL4 \f4\fs20 properties constrain the corresponding \f3\fs18 logical \f4\fs20 properties of individuals, requiring them to be either \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 as specified; the individuals to which these constraints are applied must have defined values for the constrained property or properties. Again, \f3\fs18 NULL \f4\fs20 should be supplied (as it is by default) for any property which you do not wish to constrain.\ These constraints may be used in any combination, as desired. For example, calling \f3\fs18 setConstraints("receivers", sex="M", minAge=5, tagL0=T) \f4\fs20 constrains the interaction type\'92s operation so that receivers must be males, with an \f3\fs18 age \f4\fs20 of at least \f3\fs18 5 \f4\fs20 , with a \f3\fs18 tagL0 \f4\fs20 property value of \f3\fs18 T \f4\fs20 . For that configuration the potential receivers used with the interaction type must be sexual (since \f3\fs18 sex \f4\fs20 is specified), must be in a nonWF model (since \f3\fs18 minAge \f4\fs20 is specified), and must have a defined value for their \f3\fs18 tagL0 \f4\fs20 property (since that property is constrained). Note that the \f3\fs18 sexSegregation \f4\fs20 parameter to \f3\fs18 initializeInteractionType() \f4\fs20 is a shortcut which does the same thing as the corresponding calls to \f3\fs18 setConstraints() \f4\fs20 .\ Exerter constraints are applied at \f3\fs18 evaluate() \f4\fs20 time, whereas receiver constraints are applied at query time; see the \f3\fs18 InteractionType \f4\fs20 class documentation for further discussion. The interaction constraints for an interaction type are normally a constant in simulations; in any case, they cannot be changed when an interaction has already been evaluated, so either they should be set prior to evaluation, or \f3\fs18 unevaluate() \f4\fs20 should be called first.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)setInteractionFunction(string$\'a0functionType, ...) \f5 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Set the function used to translate spatial distances into interaction strengths for an interaction type. The \f3\fs18 functionType \f4\fs20 may be \f3\fs18 "f" \f4\fs20 , in which case the ellipsis \f3\fs18 ... \f4\fs20 should supply a \f3\fs18 numeric$ \f4\fs20 fixed interaction strength; \f3\fs18 "l" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 maximum strength for a linear function; \f3\fs18 "e" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 maximum strength and a \f3\fs18 numeric$ \f4\fs20 lambda (rate) parameter for a negative exponential function; \f3\fs18 "n" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 maximum strength and a \f3\fs18 numeric$ \f4\fs20 sigma (standard deviation) parameter for a Gaussian function; \f3\fs18 "c" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 maximum strength and a \f3\fs18 numeric$ \f4\fs20 scale parameter for a Cauchy distribution function; or \f3\fs18 "t" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 maximum strength, a \f3\fs18 numeric$ \f4\fs20 degrees of freedom, and a \f3\fs18 numeric$ \f4\fs20 scale parameter for a \f1\i t \f4\i0 -distribution function. See the InteractionType class documentation for discussions of these interaction functions. Non-spatial interactions must use function type \f3\fs18 "f" \f4\fs20 , since no distance values are available in that case.\ The interaction function for an interaction type is normally a constant in simulations; in any case, it cannot be changed when an interaction has already been evaluated, so either it should be set prior to evaluation, or \f3\fs18 unevaluate() \f4\fs20 should be called first. \f5 \cf0 \ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)strength(object$\'a0receiver, [No\'a0exerters\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing the interaction strengths exerted upon \f3\fs18 receiver \f4\fs20 by the individuals in \f3\fs18 exerters \f4\fs20 . If \f3\fs18 exerters \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), then a vector of the interaction strengths exerted by all individuals in the subpopulation of \f3\fs18 receiver \f4\fs20 (including \f3\fs18 receiver \f4\fs20 itself, with a strength of \f3\fs18 0.0 \f4\fs20 ) is returned; this case may be handled much more efficiently than if a vector of all individuals in the subpopulation is explicitly provided. Otherwise, all individuals in \f3\fs18 exerters \f4\fs20 must belong to a single subpopulation (but not necessarily the same subpopulation as \f3\fs18 receiver \f4\fs20 ). The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 If the strengths of interactions exerted by a single individual upon multiple individuals are needed instead (the inverse of what this method provides), multiple calls to this method will be necessary, one per pairwise interaction queried; the interaction engine is not optimized for the inverse case, and so it will likely be quite slow to compute. If the interaction is reciprocal and has the same receiver and exerter constraints, the opposite query should provide identical results in a single efficient call (because then the interactions exerted are equal to the interactions received); otherwise, the best approach might be to define a second interaction type representing the inverse interaction that you wish to be able to query efficiently.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(lo)testConstraints(object\'a0individuals, string$\'a0constraints, [logical$\'a0returnIndividuals\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Tests the individuals in the parameter \f3\fs18 individuals \f4\fs20 against the interaction constraints specified by \f3\fs18 constraints \f4\fs20 . The value of \f3\fs18 constraints \f4\fs20 may be \f3\fs18 "receiver" \f4\fs20 to use the receiver constraints, or \f3\fs18 "exerter" \f4\fs20 to use the exerter constraints. If \f3\fs18 returnIndividuals \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), a \f3\fs18 logical \f4\fs20 vector will be returned, with \f3\fs18 T \f4\fs20 values indicating that the corresponding individual satisfied the constraints, \f3\fs18 F \f4\fs20 values indicating that it did not. If \f3\fs18 returnIndividuals \f4\fs20 is \f3\fs18 T \f4\fs20 , an \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Individual \f4\fs20 will be returned containing only those elements of \f3\fs18 individuals \f4\fs20 that satisfied the constraints (in the same order as \f3\fs18 individuals \f4\fs20 ). Note that unlike most queries, the \f3\fs18 InteractionType \f4\fs20 does not need to have been evaluated before calling this method, and the individuals passed in need not belong to a single population or even a single species.\ This method can be useful for narrowing a vector of individuals down to just those that satisfy constraints. Outside the context of \f3\fs18 InteractionType \f4\fs20 , similar functionality is provided by the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 subsetIndividuals() \f4\fs20 . Note that the use of \f3\fs18 testConstraints() \f4\fs20 is somewhat rare; usually, queries are evaluated across a vector of individuals, each of which might or might not satisfy the defined constraints. Individuals that do not satisfy constraints do not participate in interactions, so their interaction strength with other individuals will simply be zero.\ See the \f3\fs18 setConstraints() \f4\fs20 method to set up constraints, as well as the \f3\fs18 sexSegregation \f4\fs20 parameter to \f3\fs18 initializeInteractionType() \f4\fs20 . Note that if the constraints tested involve \f3\fs18 tag \f4\fs20 values (including \f3\fs18 tagL0 \f4\fs20 / \f3\fs18 tagL1 \f4\fs20 / \f3\fs18 tagL2 \f4\fs20 / \f3\fs18 tagL3 \f4\fs20 / \f3\fs18 tagL4 \f4\fs20 ), the corresponding property or properties of the tested individuals must be defined (i.e., must have been set to a value), or an error will result because the constraints cannot be applied.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)totalOfNeighborStrengths(object\'a0receivers, [No$\'a0exerterSubpop\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of the total interaction strength felt by each individual in \f3\fs18 receivers \f4\fs20 by the exerters in \f3\fs18 exerterSubpop \f4\fs20 (or, if that is \f3\fs18 NULL \f4\fs20 , then by all individuals in the receiver\'92s subpopulation). The \f3\fs18 receivers \f4\fs20 parameter does not need to be a singleton; indeed, it can be a vector of all of the individuals in a given subpopulation. All of the receivers must belong to a single subpopulation, and all of the exerters must belong to a single subpopulation, but those two subpopulations do not need to be the same. The \f3\fs18 evaluate() \f4\fs20 method must have been previously called for the receiver and exerter subpopulations, and positions saved at evaluation time will be used. If the \f3\fs18 InteractionType \f4\fs20 is non-spatial, this method may not be called.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 For one individual, this is essentially the same as calling \f3\fs18 nearestInteractingNeighbors() \f4\fs20 with a large \f3\fs18 count \f4\fs20 so as to obtain the complete vector of all interacting neighbors, calling \f3\fs18 strength() \f4\fs20 for each of those interactions to get each interaction strength, and adding those interaction strengths together with \f3\fs18 sum() \f4\fs20 . This method is much faster than that implementation, however, since all of that work is done as a single operation. Also, \f3\fs18 totalOfNeighborStrengths() \f4\fs20 can total up interactions for more than one receiver in a single vectorized call.\ Similarly, for one individual this is essentially the same as calling \f3\fs18 strength() \f4\fs20 to get the interaction strengths between a receiver and all individuals in the exerter subpopulation, and then calling \f3\fs18 sum() \f4\fs20 . Again, this method should be much faster, since this algorithm looks only at neighbors, whereas calling \f3\fs18 strength() \f4\fs20 directly assesses interaction strengths with all other individuals. This will make a particularly large difference when the subpopulation size is large and the maximum distance of the \f3\fs18 InteractionType \f4\fs20 is small.\ See \f3\fs18 localPopulationDensity() \f4\fs20 for a related method that calculates the total interaction strength divided by the amount of \'93interaction field\'94 present for an individual (i.e., the integral of the interaction function clipped to the spatial bounds of the subpopulation) to provide an estimate of the \'93interaction density\'94 felt by an individual.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)unevaluate(void) \f5 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Discards all evaluation of this interaction, for all subpopulations. The state of the \f3\fs18 InteractionType \f4\fs20 is reset to a state prior to evaluation. This can be useful if the model state has changed in such a way that the evaluation already conducted is no longer valid. For example, if the maximum distance, the interaction function, or the receiver or exerter constraints of the \f3\fs18 InteractionType \f4\fs20 need to be changed with immediate effect, or if the data used by an \f3\fs18 interaction() \f4\fs20 callback has changed in such a way that previously calculated interaction strengths are no longer correct, \f3\fs18 unevaluate() \f4\fs20 allows the interaction to begin again from scratch.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In WF models, all interactions are automatically reset to an unevaluated state at the moment when the new offspring generation becomes the parental generation (at step 4 in the tick cycle).\ In nonWF models, all interactions are automatically reset to an unevaluated state twice per tick: immediately after \f3\fs18 reproduction() \f4\fs20 callbacks have completed (after step 1 in the tick cycle), and immediately before viability/survival selection (before step 4 in the tick cycle).\ Given this automatic invalidation, most simulations have no reason to call \f3\fs18 unevaluate() \f4\fs20 .\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf2 \kerning1\expnd0\expndtw0 5.9 Class LogFile\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf2 5.9.1 \f2\fs18 LogFile \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 filePath => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The path of the log file being written to. This may be changed with \f3\fs18 setFilePath() \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 logInterval => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The interval for automatic logging; a new row of data will be logged every \f3\fs18 logInterval \f4\fs20 ticks. This may be set with the \f3\fs18 logInterval \f4\fs20 parameter to \f3\fs18 createLogFile() \f4\fs20 and changed with \f3\fs18 setLogInterval() \f4\fs20 . If automatic logging has been disabled, this property will be \f3\fs18 0 \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 precision <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The precision of \f3\fs18 float \f4\fs20 output. To be exact, \f3\fs18 precision \f4\fs20 specifies the preferred number of significant digits that will be output for \f3\fs18 float \f4\fs20 values. The default is \f3\fs18 6 \f4\fs20 ; values in [ \f3\fs18 1 \f4\fs20 , \f3\fs18 22 \f4\fs20 ] are legal, but \f3\fs18 17 \f4\fs20 is probably the largest value that makes sense given the limits of double-precision floating point.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.9.2 \f2\fs18 LogFile \f1\fs22 methods\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(void)addCustomColumn(string$\'a0columnName, string$\'a0source, [*\'a0context\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column with its name provided by \f3\fs18 columnName \f4\fs20 . The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . The value for the column, when a given row is generated, will be produced by the code supplied in \f3\fs18 source \f4\fs20 , which is expected to return either \f3\fs18 NULL \f4\fs20 (which will write out \f3\fs18 NA \f4\fs20 ), or a singleton value of any non-object type.\ The \f3\fs18 context \f4\fs20 parameter will be set up as a pseudo-parameter, named \f3\fs18 context \f4\fs20 , when \f3\fs18 source \f4\fs20 is called, allowing the same source code to be used to generate values for multiple data columns; you might, for example, pass the \f3\fs18 id \f4\fs20 of the particular \f3\fs18 Subpopulation \f4\fs20 object that you wish \f3\fs18 source \f4\fs20 to use for its calculations, and \f3\fs18 source \f4\fs20 could then use the \f3\fs18 Community \f4\fs20 method \f3\fs18 subpopulationsWithIDs() \f4\fs20 to look up the subpopulation from the \f3\fs18 id \f4\fs20 value provided in \f3\fs18 context \f4\fs20 .\ Note that the \f3\fs18 Subpopulation \f4\fs20 object itself cannot be passed in \f3\fs18 context \f4\fs20 , because class \f3\fs18 Subpopulation \f4\fs20 is not under retain-release memory management in SLiM, meaning essentially that subpopulation objects can cease to exist unpredictably (because they go extinct, for example). A reference to such an object cannot be kept long-term by your script (including by \f3\fs18 LogFile \f4\fs20 ), because if the object ceases to exist, the reference would become invalid and a crash would result. Passing such an object \'96 one not under retain-release \'96 to \f3\fs18 addCustomColumn() \f4\fs20 will therefore raise an error, to safeguard against that possible crash. The workaround for this limitation is to find a way to look up the desired object, such as the suggestion above of using the subpopulation\'92s \f3\fs18 id \f4\fs20 to look it up.\ The use of \f3\fs18 context \f4\fs20 is optional; if the default value of \f3\fs18 NULL \f4\fs20 is used, then \f3\fs18 context \f4\fs20 will be \f3\fs18 NULL \f4\fs20 when \f3\fs18 source \f4\fs20 is called.\ See \f3\fs18 addMeanSDColumns() \f4\fs20 for a useful variant.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addCycle([No$\'a0species\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column that provides the cycle counter for \f3\fs18 species \f4\fs20 (the same as the value of the \f3\fs18 cycle \f4\fs20 property of that species). The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . In single-species models, \f3\fs18 species \f4\fs20 may be \f3\fs18 NULL \f4\fs20 to indicate that single species. The column will simply be named \f3\fs18 cycle \f4\fs20 in single-species models; an underscore and the name of the species will be appended in multispecies models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addCycleStage(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column that provides the cycle stage, named \f3\fs18 cycle_stage \f4\fs20 . The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . The stage is provided as a \f3\fs18 string \f4\fs20 , and will typically be \f3\fs18 "first" \f4\fs20 , \f3\fs18 "early" \f4\fs20 , \f3\fs18 "late" \f4\fs20 , or \f3\fs18 "end" \f4\fs20 (the latter used for the point in time at which end-of-tick automatic logging occurs). Other possible values are discussed in the documentation for the \f3\fs18 cycleStage \f4\fs20 property of \f3\fs18 Community \f4\fs20 , which this column reflects.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addKeysAndValuesFrom(object$\'a0source)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This \f3\fs18 Dictionary \f4\fs20 method has an override in \f3\fs18 LogFile \f4\fs20 to make it illegal to call, since \f3\fs18 LogFile \f4\fs20 manages its \f3\fs18 Dictionary \f4\fs20 entries.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addMeanSDColumns(string$\'a0columnName, string$\'a0source, [*\'a0context\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds two new data columns with names of \f3\fs18 columnName_mean \f4\fs20 and \f3\fs18 columnName_sd \f4\fs20 . The new columns will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . When a given row is generated, the code supplied in \f3\fs18 source \f4\fs20 is expected to return either a zero-length vector of any type including \f3\fs18 NULL \f4\fs20 (which will write out \f3\fs18 NA \f4\fs20 to both columns), or a non-zero-length vector of \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 values. In the latter case, the result vector will be summarized in the two columns by its mean and standard deviation respectively. If the result vector has exactly one value, the standard deviation will be written as \f3\fs18 NA \f4\fs20 .\ The \f3\fs18 context \f4\fs20 parameter is set up as a pseudo-parameter when \f3\fs18 source \f4\fs20 is called, as described in \f3\fs18 addCustomColumn() \f4\fs20 . See the documentation for that method for further discussion of \f3\fs18 context \f4\fs20 , including limitations on its use.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addPopulationSexRatio([No$\'a0species\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column that provides the population sex ratio M:(M+F) for \f3\fs18 species \f4\fs20 . The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . In single-species models, \f3\fs18 species \f4\fs20 may be \f3\fs18 NULL \f4\fs20 to indicate that single species. The column will simply be named \f3\fs18 sex_ratio \f4\fs20 in single-species models; an underscore and the name of the species will be appended in multispecies models. If the species is hermaphroditic, \f3\fs18 NA \f4\fs20 will be written.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addPopulationSize([No$\'a0species\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column that provides the total population size for \f3\fs18 species \f4\fs20 . The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . In single-species models, \f3\fs18 species \f4\fs20 may be \f3\fs18 NULL \f4\fs20 to indicate that single species. The column will simply be named \f3\fs18 num_individuals \f4\fs20 in single-species models; an underscore and the name of the species will be appended in multispecies models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addSubpopulationSexRatio(io$\'a0subpop)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column that provides the sex ratio M:(M+F) of the subpopulation \f3\fs18 subpop \f4\fs20 , named \f3\fs18 pX_sex_ratio \f4\fs20 . The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . If the subpopulation exists but has a size of zero, \f3\fs18 NA \f4\fs20 will be written.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addSubpopulationSize(io$\'a0subpop)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column that provides the size of the subpopulation \f3\fs18 subpop \f4\fs20 , named \f3\fs18 pX_num_individuals \f4\fs20 . The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . If the subpopulation exists but has a size of zero, \f3\fs18 0 \f4\fs20 will be written.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addSuppliedColumn(string$\'a0columnName)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column with its name provided by \f3\fs18 columnName \f4\fs20 . The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . The value for the column is initially undefined, and will be written as \f3\fs18 NA \f4\fs20 . A different value may (optionally) be provided by calling \f3\fs18 setSuppliedValue() \f4\fs20 with a value for \f3\fs18 columnName \f4\fs20 . That value will be used for the column the next time a row is generated (whether automatically or by a call to \f3\fs18 logRow() \f4\fs20 ), and the column\'92s value will subsequently be undefined again. In other words, for any given logged row the default of \f3\fs18 NA \f4\fs20 may be kept, or a different value may be supplied. This allows the value for the column to be set at any point during the tick cycle, which can be convenient if the column\'92s value depends upon transient state that is no longer available at the time the row is logged.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addTick(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a new data column, named \f3\fs18 tick \f4\fs20 , that provides the tick number for the simulation. The new column will be logged each time that a row is generated, either by automatic logging or by a call to \f3\fs18 logRow() \f4\fs20 . \f3\fs18 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \cf2 \'96\'a0(void)clearKeysAndValues(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This \f3\fs18 Dictionary \f4\fs20 method has an override in \f3\fs18 LogFile \f4\fs20 to make it illegal to call, since \f3\fs18 LogFile \f4\fs20 manages its \f3\fs18 Dictionary \f4\fs20 entries.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)flush(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Flushes all buffered data to the output file, synchronously. This will make the contents of the file on disk be up-to-date with the running simulation. Flushing frequently may entail a small performance penalty. More importantly, if \f3\fs18 .gz \f4\fs20 compression has been requested with \f3\fs18 compress=T \f4\fs20 the size of the resulting file will be larger \'96 potentially much larger \'96 if \f3\fs18 flush() \f4\fs20 is called frequently. Note that automatic periodic flushing can be requested with the \f3\fs18 flushInterval \f4\fs20 parameter to \f3\fs18 createLogFile() \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)logRow(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This logs a new row of data, by evaluating all of the generators added to the \f3\fs18 LogFile \f4\fs20 with \f3\fs18 add...() \f4\fs20 calls. Note that the new row may be buffered, and thus may not be written out to disk immediately; see \f3\fs18 flush() \f4\fs20 . This method may be used instead of, or in conjunction with, automatic logging.\ You can get the \f3\fs18 LogFile \f4\fs20 instance, in order to call \f3\fs18 logRow() \f4\fs20 on it, from \f3\fs18 community.logFiles \f4\fs20 , or you can remember it in a global constant with \f3\fs18 defineConstant() \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setLogInterval([Ni$\'a0logInterval\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Sets the automatic logging interval. A \f3\fs18 logInterval \f4\fs20 of \f3\fs18 NULL \f4\fs20 stops automatic logging immediately. Other values request that a new row should be logged (as if \f3\fs18 logRow() \f4\fs20 were called) at the end of every \f3\fs18 logInterval \f4\fs20 ticks (just before the tick count increment, in both WF and nonWF models), starting at the end of the tick in which \f3\fs18 setLogInterval() \f4\fs20 was called.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setFilePath(string$\'a0filePath, [Ns\'a0initialContents\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F], [Nl$\'a0compress\'a0=\'a0NULL], [Ns$\'a0sep\'a0=\'a0NULL], [Nl$\'a0header\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Redirects the \f3\fs18 LogFile \f4\fs20 to write new rows to a new \f3\fs18 filePath \f4\fs20 . Any rows that have been buffered but not flushed will be written to the previous file first, as if \f3\fs18 flush() \f4\fs20 had been called. With this call, new \f3\fs18 initialContents \f4\fs20 may be supplied, which will either replace any existing file or will be appended to it, depending upon the value of \f3\fs18 append \f4\fs20 . New values may be supplied for \f3\fs18 compress \f4\fs20 , \f3\fs18 sep \f4\fs20 , and \f3\fs18 header \f4\fs20 ; the meaning of these parameters is identical to their meaning in \f3\fs18 createLogFile() \f4\fs20 , except that a value of \f3\fs18 NULL \f4\fs20 for these means \'93do not change this setting from its previous value\'94. In effect, then, this method lets you start a completely new log file at a new path, without having to create and configure a new \f3\fs18 LogFile \f4\fs20 object. The new file will be created (or appended) synchronously, with the specified initial contents.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setSuppliedValue(string$\'a0columnName, +$\'a0value)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Registers a value, passed in \f3\fs18 value \f4\fs20 , to be used for the supplied column named \f3\fs18 columnName \f4\fs20 when a row is next logged. This column must have been added with \f3\fs18 addSuppliedColumn() \f4\fs20 . A value of \f3\fs18 NULL \f4\fs20 may be passed to log \f3\fs18 NA \f4\fs20 , but logging \f3\fs18 NA \f4\fs20 is the default behavior for supplied columns in any case. Otherwise, the value must be a singleton, and its type should match the values previously supplied for the column (otherwise the log file may be difficult to parse, since the values within the column will not be of one consistent type). See \f3\fs18 addSuppliedColumn() \f4\fs20 for further details.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setValue(is$\'a0key, *\'a0value)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This \f3\fs18 Dictionary \f4\fs20 method has an override in \f3\fs18 LogFile \f4\fs20 to make it illegal to call, since \f3\fs18 LogFile \f4\fs20 manages its \f3\fs18 Dictionary \f4\fs20 entries.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(logical$)willAutolog(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns \f3\fs18 T \f4\fs20 if the log file is configured to log a new row automatically at the end of the current tick; otherwise, returns \f3\fs18 F \f4\fs20 . This is useful for calculating a value that will be logged only in ticks when the value is needed.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.10 Class Mutation\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.10.1 \f2\fs18 Mutation \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 chromosome => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 Chromosome \f4\fs20 object with which the mutation is associated.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier for this mutation. Each mutation created during a run receives an immutable identifier that will be unique across the duration of the run. These identifiers are not re-used during a run, except that if a population file is loaded from disk, the loaded mutations will receive their original identifier values as saved in the population file. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 isFixed => (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 T \f4\fs20 if the mutation has fixed (in the SLiM sense of having been converted to a \f3\fs18 Substitution \f4\fs20 object), \f3\fs18 F \f4\fs20 otherwise. Since fixed/substituted mutations are removed from the simulation, you will only see this flag be \f3\fs18 T \f4\fs20 if you have held onto a mutation beyond its usual lifetime.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 isSegregating => (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 T \f4\fs20 if the mutation is segregating (in the SLiM sense of not having been either lost or converted to a \f3\fs18 Substitution \f4\fs20 object), \f3\fs18 F \f4\fs20 otherwise. Since both lost and fixed/substituted mutations are removed from the simulation, you will only see this flag be \f3\fs18 F \f4\fs20 if you have held onto a mutation beyond its usual lifetime. Note that if \f3\fs18 isSegregating \f4\fs20 is \f3\fs18 F \f4\fs20 , \f3\fs18 isFixed \f4\fs20 will let you determine whether the mutation is no longer segregating because it was lost, or because it fixed.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 mutationType => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 MutationType \f4\fs20 from which this mutation was drawn. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 nucleotide <\'96> (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A \f3\fs18 string \f4\fs20 representing the nucleotide associated with this mutation; this will be \f3\fs18 "A" \f4\fs20 , \f3\fs18 "C" \f4\fs20 , \f3\fs18 "G" \f4\fs20 , or \f3\fs18 "T" \f4\fs20 . If the mutation is not nucleotide-based, this property is unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 nucleotideValue <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 An \f3\fs18 integer \f4\fs20 representing the nucleotide associated with this mutation; this will be \f3\fs18 0 \f4\fs20 (A), \f3\fs18 1 \f4\fs20 (C), \f3\fs18 2 \f4\fs20 (G), or \f3\fs18 3 \f4\fs20 (T). If the mutation is not nucleotide-based, this property is unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 originTick => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The tick in which this mutation arose. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 position => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The position in the chromosome of this mutation. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 selectionCoeff => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The selection coefficient of the mutation, drawn from the distribution of fitness effects of its \f3\fs18 MutationType \f5\fs20 . \f4 \cf2 \expnd0\expndtw0\kerning0 If a mutation has a \f3\fs18 selectionCoeff \f4\fs20 of \f1\i s \f4\i0 , the multiplicative fitness effect of the mutation in a homozygote is 1+ \f1\i s \f4\i0 ; in a heterozygote it is 1+ \f1\i hs \f4\i0 , where \f1\i h \f4\i0 is the dominance coefficient kept by the mutation type. \f5 \cf0 \kerning1\expnd0\expndtw0 \ \f4 Note that this property has a quirk: it is stored internally in SLiM using a single-precision float, not the double-precision float type normally used by Eidos. This means that if you set a mutation \f3\fs18 mut \f4\fs20 \'92s selection coefficient to some number \f3\fs18 x \f4\fs20 , \f3\fs18 mut.selectionCoeff==x \f4\fs20 may be \f3\fs18 F \f4\fs20 due to floating-point rounding error. Comparisons of floating-point numbers for exact equality is often a bad idea, but this is one case where it may fail unexpectedly. Instead, it is recommended to use the \f3\fs18 id \f4\fs20 or \f3\fs18 tag \f4\fs20 properties to identify particular mutations. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 subpopID <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier of the subpopulation in which this mutation arose. This property can be used to track the ancestry of mutations through their subpopulation of origin.\ If you don\'92t care which subpopulation a mutation originated in, the \f3\fs18 subpopID \f4\fs20 may be used as an arbitrary \f3\fs18 integer \f4\fs20 \'93tag\'94 value for any purpose you wish; SLiM does not do anything with the value of \f3\fs18 subpopID \f4\fs20 except propagate it to \f3\fs18 Substitution \f4\fs20 objects and report it in output. (It must still be \f3\fs18 >= 0 \f4\fs20 , however, since SLiM object identifiers are limited to nonnegative integers).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.10.2 \f2\fs18 Mutation \f1\fs22 methods\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 \'96\'a0(void)setMutationType(io$\'a0mutType) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the mutation type of the mutation to \f3\fs18 mutType \f4\fs20 (which may be specified as either an \f3\fs18 integer \f4\fs20 identifier or a \f3\fs18 MutationType \f4\fs20 object). This implicitly changes the dominance coefficient of the mutation to that of the new mutation type, since the dominance coefficient is a property of the mutation type. On the other hand, the selection coefficient of the mutation is not changed, since it is a property of the mutation object itself; it can be changed explicitly using the \f3\fs18 setSelectionCoeff() \f4\fs20 method if so desired.\ The mutation type of a mutation is normally a constant in simulations, so be sure you know what you are doing. Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In nucleotide-based models, a restriction applies: nucleotide-based mutations may not be changed to a non-nucleotide-based mutation type, and non-nucleotide-based mutations may not be changed to a nucleotide-based mutation type.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0(void)setSelectionCoeff(float$\'a0selectionCoeff) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the selection coefficient of the mutation to \f3\fs18 selectionCoeff \f4\fs20 . The selection coefficient will be changed for all individuals that possess the mutation, since they all share a single \f3\fs18 Mutation \f4\fs20 object (note that the dominance coefficient will remain unchanged, as it is determined by the mutation type).\ This is normally a constant in simulations, so be sure you know what you are doing; often setting up a \f3\fs18 mutationEffect() \f4\fs20 callback is preferable, in order to modify the selection coefficient in a more limited and controlled fashion. Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.11 Class MutationType\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.11.1 \f2\fs18 MutationType \f1\fs22 properties\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 color <\'96> (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The color used to display mutations of this type in SLiMgui. Outside of SLiMgui, this property still exists, but is not used by SLiM. Colors may be specified by name, or with hexadecimal RGB values of the form \f3\fs18 "#RRGGBB" \f4\fs20 . If \f3\fs18 color \f4\fs20 is the empty string, \f3\fs18 "" \f4\fs20 , SLiMgui\'92s default (selection-coefficient\'96based) color scheme is used; this is the default for new \f3\fs18 MutationType \f4\fs20 objects.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 colorSubstitution <\'96> (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The color used to display substitutions of this type in SLiMgui (see the discussion for the \f3\fs18 colorSubstitution \f4\fs20 property of the \f3\fs18 Chromosome \f4\fs20 class for details). Outside of SLiMgui, this property still exists, but is not used by SLiM. Colors may be specified by name, or with hexadecimal RGB values of the form \f3\fs18 "#RRGGBB" \f4\fs20 . If \f3\fs18 colorSubstitution \f4\fs20 is the empty string, \f3\fs18 "" \f4\fs20 , SLiMgui\'92s default (selection-coefficient\'96based) color scheme is used; this is the default for new \f3\fs18 MutationType \f4\fs20 objects.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 convertToSubstitution <\'96> (logical$)\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 This property governs whether mutations of this mutation type will be converted to \f3\fs18 Substitution \f4\fs20 objects when they reach fixation.\ In WF models this property is \f3\fs18 T \f4\fs20 by default, since conversion to \f3\fs18 Substitution \f4\fs20 objects provides large speed benefits; it should be set to \f3\fs18 F \f4\fs20 only if necessary, and only on the mutation types for which it is necessary. This might be needed, for example, if you are using a \f3\fs18 mutationEffect() \f4\fs20 callback to implement an epistatic relationship between mutations; a mutation epistatically influencing the fitness of other mutations through a \f3\fs18 mutationEffect() \f4\fs20 callback would need to continue having that influence even after reaching fixation, but if the simulation were to replace the fixed mutation with a \f3\fs18 Substitution \f4\fs20 object the mutation would no longer be considered in fitness calculations (unless the callback explicitly consulted the list of \f3\fs18 Substitution \f4\fs20 objects kept by the simulation). Other script-defined behaviors in \f3\fs18 mutationEffect() \f4\fs20 , \f3\fs18 interaction() \f4\fs20 , \f3\fs18 mateChoice() \f4\fs20 , \f3\fs18 modifyChild() \f4\fs20 , and \f3\fs18 recombination() \f4\fs20 callbacks might also necessitate the disabling of substitution for a given mutation type; this is an important consideration to keep in mind.\ In contrast, for nonWF models this property is \f3\fs18 F \f4\fs20 by default, because even mutations with no epistatis or other indirect fitness effects will continue to influence the survival probabilities of individuals. For nonWF models, only neutral mutation types with no epistasis or other side effects can safely be converted to substitutions upon fixation. When such a pure-neutral mutation type is defined in a nonWF model, this property should be set to \f3\fs18 T \f4\fs20 to tell SLiM that substitution is allowed; this may have very large positive effects on performance, so it is important to remember when modeling background neutral mutations.\ SLiM consults this flag at the end of each tick when deciding whether to substitute each fixed mutation. If this flag is \f3\fs18 T \f4\fs20 , all eligible fixed mutations will be converted at the end of the current tick, even if they were previously left unconverted because of the previous value of the flag. Setting this flag to \f3\fs18 F \f4\fs20 will prevent future substitutions, but will not cause any existing \f3\fs18 Substitution \f4\fs20 objects to be converted back into \f3\fs18 Mutation \f4\fs20 objects.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 distributionParams => (fs)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The parameters that configure the chosen distribution of fitness effects. This will be of type \f3\fs18 string \f4\fs20 for DFE type \f3\fs18 "s" \f4\fs20 , and type \f3\fs18 float \f4\fs20 for all other DFE types. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 distributionType => (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The type of distribution of fitness effects; one of \f3\fs18 "f" \f4\fs20 , \f3\fs18 "g" \f5\fs20 , \f4 \f3\fs18 "e" \f5\fs20 , \f4 \f3\fs18 "n" \f5\fs20 , \f4 \f3\fs18 "w" \f5\fs20 , \f4 or \f3\fs18 "s" \f5\fs20 :\ \f3\fs18 "f" \f4\fs22 \'96 A \f0\b f \f4\b0 ixed fitness effect. This DFE type has a single parameter, the selection coefficient \f1\i s \f4\i0 to be used by all mutations of the mutation type.\ \f3\fs18 "g" \f4\fs22 \'96 A \f0\b g \f4\b0 amma-distributed fitness effect. This DFE type is specified by two parameters, a shape parameter and a mean value. The gamma distribution from which mutations are drawn is given by the probability density function \f6\i P \f5\i0 ( \f6\i s \f5\i0 \'a0|\'a0 \f8\i \uc0\u945 \f5\i0 , \f8\i \uc0\u946 \f5\i0 )\'a0 \f9 = [\uc0\u915 ( \f8\i \uc0\u945 \f5\i0 ) \f8\i \uc0\u946 \u945 \f5\i0 ]\super \uc0\u8722 1\nosupersub exp(\uc0\u8722 \f6\i s \f5\i0 / \f8\i \uc0\u946 \f5\i0 ) \f4 , where \f8\i \uc0\u945 \f4\i0 is the shape parameter, and the specified mean for the distribution is equal to \f8\i \uc0\u945 \u946 \f4\i0 . Note that this parameterization is the same as for the Eidos function \f3\fs18 rgamma() \f4\fs22 . A gamma distribution is often used to model deleterious mutations at functional sites.\ \f3\fs18 "e" \f4\fs22 \'96 An \f0\b e \f4\b0 xponentially-distributed fitness effect. This DFE type is specified by a single parameter, the mean of the distribution. The exponential distribution from which mutations are drawn is given by the probability density function \f6\i P \f5\i0 ( \f6\i s \f5\i0 \'a0|\'a0 \f8\i \uc0\u946 \f5\i0 )\'a0= \f8\i \uc0\u946 \f5\i0 \super \uc0\u8722 1\nosupersub exp(\uc0\u8722 \f6\i s \f5\i0 / \f8\i \uc0\u946 \f5\i0 ) \f4 , where \f8\i \uc0\u946 \f4\i0 is the specified mean for the distribution. This parameterization is the same as for the Eidos function \f3\fs18 rexp() \f4\fs22 . An exponential distribution is often used to model beneficial mutations.\ \f3\fs18 "n" \f4\fs22 \'96 A \f0\b n \f4\b0 ormally-distributed fitness effect. This DFE type is specified by two parameters, a mean and a standard deviation. The normal distribution from which mutations are drawn is given by the probability density function \f6\i P \f5\i0 ( \f6\i s \f5\i0 \'a0|\'a0 \f8\i \uc0\u956 \f5\i0 , \f8\i \uc0\u963 \f5\i0 )\'a0= (2 \f9 \uc0\u960 \f8\i \uc0\u963 \f5\i0 \super 2\nosupersub )\super \uc0\u8722 1/2\nosupersub exp(\uc0\u8722 ( \f6\i s \f5\i0 \uc0\u8722 \f8\i \uc0\u956 \f5\i0 )\super 2\nosupersub /2 \f8\i \uc0\u963 \f5\i0 \super 2\nosupersub ) \f4 , where \f8\i \uc0\u956 \f4\i0 is the mean and \f8\i \uc0\u963 \f4\i0 is the standard deviation. This parameterization is the same as for the Eidos function \f3\fs18 rnorm() \f4\fs22 . A normal distribution is often used to model mutations that can be either beneficial or deleterious, since both tails of the distribution are unbounded.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs18 \cf2 "p" \f4\fs22 \'96 A La \f0\b p \f4\b0 lace-distributed fitness effect. This DFE type is specified by two parameters, a mean and a scale. The Laplace distribution from which mutations are drawn is given by the probability density function \f6\i P \f5\i0 ( \f6\i s \f5\i0 \'a0|\'a0 \f6\i \uc0\u956 \f5\i0 , \f6\i b \f5\i0 )\'a0= exp(\uc0\u8722 | \f6\i s \f5\i0 \uc0\u8722 \f6\i \uc0\u956 \f5\i0 |/ \f6\i b \f5\i0 )/2 \f6\i b \f4\i0 , where \f6\i \uc0\u956 \f4\i0 is the mean and \f6\i b \f4\i0 is the scale parameter. A Laplace distribution is sometimes used to model a mix of both deleterious and beneficial mutations.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\fs18 \cf0 "w" \f4\fs22 \'96 A \f0\b W \f4\b0 eibull-distributed fitness effect. This DFE type is specified by a scale parameter and a shape parameter. The Weibull distribution from which mutations are drawn is given by the probability density function \f6\i P \f5\i0 ( \f6\i s \f5\i0 \'a0|\'a0 \f8\i \uc0\u955 \f5\i0 , \f6\i k \f5\i0 )\'a0= ( \f6\i k \f5\i0 / \f8\i \uc0\u955 \f6 \super k \f5\i0 \nosupersub ) \f6\i s\super k \f5\i0 \uc0\u8722 1\nosupersub exp(\uc0\u8722 ( \f6\i s \f5\i0 / \f8\i \uc0\u955 \f5\i0 ) \f6\i \super k \f5\i0 \nosupersub ) \f4 , where \f8\i \uc0\u955 \f4\i0 is the scale parameter and \f6\i k \f4\i0 is the shape parameter. This parameterization is the same as for the Eidos function \f3\fs18 rweibull() \f4\fs22 . A Weibull distribution is often used to model mutations following extreme-value theory.\ \f3\fs18 "s" \f4\fs22 \'96 A \f0\b s \f4\b0 cript-based fitness effect. This DFE type is specified by a script parameter of type \f3\fs18 string \f4\fs22 , specifying an Eidos script to be executed to produce each new selection coefficient. For example, the script \f3\fs18 "return rbinom(1);" \f4\fs22 could be used to generate selection coefficients drawn from a binomial distribution, using the Eidos function \f3\fs18 rbinom() \f4\fs22 , even though that mutational distribution is not supported by SLiM directly. The script must return a singleton float or integer.\ Note that these distributions can in principle produce selection coefficients smaller than \f3\fs18 -1.0. \f4\fs22 In that case \f5 , \f4 the mutations will be evaluated as \'93lethal\'94 by SLiM, and the relative fitness of the individual will be set to \f3\fs18 0.0 \f5\fs22 . \fs20 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 dominanceCoeff <\'96> (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The dominance coefficient used for mutations of this type when heterozygous. Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ Note that the dominance coefficient is not bounded. A dominance coefficient greater than \f3\fs18 1.0 \f4\fs20 may be used to achieve an overdominance effect. By making the selection coefficient very small and the dominance coefficient very large, an overdominance scenario in which both homozygotes have the same fitness may be approximated, to a nearly arbitrary degree of precision.\ Note that this property has a quirk: it is stored internally in SLiM using a single-precision float, not the double-precision float type normally used by Eidos. This means that if you set a mutation type \f3\fs18 muttype \f4\fs20 \'92s dominance coefficient to some number \f3\fs18 x \f4\fs20 , \f3\fs18 muttype.dominanceCoeff==x \f4\fs20 may be \f3\fs18 F \f4\fs20 due to floating-point rounding error. Comparisons of floating-point numbers for exact equality is often a bad idea, but this is one case where it may fail unexpectedly. Instead, it is recommended to use the \f3\fs18 id \f4\fs20 or \f3\fs18 tag \f4\fs20 properties to identify particular mutation types. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 hemizygousDominanceCoeff <\'96> (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The dominance coefficient used for mutations of this type when they occur opposite a null haplosome (as can occur in sex-chromosome models and models involving a mix of haploids and diploids). This defaults to \f3\fs18 1.0 \f4\fs20 , and is used only in models where null haplosomes are present; the \f3\fs18 dominanceCoeff \f4\fs20 property is the dominance coefficient used in most circumstances. Changing this will normally affect the fitness values calculated toward the end of the current tick; if you want current fitness values to be affected, you can call the \f3\fs18 Species \f4\fs20 method \f3\fs18 recalculateFitness() \f4\fs20 \'96 but see the documentation of that method for caveats.\ As with the \f3\fs18 dominanceCoeff \f4\fs20 property, this is stored internally using a single-precision float; see the documentation for \f3\fs18 dominanceCoeff \f4\fs20 for discussion.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier for this mutation type; for mutation type \f3\fs18 m3 \f4\fs20 , for example, this is \f3\fs18 3 \f5\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 mutationStackGroup <\'96> (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The group into which this mutation type belongs for purposes of mutation stacking policy. This is equal to the mutation type\'92s \f3\fs18 id \f4\fs20 by default. See \f3\fs18 mutationStackPolicy \f4\fs20 , below, for discussion.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In nucleotide-based models, the stacking group for nucleotide-based mutation types is always \f3\fs18 -1 \f4\fs20 , and cannot be changed. Non-nucleotide-based mutation types may also be set to share the \f3\fs18 -1 \f4\fs20 stacking group, if they should participate in the same stacking policy as nucleotide-based mutations, but that would be quite unusual.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 mutationStackPolicy <\'96> (string$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 This property and the \f3\fs18 mutationStackGroup \f4\fs20 property together govern whether mutations of this mutation type\'92s stacking group can \'93stack\'94 \'96 can occupy the same position in a single individual. A set of mutation types with the same value for \f3\fs18 mutationStackGroup \f4\fs20 is called a \'93stacking group\'94, and all mutation types in a given stacking group must have the same \f3\fs18 mutationStackPolicy \f4\fs20 value, which defines the stacking behavior of all mutations of the mutation types in the stacking group. In other words, one stacking group might allow its mutations to stack, while another stacking group might not, but the policy within each stacking group must be unambiguous.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 This property is \f3\fs18 "s" \f4\fs20 by default, indicating that mutations in this stacking group should be allowed to stack without restriction. If the policy is set to \f3\fs18 "f" \f4\fs20 , the \f1\i first \f4\i0 mutation of stacking group at a given site is retained; further mutations of this stacking group at the same site are discarded with no effect. This can be useful for modeling one-way changes; once a gene is disabled by a premature stop codon, for example, you might wish to assume, for simplicity, that further mutations cannot alter that fact. If the policy is set to \f3\fs18 "l" \f4\fs20 , the \f1\i last \f4\i0 mutation of this stacking group at a given site is retained; earlier mutation of this stacking group at the same site are discarded. This can be useful for modeling an \'93infinite-alleles\'94 scenario in which every new mutation at a site generates a completely new allele, rather than retaining the previous mutations at the site.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 \kerning1\expnd0\expndtw0 The mutation stacking policy applies only within the given mutation type\'92s stacking group; mutations of different stacking groups are always allowed to stack in SLiM. The policy applies to all mutations added to the model after the policy is set, whether those mutations are introduced by calls such as \f3\fs18 addMutation() \f4\fs20 , \f3\fs18 addNewMutation() \f4\fs20 , or \f3\fs18 addNewDrawnMutation() \f4\fs20 , or are added by SLiM\'92s own mutation-generation machinery. However, no attempt is made to enforce the policy for mutations already existing at the time the policy is set; typically, therefore, the policy is set in an \f3\fs18 initialize() \f4\fs20 callback so that it applies throughout the simulation. The policy is also not enforced upon the mutations loaded from a file with \f3\fs18 readFromPopulationFile() \f4\fs20 ; such mutations were governed by whatever stacking policy was in effect when the population file was generated. \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4 \cf2 \expnd0\expndtw0\kerning0 In nucleotide-based models, the stacking policy for nucleotide-based mutation types is always \f3\fs18 "l" \f4\fs20 , and cannot be changed. This ensures that new nucleotide mutations always replace the previous nucleotide at a site, and that more than one nucleotide mutation is never present at the same position in a single haplosome.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 nucleotideBased => (logical$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If the mutation type was created with \f3\fs18 initializeMutationType() \f4\fs20 , it is not nucleotide-based, and this property is \f3\fs18 F \f4\fs20 . If it was created with \f3\fs18 initializeMutationTypeNuc() \f4\fs20 , it is nucleotide-based, and this property is \f3\fs18 T \f4\fs20 . See those methods for further discussion.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 species => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The species to which the target object belongs.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to mutation types. \f5 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.11.2 \f2\fs18 MutationType \f1\fs22 methods\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(float)drawSelectionCoefficient([integer$\'a0n\'a0=\'a01])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Draws and returns a vector of \f3\fs18 n \f4\fs20 selection coefficients using the currently defined distribution of fitness effects (DFE) for the target mutation type. If the DFE is type \f3\fs18 "s" \f4\fs20 , this method will result in synchronous execution of the DFE\'92s script.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0(void)setDistribution(string$\'a0distributionType, ...) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Set the distribution of fitness effects for a mutation type. The \f3\fs18 distributionType \f4\fs20 may be \f3\fs18 "f" \f4\fs20 , in which case the ellipsis \f3\fs18 ... \f4\fs20 should supply a \f3\fs18 numeric$ \f4\fs20 fixed selection coefficient; \f3\fs18 "e" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 mean selection coefficient for the exponential distribution; \f3\fs18 "g" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 mean selection coefficient and a \f3\fs18 numeric$ \f4\fs20 alpha shape parameter for a gamma distribution; \f3\fs18 "n" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 mean selection coefficient and a \f3\fs18 numeric$ \f4\fs20 sigma (standard deviation) parameter for a normal distribution; \f3\fs18 "p" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 mean selection coefficient and a \f3\fs18 numeric$ \f4\fs20 scale parameter for a Laplace distribution; \f3\fs18 "w" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 \f5 \uc0\u955 \f4 scale parameter and a \f3\fs18 numeric$ \f4\fs20 k shape parameter for a Weibull distribution; or \f3\fs18 "s" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 string$ \f4\fs20 Eidos script parameter. The DFE for a mutation type is normally a constant in simulations, so be sure you know what you are doing.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf2 5.12 Class Plot\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf2 5.12.1 \f2\fs18 Plot \f1\fs22 properties\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 title => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The title of the plot, as originally passed to \f3\fs18 createPlot() \f4\fs20 . See also the \f3\fs18 plotWithTitle() \f4\fs20 method of \f3\fs18 SLiMgui \f4\fs20 .\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.12.2 \f2\fs18 Plot \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(void)abline([Nif\'a0a\'a0=\'a0NULL], [Nif\'a0b\'a0=\'a0NULL], [Nif\'a0h\'a0=\'a0NULL], [Nif\'a0v\'a0=\'a0NULL], [string\'a0color\'a0=\'a0"red"], [numeric\'a0lwd\'a0=\'a01.0], [float\'a0alpha\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds one or more straight lines to the plot. There are three supported modes of operation for this method. In the first mode, the lines are specified by \f3\fs18 a \f4\fs20 and \f3\fs18 b \f4\fs20 , representing the intercepts and slopes of the lines, respectively; in this case, \f3\fs18 a \f4\fs20 and \f3\fs18 b \f4\fs20 may be the same length, or one of them may be a singleton to provide a single value used for all of the lines specified by the other. In the second mode, the lines are specified by \f3\fs18 h \f4\fs20 , representing the \f1\i y \f4\i0 -values of horizontal lines. In the third mode, the lines are specified by \f3\fs18 v \f4\fs20 , representing the \f1\i x \f4\i0 -values of vertical lines. These modes are mutually exclusive and cannot be mixed within one call to \f3\fs18 abline() \f4\fs20 . The new lines will be plotted on top of any previously added data.\ The lines will be drawn in colors, line widths, and alpha (opacity) values specified by \f3\fs18 color \f4\fs20 , \f3\fs18 lwd \f4\fs20 , and \f3\fs18 alpha \f4\fs20 , each of which may be either a singleton (to provide one value used for all lines) or a vector equal in length to the number of lines plotted (to provide one value per line). Alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque.\ See also \f3\fs18 lines() \f4\fs20 and \f3\fs18 segments() \f4\fs20 for more common approaches to line plotting. The \f3\fs18 abline() \f4\fs20 method is different, and more specialized; the lines plotted by it span the full extent of the plot area, and their coordinates are not considered when dynamically resizing the axes of the plot (i.e., when \f3\fs18 createPlot() \f4\fs20 is not passed explicit, non- \f3\fs18 NULL \f4\fs20 values for \f3\fs18 xrange \f4\fs20 or \f3\fs18 yrange \f4\fs20 ). This is typically useful for plotting things such as expected values and fit lines.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addLegend([Ns$\'a0position\'a0=\'a0NULL], [Ni$\'a0inset\'a0=\'a0NULL], [Nif$\'a0labelSize\'a0=\'a0NULL], [Nif$\'a0lineHeight\'a0=\'a0NULL], [Nif$\'a0graphicsWidth\'a0=\'a0NULL], [Nif$\'a0exteriorMargin\'a0=\'a0NULL], [Nif$\'a0interiorMargin\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a legend to the plot. The legend will be displayed within the plot at the location specified by position, which must be \f3\fs18 "topRight" \f4\fs20 , \f3\fs18 "topLeft" \f4\fs20 , \f3\fs18 "bottomRight" \f4\fs20 , or \f3\fs18 "bottomLeft" \f4\fs20 , or \f3\fs18 NULL \f4\fs20 (the default) requesting that SLiMgui choose the position. The position of the legend will be inset from the chosen corner by a margin \f3\fs18 inset \f4\fs20 , measured in pixels; the default of \f3\fs18 NULL \f4\fs20 allows SLiMgui to choose the inset.\ The internal layout of the legend is a bit complex, and can be controlled by five parameters; in all cases, the default value of \f3\fs18 NULL \f4\fs20 requests that SLiMgui provide a reasonable default. The \f3\fs18 labelSize \f4\fs20 parameter specifies the font size used for the text labels for each legend entry (measured in \'93points\'94, the standard metric of font sizes). The \f3\fs18 lineHeight \f4\fs20 parameter specifies the vertical size, in pixels, of one entry line. The \f3\fs18 graphicsWidth \f4\fs20 parameter species the width, in pixels, of the column used to display the \'93graphics\'94 (whether a line segment, a point symbol, a swatch, or a combination of those) associated with each entry. The \f3\fs18 exteriorMargin \f4\fs20 parameter specifies the width/height of margins, in pixels, outside of the entries (between the entries and the legend\'92s frame). Finally, the \f3\fs18 interiorMargin \f4\fs20 parameter specifies the width/height of margins, in pixels, vertically between entries, and also between the \'93graphics\'94 column and the label. It is easy to produce a legend that looks terrible, using these layout metrics; SLiMgui does only minimal sanity-checking of their values, to provide maximal flexibility.\ The legend is initially empty; entries for it can be added with \f3\fs18 legendLineEntry() \f4\fs20 , \f3\fs18 legendPointEntry() \f4\fs20 , and \f3\fs18 legendSwatchEntry() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)axis(integer$\'a0side, [Nif\'a0at\'a0=\'a0NULL], [ls\'a0labels\'a0=\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Configures an axis of the plot. The \f3\fs18 side \f4\fs20 parameter controls which axis is being configured; at present, it may be \f3\fs18 1 \f4\fs20 for the \f1\i x \f4\i0 -axis (at the bottom of the plot), or \f3\fs18 2 \f4\fs20 for the \f1\i y \f4\i0 -axis (at the left of the plot).\ The positions of tick marks (and of any associated labels) are controlled by \f3\fs18 at \f4\fs20 ; if \f3\fs18 at \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default) these positions will be computed automatically based upon the range of the data in the plot, otherwise \f3\fs18 at \f4\fs20 must be a vector of \f3\fs18 numeric \f4\fs20 positions. Note that the coordinate system of the plot is controlled not by this method, but by the \f3\fs18 xrange \f4\fs20 and \f3\fs18 yrange \f4\fs20 parameters of \f3\fs18 createPlot() \f4\fs20 ; \f3\fs18 at \f4\fs20 controls only the positions of axis ticks \f1\i within \f4\i0 that coordinate system.\ The \f3\fs18 labels \f4\fs20 parameter controls the text labels displayed for ticks; it may be \f3\fs18 T \f4\fs20 (the default) to label ticks with their numeric positions, \f3\fs18 F \f4\fs20 to suppress all tick labels, or a vector of type \f3\fs18 string \f4\fs20 , equal in length to \f3\fs18 at \f4\fs20 , providing the label for each position in \f3\fs18 at \f4\fs20 . Label values may be the empty string, \f3\fs18 "" \f4\fs20 , if a label at a given position is not desired, and if \f3\fs18 labels \f4\fs20 is \f3\fs18 T \f4\fs20 , some ticks may not receive a label for readability.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)image(object$\'a0image, numeric$\'a0x1, numeric$\'a0y1, numeric$\'a0x2, numeric$\'a0y2, [logical$\'a0flipped\'a0=\'a0F], [float$\'a0alpha\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds image data given by \f3\fs18 image \f4\fs20 to the plot. The data will be plotted as a raster (bitmap, pixel) image in the rectangle specified by \f3\fs18 x1 \f4\fs20 , \f3\fs18 y1 \f4\fs20 , \f3\fs18 x2 \f4\fs20 , and \f3\fs18 y2 \f4\fs20 , which must be sorted such that \f3\fs18 x1\'a0<=\'a0x2 \f4\fs20 and \f3\fs18 y1\'a0<=\'a0y2 \f4\fs20 . When \f3\fs18 flipped \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), the plotted image is shown in a default orientation that is typically best given the source of the image; if \f3\fs18 flipped \f4\fs20 is \f3\fs18 T \f4\fs20 the image\'92s orientation is flipped vertically relative to that default. The plotted image will use opacity \f3\fs18 alpha \f4\fs20 ; alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. The new image data will be plotted on top of any previously added data.\ The image data may be specified in one of two ways. First, \f3\fs18 image \f4\fs20 may be a singleton \f3\fs18 Image \f4\fs20 object. In this case, the image is simply plotted inside the rectangle specified by \f3\fs18 x1 \f4\fs20 / \f3\fs18 y1 \f4\fs20 / \f3\fs18 x2 \f4\fs20 / \f3\fs18 y2 \f4\fs20 , fully displaying all of its pixels (in contrast to how the grid values of a spatial map area displayed, as described below). The image may be either RGB or grayscale.\ Second, \f3\fs18 image \f4\fs20 may be a singleton \f3\fs18 SpatialMap \f4\fs20 object. In this case, the grid values of the spatial map are plotted aligned to the corners of the rectangle specified by \f3\fs18 x1 \f4\fs20 / \f3\fs18 y1 \f4\fs20 / \f3\fs18 x2 \f4\fs20 / \f3\fs18 y2 \f4\fs20 , as the map would be used by SLiM; the edge and corner grid points of the map are therefore only partially displayed. (If desired, the \f3\fs18 gridValues() \f4\fs20 or \f3\fs18 mapImage() \f4\fs20 methods of \f3\fs18 SpatialMap \f4\fs20 could be used to convert the map to a matrix or \f3\fs18 Image \f4\fs20 representation that could be plotted differently.) The spatial map\'92s display uses the color scheme that was specified for the map. The map\'92s values are not interpolated, regardless of the map\'92s \f3\fs18 interpolate \f4\fs20 property; only the raw grid data of the spatial map is displayed. (If interpolated display is desired, the \f3\fs18 interpolate() \f4\fs20 method of \f3\fs18 SpatialMap \f4\fs20 can be used to increase the grid resolution of the map prior to display.)\ This method caches the image data being plotted so that plotting the same \f3\fs18 SpatialMap \f4\fs20 or \f3\fs18 Image \f4\fs20 object multiple times is more efficient. See the \f3\fs18 matrix() \f4\fs20 method of \f3\fs18 Plot \f4\fs20 for an alternative way of plotting raster data that may be more suitable in some situations, but that is less efficient because it does not provide such caching.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)legendLineEntry(string$\'a0label, [string$\'a0color\'a0=\'a0"red"], [numeric$\'a0lwd\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a legend entry with text \f3\fs18 label \f4\fs20 to the plot. The entry will be displayed as a line segment drawn in color \f3\fs18 color \f4\fs20 , using line width \f3\fs18 lwd \f4\fs20 , mirroring the appearance produced by \f3\fs18 lines() \f4\fs20 for the same parameters. If one or more other legend entries already exist with the same label, the new entry will be drawn on top of the previously set entries for that label (allowing legend entries that display both a line and a point, for example).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)legendPointEntry(string$\'a0label, [integer$\'a0symbol\'a0=\'a00], [string$\'a0color\'a0=\'a0"red"], [string$\'a0border\'a0=\'a0"black"], [numeric$\'a0lwd\'a0=\'a01.0], [numeric$\'a0size\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a legend entry with text \f3\fs18 label \f4\fs20 to the plot. The entry will be displayed as a point symbol specified by \f3\fs18 symbol \f4\fs20 , \f3\fs18 color \f4\fs20 , \f3\fs18 border \f4\fs20 , \f3\fs18 lwd \f4\fs20 , and \f3\fs18 size \f4\fs20 , mirroring the appearance produced by \f3\fs18 points() \f4\fs20 for the same parameters; see \f3\fs18 points() \f4\fs20 for further details. If one or more other legend entries already exist with the same label, the new entry will be drawn on top of the previously set entries for that label (allowing legend entries that display both a line and a point, for example).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)legendSwatchEntry(string$\'a0label, [string$\'a0color\'a0=\'a0"red"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a legend entry with text \f3\fs18 label \f4\fs20 to the plot. The entry will be displayed as a swatch drawn in color \f3\fs18 color \f4\fs20 . If one or more other legend entries already exist with the same label, the new entry will be drawn on top of the previously set entries for that label (allowing legend entries that display both a line and a point, for example).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)legendTitleEntry(string$\'a0label)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds a legend entry with text \f3\fs18 label \f4\fs20 to the plot. The entry will be displayed as a title, left-aligned with no graphical representation (no line, point, or swatch). A label of \f3\fs18 "" \f4\fs20 , the empty string, is allowed and will produce a blank line in the legend (for spacing). Note that title entries always produce their own separate line in the legend; they do not participate in the overdrawing scheme used for other types of legend entries.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)lines(numeric\'a0x, numeric\'a0y, [string$\'a0color\'a0=\'a0"red"], [numeric$\'a0lwd\'a0=\'a01.0], [float$\'a0alpha\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds line data given by \f3\fs18 x \f4\fs20 and \f3\fs18 y \f4\fs20 to the plot. The data will be plotted as a series of connected line segments, following the ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ) positions given; note that the \f3\fs18 x \f4\fs20 and \f3\fs18 y \f4\fs20 vectors must be the same length. The new line data will be plotted on top of any previously added data.\ The lines will be drawn in color \f3\fs18 color \f4\fs20 , using line width \f3\fs18 lwd \f4\fs20 , with opacity \f3\fs18 alpha \f4\fs20 . Alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. Unlike \f3\fs18 points() \f4\fs20 and \f3\fs18 text() \f4\fs20 , the parameters \f3\fs18 color \f4\fs20 , \f3\fs18 lwd \f4\fs20 , and \f3\fs18 alpha \f4\fs20 must be singletons, since each point (except the two ends) is shared by two line segments; if you wish to vary the color/width/opacity for each line segment, separate calls to \f3\fs18 lines() \f4\fs20 are necessary, or the \f3\fs18 segments() \f4\fs20 method may be useful. See also \f3\fs18 abline() \f4\fs20 and \f3\fs18 segments() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)matrix(numeric\'a0matrix, numeric$\'a0x1, numeric$\'a0y1, numeric$\'a0x2, numeric$\'a0y2, [logical$\'a0flipped\'a0=\'a0F], [Nif\'a0valueRange\'a0=\'a0NULL], [Ns$\'a0colors\'a0=\'a0NULL], [float$\'a0alpha\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds image data given by \f3\fs18 matrix \f4\fs20 to the plot. The image data must be specified as a \f3\fs18 numeric \f4\fs20 (i.e., \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 ) matrix, as for example produced by the \f3\fs18 matrix() \f4\fs20 function in Eidos. The data will be plotted as a raster (bitmap, pixel) image in the rectangle specified by \f3\fs18 x1 \f4\fs20 , \f3\fs18 y1 \f4\fs20 , \f3\fs18 x2 \f4\fs20 , and \f3\fs18 y2 \f4\fs20 , which must be sorted such that \f3\fs18 x1\'a0<=\'a0x2 \f4\fs20 and \f3\fs18 y1\'a0<=\'a0y2 \f4\fs20 . When \f3\fs18 flipped \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), the plotted image is shown in a default orientation that matches the orientation of the matrix (with row \f3\fs18 0 \f4\fs20 topmost); if \f3\fs18 flipped \f4\fs20 is \f3\fs18 T \f4\fs20 the image\'92s orientation is flipped vertically relative to that default (with row \f3\fs18 0 \f4\fs20 bottommost).\ Prior to display, the values in the matrix will be rescaled according to the parameter \f3\fs18 valueRange \f4\fs20 . If \f3\fs18 valueRange \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), the values will be left unscaled; this is equivalent to passing \f3\fs18 c(0,1) \f4\fs20 for \f3\fs18 valueRange \f4\fs20 . Otherwise, \f3\fs18 valueRange \f4\fs20 should be a \f3\fs18 numeric \f4\fs20 vector containing two elements that define the range of values that will be rescaled to span the interval [0, 1] \'96 the range to which the color scheme will then be applied. After rescaling the given range to [ \f3\fs18 0 \f4\fs20 , \f3\fs18 1 \f4\fs20 ], the resulting values are clamped to the range [ \f3\fs18 0 \f4\fs20 , \f3\fs18 1 \f4\fs20 ]. It is legal for \f3\fs18 valueRange[0] \f4\fs20 to be greater than \f3\fs18 valueRange[1] \f4\fs20 ; in this case, the rescaling operation will, in effect, reverse the direction of the color scheme by reversing the ranks of the reordered values.\ The rescaled and clamped values are then colored according to the color scheme named by \f3\fs18 colors \f4\fs20 , which should be one of the named schemes supported by the Eidos function \f3\fs18 colors() \f4\fs20 : \f3\fs18 "cm" \f4\fs20 , \f3\fs18 "heat" \f4\fs20 , \f3\fs18 "terrain" \f4\fs20 , \f3\fs18 "parula" \f4\fs20 , \f3\fs18 "hot" \f4\fs20 , \f3\fs18 "jet" \f4\fs20 , \f3\fs18 "turbo" \f4\fs20 , \f3\fs18 "gray" \f4\fs20 , \f3\fs18 "magma" \f4\fs20 , \f3\fs18 "inferno" \f4\fs20 , \f3\fs18 "plasma" \f4\fs20 , \f3\fs18 "viridis" \f4\fs20 , or \f3\fs18 "cividis" \f4\fs20 . If \f3\fs18 colors \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), the color scheme used is the reverse of the \f3\fs18 "gray" \f4\fs20 color scheme, shading from black for \f3\fs18 0 \f4\fs20 up to white for \f3\fs18 1 \f4\fs20 . In all cases, the color scheme is applied across the range [ \f3\fs18 0 \f4\fs20 , \f3\fs18 1 \f4\fs20 ] for the rescaled and clamped values.\ The plotted image will use opacity \f3\fs18 alpha \f4\fs20 ; alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. The new image data will be plotted on top of any previously added data.\ See the \f3\fs18 image() \f4\fs20 method of \f3\fs18 Plot \f4\fs20 for an alternative way of plotting raster data from \f3\fs18 SpatialMap \f4\fs20 and \f3\fs18 Image \f4\fs20 objects that may be more suitable in some situations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)mtext(numeric\'a0x, numeric\'a0y, string\'a0labels, [string\'a0color\'a0=\'a0"black"], [numeric\'a0size\'a0=\'a010.0], [Nif\'a0adj\'a0=\'a0NULL], [float\'a0alpha\'a0=\'a01.0], [numeric\'a0angle\'a0=\'a00.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds marginal text data given by \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 labels \f4\fs20 to the plot. The string values in \f3\fs18 labels \f4\fs20 will be plotted at the ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ) positions given; note that \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 labels \f4\fs20 must all be the same length. This method differs from the \f3\fs18 text() \f4\fs20 method in two respects. The first \'96 the reason that this method draws \'93marginal\'94 text \'96 is that the text drawn is not clipped to the plot area, allowing text to be drawn in the axis and border areas outside the plot itself. The second is that the \f3\fs18 x \f4\fs20 and \f3\fs18 y \f4\fs20 coordinates are interpreted differently than other \f3\fs18 Plot \f4\fs20 methods; rather than being in the coordinate system of the plot, they are in a coordinate system in which the plot area spans [0,1] on both axes. This is intended to make positioning text outside of the plot area not depend upon the axis ranges of the plot; those axis ranges might vary, but the positions at which \f3\fs18 mtext() \f4\fs20 draws, relative to the plot area, will remain fixed. The new marginal text data will be plotted on top of any previously added data.\ The text will be drawn in color \f3\fs18 color \f4\fs20 , at the font size given by \f3\fs18 size \f4\fs20 (measured in \'93points\'94, the standard metric of font sizes); the font family and style cannot be controlled at this time. Opacity values may be supplied with \f3\fs18 alpha \f4\fs20 ; alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. The angle at which the text is drawn can be specified with \f3\fs18 angle \f4\fs20 ; angles are measured in degrees, clockwise. All of these parameters ( \f3\fs18 color \f4\fs20 , \f3\fs18 size \f4\fs20 , \f3\fs18 alpha \f4\fs20 , and \f3\fs18 angle \f4\fs20 ) may either be a singleton value applied to all points, or a vector with one value per corresponding point.\ The exact position of the text, relative to each point ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ), is adjusted by the optional parameter \f3\fs18 adj \f4\fs20 . The value of \f3\fs18 adj \f4\fs20 , if specified, must be a vector of length \f3\fs18 2 \f4\fs20 , where \f3\fs18 adj[0] \f4\fs20 adjusts the \f1\i x \f4\i0 position and \f3\fs18 adj[1] \f4\fs20 adjusts the \f1\i y \f4\i0 position of the text. Relative to a given \f1\i x \f4\i0 position, a value of \f3\fs18 0.0 \f4\fs20 aligns the left edge of the text to it; a value of \f3\fs18 0.5 \f4\fs20 aligns the center of the text to it; and a value of \f3\fs18 1.0 \f4\fs20 aligns the right edge of the text to it. Similarly, relative to a given \f1\i y \f4\i0 position, a value of \f3\fs18 0.0 \f4\fs20 aligns the bottom edge of the text to it; a value of \f3\fs18 0.5 \f4\fs20 aligns the center of the text to it; and a value of \f3\fs18 1.0 \f4\fs20 aligns the top edge of the text to it. Intermediate values will produce intermediate alignments, and values of \f3\fs18 adj \f4\fs20 outside of [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ] are also allowed. The default value of \f3\fs18 adj \f4\fs20 , \f3\fs18 NULL \f4\fs20 , is equivalent to \f3\fs18 c(0.5,\'a00.5) \f4\fs20 , aligning the center of the text to ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ) both horizontally and vertically.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)points(numeric\'a0x, numeric\'a0y, [integer\'a0symbol\'a0=\'a00], [string\'a0color\'a0=\'a0"red"], [string\'a0border\'a0=\'a0"black"], [numeric\'a0lwd\'a0=\'a01.0], [numeric\'a0size\'a0=\'a01.0], [float\'a0alpha\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds point data given by \f3\fs18 x \f4\fs20 and \f3\fs18 y \f4\fs20 to the plot. The data will be plotted as a set of point symbols, centered at the ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ) positions given; note that the \f3\fs18 x \f4\fs20 and \f3\fs18 y \f4\fs20 vectors must be the same length. The new point data will be plotted on top of any previously added data.\ The symbol plotted for each point depends upon the value of \f3\fs18 symbol \f4\fs20 . In general, symbols will be drawn in color \f3\fs18 color \f4\fs20 , with a line width \f3\fs18 lwd \f4\fs20 used for lines (if any) in the symbol, and an overall size scaled by \f3\fs18 size \f4\fs20 . Symbols \f3\fs18 21 \f4\fs20 \'96 \f3\fs18 25 \f4\fs20 involve both a filled shape and a border line around that shape; for those symbols, the border line will use the color provided by \f3\fs18 border \f4\fs20 , while the filled shape will use color \f3\fs18 color \f4\fs20 . Opacity values may be supplied with \f3\fs18 alpha \f4\fs20 ; alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. All of these parameters ( \f3\fs18 symbol \f4\fs20 , \f3\fs18 color \f4\fs20 , \f3\fs18 border \f4\fs20 , \f3\fs18 lwd \f4\fs20 , \f3\fs18 size \f4\fs20 , and \f3\fs18 alpha \f4\fs20 ) may either be a singleton value applied to all points, or a vector with one value per corresponding point.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)rects(numeric\'a0x1, numeric\'a0y1, numeric\'a0x2, numeric\'a0y2, [string\'a0color\'a0=\'a0"red"], [string\'a0border\'a0=\'a0"black"], [numeric\'a0lwd\'a0=\'a01.0], [float\'a0alpha\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds rectangle data given by \f3\fs18 x1 \f4\fs20 , \f3\fs18 y1 \f4\fs20 , \f3\fs18 x2 \f4\fs20 , and \f3\fs18 y2 \f4\fs20 to the plot; note that these four vectors must all be the same length. The data will be plotted as a series of rectangles, defined by each ( \f1\i x1 \f4\i0 , \f1\i y1 \f4\i0 , \f1\i x2 \f4\i0 , \f1\i y2 \f4\i0 ) quadret providing the left, right, top and bottom of each rectangle. The coordinates of each rectangle do not need to be sorted; in other words, it is not required that \f3\fs18 x1[i] <= x2[i] \f4\fs20 , or that \f3\fs18 y1[i] <= y2[i] \f4\fs20 , and different sorting orders for the coordinates of a given rectangle will draw identically. The fill of a each rectangle is contained within its coordinates, but the frame of each rectangle is composed of lines of some width, drawn between the vertices of the rectangle, and will therefore extend beyond the coordinates of the rectangle by one-half of the line width; if this is not desired, inset the rectangle coordinates by one-half of the line width to compensate. The new rectangle data will be plotted on top of any previously added data.\ The rectangles will be drawn in fill colors \f3\fs18 color \f4\fs20 , with border colors \f3\fs18 border \f4\fs20 , using line widths \f3\fs18 lwd \f4\fs20 , with opacities \f3\fs18 alpha \f4\fs20 applied to both the fill and the border. The \f3\fs18 color \f4\fs20 , \f3\fs18 border \f4\fs20 , \f3\fs18 lwd \f4\fs20 , and \f3\fs18 alpha \f4\fs20 parameters may each be either a singleton value (applying to all rectangles) or a vector with one value per rectangle. Alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. If a filled rectangle with no border is desired, a \f3\fs18 border \f4\fs20 value of \f3\fs18 "none" \f4\fs20 can be used; similarly, if a framed rectangle with no fill is desired, a \f3\fs18 color \f4\fs20 value of \f3\fs18 "none" \f4\fs20 can be used. Note that that \f3\fs18 "none" \f4\fs20 a special color value supported only by \f3\fs18 rects() \f4\fs20 ; it is not part of the standard set of named colors in Eidos.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)segments(numeric\'a0x1, numeric\'a0y1, numeric\'a0x2, numeric\'a0y2, [string\'a0color\'a0=\'a0"red"], [numeric\'a0lwd\'a0=\'a01.0], [float\'a0alpha\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds line segment data given by \f3\fs18 x1 \f4\fs20 , \f3\fs18 y1 \f4\fs20 , \f3\fs18 x2 \f4\fs20 , and \f3\fs18 y2 \f4\fs20 to the plot; note that these four vectors must all be the same length. The data will be plotted as a series of unconnected line segments, from each ( \f1\i x1 \f4\i0 , \f1\i y1 \f4\i0 ) position to the corresponding ( \f1\i x2 \f4\i0 , \f1\i y2 \f4\i0 ) position. The new line segment data will be plotted on top of any previously added data.\ The line segments will be drawn in colors \f3\fs18 color \f4\fs20 , using line widths \f3\fs18 lwd \f4\fs20 , with opacities \f3\fs18 alpha \f4\fs20 . The \f3\fs18 color \f4\fs20 , \f3\fs18 lwd \f4\fs20 , and \f3\fs18 alpha \f4\fs20 parameters may each be either a singleton value (applying to all line segments) or a vector with one value per line segment. Alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. See also \f3\fs18 lines() \f4\fs20 and \f3\fs18 abline() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)setBorderless([numeric$\'a0marginLeft\'a0=\'a00.0], [numeric$\'a0marginTop\'a0=\'a00.0], [numeric$\'a0marginRight\'a0=\'a00.0], [numeric$\'a0marginBottom\'a0=\'a00.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Reconfigures the plot to be borderless \'96 to not display axes, ticks, or the labels for axes and ticks. Since those elements are not present, the plot area in borderless plots essentially fills the window\'92s available area, except for being inset by the margins given in \f3\fs18 marginLeft \f4\fs20 , \f3\fs18 marginTop \f4\fs20 , \f3\fs18 marginRight \f4\fs20 , and \f3\fs18 marginBottom \f4\fs20 , which are specified in pixels. The axis ranges are not extended in the usual manner; if the range of the \f1\i x \f4\i0 axis is specified as \f3\fs18 c(0,1) \f4\fs20 , for example, then that is the range actually used for the \f1\i x \f4\i0 axis. (This is similar to R\'92s \f3\fs18 "xaxs" \f4\fs20 and \f3\fs18 "yaxs" \f4\fs20 being specified as \f3\fs18 "i" \f4\fs20 , rather than the usual behavior specified by the default of \f3\fs18 "r" \f4\fs20 .) If you want the plotted data to have \'93breathing room\'94 around it, you should therefore specify non-zero margins.\ Note that the \f3\fs18 fullBox \f4\fs20 parameter of \f3\fs18 createPlot() \f4\fs20 is still honored; for borderless plots, the box is drawn at the outer edge of the margin area. If you don\'92t want the box to overdraw any of the data area of the plot (within the extents of the axes), you should specify each margin value as \f3\fs18 1 \f4\fs20 (or greater), to provide a pixel of margin space within which the box will be drawn without impinging upon the plotted data.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)text(numeric\'a0x, numeric\'a0y, string\'a0labels, [string\'a0color\'a0=\'a0"black"], [numeric\'a0size\'a0=\'a010.0], [Nif\'a0adj\'a0=\'a0NULL], [float\'a0alpha\'a0=\'a01.0], [numeric\'a0angle\'a0=\'a00.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds text data given by \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 labels \f4\fs20 to the plot. The string values in \f3\fs18 labels \f4\fs20 will be plotted at the ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ) positions given; note that \f3\fs18 x \f4\fs20 , \f3\fs18 y \f4\fs20 , and \f3\fs18 labels \f4\fs20 must all be the same length. The new text data will be plotted on top of any previously added data.\ The text will be drawn in color \f3\fs18 color \f4\fs20 , at the font size given by \f3\fs18 size \f4\fs20 (measured in \'93points\'94, the standard metric of font sizes); the font family and style cannot be controlled at this time. Opacity values may be supplied with \f3\fs18 alpha \f4\fs20 ; alpha values must be in [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ], where \f3\fs18 0.0 \f4\fs20 is fully transparent and \f3\fs18 1.0 \f4\fs20 is fully opaque. The angle at which the text is drawn can be specified with \f3\fs18 angle \f4\fs20 ; angles are measured in degrees, clockwise. All of these parameters ( \f3\fs18 color \f4\fs20 , \f3\fs18 size \f4\fs20 , \f3\fs18 alpha \f4\fs20 , and \f3\fs18 angle \f4\fs20 ) may either be a singleton value applied to all points, or a vector with one value per corresponding point.\ The exact position of the text, relative to each point ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ), is adjusted by the optional parameter \f3\fs18 adj \f4\fs20 . The value of \f3\fs18 adj \f4\fs20 , if specified, must be a vector of length \f3\fs18 2 \f4\fs20 , where \f3\fs18 adj[0] \f4\fs20 adjusts the \f1\i x \f4\i0 position and \f3\fs18 adj[1] \f4\fs20 adjusts the \f1\i y \f4\i0 position of the text. Relative to a given \f1\i x \f4\i0 position, a value of \f3\fs18 0.0 \f4\fs20 aligns the left edge of the text to it; a value of \f3\fs18 0.5 \f4\fs20 aligns the center of the text to it; and a value of \f3\fs18 1.0 \f4\fs20 aligns the right edge of the text to it. Similarly, relative to a given \f1\i y \f4\i0 position, a value of \f3\fs18 0.0 \f4\fs20 aligns the bottom edge of the text to it; a value of \f3\fs18 0.5 \f4\fs20 aligns the center of the text to it; and a value of \f3\fs18 1.0 \f4\fs20 aligns the top edge of the text to it. Intermediate values will produce intermediate alignments, and values of \f3\fs18 adj \f4\fs20 outside of [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ] are also allowed. The default value of \f3\fs18 adj \f4\fs20 , \f3\fs18 NULL \f4\fs20 , is equivalent to \f3\fs18 c(0.5,\'a00.5) \f4\fs20 , aligning the center of the text to ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ) both horizontally and vertically.\ See also the \f3\fs18 mtext() \f4\fs20 method, for drawing text outside of the plot area.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)write(string$\'a0filePath)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Writes the plot to the given filesystem path \f3\fs18 filePath \f4\fs20 as a PDF file. It is suggested, but not required, that \f3\fs18 filePath \f4\fs20 should end in a \f3\fs18 .pdf \f4\fs20 or \f3\fs18 .PDF \f4\fs20 filename extension. If the file cannot be written, an error will result.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.13 Class SLiMEidosBlock\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.13.1 \f2\fs18 SLiMEidosBlock \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 active <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If this evaluates to \f3\fs18 logical \f4\fs20 \f3\fs18 F \f4\fs20 (i.e., is equal to \f3\fs18 0 \f4\fs20 ), the script block is inactive and will not be called. The value of \f3\fs18 active \f4\fs20 for all registered script blocks is reset to \f3\fs18 -1 \f4\fs20 at the beginning of each tick, prior to script events being called, thus activating all blocks (except callbacks associated with a species that is not active in that tick, which are deactivated as part of the deactivation of the species). Any \f3\fs18 integer \f4\fs20 value other than \f3\fs18 -1 \f4\fs20 may be used instead of \f3\fs18 -1 \f4\fs20 to represent that a block is active; for example, \f3\fs18 active \f4\fs20 may be used as a counter to make a block execute a fixed number of times in each tick. This value is not cached by SLiM; if it is changed, the new value takes effect immediately. For example, a callback might be activated and inactivated repeatedly during a single tick.\cf0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 end => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The last tick in which the script block is active. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier for this script block; for script \f3\fs18 s3 \f4\fs20 , for example, this is \f3\fs18 3 \f4\fs20 . A script block for which no \f3\fs18 id \f4\fs20 was given will have an \f3\fs18 id \f4\fs20 of \f3\fs18 -1 \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 source => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The source code string of the script block. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 speciesSpec => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 species \f4\fs20 specifier for the script block. The species specifier for a callback block indicates the callback\'92s associated species; the callback is called to modify the default behavior for that species. If the script block has no \f3\fs18 species \f4\fs20 specifier, this property\'92s value is a zero-length \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Species \f4\fs20 . This property is read-only; normally it is set by preceding the definition of a callback with a \f3\fs18 species \f4\fs20 specifier, of the form \f3\fs18 species \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 start => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The first tick in which the script block is active. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 ticksSpec => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 ticks \f4\fs20 specifier for the script block. The \f3\fs18 ticks \f4\fs20 specifier for an event block indicates the event\'92s associated species; the event executes only in ticks when that species is active. If the script block has no \f3\fs18 ticks \f4\fs20 specifier, this property\'92s value is a zero-length \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Species \f4\fs20 . This property is read-only; normally it is set by preceding the definition of an event with a \f3\fs18 ticks \f4\fs20 specifier, of the form \f3\fs18 ticks \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 type => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The type of the script block; this will be \f3\fs18 "first" \f4\fs20 , \f3\fs18 "early" \f4\fs20 , or \f3\fs18 "late" \f4\fs20 for the three types of Eidos events, or \f3\fs18 "initialize" \f4\fs20 , \f3\fs18 "fitnessEffect" \f4\fs20 , \f3\fs18 "interaction" \f4\fs20 , \f3\fs18 "mateChoice" \f4\fs20 , \f3\fs18 "modifyChild" \f4\fs20 , \f3\fs18 "mutation" \f4\fs20 , \f3\fs18 "mutationEffect" \f4\fs20 , \f3\fs18 "recombination" \f4\fs20 , \f3\fs18 "reproduction" \f4\fs20 , or \f3\fs18 "survival" \f4\fs20 for the respective types of Eidos callbacks. \f3\fs18 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.13.2 \f2\fs18 SLiMEidosBlock \f1\fs22 methods\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f5\i0 \cf0 \ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b \cf0 5.14 Class SLiMgui\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.14.1 \f2\fs18 SLiMgui \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \expnd0\expndtw0\kerning0 pid => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The Un*x process identifier (commonly called the \'93pid\'94) of the running SLiMgui application. This can be useful for scripts that wish to use system calls to influence the SLiMgui application.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 \kerning1\expnd0\expndtw0 5.14.2 \f2\fs18 SLiMgui \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(No$)createPlot(string$\'a0title, [Nif\'a0xrange\'a0=\'a0NULL], [Nif\'a0yrange\'a0=\'a0NULL], [string$\'a0xlab\'a0=\'a0"x"], [string$\'a0ylab\'a0=\'a0"y"], [Nif$\'a0width\'a0=\'a0NULL], [Nif$\'a0height\'a0=\'a0NULL], [logical$\'a0horizontalGrid\'a0=\'a0F], [logical$\'a0verticalGrid\'a0=\'a0F], [logical$\'a0fullBox\'a0=\'a0T], [numeric$\'a0axisLabelSize\'a0=\'a015], [numeric$\'a0tickLabelSize\'a0=\'a010])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Creates and returns a new custom plot referred to by \f3\fs18 title \f4\fs20 , or restarts and returns the existing plot with that title; if the plot cannot be created (notably, when running under SLiMguiLegacy rather than SLiMgui), \f3\fs18 NULL \f4\fs20 is returned. The range for the \f1\i x \f4\i0 and \f1\i y \f4\i0 axes of the plot can optionally be provided in \f3\fs18 xrange \f4\fs20 and \f3\fs18 yrange \f4\fs20 , as vectors of length \f3\fs18 2 \f4\fs20 containing the minimum and maximum values for the corresponding axis; the default of \f3\fs18 NULL \f4\fs20 for these parameters requests that the axis ranges be determined heuristically based upon the data subsequently added to the plot. Labels for the \f1\i x \f4\i0 and \f1\i y \f4\i0 axes can be provided in \f3\fs18 xlab \f4\fs20 and \f3\fs18 ylab \f4\fs20 ; if no axis label is desired, the empty string \f3\fs18 "" \f4\fs20 may be passed. The width and height of the window itself can optionally be set with \f3\fs18 width \f4\fs20 and \f3\fs18 height \f4\fs20 , in units of pixels (perhaps 70\'96100 pixels per inch, depending on your screen\'92s pixel density); the default of \f3\fs18 NULL \f4\fs20 for these parameters requests SLiMgui\'92s default plot window size. The display of horizontal grid lines, vertical grid lines, and a full box around the plot area can be controlled with \f3\fs18 horizontalGrid \f4\fs20 , \f3\fs18 verticalGrid \f4\fs20 , and \f3\fs18 fullBox \f4\fs20 respectively, and the size (in points) of axis and tick labels can be controlled with \f3\fs18 axisLabelSize \f4\fs20 and \f3\fs18 tickLabelSize \f4\fs20 respectively.\ Once the plot has been created, data can be added to it using \f3\fs18 Plot \f4\fs20 methods such as \f3\fs18 lines() \f4\fs20 , \f3\fs18 points() \f4\fs20 , and \f3\fs18 text() \f4\fs20 . As with other plot windows in SLiMgui, the \'93action button\'94 can be used to access plot configuration options, and to copy or save the final plot as a raster image or a PDF file.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(Nfs)logFileData(object$\'a0logFile, is$\'a0column)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing data from the \f3\fs18 LogFile \f4\fs20 object \f3\fs18 logFile \f4\fs20 , taken from a specified column (identified in \f3\fs18 column \f4\fs20 either by the column\'92s name or by its zero-based index). If the data are all numeric, they will be returned as a \f3\fs18 float \f4\fs20 vector. Otherwise \'96 if the data are non-numeric \'96 they will be returned as a \f3\fs18 string \f4\fs20 vector. If the specified column does not exist in the log file, \f3\fs18 NULL \f4\fs20 will be returned.\ This functionality is provided as a method on the \f3\fs18 SLiMgui \f4\fs20 class, rather than on \f3\fs18 LogFile \f4\fs20 , because in SLiMgui logged data is kept in memory anyway, for display in the debugging output viewer window. When running at the command line logged data is not kept in memory, and thus is not available.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(void)openDocument(string$\'a0filePath)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Open the document at \f3\fs18 filePath \f4\fs20 in SLiMgui, if possible. Supported document types include SLiM model files (typically with a \f3\fs18 .slim \f4\fs20 path extension), text files (typically with a \f3\fs18 .txt \f4\fs20 path extension, and opened as untitled model files), and PNG, JPG/JPEG, BMP, and GIF image file formats (typically \f3\fs18 .png \f4\fs20 / \f3\fs18 .jpg \f4\fs20 / \f3\fs18 .jpeg \f4\fs20 / \f3\fs18 .bmp \f4\fs20 / \f3\fs18 .gif \f4\fs20 , respectively). (Note that in SLiMguiLegacy, PDF files ( \f3\fs18 .pdf \f4\fs20 ) are supported but these other image file formats are not.) This method can be particularly useful for opening images created by the simulation itself, often by sublaunching a plotting process in R or another environment.\expnd0\expndtw0\kerning0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)pauseExecution(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Pauses a model that is playing in SLiMgui. This is essentially equivalent to clicking the \'93Play\'94 button to stop the execution of the model. Execution can be resumed by the user, by clicking the \'93Play\'94 button again; unlike calling \f3\fs18 stop() \f4\fs20 or \f3\fs18 simulationFinished() \f4\fs20 , the simulation is not terminated. This method can be useful for debugging or exploratory purposes, to pause the model at a point of interest. Execution is paused at the end of the currently executing tick, not mid-tick.\ If the model is being profiled, or is executing forward to a tick number entered in the tick field, \f3\fs18 pauseExecution() \f4\fs20 will do nothing; by design, \f3\fs18 pauseExecution() \f4\fs20 only pauses execution when SLiMgui is doing a simple \'93Play\'94 of the model.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 \'96\'a0(No$)plotWithTitle(string$\'a0title)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns an existing plot that was created by \f3\fs18 createPlot() \f4\fs20 with \f3\fs18 title \f4\fs20 ; if such a plot does not exist, \f3\fs18 NULL \f4\fs20 is returned. Note that other SLiMgui plots cannot be accessed through this method; only plots created by \f3\fs18 createPlot() \f4\fs20 are available in Eidos.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf2 5.15 Class SpatialMap \f4\b0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 (object$)SpatialMap(string$\'a0name, object$\'a0map)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Creates a new \f3\fs18 SpatialMap \f4\fs20 object that is a copy of \f3\fs18 map \f4\fs20 , named \f3\fs18 name \f4\fs20 .\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.15.1 \f2\fs18 SpatialMap \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 gridDimensions => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The dimensions of the spatial map\'92s grid of values, in the order of the components of the map\'92s spatiality. For example, a map with spatiality \f3\fs18 "xz" \f4\fs20 and a grid of values that is \f3\fs18 500 \f4\fs20 in the \f3\fs18 "x" \f4\fs20 dimension by \f3\fs18 300 \f4\fs20 in the \f3\fs18 "z" \f4\fs20 dimension would return \f3\fs18 c(500, 300) \f4\fs20 for this property.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 interpolate <\'96> (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Whether interpolation between grid values is enabled ( \f3\fs18 T \f4\fs20 ) or disabled ( \f3\fs18 F \f4\fs20 ). The initial value of this property is set by \f3\fs18 defineSpatialMap() \f4\fs20 , but it can be changed. The interpolation performed is linear; for cubic interpolation, use the \f3\fs18 interpolate() \f4\fs20 method.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 name => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The name of the spatial map, usually as provided to \f3\fs18 defineSpatialMap() \f4\fs20 . The names of spatial maps must be unique within any given subpopulation, but the same name may be reused for different spatial maps in different subpopulations. The name is used to identify a map for methods such as \f3\fs18 spatialMapValue() \f4\fs20 , and is also used for display in SLiMgui.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 spatialBounds => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The spatial bounds to which the spatial map is aligned. These bounds come from the subpopulation that originally created the map, with the \f3\fs18 defineSpatialMap() \f4\fs20 method, and cannot be subsequently changed. All subpopulations that use a given spatial map must match that map\'92s spatial bounds, so that the map does not stretch or shrink relative to its initial configuration. The components of the spatial bounds of a map correspond to the components of the map\'92s spatiality; for example, a map with spatiality \f3\fs18 "xz" \f4\fs20 will have bounds ( \f3\fs18 x0 \f4\fs20 , \f3\fs18 z0 \f4\fs20 , \f3\fs18 x1 \f4\fs20 , \f3\fs18 z1 \f4\fs20 ); bounds for \f3\fs18 "y" \f4\fs20 are not included, since that dimension is not used by the spatial map.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 spatiality => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The spatiality of the map: the subset of the model\'92s dimensions that are used by the spatial map. The spatiality of a map is configured by \f3\fs18 defineSpatialMap() \f4\fs20 and cannot subsequently be changed. For example, a 3D model (with dimensionality \f3\fs18 "xyz" \f4\fs20 ) might define a 2D spatial map with spatiality \f3\fs18 "xz" \f4\fs20 , providing spatial values that do not depend upon the \f3\fs18 "y" \f4\fs20 dimension. Often, however, the spatiality of a map will match the dimensionality of the model.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined, and it is an error to try to read it; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual), for another way of attaching state to spatial maps.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf2 5.15.2 \f2\fs18 SpatialMap \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(object$)add(ifo\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds \f3\fs18 x \f4\fs20 to the spatial map. One possibility is that \f3\fs18 x \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 value; in this case, \f3\fs18 x \f4\fs20 is added to each grid value of the target spatial map. Another possibility is that \f3\fs18 x \f4\fs20 is an \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 vector/matrix/array of the same dimensions as the target spatial map\'92s grid; in this case, each value of \f3\fs18 x \f4\fs20 is added to the corresponding grid value of the target spatial map. The third possibility is that \f3\fs18 x \f4\fs20 is itself a (singleton) spatial map; in this case, each grid value of \f3\fs18 x \f4\fs20 is added to the corresponding grid value of the target spatial map (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions). The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)blend(ifo\'a0x, float$\'a0xFraction)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Blends \f3\fs18 x \f4\fs20 into the spatial map, giving \f3\fs18 x \f4\fs20 a weight of \f3\fs18 xFraction \f4\fs20 and the existing values in the target spatial map a weight of \f3\fs18 1 - xFraction \f4\fs20 , such that the resulting values in the target spatial map are then given by \f3\fs18 x\'a0*\'a0xFraction + target\'a0*\'a0(1\'a0-\'a0xFraction) \f4\fs20 . The value of \f3\fs18 xFraction \f4\fs20 must be in [0.0, 1.0].\ One possibility is that \f3\fs18 x \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 value; in this case, \f3\fs18 x \f4\fs20 is blended with each grid value of the target spatial map. Another possibility is that \f3\fs18 x \f4\fs20 is an \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 vector/matrix/array of the same dimensions as the target spatial map\'92s grid; in this case, each value of \f3\fs18 x \f4\fs20 is blended with the corresponding grid value of the target spatial map. The third possibility is that \f3\fs18 x \f4\fs20 is itself a (singleton) spatial map; in this case, each grid value of \f3\fs18 x \f4\fs20 is blended with the corresponding grid value of the target spatial map (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions). The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)changeColors([Nif\'a0valueRange\'a0=\'a0NULL], [Ns\'a0colors\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Changes the color scheme for the target spatial map. The meaning of \f3\fs18 valueRange \f4\fs20 and \f3\fs18 colors \f4\fs20 are identical to their meaning in \f3\fs18 defineSpatialMap() \f4\fs20 , but are also described here.\ The \f3\fs18 valueRange \f4\fs20 and \f3\fs18 colors \f4\fs20 parameters travel together; either both are \f3\fs18 NULL \f4\fs20 , or both are specified. They control how map values will be transformed into colors, by SLiMgui and by the \f3\fs18 mapColor() \f4\fs20 method. The \f3\fs18 valueRange \f4\fs20 parameter establishes the color-mapped range of spatial map values, as a vector of length two specifying a minimum and maximum; this does not need to match the actual range of values in the map. The \f3\fs18 colors \f4\fs20 parameter then establishes the corresponding colors for values within the interval defined by \f3\fs18 valueRange \f4\fs20 : values less than or equal to \f3\fs18 valueRange[0] \f4\fs20 will map to \f3\fs18 colors[0] \f4\fs20 , values greater than or equal to \f3\fs18 valueRange[1] \f4\fs20 will map to the last \f3\fs18 colors \f4\fs20 value, and intermediate values will shade continuously through the specified vector of colors, with interpolation between adjacent colors to produce a continuous spectrum. This is much simpler than it sounds in this description; see the recipes for an illustration of its use.\ If \f3\fs18 valueRange \f4\fs20 and \f3\fs18 colors \f4\fs20 are both \f3\fs18 NULL \f4\fs20 , a default grayscale color scheme will be used in SLiMgui, but an error will result if \f3\fs18 mapColor() \f4\fs20 is called.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)changeValues(ifo\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Changes the grid values used for the target spatial map. The parameter \f3\fs18 x \f4\fs20 should be either a \f3\fs18 SpatialMap \f4\fs20 object from which values are taken directly, or a vector, matrix, or array of numeric values as described in the documentation for \f3\fs18 defineSpatialMap() \f4\fs20 . Other characteristics of the spatial map, such as its color mapping (if defined), its spatial bounds, and its spatiality, will remain unchanged. The grid resolution of the spatial map is allowed to change with this method. This method is useful for changing the values of a spatial map over time, such as to implement changes to the landscape\'92s characteristics due to seasonality, climate change, processes such as fire or urbanization, and so forth. As with the original map values provided to \f3\fs18 defineSpatialMap() \f4\fs20 , it is often useful to read map values from a PNG image file using the Eidos class \f3\fs18 Image \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)divide(ifo\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Divides the spatial map by \f3\fs18 x \f4\fs20 . One possibility is that \f3\fs18 x \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 value; in this case, each grid value of the target spatial map is divided by \f3\fs18 x \f4\fs20 . Another possibility is that \f3\fs18 x \f4\fs20 is an \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 vector/matrix/array of the same dimensions as the target spatial map\'92s grid; in this case, each grid value of the target spatial map is divided by the corresponding value of \f3\fs18 x \f4\fs20 . The third possibility is that \f3\fs18 x \f4\fs20 is itself a (singleton) spatial map; in this case, each grid value of the target spatial map is divided by the corresponding grid value of \f3\fs18 x \f4\fs20 (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions). The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)exp(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Exponentiates the values of the spatial map. More precisely, each grid value \f1\i x \f4\i0 of the target spatial map is exponentiated \'96 replaced by the value \f1\i e \fs13\fsmilli6667 \super x \f4\i0\fs20 \nosupersub . The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)gridValues(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the values for the spatial map\'92s grid as a vector (for a 1D map), a matrix (for a 2D map), or an array (for a 3D map). The form and orientation of the returned values is such that it could be used to create a new spatial map, with \f3\fs18 defineSpatialMap() \f4\fs20 , which would be identical to the original.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)interpolate(integer$\'a0factor, [string$\'a0method\'a0=\'a0"linear"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Increases the resolution of the spatial map by \f3\fs18 factor \f4\fs20 , changing the dimensions of the spatial map\'92s grid of values (while leaving its spatial bounds unchanged), by interpolating new values between the existing values. The parameter \f3\fs18 factor \f4\fs20 must be an integer in [ \f3\fs18 2 \f4\fs20 , \f3\fs18 10001 \f4\fs20 ], somewhat arbitrarily. The target spatial map is returned, to allow easy chaining of operations.\ For a 1D spatial map, \f3\fs18 factor-1 \f4\fs20 new values will be inserted between every pair of values in the original value grid. A \f3\fs18 factor \f4\fs20 of \f3\fs18 2 \f4\fs20 would therefore insert one new value between each pair of existing values, thereby increasing the map\'92s resolution by a factor of two. Note that if the spatial map\'92s original grid dimension was \f1\i N \f4\i0 , the new grid dimension with a \f3\fs18 factor \f4\fs20 of \f1\i k \f4\i0 would be \f1\i k \f4\i0 ( \f1\i N \f4\i0 \uc0\u8722 1)+1, not \f1\i kN \f4\i0 , because new values are inserted only \f1\i between \f4\i0 existing values. For 2D and 3D spatial maps, essentially the same process is conducted along each axis of the map\'92s spatiality, increasing the resolution of the map by \f3\fs18 factor \f4\fs20 in every dimension.\ If \f3\fs18 method \f4\fs20 is \f3\fs18 "linear" \f4\fs20 (the default), linear (or bilinear or trilinear, for 2D/3D maps) interpolation will be used to interpolate the values for the new grid points. Alternatively, if \f3\fs18 method \f4\fs20 is \f3\fs18 "nearest" \f4\fs20 , the nearest value in the old grid will be used for new grid points; with this method, it is recommended that \f3\fs18 factor \f4\fs20 be odd, not even, to avoid artifacts due to rounding of coordinates midway between the original grid positions. If method is \f3\fs18 "cubic" \f4\fs20 , cubic (or bicubic, for 2D maps) will be used; this generally produces smoother interpolation with fewer artifacts than \f3\fs18 "linear" \f4\fs20 , but it is not supported for 3D maps. The choice of interpolation method used here is independent of the map\'92s \f3\fs18 interpolate \f4\fs20 property. Note that while the \f3\fs18 "nearest" \f4\fs20 and \f3\fs18 "linear" \f4\fs20 interpolation methods will leave the range of values in the map unchanged, \f3\fs18 "cubic" \f4\fs20 interpolation may produce interpolated values that are outside the original range of values (by design). Periodic boundaries are currently supported only for \f3\fs18 "nearest" \f4\fs20 , \f3\fs18 "linear" \f4\fs20 , and 1D \f3\fs18 "cubic" \f4\fs20 interpolation.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(string)mapColor(numeric\'a0value)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Uses the spatial map\'92s color-translation machinery (as defined by the \f3\fs18 valueRange \f4\fs20 and \f3\fs18 colors \f4\fs20 parameters to \f3\fs18 defineSpatialMap() \f4\fs20 ) to translate each element of \f3\fs18 value \f4\fs20 into a corresponding color string. If the spatial map does not have color-translation capabilities, an error will result. See the documentation for \f3\fs18 defineSpatialMap() \f4\fs20 for information regarding the details of color translation. See the Eidos manual for further information on color strings.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)mapImage([Ni$\'a0width\'a0=\'a0NULL], [Ni$\'a0height\'a0=\'a0NULL], [logical$\'a0centers\'a0=\'a0F], [logical$\'a0color\'a0=\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns an \f3\fs18 Image \f4\fs20 object sampled from the spatial map. The image will be \f3\fs18 width \f4\fs20 pixels wide and \f3\fs18 height \f4\fs20 pixels tall; the intrinsic size of the spatial map itself will be used if one of these parameters is \f3\fs18 NULL \f4\fs20 . The image will be oriented in the same way as it is displayed in SLiMgui (which conceptually entails a transformation from matrix coordinates, which store values by column, to standard image coordinates, which store values by row; see the Eidos manual\'92s documentation of \f3\fs18 Image \f4\fs20 for details). This method may only be called for 2D spatial maps at present.\ The sampling of the spatial map can be done in one of two ways, as controlled by the \f3\fs18 centers \f4\fs20 parameter. If \f3\fs18 centers \f4\fs20 is \f3\fs18 T \f4\fs20 , a ( \f3\fs18 width+1 \f4\fs20 ) \'d7 ( \f3\fs18 height+1 \f4\fs20 ) grid of lines that delineates \f3\fs18 width \f4\fs20 \'d7 \f3\fs18 height \f4\fs20 rectangular pixels will be overlaid on top of the spatial map, and values will be sampled from the spatial map at the \f1\i center \f4\i0 of each of these pixels. If \f3\fs18 centers \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), a \f3\fs18 width \f4\fs20 \'d7 \f3\fs18 height \f4\fs20 grid of lines will be overlaid on top of the spatial map, and values will be sampled from the spatial map at the \f1\i vertices \f4\i0 of the grid. If interpolation is not enabled for the spatial map, these two options will both recover the original matrix of values used to define the spatial map (assuming, here and below, that \f3\fs18 width \f4\fs20 and \f3\fs18 height \f4\fs20 are \f3\fs18 NULL \f4\fs20 ). If interpolation is enabled for the spatial map, however, \f3\fs18 centers == F \f4\fs20 will recover the original values, but will not capture the \'93typical\'94 value of each pixel in the image; \f3\fs18 centers == T \f4\fs20 , on the other hand, will not recover the original values, but will capture the \'93typical\'94 value of each pixel in the image (i.e., the value at the center of each pixel, as produced by interpolation).\ If \f3\fs18 color \f4\fs20 is \f3\fs18 T \f4\fs20 (the default), the \f3\fs18 valueRange \f4\fs20 and \f3\fs18 colors \f4\fs20 parameters supplied to \f3\fs18 defineSpatialMap() \f4\fs20 will be used to translate map values to RGB color values as described in the documentation of that method, providing the same appearance as in SLiMgui; of course those parameters must have been supplied, otherwise an error will result. If \f3\fs18 color \f4\fs20 is \f3\fs18 F \f4\fs20 , on the other hand, a grayscale image will be produced that directly reflects the map values without color translation. In this case, this method needs to translate map values, which can have any \f3\fs18 float \f4\fs20 value, into grayscale pixel values that are integers in [ \f3\fs18 0 \f4\fs20 , \f3\fs18 255 \f4\fs20 ]. To do so, the map values are multiplied by \f3\fs18 255.0 \f4\fs20 , clamped to [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 255.0 \f4\fs20 ], and then rounded to the nearest integer. This translation scheme essentially assumes that map values are in [0, 1]; for spatial maps that were defined using the \f3\fs18 floatK \f4\fs20 channel of a grayscale PNG image, this should recover the original image\'92s pixel values. (If a different translation scheme is desired, \f3\fs18 color=T \f4\fs20 with the desired \f3\fs18 valueRange \f4\fs20 and \f3\fs18 colors \f4\fs20 should be used.)\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)mapValue(float\'a0point)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Uses the spatial map\'92s mapping machinery (as defined by the \f3\fs18 gridSize \f4\fs20 , \f3\fs18 values \f4\fs20 , and \f3\fs18 interpolate \f4\fs20 parameters to \f3\fs18 defineSpatialMap() \f4\fs20 ) to translate the coordinates of \f3\fs18 point \f4\fs20 into a corresponding map value. The length of \f3\fs18 point \f4\fs20 must be equal to the spatiality of the spatial map; in other words, for a spatial map with spatiality \f3\fs18 "xz" \f4\fs20 , \f3\fs18 point \f4\fs20 must be of length \f3\fs18 2 \f4\fs20 , specifying the \f1\i x \f4\i0 and \f1\i z \f4\i0 coordinates of the point to be evaluated. Interpolation will automatically be used if it was enabled for the spatial map. Point coordinates are clamped into the range defined by the spatial boundaries, even if the spatial boundaries are periodic; use \f3\fs18 pointPeriodic() \f4\fs20 to wrap the point coordinates first if desired. See the documentation for \f3\fs18 defineSpatialMap() \f4\fs20 for information regarding the details of value mapping.\ The \f3\fs18 point \f4\fs20 parameter may also contain more than one point to be looked up. In this case, the length of \f3\fs18 point \f4\fs20 must be an exact multiple of the spatiality of the spatial map; for a spatial map with spatiality \f3\fs18 "xz" \f4\fs20 , for example, the length of \f3\fs18 point \f4\fs20 must be an exact multiple of \f3\fs18 2 \f4\fs20 , and successive pairs of elements from point (elements \f3\fs18 0 \f4\fs20 and \f3\fs18 1 \f4\fs20 , then elements \f3\fs18 2 \f4\fs20 and \f3\fs18 3 \f4\fs20 , etc.) will be taken as the \f1\i x \f4\i0 and \f1\i z \f4\i0 coordinates of the points to be evaluated. This allows \f3\fs18 mapValue() \f4\fs20 to be used in a vectorized fashion.\ The \f3\fs18 spatialMapValue() \f4\fs20 method of \f3\fs18 Subpopulation \f4\fs20 provides essentially the same functionality as this method; it may be more convenient to use, for some usage cases, and it checks that the spatial map is actually added to the subpopulation in question, providing an additional consistency check. However, either method may be used.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)multiply(ifo\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Multiplies the spatial map by \f3\fs18 x \f4\fs20 . One possibility is that \f3\fs18 x \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 value; in this case, each grid value of the target spatial map is multiplied by \f3\fs18 x \f4\fs20 . Another possibility is that \f3\fs18 x \f4\fs20 is an \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 vector/matrix/array of the same dimensions as the target spatial map\'92s grid; in this case, each grid value of the target spatial map is multiplied by the corresponding value of \f3\fs18 x \f4\fs20 . The third possibility is that \f3\fs18 x \f4\fs20 is itself a (singleton) spatial map; in this case, each grid value of the target spatial map is multiplied by the corresponding grid value of \f3\fs18 x \f4\fs20 (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions). The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)power(ifo\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Raises the spatial map to the power \f3\fs18 x \f4\fs20 . One possibility is that \f3\fs18 x \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 value; in this case, each grid value of the target spatial map is raised to the power \f3\fs18 x \f4\fs20 . Another possibility is that \f3\fs18 x \f4\fs20 is an \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 vector/matrix/array of the same dimensions as the target spatial map\'92s grid; in this case, each grid value of the target spatial map is raised to the power of the corresponding value of \f3\fs18 x \f4\fs20 . The third possibility is that \f3\fs18 x \f4\fs20 is itself a (singleton) spatial map; in this case, each grid value of the target spatial map is raised to power of the corresponding grid value of \f3\fs18 x \f4\fs20 (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions). The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)range(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the range of values contained in the spatial map. The result is a \f3\fs18 float \f4\fs20 vector of length \f3\fs18 2 \f4\fs20 ; the first element is the minimum map value, and the second element is the maximum map value.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)rescale([numeric$\'a0min\'a0=\'a00.0], [numeric$\'a0max\'a0=\'a01.0])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Rescales the values of the spatial map to the range [ \f3\fs18 min \f4\fs20 , \f3\fs18 max \f4\fs20 ]. By default, the rescaling is to the range [ \f3\fs18 0.0 \f4\fs20 , \f3\fs18 1.0 \f4\fs20 ]. It is required that \f3\fs18 min \f4\fs20 be less than \f3\fs18 max \f4\fs20 , and that both be finite. Note that the final range may not be exactly [ \f3\fs18 min \f4\fs20 , \f3\fs18 max \f4\fs20 ] due to numerical error. The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)sampleImprovedNearbyPoint(float\'a0point, float$\'a0maxDistance, string$\'a0functionType, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This variant of \f3\fs18 sampleNearbyPoint() \f4\fs20 samples a Metropolis\'96Hastings move on the spatial map. See \f3\fs18 sampleNearbyPoint() \f4\fs20 for discussion of the basic idea. This method proposes a nearby point drawn from the given kernel. If the drawn point has a larger map value than the original point, the new point is returned. If the drawn point has a smaller map value than the original point, it is returned with a probability equal to the ratio between its map value and the original map value, otherwise the original point is returned. The distribution of points that move (or not) to new locations governed by this method will converge upon the map itself, in a similar manner to how MCMC converges upon the posterior distribution (assuming no other forces, such as birth or death, influence the distribution of individuals). Movement governed by this method is \'93improved\'94 in the sense that points will tend to remain where they are unless the new sampled point is an improvement for them \'96 a higher map value. Note that unlike \f3\fs18 sampleNearbyPoint() \f4\fs20 , this method requires that all map values are non-negative.\ The parameter \f3\fs18 point \f4\fs20 may contain any number of points; the returned vector will contain corresponding points sampled as described above. Each supplied point must provide coordinates precisely as specified by the spatiality of the target map; for example, if the target map\'92s spatiality is \f3\fs18 "xz" \f4\fs20 (in an \f3\fs18 "xyz" \f4\fs20 species), each point must contain two elements, providing the \f1\i x \f4\i0 and \f1\i z \f4\i0 coordinate. Be careful; this means that in general it is not safe to pass an individual\'92s \f3\fs18 spatialPosition \f4\fs20 property for \f3\fs18 point \f4\fs20 , for example (although it is safe if the spatiality of the map matches the dimensionality of the simulation); other properties on \f3\fs18 Individual \f4\fs20 exist for getting the individual\'92s coordinates in a particular spatiality, such as the \f3\fs18 xz \f4\fs20 property for this example. Supplied points are not required to be within bounds, but since nearby points are sampled from the given kernel and must be within bounds, an infinite loop might result if a supplied point is substantially outside bounds.\ The kernel is specified with a kernel type, \f3\fs18 functionType \f4\fs20 , followed by zero or more ellipsis arguments; see \f3\fs18 smooth() \f4\fs20 for further information. For this method, at present only kernel types \f3\fs18 "f" \f4\fs20 , \f3\fs18 "l" \f4\fs20 , \f3\fs18 "e" \f4\fs20 , \f3\fs18 "n" \f4\fs20 , and \f3\fs18 "t" \f4\fs20 are supported, and type \f3\fs18 "t" \f4\fs20 is not presently supported for 3D kernels. The parameters that define the kernel\'92s shape \'96 the ellipsis arguments that follow \f3\fs18 functionType \f4\fs20 \'96 may each, independently, be either a singleton or a vector with length equal to the number of points, providing a separate value for each point being processed. In this way, all of the nearby points can be drawn from the same kernel, or each from a separately defined kernel. Since \f3\fs18 maxDistance \f4\fs20 and \f3\fs18 functionType \f4\fs20 are required to be singletons, however, their values cannot vary from point to point in the present design.\ See also the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 deviatePositionsWithMap() \f4\fs20 , which is conceptually similar to this method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)sampleNearbyPoint(float\'a0point, float$\'a0maxDistance, string$\'a0functionType, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 For a spatial point supplied in \f3\fs18 point \f4\fs20 , returns a nearby point sampled from a kernel weighted by the spatial map\'92s values. Only points within the maximum distance of the kernel, \f3\fs18 maxDistance \f4\fs20 , will be chosen, and the probability that a given point is chosen will be proportional to the density of the kernel at that point multiplied by the value of the map at that point (interpolated, if interpolation is enabled for the map). Negative values of the map will be treated as zero. The point returned will be within spatial bounds, respecting periodic boundaries if in effect (so there is no need to call \f3\fs18 pointPeriodic() \f4\fs20 on the result).\ The parameter \f3\fs18 point \f4\fs20 may contain any number of points; the returned vector will contain corresponding points sampled as described above. Each supplied point must provide coordinates precisely as specified by the spatiality of the target map; for example, if the target map\'92s spatiality is \f3\fs18 "xz" \f4\fs20 (in an \f3\fs18 "xyz" \f4\fs20 species), each point must contain two elements, providing the \f1\i x \f4\i0 and \f1\i z \f4\i0 coordinate. Be careful; this means that in general it is not safe to pass an individual\'92s \f3\fs18 spatialPosition \f4\fs20 property for \f3\fs18 point \f4\fs20 , for example (although it is safe if the spatiality of the map matches the dimensionality of the simulation); other properties on \f3\fs18 Individual \f4\fs20 exist for getting the individual\'92s coordinates in a particular spatiality, such as the \f3\fs18 xz \f4\fs20 property for this example. Supplied points are not required to be within bounds, but since nearby points are sampled from the given kernel and must be within bounds, an infinite loop might result if a supplied point is substantially outside bounds.\ The kernel is specified with a kernel type, \f3\fs18 functionType \f4\fs20 , followed by zero or more ellipsis arguments; see \f3\fs18 smooth() \f4\fs20 for further information. For this method, at present only kernel types \f3\fs18 "f" \f4\fs20 , \f3\fs18 "l" \f4\fs20 , \f3\fs18 "e" \f4\fs20 , \f3\fs18 "n" \f4\fs20 , and \f3\fs18 "t" \f4\fs20 are supported, and type \f3\fs18 "t" \f4\fs20 is not presently supported for 3D kernels. The parameters that define the kernel\'92s shape \'96 the ellipsis arguments that follow \f3\fs18 functionType \f4\fs20 \'96 may each, independently, be either a singleton or a vector with length equal to the number of points, providing a separate value for each point being processed. In this way, all of the nearby points can be drawn from the same kernel, or each from a separately defined kernel. Since \f3\fs18 maxDistance \f4\fs20 and \f3\fs18 functionType \f4\fs20 are required to be singletons, however, their values cannot vary from point to point in the present design.\ This method can be used to find points in the vicinity of individuals that are favorable \'96 possessing more resources, or better environmental conditions, etc. It can also be used to guide the dispersal or foraging behavior of individuals. See \f3\fs18 sampleImprovedNearbyPoint() \f4\fs20 for a variant that may be useful for directed movement across a landscape. Note that the algorithm for \f3\fs18 sampleNearbyPoint() \f4\fs20 works by rejection sampling, and so will be very inefficient if the maximum value of the map (anywhere, across the entire map) is much larger than the typical value of the map where individuals are. The algorithm for \f3\fs18 sampleImprovedNearbyPoint() \f4\fs20 is different, and does not exhibit this performance issue.\ See also the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 deviatePositionsWithMap() \f4\fs20 , which is conceptually similar to this method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)smooth(float$\'a0maxDistance, string$\'a0functionType, ...)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Smooths (or blurs, one could say) the values of the spatial map by convolution with a kernel. The kernel is specified with a maximum distance \f3\fs18 maxDistance \f4\fs20 (beyond which the kernel cuts off to a value of zero), a kernel type \f3\fs18 functionType \f4\fs20 that should be \f3\fs18 "f" \f4\fs20 , \f3\fs18 "l" \f4\fs20 , \f3\fs18 "e" \f4\fs20 , \f3\fs18 "n" \f4\fs20 , \f3\fs18 "c" \f4\fs20 , or \f3\fs18 "t" \f4\fs20 , and additional parameters in the ellipsis \f3\fs18 ... \f4\fs20 that depend upon the kernel type and further specify its shape. The target spatial map is returned, to allow easy chaining of operations.\ The kernel specification is similar to that for the \f3\fs18 setInteractionType() \f4\fs20 method of \f3\fs18 InteractionType \f4\fs20 , but omits the maximum value of the kernel. Specifically, \f3\fs18 functionType \f4\fs20 may be \f3\fs18 "f" \f4\fs20 , in which case no ellipsis arguments should be supplied; \f3\fs18 "l" \f4\fs20 , similarly with no ellipsis arguments; \f3\fs18 "e" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 lambda (rate) parameter for a negative exponential function; \f3\fs18 "n" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 sigma (standard deviation) parameter for a Gaussian function; \f3\fs18 "c" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 scale parameter for a Cauchy distribution function; or \f3\fs18 "t" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 degrees of freedom and a \f3\fs18 numeric$ \f4\fs20 scale parameter for a \f1\i t \f4\i0 -distribution function. See the \f3\fs18 InteractionType \f4\fs20 class documentation for discussions of these kernel types.\ Distance metrics specified to this method, such as \f3\fs18 maxDistance \f4\fs20 and the additional kernel shape parameters, are measured in the distance scale of the spatial map \'96 the same distance scale in which the spatial bounds of the map are specified. The operation is performed upon the grid values of the spatial map; distances are internally translated into the scale of the value grid. For non-periodic boundaries, clipping at the edge of the spatial map is done; in a 2D map with no periodic boundaries, for example, the weights of edge and corner grid values are adjusted for their partial (one-half and one-quarter) coverage. For periodic boundaries, the smoothing operation will automatically wrap around based upon the assumption that the grid values at the two connected edges of the periodic boundary have identical values (which they should, since by definition they represent the same position in space).\ The density scale of the kernel has no effect and will be normalized; this is the reason that \f3\fs18 smooth() \f4\fs20 , unlike \f3\fs18 InteractionType \f4\fs20 , does not require specification of the maximum value of the kernel. This normalization prevents the kernel from increasing or decreasing the average spatial map value (apart from possible edge effects).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)subtract(ifo\'a0x)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Subtracts \f3\fs18 x \f4\fs20 from the spatial map. One possibility is that \f3\fs18 x \f4\fs20 is a singleton \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 value; in this case, \f3\fs18 x \f4\fs20 is subtracted from each grid value of the target spatial map. Another possibility is that \f3\fs18 x \f4\fs20 is an \f3\fs18 integer \f4\fs20 or \f3\fs18 float \f4\fs20 vector/matrix/array of the same dimensions as the target spatial map\'92s grid; in this case, each value of \f3\fs18 x \f4\fs20 is subtracted from the corresponding grid value of the target spatial map. The third possibility is that \f3\fs18 x \f4\fs20 is itself a (singleton) spatial map; in this case, each grid value of \f3\fs18 x \f4\fs20 is subtracted from the corresponding grid value of the target spatial map (and thus the two spatial maps must match in their spatiality, their spatial bounds, and their grid dimensions). The target spatial map is returned, to allow easy chaining of operations.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.16 Class Species\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.16.1 \f2\fs18 Species \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 avatar => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The avatar string used to represent this species in SLiMgui. Outside of SLiMgui, this property still exists, but is not used by SLiM. Avatars are typically one-character strings, often using an emoji that symbolizes the species. This property is read-only; its value should be set with the \f3\fs18 avatar \f4\fs20 parameter of \f3\fs18 initializeSpecies() \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 chromosome => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 Chromosome \f4\fs20 object used by the species. This property may only be accessed in a single-chromosome model; if there are multiple chromosomes (or none), the \f3\fs18 chromosomes \f4\fs20 property must be used instead.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 chromosomes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 Chromosome \f4\fs20 objects used by the species, in the order in which they were defined. See also the \f3\fs18 sexChromosomes \f4\fs20 property.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 color => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The color used to display information about this species in SLiMgui. Outside of SLiMgui, this property still exists, but is not used by SLiM. Colors may be specified by name, or with hexadecimal RGB values of the form \f3\fs18 "#RRGGBB" \f4\fs20 (see the Eidos manual). This property is read-only; its value should be set with the \f3\fs18 color \f4\fs20 parameter of \f3\fs18 initializeSpecies() \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 cycle <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The current cycle count for this species. This counter begins at 1, and increments at the end of every tick in which the species is active. In models with non-overlapping generations, particularly WF models, this can be thought of as a generation counter.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 description <\'96> (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A human-readable \f3\fs18 string \f4\fs20 description for the species. By default, this is the empty string, \f3\fs18 "" \f4\fs20 ; however, it may be set to whatever you wish.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 dimensionality => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The spatial dimensionality of the simulation for this species, as specified in \f3\fs18 initializeSLiMOptions() \f4\fs20 . This will be \f3\fs18 "" \f4\fs20 (the empty string) for non-spatial simulations (the default), or \f3\fs18 "x" \f4\fs20 , \f3\fs18 "xy" \f4\fs20 , or \f3\fs18 "xyz" \f4\fs20 , for simulations using those spatial dimensions respectively.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 genomicElementTypes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 GenomicElementType \f4\fs20 objects being used in the species.\cf2 These are guaranteed to be in sorted order, by their \f3\fs18 id \f4\fs20 property. \f5 \cf0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The identifier for this species. Species identifiers are determined by their declaration order in the script; the first declared species is given an \f3\fs18 id \f4\fs20 of \f3\fs18 0 \f4\fs20 , the second is given an \f3\fs18 id \f4\fs20 of \f3\fs18 1 \f4\fs20 , and so forth.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 mutationTypes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 MutationType \f4\fs20 objects being used in the species.\cf2 These are guaranteed to be in sorted order, by their \f3\fs18 id \f4\fs20 property. \f5 \cf0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 mutations => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 Mutation \f4\fs20 objects that are currently active in the species. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 name => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A human-readable \f3\fs18 string \f4\fs20 name for the subpopulation. This is always the declared name of the species, as given in the explicit species declaration in script, and cannot be changed. The \f3\fs18 name \f4\fs20 of a species may appear as a label in SLiMgui, and it can be useful in generating output, debugging, and other purposes. See also the description property, which can be changed by the user and used for any purpose.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 nucleotideBased => (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If \f3\fs18 T \f4\fs20 , the model for this species is nucleotide-based; if \f3\fs18 F \f4\fs20 , it is not. See the discussion of the \f3\fs18 nucleotideBased \f4\fs20 parameter to \f3\fs18 initializeSLiMOptions() \f4\fs20 for discussion.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 periodicity => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The spatial periodicity of the simulation for this species, as specified in \f3\fs18 initializeSLiMOptions() \f4\fs20 . This will be \f3\fs18 "" \f4\fs20 (the empty string) for non-spatial simulations and simulations with no periodic spatial dimensions (the default). Otherwise, it will be a string representing the subset of spatial dimensions that have been declared to be periodic, as specified to \f3\fs18 initializeSLiMOptions() \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 scriptBlocks => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects in the simulation that have been declared with this species as their \f3\fs18 species \f4\fs20 specifier ( \f1\i not \f4\i0 \f3\fs18 ticks \f4\fs20 specifier). These will always be callback blocks; callbacks are species-specific, while other types of blocks are not.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 sexChromosomes => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 Chromosome \f4\fs20 objects used by the species that represent sex chromosomes, in the order in which they were defined. Sex chromosomes are specifically those of type \f3\fs18 "X" \f4\fs20 , \f3\fs18 "Y" \f4\fs20 , \f3\fs18 "W" \f4\fs20 , \f3\fs18 "Z" \f4\fs20 , and \f3\fs18 "-Y" \f4\fs20 . See also the \f3\fs18 chromosomes \f4\fs20 property, and the \f3\fs18 isSexChromosome \f4\fs20 property of \f3\fs18 Chromosome \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 sexEnabled => (logical$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 If \f3\fs18 T \f4\fs20 , sex is enabled for this species; if \f3\fs18 F \f4\fs20 , individuals are hermaphroditic. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 subpopulations => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 Subpopulation \f4\fs20 instances currently defined in the species.\cf2 These are guaranteed to be in sorted order, by their \f3\fs18 id \f4\fs20 property. \f5 \cf0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 substitutions => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A vector of \f3\fs18 Substitution \f4\fs20 objects, representing all mutations that have been fixed in this species. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to the simulation. \f5 \ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.16.2 \f2\fs18 Species \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \'96\'a0(object$)addPatternForClone(iso$\'a0chromosome, No$\'a0pattern, object$\'a0parent, [Ns$\'a0sex\'a0=\'a0NULL])\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds an inheritance dictionary for the specified chromosome to the pattern dictionary \f3\fs18 pattern \f4\fs20 , representing producing a clone of \f3\fs18 parent \f4\fs20 , with sex optionally specified by \f3\fs18 sex \f4\fs20 . The parameter \f3\fs18 chromosome \f4\fs20 can provide a chromosome \f3\fs18 id \f4\fs20 (an \f3\fs18 integer \f4\fs20 ), a chromosome \f3\fs18 symbol \f4\fs20 (a \f3\fs18 string \f4\fs20 ), or a \f3\fs18 Chromosome \f4\fs20 object. The resulting pattern dictionary is intended for use with the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 addMultiRecombinant() \f4\fs20 ; see that method for background on the use of pattern dictionaries.\ The parameter \f3\fs18 pattern \f4\fs20 must be a \f3\fs18 Dictionary \f4\fs20 (or a subclass of \f3\fs18 Dictionary \f4\fs20 ), or \f3\fs18 NULL \f4\fs20 . If \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , a new singleton object of class \f3\fs18 Dictionary \f4\fs20 will be created, set up, and returned; otherwise, the returned object is the same object passed in as \f3\fs18 pattern \f4\fs20 . The inheritance dictionary generated by \f3\fs18 addPatternForClone() \f4\fs20 will be added to \f3\fs18 pattern \f4\fs20 as the value for a particular key. If \f3\fs18 pattern \f4\fs20 is already configured to use \f3\fs18 string \f4\fs20 keys, the key used will be the \f3\fs18 symbol \f4\fs20 property of the chromosome; otherwise, including if \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the key used will be the \f3\fs18 id \f4\fs20 property of the chromosome. If the key in question already exists in \f3\fs18 pattern \f4\fs20 , its value will be replaced.\ The precise inheritance pattern generated by this method depends upon the chromosome\'92s type; see \f3\fs18 initializeChromosome() \f4\fs20 for a description of the different chromosome types and the ways in which they are inherited. The pattern will be the same as would be used by the \f3\fs18 addCloned() \f4\fs20 method for the chromosome. If \f3\fs18 sex \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the sex of the offspring is assumed to be the same as the parent; in non-sexual models, \f3\fs18 NULL \f4\fs20 must be passed. If the sex of the offspring will be different from the parent, \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 should be passed; if changing the sex from parent to offspring presents any genetic problems (if the chromosome is a sex chromosome, for example), an error will be raised, but if the chromosome does not depend upon sex, the change of sex will be allowed. Note that the generated inheritance dictionary does not encode the offspring sex; the \f3\fs18 sex \f4\fs20 parameter is simply used to determine and validate the inheritance pattern for the specified chromosome. The final pattern dictionary passed to \f3\fs18 addMultiRecombinant() \f4\fs20 will be validated against the \f3\fs18 sex \f4\fs20 parameter given to that method.\ It is typically not necessary to call \f3\fs18 addPatternForClone() \f4\fs20 , since \f3\fs18 addMultiRecombinant() \f4\fs20 will usually automatically infer the correct inheritance pattern from its parental individual \f3\fs18 parent1 \f4\fs20 if an inheritance dictionary for the chromosome is not supplied in \f3\fs18 pattern \f4\fs20 . This method is needed primarily for edge cases, such as if \f3\fs18 addMultiRecombinant() \f4\fs20 is being used to generate a biparental cross, but a particular chromosome should be cloned from just one of the parents in a manner that the biparental cross would not do automatically.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)addPatternForCross(iso$\'a0chromosome, No$\'a0pattern, object$\'a0parent1, object$\'a0parent2, [Ns$\'a0sex\'a0=\'a0NULL])\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds an inheritance dictionary for the specified chromosome to the pattern dictionary \f3\fs18 pattern \f4\fs20 , representing a biparental cross between \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 to generate an offspring, with sex optionally specified by \f3\fs18 sex \f4\fs20 . The parameter \f3\fs18 chromosome \f4\fs20 can provide a chromosome \f3\fs18 id \f4\fs20 (an \f3\fs18 integer \f4\fs20 ), a chromosome \f3\fs18 symbol \f4\fs20 (a \f3\fs18 string \f4\fs20 ), or a \f3\fs18 Chromosome \f4\fs20 object. The resulting pattern dictionary is intended for use with the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 addMultiRecombinant() \f4\fs20 ; see that method for background on the use of pattern dictionaries.\ The parameter \f3\fs18 pattern \f4\fs20 must be a \f3\fs18 Dictionary \f4\fs20 (or a subclass of \f3\fs18 Dictionary \f4\fs20 ), or \f3\fs18 NULL \f4\fs20 . If \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , a new singleton object of class \f3\fs18 Dictionary \f4\fs20 will be created, set up, and returned; otherwise, the returned object is the same object passed in as \f3\fs18 pattern \f4\fs20 . The inheritance dictionary generated by \f3\fs18 addPatternForCross() \f4\fs20 will be added to \f3\fs18 pattern \f4\fs20 as the value for a particular key. If \f3\fs18 pattern \f4\fs20 is already configured to use \f3\fs18 string \f4\fs20 keys, the key used will be the \f3\fs18 symbol \f4\fs20 property of the chromosome; otherwise, including if \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the key used will be the \f3\fs18 id \f4\fs20 property of the chromosome. If the key in question already exists in \f3\fs18 pattern \f4\fs20 , its value will be replaced.\ The precise inheritance pattern generated by this method depends upon the chromosome\'92s type; see \f3\fs18 initializeChromosome() \f4\fs20 for a description of the different chromosome types and the ways in which they are inherited. The pattern will be the same as would be used by the \f3\fs18 addCrossed() \f4\fs20 method for the chromosome. In some cases, the value of \f3\fs18 sex \f4\fs20 is unimportant and may be left as \f3\fs18 NULL \f4\fs20 ; a \f3\fs18 NULL \f4\fs20 value for \f3\fs18 sex \f4\fs20 essentially asserts that the sex of the offspring is unimportant to the inheritance pattern for the chromosome with the given parents. If that assertion is untrue \'96 if the sex needs to be known, for example to know how a sex chromosome should be inherited \'96 an error will be raised. When the sex needs to be known, \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 must be passed so that the correct inheritance pattern can be generated. In non-sexual models, \f3\fs18 NULL \f4\fs20 must be passed. Note that the generated inheritance dictionary does not encode the offspring sex; the \f3\fs18 sex \f4\fs20 parameter is simply used to determine and validate the inheritance pattern for the specified chromosome. The final pattern dictionary passed to \f3\fs18 addMultiRecombinant() \f4\fs20 will be validated against the \f3\fs18 sex \f4\fs20 parameter given to that method.\ It is typically not necessary to call \f3\fs18 addPatternForCross() \f4\fs20 , since \f3\fs18 addMultiRecombinant() \f4\fs20 will usually automatically infer the correct inheritance pattern from its parental individuals \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 if an inheritance dictionary for the chromosome is not supplied in \f3\fs18 pattern \f4\fs20 . This method is needed primarily for edge cases, such as if \f3\fs18 addMultiRecombinant() \f4\fs20 is being used to generate a clonal offspring, but a particular chromosome should be produced with recombination.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)addPatternForNull(iso$\'a0chromosome, No$\'a0pattern, [Ns$\'a0sex\'a0=\'a0NULL])\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds an inheritance dictionary for the specified chromosome to the pattern dictionary \f3\fs18 pattern \f4\fs20 , representing a non-inheritance event producing null haplosomes, with sex optionally specified by \f3\fs18 sex \f4\fs20 . The parameter \f3\fs18 chromosome \f4\fs20 can provide a chromosome \f3\fs18 id \f4\fs20 (an \f3\fs18 integer \f4\fs20 ), a chromosome \f3\fs18 symbol \f4\fs20 (a \f3\fs18 string \f4\fs20 ), or a \f3\fs18 Chromosome \f4\fs20 object. The resulting pattern dictionary is intended for use with the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 addMultiRecombinant() \f4\fs20 ; see that method for background on the use of pattern dictionaries.\ The parameter \f3\fs18 pattern \f4\fs20 must be a \f3\fs18 Dictionary \f4\fs20 (or a subclass of \f3\fs18 Dictionary \f4\fs20 ), or \f3\fs18 NULL \f4\fs20 . If \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , a new singleton object of class \f3\fs18 Dictionary \f4\fs20 will be created, set up, and returned; otherwise, the returned object is the same object passed in as \f3\fs18 pattern \f4\fs20 . The inheritance dictionary generated by \f3\fs18 addPatternForNull() \f4\fs20 will be added to \f3\fs18 pattern \f4\fs20 as the value for a particular key. If \f3\fs18 pattern \f4\fs20 is already configured to use \f3\fs18 string \f4\fs20 keys, the key used will be the \f3\fs18 symbol \f4\fs20 property of the chromosome; otherwise, including if \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the key used will be the \f3\fs18 id \f4\fs20 property of the chromosome. If the key in question already exists in \f3\fs18 pattern \f4\fs20 , its value will be replaced.\ For all chromosome types, this method will simply produce a null haplosome or haplosomes for the specified chromosome. If the chromosome does not allow a null haplosome, an error will be raised. In some cases, the value of \f3\fs18 sex \f4\fs20 is unimportant and may be left as \f3\fs18 NULL \f4\fs20 ; a \f3\fs18 NULL \f4\fs20 value for \f3\fs18 sex \f4\fs20 essentially asserts that the chromosome allows null haplosomes for any sex. If that assertion is untrue \'96 if the sex needs to be known, for example to know whether a null haplosome is allowed for a sex chromosome \'96 an error will be raised. When the sex needs to be known, \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 must be passed so that \f3\fs18 addPatternForNull() \f4\fs20 is satisfied that a legal pattern for the chromosome will be generated. In non-sexual models, \f3\fs18 NULL \f4\fs20 must be passed. Note that the generated inheritance dictionary does not encode the offspring sex; the \f3\fs18 sex \f4\fs20 parameter is simply used to determine and validate the inheritance pattern for the specified chromosome. The final pattern dictionary passed to \f3\fs18 addMultiRecombinant() \f4\fs20 will be validated against the \f3\fs18 sex \f4\fs20 parameter given to that method.\ If only one of the two haplosomes for a diploid chromosome should be a null haplosome, and \f3\fs18 addPatternForCrossed() \f4\fs20 and \f3\fs18 addPatternForCloned() \f4\fs20 would not produce the desired pattern, use \f3\fs18 addPatternForRecombinant() \f4\fs20 , which provides complete control.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)addPatternForRecombinant(iso$\'a0chromosome, No$\'a0pattern, No$\'a0strand1, No$\'a0strand2, Ni\'a0breaks1, No$\'a0strand3, No$\'a0strand4, Ni\'a0breaks2, [Ns$\'a0sex\'a0=\'a0NULL], [logical$\'a0randomizeStrands\'a0=\'a0T])\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds an inheritance dictionary for the specified chromosome to the pattern dictionary \f3\fs18 pattern \f4\fs20 , representing inheritance by cloning, recombination, or both, to generate an offspring from up to four parental haplosomes, with sex optionally specified by \f3\fs18 sex \f4\fs20 . The parameter \f3\fs18 chromosome \f4\fs20 can provide a chromosome \f3\fs18 id \f4\fs20 (an \f3\fs18 integer \f4\fs20 ), a chromosome \f3\fs18 symbol \f4\fs20 (a \f3\fs18 string \f4\fs20 ), or a \f3\fs18 Chromosome \f4\fs20 object. The resulting pattern dictionary is intended for use with the \f3\fs18 Subpopulation \f4\fs20 method \f3\fs18 addMultiRecombinant() \f4\fs20 ; see that method for background on the use of pattern dictionaries.\ The parameter \f3\fs18 pattern \f4\fs20 must be a \f3\fs18 Dictionary \f4\fs20 (or a subclass of \f3\fs18 Dictionary \f4\fs20 ), or \f3\fs18 NULL \f4\fs20 . If \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , a new singleton object of class \f3\fs18 Dictionary \f4\fs20 will be created, set up, and returned; otherwise, the returned object is the same object passed in as \f3\fs18 pattern \f4\fs20 . The inheritance dictionary generated by \f3\fs18 addPatternForRecombinant() \f4\fs20 will be added to \f3\fs18 pattern \f4\fs20 as the value for a particular key. If \f3\fs18 pattern \f4\fs20 is already configured to use \f3\fs18 string \f4\fs20 keys, the key used will be the \f3\fs18 symbol \f4\fs20 property of the chromosome; otherwise, including if \f3\fs18 pattern \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the key used will be the \f3\fs18 id \f4\fs20 property of the chromosome. If the key in question already exists in \f3\fs18 pattern \f4\fs20 , its value will be replaced.\ When passed the resulting pattern dictionary, the \f3\fs18 addMultiRecombinant() \f4\fs20 method will produce the first offspring haplosome using the \f3\fs18 Haplosome \f4\fs20 objects \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 with the vector of recombination breakpoints \f3\fs18 breaks1 \f4\fs20 , and likewise will produce the second offspring haplosome using \f3\fs18 strand3 \f4\fs20 , \f3\fs18 strand4 \f4\fs20 , and \f3\fs18 breaks2 \f4\fs20 . If both parental strands for an offspring haplosome are \f3\fs18 NULL \f4\fs20 , the breaks vector must be \f3\fs18 NULL \f4\fs20 or empty, and a null haplosome will be produced. If the first parental strand is non- \f3\fs18 NULL \f4\fs20 and the second is \f3\fs18 NULL \f4\fs20 for an offspring haplosome, the breaks vector must again be \f3\fs18 NULL \f4\fs20 or empty, and the first strand will be cloned with mutation. If both parental strands for an offspring haplosome are non- \f3\fs18 NULL \f4\fs20 , recombination between the strands will be done using the supplied breaks vector; in this case, if the breaks vector is \f3\fs18 NULL \f4\fs20 then \f3\fs18 addMultiRecombinant() \f4\fs20 will automatically generate breakpoints for the recombination. All of these semantics are discussed further in the documentation for \f3\fs18 addMultiRecombinant() \f4\fs20 ; this method is just a helper for that method. The documentation for \f3\fs18 addRecombinant() \f4\fs20 may also be helpful for understanding the concepts here, since it is the conceptual foundation upon which the very complex architecture of the \f3\fs18 addMultiRecombinant() \f4\fs20 method is built.\ Unlike \f3\fs18 addPatternForClone() \f4\fs20 and \f3\fs18 addPatternForCrossed() \f4\fs20 , which must infer the inheritance pattern given the chromosome type and the offspring sex, \f3\fs18 addPatternForRecombinant() \f4\fs20 is given the inheritance pattern, and must simply confirm that it is valid. If \f3\fs18 sex \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), this validation only needs to check that the inheritance pattern is possible, for some sex. For example, if the chromosome type is \f3\fs18 "X" \f4\fs20 then the inheritance pattern must produce a non-null first haplosome, and the second haplosome can be null (for a male, X\'96) or non-null (for a female, XX); other inheritance patterns would fail validation. When the sex is known, \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 may optionally be passed to validate against that sex; X\'96 would then fail validation if a female is specified, for example. In non-sexual models, \f3\fs18 NULL \f4\fs20 must be passed. Note that the generated inheritance dictionary does not encode the offspring sex; the \f3\fs18 sex \f4\fs20 parameter is simply used for validation. The final pattern dictionary passed to \f3\fs18 addMultiRecombinant() \f4\fs20 will be validated against the \f3\fs18 sex \f4\fs20 parameter given to that method.\ The \f3\fs18 randomizeStrands \f4\fs20 parameter indicates whether or not the order of recombining parental strands should be randomized in the inheritance dictionary. If \f3\fs18 randomizeStrands \f4\fs20 is \f3\fs18 T \f4\fs20 , then if \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 are both non- \f3\fs18 NULL \f4\fs20 , their order will be randomized; and similarly for \f3\fs18 strand3 \f4\fs20 and \f3\fs18 strand4 \f4\fs20 . If \f3\fs18 randomizeStrands \f4\fs20 is \f3\fs18 F \f4\fs20 , no randomization will be done in the inheritance dictionary \'96 but strand order randomization may still be done by \f3\fs18 addMultiRecombinant() \f4\fs20 if \f3\fs18 T \f4\fs20 is passed for its own \f3\fs18 randomizeStrands \f4\fs20 parameter. Randomizing the strand order is usually desirable, to avoid an inheritance bias due to a lack of randomization in the initial copy strand. Whether you wish to randomize strand order in \f3\fs18 addPatternForRecombinant() \f4\fs20 or in \f3\fs18 addMultiRecombinant() \f4\fs20 is up to you; it is harmless to do it in both places, apart from a small performance penalty, but there is no benefit.\ Of the family of \f3\fs18 addPatternFor...() \f4\fs20 methods, \f3\fs18 addPatternForRecombinant() \f4\fs20 is the most commonly used. Typically, \f3\fs18 addMultiRecombinant() \f4\fs20 can automatically infer the correct inheritance pattern for crossing or cloning (as described in its documentation), but for more complex inheritance patterns, using \f3\fs18 addPatternForRecombinant() \f4\fs20 is necessary (unless you want to build the pattern dictionary yourself).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)addSubpop(is$\'a0subpopID, integer$\'a0size, [float$\'a0sexRatio\'a0=\'a00.5]\cf2 , [logical$\'a0haploid\'a0=\'a0F]\cf0 ) \f5 \ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Add a new subpopulation with id \f3\fs18 subpopID \f4\fs20 and \f3\fs18 size \f4\fs20 individuals. The \f3\fs18 subpopID \f4\fs20 parameter may be either an \f3\fs18 integer \f4\fs20 giving the ID of the new subpopulation, or a \f3\fs18 string \f4\fs20 giving the name of the new subpopulation (such as \f3\fs18 "p5" \f4\fs20 to specify an ID of 5). Only if sex is enabled for the species, the initial sex ratio may optionally be specified as \f3\fs18 sexRatio \f4\fs20 (as the male fraction, M:M+F); if it is not specified, a default of \f3\fs18 0.5 \f4\fs20 is used. The new subpopulation will be defined as a global variable immediately by this method, and will also be returned by this method. Subpopulations added by this method will initially consist of individuals with empty haplosomes. In order to model subpopulations that split from an already existing subpopulation, use \f3\fs18 addSubpopSplit() \f4\fs20 .\ The \f3\fs18 haploid \f4\fs20 parameter defaults to \f3\fs18 F \f4\fs20 , indicating that the generated individuals should adopt their natural ploidy; in particular, type \f3\fs18 "A" \f4\fs20 chromosomes should be represented in each individual by two empty haplosomes. The \f3\fs18 haploid \f4\fs20 parameter may instead be \f3\fs18 T \f4\fs20 ; in this case, for all chromosomes of type \f3\fs18 "A" \f4\fs20 (and only that type), the second haplosome of each new individual will be a null haplosome, rather than an empty haplosome. This could be useful in a model of haplodiploidy, for example, to generate initial individuals that are haploid for the autosomal chromosomes of the species. For even greater control in nonWF models, you can call \f3\fs18 addSubpop() \f4\fs20 with an initial size of \f3\fs18 0 \f4\fs20 and then stock the population with new individuals created however you wish in the next tick\'92s \f3\fs18 reproduction() \f4\fs20 callback, such as with the \f3\fs18 addEmpty() \f4\fs20 method, providing separate control over the configuration of each individual.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)addSubpopSplit(is$\'a0subpopID, integer$\'a0size, io$\'a0sourceSubpop, [float$\'a0sexRatio\'a0=\'a00.5]) \f5 \ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Split off a new subpopulation with id \f3\fs18 subpopID \f4\fs20 and \f3\fs18 size \f4\fs20 individuals derived from subpopulation \f3\fs18 sourceSubpop \f4\fs20 . The \f3\fs18 subpopID \f4\fs20 parameter may be either an \f3\fs18 integer \f4\fs20 giving the ID of the new subpopulation, or a \f3\fs18 string \f4\fs20 giving the name of the new subpopulation (such as \f3\fs18 "p5" \f4\fs20 to specify an ID of 5). The \f3\fs18 sourceSubpop \f4\fs20 parameter may specify the source subpopulation either as a \f3\fs18 Subpopulation \f4\fs20 object or by \f3\fs18 integer \f4\fs20 identifier. Only if sex is enabled for the species, the initial sex ratio may optionally be specified as \f3\fs18 sexRatio \f4\fs20 (as the male fraction, M:M+F); if it is not specified, a default of \f3\fs18 0.5 \f4\fs20 is used. The new subpopulation will be defined as a global variable immediately by this method, and will also be returned by this method.\ Subpopulations added by this method will consist of individuals that are clonal copies of individuals from the source subpopulation, randomly chosen with probabilities proportional to fitness. The fitness of all of these initial individuals is considered to be 1.0, to avoid a doubled round of selection in the initial tick, given that fitness values were already used to choose the individuals to clone. Once this initial set of individuals has mated to produce offspring, the model is effectively of parental individuals in the source subpopulation mating randomly according to fitness, as usual in SLiM, with juveniles migrating to the newly added subpopulation. Effectively, then, then new subpopulation is created empty, and is filled by migrating juveniles from the source subpopulation, in accordance with SLiM\'92s usual model of juvenile migration.\ \pard\pardeftab529\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)chromosomesOfType(string$\'a0type)\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of \f3\fs18 Chromosome \f4\fs20 objects of the chromosome type supplied in \f3\fs18 type \f4\fs20 , in the order if which they were defined. If \f3\fs18 type \f4\fs20 does not correspond to a chromosome type accepted by \f3\fs18 initializeChromosome() \f4\fs20 , an error will be raised. See also \f3\fs18 chromosomesWithIDs() \f4\fs20 and \f3\fs18 chromosomesWithSymbols() \f4\fs20 .\ \pard\pardeftab529\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)chromosomesWithIDs(integer\'a0ids)\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of \f3\fs18 Chromosome \f4\fs20 objects corresponding to the chromosome ids supplied in \f3\fs18 ids \f4\fs20 , in the same order. If any chromosome id in \f3\fs18 ids \f4\fs20 does not correspond to a chromosome in the target species, an error will be raised. See also \f3\fs18 chromosomesOfType() \f4\fs20 and \f3\fs18 chromosomesWithSymbols() \f4\fs20 .\ \pard\pardeftab529\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)chromosomesWithSymbols(string\'a0symbols)\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of \f3\fs18 Chromosome \f4\fs20 objects corresponding to the chromosome symbols supplied in \f3\fs18 symbols \f4\fs20 , in the same order. If any chromosome symbol in \f3\fs18 symbols \f4\fs20 does not correspond to a chromosome in the target species, an error will be raised. See also \f3\fs18 chromosomesOfType() \f4\fs20 and \f3\fs18 chromosomesWithIDs() \f4\fs20 .\ \pard\pardeftab529\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0\cf0 (integer$)countOfMutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns the number of mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations that are currently active in the species. If you need a vector of the matching \f3\fs18 Mutation \f4\fs20 objects, rather than just a count, use \f3\fs18 -mutationsOfType() \f5\fs20 . \f4 This method is often used to determine whether an introduced mutation is still active (as opposed to being either lost or fixed). This method is provided for speed; it is much faster than the corresponding Eidos code. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)individualsWithPedigreeIDs(integer\'a0pedigreeIDs, [Nio\'a0subpops\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Looks up individuals by pedigree ID, optionally within specific subpopulations. Pedigree tracking must be turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 to use this method, otherwise an error will result. This method is vectorized; more than one pedigree id may be passed in \f3\fs18 pedigreeID \f4\fs20 , in which case the returned vector will contain all of the individuals for which a match was found (in the same order in which they were supplied). If a given id is not found, the returned vector will contain no entry for that id (so the length of the returned vector may not match the length of \f3\fs18 pedigreeIDs \f4\fs20 ). If none of the given ids were found, the returned vector will be \f3\fs18 object(0) \f4\fs20 , an empty \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Individual \f4\fs20 . If you have more than one pedigree ID to look up, calling this method just once, in vectorized fashion, may be much faster than calling it once for each ID, due to internal optimizations.\ To find individuals within all subpopulations, pass the default of \f3\fs18 NULL \f4\fs20 for \f3\fs18 subpops \f4\fs20 . If you are interested only in matches within a specific subpopulation, pass that subpopulation for \f3\fs18 subpops \f4\fs20 ; that will make the search faster. Similarly, if you know that a particular subpopulation is the most likely to contain matches, you should supply that subpopulation first in the \f3\fs18 subpops \f4\fs20 vector so that it will be searched first; the supplied subpopulations are searched in order. Subpopulations may be supplied either as \f3\fs18 integer \f4\fs20 IDs, or as \f3\fs18 Subpopulation \f4\fs20 objects.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)killIndividuals(object\'a0individuals)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Immediately kills the individuals in \f3\fs18 individuals \f4\fs20 . This removes them from their subpopulation and gives them an \f3\fs18 index \f4\fs20 value of \f3\fs18 -1 \f4\fs20 . The \f3\fs18 Individual \f4\fs20 objects are not freed immediately, since references to them could still exist in local Eidos variables; instead, the individuals are kept in a temporary \'93graveyard\'94 until they can be freed safely. It therefore continues to be safe to use them and their haplosomes, except that accessing their \f3\fs18 subpopulation \f4\fs20 property will raise an error since they no longer have a subpopulation.\ Note that the indices and order of individuals and haplosomes in all source subpopulations will change unpredictably as a side effect of this method. All evaluated interactions are invalidated as a side effect of calling this method.\ Note that this method is only for use in nonWF models, in which mortality is managed manually by the model script. In WF models, mortality is managed automatically by the SLiM core when the new offspring generation becomes the parental generation and the previous parental generation dies; mortality does not otherwise occur in WF models. In nonWF models, mortality normally occurs during the survival stage of the tick cycle, based upon the fitness values calculated by SLiM, and \f3\fs18 survival() \f4\fs20 callbacks can influence the outcome of that survival stage. Calls to \f3\fs18 killIndividuals() \f4\fs20 , on the other hand, can be made at any time during \f3\fs18 first() \f4\fs20 , \f3\fs18 early() \f4\fs20 , or \f3\fs18 late() \f4\fs20 events, and the result cannot be modified by \f3\fs18 survival() \f4\fs20 callbacks; the given individuals are simply immediately killed. This method therefore provides an alternative, and relatively rarely used, mortality mechanism that is disconnected from fitness.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(integer)mutationCounts(Nio\'a0subpops, [No\'a0mutations\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Return an \f3\fs18 integer \f4\fs20 vector with the frequency counts of all of the \f3\fs18 Mutation \f4\fs20 objects passed in \f3\fs18 mutations \f4\fs20 , within the \f3\fs18 Subpopulation \f4\fs20 objects in \f3\fs18 subpops \f4\fs20 . The \f3\fs18 subpops \f4\fs20 argument is required, but you may pass \f3\fs18 NULL \f4\fs20 to get population-wide frequency counts.\cf2 Subpopulations may be supplied either as \f3\fs18 integer \f4\fs20 IDs, or as \f3\fs18 Subpopulation \f4\fs20 objects.\cf0 If the optional \f3\fs18 mutations \f4\fs20 argument is \f3\fs18 NULL \f4\fs20 (the default), frequency counts will be returned for all of the active \f3\fs18 Mutation \f4\fs20 objects in the species \'96 the same \f3\fs18 Mutation \f4\fs20 objects, and in the same order, as would be returned by the \f3\fs18 mutations \f4\fs20 property of \f3\fs18 sim \f4\fs20 , in other words.\ See the \f3\fs18 -mutationFrequencies() \f4\fs20 method to obtain \f3\fs18 float \f4\fs20 frequencies instead of \f3\fs18 integer \f4\fs20 counts.\cf2 See also the \f3\fs18 \cf0 Haplosome \f4\fs20 \cf2 methods \f3\fs18 mutationCountsIn\cf0 Haplosome\cf2 s() \f4\fs20 and \f3\fs18 mutationFrequenciesIn\cf0 Haplosome\cf2 s() \f4\fs20 . \f5 \cf0 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(float)mutationFrequencies(Nio\'a0subpops, [No\'a0mutations\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Return a \f3\fs18 float \f4\fs20 vector with the frequencies of all of the \f3\fs18 Mutation \f4\fs20 objects passed in \f3\fs18 mutations \f4\fs20 , within the \f3\fs18 Subpopulation \f4\fs20 objects in \f3\fs18 subpops \f4\fs20 . The \f3\fs18 subpops \f4\fs20 argument is required, but you may pass \f3\fs18 NULL \f4\fs20 to get population-wide frequencies.\cf2 Subpopulations may be supplied either as \f3\fs18 integer \f4\fs20 IDs, or as \f3\fs18 Subpopulation \f4\fs20 objects.\cf0 If the optional \f3\fs18 mutations \f4\fs20 argument is \f3\fs18 NULL \f4\fs20 (the default), frequencies will be returned for all of the active \f3\fs18 Mutation \f4\fs20 objects in the species \'96 the same \f3\fs18 Mutation \f4\fs20 objects, and in the same order, as would be returned by the \f3\fs18 mutations \f4\fs20 property of \f3\fs18 sim \f4\fs20 , in other words. \f5 \ \f4 See the \f3\fs18 -mutationCounts() \f4\fs20 method to obtain \f3\fs18 integer \f4\fs20 counts instead of \f3\fs18 float \f4\fs20 frequencies.\cf2 See also the \f3\fs18 \cf0 Haplosome \f4\fs20 \cf2 methods \f3\fs18 mutationCountsIn\cf0 Haplosome\cf2 s() \f4\fs20 and \f3\fs18 mutationFrequenciesIn\cf0 Haplosome\cf2 s() \f4\fs20 . \f5 \cf0 \ \pard\pardeftab529\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (object)mutationsOfType(io$\'a0mutType) \f5 \ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Returns an \f3\fs18 object \f4\fs20 vector of all the mutations that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the mutations that are currently active in the species. If you just need a count of the matching \f3\fs18 Mutation \f4\fs20 objects, rather than a vector of the matches, use \f3\fs18 -countOfMutationsOfType() \f5\fs20 . \f4 This method is often used to look up an introduced mutation at a later point in the simulation, since there is no way to keep persistent references to objects in SLiM. This method is provided for speed; it is much faster than the corresponding Eidos code. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)outputFixedMutations([Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F]\cf2 , [logical$\'a0objectTags\'a0=\'a0F]\cf0 ) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Output all fixed mutations \'96 all \f3\fs18 Substitution \f4\fs20 objects, in other words \'96 in a SLiM native format. If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f5\fs20 . \f4 Mutations which have fixed but have not been turned into \f3\fs18 Substitution \f4\fs20 objects \'96 typically because \f3\fs18 convertToSubstitution \f4\fs20 has been set to \f3\fs18 F \f4\fs20 for their mutation type \'96 are not output; they are still considered to be segregating mutations by SLiM.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In SLiM 3.3 and later, the output format includes the nucleotides associated with any nucleotide-based mutations.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 In SLiM 5.0 and later, in models with multiple chromosome the output includes the symbol of the chromosome associated with each mutation.\ Beginning with SLiM 5.0, the \f3\fs18 objectTags \f4\fs20 parameter may be used to request that tag values for substitutions be written out.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)outputFull([Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0binary\'a0=\'a0F], [logical$\'a0append\'a0=\'a0F], [logical$\'a0spatialPositions\'a0=\'a0T]\cf2 \expnd0\expndtw0\kerning0 , [logical$\'a0ages\'a0=\'a0T], [logical$\'a0ancestralNucleotides\'a0=\'a0T]\kerning1\expnd0\expndtw0 , [logical$\'a0pedigreeIDs\'a0=\'a0F], [logical$\'a0objectTags\'a0=\'a0F], [logical$\'a0substitutions\'a0=\'a0F]\cf0 ) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Output the state of the entire population. If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f5\fs20 . \f4 When writing to a file, a \f3\fs18 logical \f4\fs20 flag, \f3\fs18 binary \f4\fs20 , may be supplied as well. If \f3\fs18 binary \f4\fs20 is \f3\fs18 T \f4\fs20 , the population state will be written as a binary file instead of a text file (binary data cannot be written to the standard output stream). The binary file is usually smaller, and in any case will be read much faster than the corresponding text file would be read. Binary files are not guaranteed to be portable between platforms; in other words, a binary file written on one machine may not be readable on a different machine (but in practice it usually will be, unless the platforms being used are fairly unusual). If \f3\fs18 binary \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), a text file will be written.\ Beginning with SLiM 2.3, the \f3\fs18 spatialPositions \f4\fs20 parameter may be used to control the output of the spatial positions of individuals in species for which continuous space has been enabled using the \f3\fs18 dimensionality \f4\fs20 option of \f3\fs18 initializeSLiMOptions() \f5\fs20 . \f4 If \f3\fs18 spatialPositions \f4\fs20 is \f3\fs18 F \f4\fs20 , the output will not contain spatial positions, and will be identical to the output generated by SLiM 2.1 and later. If \f3\fs18 spatialPositions \f4\fs20 is \f3\fs18 T \f4\fs20 , spatial position information will be output if it is available. If the species does not have continuous space enabled, the \f3\fs18 spatialPositions \f4\fs20 parameter will be ignored. Positional information may be output for all output destinations \'96 the Eidos output stream, a text file, or a binary file. \f5 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4 \cf2 \expnd0\expndtw0\kerning0 Beginning with SLiM 3.0, the \f3\fs18 ages \f4\fs20 parameter may be used to control the output of the ages of individuals in nonWF simulations. If \f3\fs18 ages \f4\fs20 is \f3\fs18 F \f4\fs20 , the output will not contain ages, preserving backward compatibility with the output format of SLiM 2.1 and later. If \f3\fs18 ages \f4\fs20 is \f3\fs18 T \f4\fs20 , ages will be output for nonWF models. In WF simulations, the \f3\fs18 ages \f4\fs20 parameter will be ignored.\ Beginning with SLiM 3.3, the \f3\fs18 ancestralNucleotides \f4\fs20 parameter may be used to control the output of the ancestral nucleotide sequence in nucleotide-based models. If \f3\fs18 ancestralNucleotides \f4\fs20 is \f3\fs18 F \f4\fs20 , the output will not contain ancestral nucleotide information, and so the ancestral sequence will not be restored correctly if the saved file is loaded with \f3\fs18 readPopulationFile() \f4\fs20 . This option is provided because the ancestral sequence may be quite large, for models with a long chromosome (e.g., 1 GB if the chromosome is 10 \fs13\fsmilli6667 \super 9 \fs20 \nosupersub bases long, when saved in text format, or 0.25 GB when saved in binary format). If the model is not nucleotide-based (as enabled with the \f3\fs18 nucleotideBased \f4\fs20 parameter to \f3\fs18 initializeSLiMOptions() \f4\fs20 ), the \f3\fs18 ancestralNucleotides \f4\fs20 parameter will be ignored. Note that in nucleotide-based models the output format will \f1\i always \f4\i0 include the nucleotides associated with any nucleotide-based mutations; the \f3\fs18 ancestralNucleotides \f4\fs20 flag governs only the ancestral sequence.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 Beginning with SLiM 3.5, the \f3\fs18 pedigreeIDs \f4\fs20 parameter may be used to request that pedigree IDs be written out (and read in by \f3\fs18 readFromPopulationFile() \f4\fs20 , subsequently). This option is turned off ( \f3\fs18 F \f4\fs20 ) by default, for brevity. This option may only be used if SLiM\'92s optional pedigree tracking has been enabled with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 .\ Beginning with SLiM 5.0, the \f3\fs18 objectTags \f4\fs20 parameter may be used to request that tag values for objects be written out. This option is turned off ( \f3\fs18 F \f4\fs20 ) by default, for brevity; if it turned on ( \f3\fs18 T \f4\fs20 ), the values of all tags for all objects of supported classes ( \f3\fs18 Chromosome \f4\fs20 , \f3\fs18 Subpopulation \f4\fs20 , \f3\fs18 Individual \f4\fs20 , \f3\fs18 Haplosome \f4\fs20 , \f3\fs18 Mutation \f4\fs20 , \f3\fs18 Substitution \f4\fs20 ) will be written. For individuals, the \f3\fs18 tag \f4\fs20 , \f3\fs18 tagF \f4\fs20 , \f3\fs18 tagL0 \f4\fs20 , \f3\fs18 tagL1 \f4\fs20 , \f3\fs18 tagL2 \f4\fs20 , \f3\fs18 tagL3 \f4\fs20 , and \f3\fs18 tagL4 \f4\fs20 properties will be written; for chromosomes, subpopulations, haplosomes, and mutations, the \f3\fs18 tag \f4\fs20 property will be written. The saved tag information can be read in by \f3\fs18 readFromPopulationFile() \f4\fs20 , but only if the output is in binary format ( \f3\fs18 binary=T \f4\fs20 ). Note that if there is other state that you wish you persist, such as tags on objects of other classes, values attached to objects with \f3\fs18 setValue() \f4\fs20 , and so forth, you should persist that state in separate files using calls such as \f3\fs18 writeFile() \f4\fs20 .\ Beginning with SLiM 5.0, the \f3\fs18 substitutions \f4\fs20 parameter may be used to request that information about \f3\fs18 Substitution \f4\fs20 objects in the simulation be written out. This option is turned off ( \f3\fs18 F \f4\fs20 ) by default, for brevity. The saved substitution information can be read in by \f3\fs18 readFromPopulationFile() \f4\fs20 , but only if the output is in binary format ( \f3\fs18 binary=T \f4\fs20 ).\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf0 Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)outputMutations(object\'a0mutations, [Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F]\cf2 , [logical$\'a0objectTags\'a0=\'a0F]\cf0 ) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Output all of the given mutations. This can be used to output all mutations of a given mutation type, for example. \cf2 \expnd0\expndtw0\kerning0 If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 . \f5 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4 \cf2 \expnd0\expndtw0\kerning0 In SLiM 3.3 and later, the output format includes the nucleotides associated with any nucleotide-based mutations.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 In SLiM 5 and later, in models with multiple chromosome the output includes the symbol of the chromosome associated with each mutation.\ Beginning with SLiM 5.0, the \f3\fs18 objectTags \f4\fs20 parameter may be used to request that tag values for mutations be written out.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(integer$)readFromPopulationFile(string$\'a0filePath\cf2 , [No$\'a0subpopMap\'a0=\'a0NULL]\cf0 )\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Read from a population file, whether in text or binary format as previously specified to \f3\fs18 outputFull() \f4\fs20 , and return the tick counter value represented by the file\'92s contents (i.e., the tick at which the file was generated). Although this is most commonly used to set up initial populations (often in an Eidos event set to run in tick \f3\fs18 1 \f4\fs20 , immediately after simulation initialization), it may be called in any \f3\fs18 early() \f4\fs20 or \f3\fs18 late() \f4\fs20 event; the current state of all populations in the target species will be wiped and replaced by the state in the file at \f3\fs18 filePath \f4\fs20 . All Eidos variables that are of type \f3\fs18 object \f4\fs20 and have element class \f3\fs18 Subpopulation \f4\fs20 , \f3\fs18 Haplosome \f4\fs20 , \f3\fs18 Mutation \f4\fs20 , \f3\fs18 Individual \f4\fs20 , or \f3\fs18 Substitution \f4\fs20 will be removed as a side effect of this method if they contain any element that belongs to the target species, because those objects will no longer exist in the SLiM simulation; if you want to preserve any of that state, you should output it or save it to a file prior to this call. New symbols ( \f3\fs18 p1 \f4\fs20 , \f3\fs18 p2 \f4\fs20 , etc.) will be defined to refer to the new \f3\fs18 Subpopulation \f4\fs20 objects loaded from the file. Note that fitness values are not calculated as a side effect of this call (because the simulation will often need to evaluate interactions or modify other state prior to doing so).\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 In SLiM 2.3 and later when using the WF model, calling \f3\fs18 readFromPopulationFile() \f4\fs20 from any context other than a \f3\fs18 late() \f4\fs20 event causes a warning; calling from a \f3\fs18 late() \f4\fs20 event is almost always correct in WF models, so that fitness values can be automatically recalculated by SLiM at the usual time in the tick cycle without the need to force their recalculation (see comments on \f3\fs18 recalculateFitness() \f4\fs20 ).\ In SLiM 3.0 when using the nonWF model, calling \f3\fs18 readFromPopulationFile() \f4\fs20 from any context other than an \f3\fs18 early() \f4\fs20 event causes a warning; calling from an \f3\fs18 early() \f4\fs20 event is almost always correct in nonWF models, so that fitness values can be automatically recalculated by SLiM at the usual time in the tick cycle without the need to force their recalculation (see comments on \f3\fs18 recalculateFitness() \f4\fs20 ).\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 This method changes the tick and cycle counters to the tick and cycle read from the file. If you do not want these counters to be changed, you can change them back after reading, by setting \f3\fs18 community.tick \f4\fs20 and \f3\fs18 sim.cycle \f4\fs20 to whatever values you wish. Note that restoring a saved past state and running forward again will not yield the same simulation results, because the random number generator\'92s state will not be the same; if you wish to ensure reproducibility from a given time point, \f3\fs18 setSeed() \f4\fs20 can be used to establish a new seed value.\ Any changes made to the structure of the species (mutation types, genomic element types, etc.) will not be wiped and re-established by \f3\fs18 readFromPopulationFile() \f4\fs20 ; this method loads only the population\'92s state, not the species configuration, so care should be taken to ensure that the species structure meshes coherently with the loaded data. Indeed, state such as the selfing and cloning rates of subpopulations, and values set onto objects with \f3\fs18 setValue() \f4\fs20 , will also be lost, since it is not saved out by \f3\fs18 outputFull() \f4\fs20 . Only information saved by \f3\fs18 outputFull() \f4\fs20 will be restored; all other state associated with the mutations, haplosomes, individuals, and subpopulations in the simulation will be lost, and should be re-established by the model if it is still needed. Note that some state is saved by \f3\fs18 outputFull() \f4\fs20 only optionally, such as the \f3\fs18 tag \f4\fs20 values of individuals; if a given option is enabled and the corresponding information is saved, then that information will be restored, otherwise it will not be.\ As of SLiM 2.3, this method will read and restore the spatial positions of individuals if that information is present in the output file and the species has enabled continuous space. If spatial positions are present in the output file but the species has not enabled continuous space (or the number of spatial dimensions does not match), an error will result. If the species has enabled continuous space but spatial positions are not present in the output file, the spatial positions of the individuals read will be undefined, but an error is not raised.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 As of SLiM 3.0, this method will read and restore the ages of individuals if that information is present in the output file and the simulation is based upon the nonWF model. If ages are present but the simulation uses a WF model, an error will result; the WF model does not use age information. If ages are not present but the simulation uses a nonWF model, an error will also result; the nonWF model requires age information.\ As of SLiM 3.3, this method will restore the nucleotides of nucleotide-based mutations, and will restore the ancestral nucleotide sequence, if that information is present in the output file. Loading an output file that contains nucleotide information in a non-nucleotide-based model, and \f1\i vice versa \f4\i0 , will produce an error.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 As of SLiM 3.5, this method will read and restore the pedigree IDs of individuals and haplosomes if that information is present in the output file (as requested with \f3\fs18 outputFull(pedigreeIDs=T) \f4\fs20 ) \f1\i and \f4\i0 if SLiM\'92s optional pedigree tracking has been enabled with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 .\ As of SLiM 5.0, this method will read and restore tag values for objects of supported classes ( \f3\fs18 Chromosome \f4\fs20 , \f3\fs18 Subpopulation \f4\fs20 , \f3\fs18 Individual \f4\fs20 , \f3\fs18 Haplosome \f4\fs20 , \f3\fs18 Mutation \f4\fs20 , \f3\fs18 Substitution \f4\fs20 ) if they were saved by \f3\fs18 outputFull() \f4\fs20 with its \f3\fs18 objectTags=T \f4\fs20 option. This facility is only available when reading binary output from \f3\fs18 outputFull() \f4\fs20 , as chosen by its \f3\fs18 binary=T \f4\fs20 option; otherwise, an error will result.\ As of SLiM 5.0, this method will read and restore substitutions if they were saved by \f3\fs18 outputFull() \f4\fs20 with its \f3\fs18 substitutions=T \f4\fs20 option. This facility is only available when reading binary output from \f3\fs18 outputFull() \f4\fs20 , as chosen by its \f3\fs18 binary=T \f4\fs20 option; otherwise, an error will result.\ This method can also be used to read tree-sequence information, in the form of single-chromosome \f3\fs18 .trees \f4\fs20 files and multi-chromosome trees archives, as saved by \f3\fs18 treeSeqOutput() \f4\fs20 or generated by the Python \f3\fs18 pyslim \f4\fs20 package. Note that the user metadata for a tree-sequence file can be read separately with the \f3\fs18 treeSeqMetadata() \f4\fs20 function. Beginning with SLiM 4, the \f3\fs18 subpopMap \f4\fs20 parameter may be supplied to re-order the populations of the input tree sequence when it is loaded in to SLiM. This parameter must have a value that is a \f3\fs18 Dictionary \f4\fs20 ; the keys of this dictionary should be SLiM population identifiers as \f3\fs18 string \f4\fs20 values (e.g., \f3\fs18 "p2" \f4\fs20 ), and the values should be indexes of populations in the input tree sequence; a key/value pair of \f3\fs18 "p2", 4 \f4\fs20 would mean that the fifth population in the input (the one at zero-based index \f3\fs18 4 \f4\fs20 ) should become \f3\fs18 p2 \f4\fs20 on loading into SLiM. If \f3\fs18 subpopMap \f4\fs20 is non- \f3\fs18 NULL \f4\fs20 , \f1\i all \f4\i0 populations in the tree sequence must be explicitly mapped, even if their index will not change and even if they will not be used by SLiM; the only exception is for unused slots in the population table, which can be explicitly remapped but do not have to be. For instance, suppose we have a tree sequence in which population \f3\fs18 0 \f4\fs20 is unused, population \f3\fs18 1 \f4\fs20 is not a SLiM population (for example, an ancestral population produced by \f3\fs18 msprime \f4\fs20 ), and population 2 is a SLiM population, and we want to load this in with population 2 as \f3\fs18 p0 \f4\fs20 in SLiM. To do this, we could supply a value of \f3\fs18 Dictionary("p0", 2, "p1", 1, "p2", 0) \f4\fs20 for \f3\fs18 subpopMap \f4\fs20 , or we could leave out slot \f3\fs18 0 \f4\fs20 since it is unused, with \f3\fs18 Dictionary("p0", 2, "p1", 1) \f4\fs20 . Although this facility cannot be used to remove populations in the tree sequence, note that it may \f1\i add \f4\i0 populations that will be visible when \f3\fs18 treeSeqOutput() \f4\fs20 is called (although these will not be SLiM populations); if, in this example, we had used \f3\fs18 Dictionary("p0", 0, "p1", 1, "p5", 2) \f4\fs20 and then we wrote the result out with \f3\fs18 treeSeqOutput() \f4\fs20 , the resulting tree sequence would have six populations, although three of them would be empty and would not be used by SLiM. The use of \f3\fs18 subpopMap \f4\fs20 makes it easier to load simulation data that was generated in Python, since that typically uses an id of \f3\fs18 0 \f4\fs20 . The \f3\fs18 subpopMap \f4\fs20 parameter may not be used with file formats other than tree-sequence files, at the present time; setting up the correct subpopulation ids is typically easier when working with those other formats. Note the \f3\fs18 tskit \f4\fs20 command-line interface can be used, like \f3\fs18 python3 -m tskit populations file.trees \f4\fs20 , to find out the number of subpopulations in a tree-sequence file and their IDs.\ When loading a tree sequence, a crosscheck of the loaded data will be performed to ensure that the tree sequence was well-formed and was loaded correctly. When running a Release build of SLiM, however, this crosscheck will only occur the first time that \f3\fs18 readFromPopulationFile() \f4\fs20 is called to load a tree sequence; subsequent calls will not perform this crosscheck, for greater speed when running models that load saved population state many times (such as models that are conditional on fixation). If you suspect that a tree sequence file might be corrupted, or might be read incorrectly, running a Debug build of SLiM enables crosschecks after every load.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)recalculateFitness([Ni$\'a0tick\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Force an immediate recalculation of fitness values for all individuals in all subpopulations. Normally fitness values are calculated at a fixed point in each tick, and those values are cached and used until the next recalculation. If simulation parameters are changed in script in a way that affects fitness calculations, and if you wish those changes to take effect immediately rather than taking effect at the next automatic recalculation, you may call \f3\fs18 recalculateFitness() \f4\fs20 to force an immediate recalculation and recache.\ The optional parameter \f3\fs18 tick \f4\fs20 provides the tick for which \f3\fs18 mutationEffect() \f4\fs20 and \f3\fs18 fitnessEffect() \f4\fs20 callbacks should be selected; if it is \f3\fs18 NULL \f4\fs20 (the default), the current tick value for the simulation, \f3\fs18 community.tick \f4\fs20 , is used. If you call \f3\fs18 recalculateFitness() \f4\fs20 in an \f3\fs18 early() \f4\fs20 event in a WF model, you may wish this to be \f3\fs18 community.tick - 1 \f4\fs20 in order to utilize the \f3\fs18 mutationEffect() \f4\fs20 and \f3\fs18 fitnessEffect() \f4\fs20 callbacks for the previous tick, as if the changes that you have made to fitness-influencing parameters were already in effect at the end of the previous tick when the new generation was first created and evaluated (usually it is simpler to just make such changes in a \f3\fs18 late() \f4\fs20 event instead, however, in which case calling \f3\fs18 recalculateFitness() \f4\fs20 is probably not necessary at all since fitness values will be recalculated immediately afterwards). Regardless of the value supplied for \f3\fs18 tick \f4\fs20 here, \f3\fs18 community.tick \f4\fs20 inside callbacks will report the true tick number, so if your callbacks consult that parameter in order to create tick-specific fitness effects you will need to handle the discrepancy somehow. (Similar considerations apply for nonWF models that call \f3\fs18 recalculateFitness() \f4\fs20 in a \f3\fs18 late() \f4\fs20 event, which is also not advisable in general.)\ After this call, the fitness values used for all purposes in SLiM will be the newly calculated values. Calling this method will trigger the calling of any enabled and applicable \f3\fs18 mutationEffect() \f4\fs20 and \f3\fs18 fitnessEffect() \f4\fs20 callbacks, so this is quite a heavyweight operation; you should think carefully about what side effects might result (which is why fitness recalculation does not just occur automatically after changes that might affect fitness values).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerFitnessEffectCallback(Nis$\'a0id, string$\'a0source, [Nio$\'a0subpop \f5 \'a0 \f3 =\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 fitnessEffect() \f4\fs20 callback in the current simulation (specific to the target species), with an optional subpopulation \f3\fs18 subpop \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations), and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerMateChoiceCallback(Nis$\'a0id, string$\'a0source, [Nio$\'a0subpop\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 mateChoice() \f4\fs20 callback in the current simulation\cf2 (specific to the target species)\cf0 , with optional subpopulation \f3\fs18 subpop \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations) and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerModifyChildCallback(Nis$\'a0id, string$\'a0source, [Nio$\'a0subpop \f5 \'a0 \f3 =\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 modifyChild() \f4\fs20 callback in the current simulation\cf2 (specific to the target species)\cf0 , with optional subpopulation \f3\fs18 subpop \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations) and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(object$)registerMutationCallback(Nis$\'a0id, string$\'a0source, [Nio$\'a0mutType\'a0=\'a0NULL], [Nio$\'a0subpop\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 mutation() \f4\fs20 callback in the current simulation\kerning1\expnd0\expndtw0 (specific to the target species)\expnd0\expndtw0\kerning0 , with an optional mutation type \f3\fs18 mutType \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 mutation type identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all mutation types), optional subpopulation \f3\fs18 subpop \f4\fs20 (which may also be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations), and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 \'96\'a0(object$)registerMutationEffectCallback(Nis$\'a0id, string$\'a0source, io$\'a0mutType, [Nio$\'a0subpop\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 mutationEffect() \f4\fs20 callback in the current simulation (specific to the target species), with a required mutation type \f3\fs18 mutType \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 mutation type identifier), optional subpopulation \f3\fs18 subpop \f4\fs20 (which may also be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations), and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerRecombinationCallback(Nis$\'a0id, string$\'a0source, [Nio$\'a0subpop \f5 \'a0 \f3 =\'a0NULL]\cf2 , [Niso$\'a0chromosome\'a0=\'a0NULL]\cf0 , [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 recombination() \f4\fs20 callback in the current simulation (specific to the target species), with optional subpopulation \f3\fs18 subpop \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations) and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. In multi-chromosome models, parameter \f3\fs18 chromosome \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a chromosome to which the callback will apply (as either an \f3\fs18 integer \f4\fs20 id, a \f3\fs18 string \f4\fs20 symbol, or a \f3\fs18 Chromosome \f4\fs20 object); otherwise, \f3\fs18 NULL \f4\fs20 indicates that the callback applies to all chromosomes. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(object$)registerReproductionCallback(Nis$\'a0id, string$\'a0source, [Nio$\'a0subpop \f5 \'a0 \f3 =\'a0NULL], \cf2 \expnd0\expndtw0\kerning0 [Ns$\'a0sex\'a0=\'a0NULL], \cf0 \kerning1\expnd0\expndtw0 [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 reproduction() \f4\fs20 callback in the current simulation\kerning1\expnd0\expndtw0 (specific to the target species)\expnd0\expndtw0\kerning0 , with optional subpopulation \f3\fs18 subpop \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations), optional sex-specificity \f3\fs18 sex \f4\fs20 (which may be \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 in sexual species to make the callback specific to males or females respectively, or \f3\fs18 NULL \f4\fs20 for no sex-specificity), and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0(object$)registerSurvivalCallback(Nis$\'a0id, string$\'a0source, [Nio$\'a0subpop \f5 \'a0 \f3 =\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Register a block of Eidos source code, represented as the \f3\fs18 string \f4\fs20 singleton \f3\fs18 source \f4\fs20 , as an Eidos \f3\fs18 survival() \f4\fs20 callback in the current simulation\cf2 (specific to the target species)\cf0 , with optional subpopulation \f3\fs18 subpop \f4\fs20 (which may be an \f3\fs18 integer \f4\fs20 identifier, or \f3\fs18 NULL \f4\fs20 , the default, to indicate all subpopulations) and optional \f3\fs18 start \f4\fs20 and \f3\fs18 end \f4\fs20 ticks all limiting its applicability. The script block will be given identifier \f3\fs18 id \f4\fs20 (specified as an \f3\fs18 integer \f4\fs20 , or as a \f3\fs18 string \f4\fs20 symbolic name such as \f3\fs18 "s5" \f4\fs20 ); this may be \f3\fs18 NULL \f4\fs20 if there is no need to be able to refer to the block later. The registered callback is added to the end of the list of registered \f3\fs18 SLiMEidosBlock \f4\fs20 objects, and is active immediately; it \f1\i may \f4\i0 be eligible to execute in the current tick. The new \f3\fs18 SLiMEidosBlock \f4\fs20 will be defined as a global variable immediately by this method, and will also be returned by this method. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)simulationFinished(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Declare the current simulation finished. This method is equivalent to the \f3\fs18 Community \f4\fs20 method \f3\fs18 simulationFinished() \f4\fs20 , except that this method is only legal to call in single-species models (to provide backward compatibility). It is recommended that new code should call the \f3\fs18 Community \f4\fs20 method; this method may be deprecated in the future.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)skipTick(void)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Deactivate the target species for the current tick. This sets the \f3\fs18 active \f4\fs20 property of the species to \f3\fs18 F \f4\fs20 ; it also set the \f3\fs18 active \f4\fs20 property of all callbacks that belong to the species (with the species as their \f3\fs18 species \f4\fs20 specifier) to \f3\fs18 F \f4\fs20 , and sets the active property of all events that are synchronized with the species (with the species as their \f3\fs18 ticks \f4\fs20 specifier) to \f3\fs18 F \f4\fs20 . The cycle counter for the species will not be incremented at the end of the tick. This method may only be called in \f3\fs18 first() \f4\fs20 events, to ensure that species are either active or inactive throughout a given tick.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)subsetMutations([No$\'a0exclude\'a0=\'a0NULL], [Nio$\'a0mutType\'a0=\'a0NULL], [Ni$\'a0position\'a0=\'a0NULL], [Nis$\'a0nucleotide\'a0=\'a0NULL], [Ni$\'a0tag\'a0=\'a0NULL], [Ni$\'a0id\'a0=\'a0NULL], [Niso\'a0chromosome\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of mutations subset from the list of all active mutations in the species (as would be provided by the \f3\fs18 mutations \f4\fs20 property). The parameters specify constraints upon the subset of mutations that will be returned. Parameter \f3\fs18 exclude \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a specific mutation that should not be included (typically the focal mutation in some operation). Parameter \f3\fs18 mutType \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a mutation type for the mutations to be returned (as either a \f3\fs18 MutationType \f4\fs20 object or an \f3\fs18 integer \f4\fs20 identifier). Parameter \f3\fs18 position \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a base position for the mutations to be returned. Parameter \f3\fs18 nucleotide \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a nucleotide for the mutations to be returned (either as a string, \f3\fs18 "A" \f4\fs20 / \f3\fs18 "C" \f4\fs20 / \f3\fs18 "G" \f4\fs20 / \f3\fs18 "T" \f4\fs20 , or as an integer, \f3\fs18 0 \f4\fs20 / \f3\fs18 1 \f4\fs20 / \f3\fs18 2 \f4\fs20 / \f3\fs18 3 \f4\fs20 respectively). Parameter \f3\fs18 tag \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a tag value for the mutations to be returned. Parameter \f3\fs18 id \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a required value for the \f3\fs18 id \f4\fs20 property of the mutations to be returned. Parameter \f3\fs18 chromosome \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a chromosome or chromosomes with which the mutations returned must be associated (as either \f3\fs18 integer \f4\fs20 ids, \f3\fs18 string \f4\fs20 symbols, or \f3\fs18 Chromosome \f4\fs20 objects).\ This method is shorthand for getting the \f3\fs18 mutations \f4\fs20 property of the subpopulation, and then using operator \f3\fs18 [] \f4\fs20 to select only mutations with the desired properties; besides being much simpler than the equivalent Eidos code, it is also much faster. Note that if you only need to select on mutation type, the \f3\fs18 mutationsOfType() \f4\fs20 method will be even faster.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)substitutionsOfType(io$\'a0mutType)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns an \f3\fs18 object \f4\fs20 vector of all the substitutions that are of the type specified by \f3\fs18 mutType \f4\fs20 , out of all of the substitutions that are currently present in the species. This method is provided for speed; it is much faster than the corresponding Eidos code. See also \f3\fs18 mutationsOfType() \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(logical$)treeSeqCoalesced(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns the coalescence state for the recorded tree sequence at the last simplification. The returned value is a logical singleton flag, \f3\fs18 T \f4\fs20 to indicate that full coalescence was observed at the last tree-sequence simplification (meaning that there is a single ancestral individual that roots all ancestry trees at all sites along the chromosome \'96 although not necessarily the \f1\i same \f4\i0 ancestor at all sites), or \f3\fs18 F \f4\fs20 if full coalescence was not observed. For simple models, reaching coalescence may indicate that the model has reached an equilibrium state, but this may not be true in models that modify the dynamics of the model during execution by changing migration rates, introducing new mutations programmatically, dictating non-random mating, etc., so be careful not to attach more meaning to coalescence than it is due; some models may require burn-in beyond coalescence to reach equilibrium, or may not have an equilibrium state at all. Also note that some actions by a model, such as adding a new subpopulation, may cause the coalescence state to revert from \f3\fs18 T \f4\fs20 back to \f3\fs18 F \f4\fs20 (at the next simplification), so a return value of \f3\fs18 T \f4\fs20 may not necessarily mean that the model is coalesced at the present moment \'96 only that it \f1\i was \f4\i0 coalesced at the last simplification.\ This method may only be called if tree sequence recording has been turned on with \f3\fs18 initializeTreeSeq() \f4\fs20 ; in addition, \f3\fs18 checkCoalescence=T \f4\fs20 must have been supplied to \f3\fs18 initializeTreeSeq() \f4\fs20 , so that the necessary work is done during each tree-sequence simplification. Since this method does not perform coalescence checking itself, but instead simply returns the coalescence state observed at the last simplification, it may be desirable to call \f3\fs18 treeSeqSimplify() \f4\fs20 immediately before \f3\fs18 treeSeqCoalesced() \f4\fs20 to obtain up-to-date information. However, the speed penalty of doing this in every tick would be large, and most models do not need this level of precision; usually it is sufficient to know that the model has coalesced, without knowing whether that happened in the current tick or in a recent preceding tick.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)treeSeqOutput(string$\'a0path, [logical$\'a0simplify\'a0=\'a0T], [logical$\'a0includeModel\'a0=\'a0T], \kerning1\expnd0\expndtw0 [No$\'a0metadata\'a0=\'a0NULL], [logical$\'a0overwriteDirectory\'a0=\'a0F]\expnd0\expndtw0\kerning0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Outputs the current tree sequence recording tables to the path specified by \f3\fs18 path \f4\fs20 . This method may only be called if tree sequence recording has been turned on with \f3\fs18 initializeTreeSeq() \f4\fs20 . If \f3\fs18 simplify \f4\fs20 is \f3\fs18 T \f4\fs20 (the default), simplification will be done immediately prior to output; this is almost always desirable, unless a model wishes to avoid simplification entirely. (Note that if simplification is not done, then all haplosomes since the last simplification will be marked as samples in the resulting tree sequence.)\ In a model of a single chromosome, a binary tree sequence file will be written to the specified path; a filename extension of \f3\fs18 .trees \f4\fs20 is suggested for this type of file, and such a file is often referred to as a \'93 \f3\fs18 .trees \f4\fs20 file\'94. In a multi-chromosome model, a directory will instead be created at the specified path, and a separate \f3\fs18 .trees \f4\fs20 file will be created within that directory for each chromosome in the model, mirroring the fact that SLiM keeps a separate tree sequence for each chromosome in a multi-chromosome model. These \f3\fs18 .trees \f4\fs20 files will be given filenames based upon the \f3\fs18 symbol \f4\fs20 property of each chromosome, as provided to \f3\fs18 initializeChromosome() \f4\fs20 ; for example, the tree sequence for a chromosome with symbol \f3\fs18 "X" \f4\fs20 will be saved as \f3\fs18 chromosome_X.trees \f4\fs20 within the specified directory. For the name of the directory itself, a suffix of \f3\fs18 _trees \f4\fs20 is suggested, rather than \f3\fs18 .trees \f4\fs20 , since the use of dot-extensions in directory names is not common; for example, \f3\fs18 "model_Q_seed_17_trees" \f4\fs20 would be a path you might pass to \f3\fs18 treeSeqOutput() \f4\fs20 as a directory name in a multi-chromosome model. Such a directory, containing separate \f3\fs18 .trees \f4\fs20 files for each chromosome, is called a \'93trees archive\'94. Both \f3\fs18 .trees \f4\fs20 files and trees archives can be read by \f3\fs18 readFromPopulationFile() \f4\fs20 , as discussed in its documentation.\ Normally, the full SLiM script used to generate the tree sequence is written out to the provenance entry of the tree sequence file, to the \f3\fs18 model \f4\fs20 subkey of the \f3\fs18 parameters \f4\fs20 top-level key. Supplying \f3\fs18 F \f4\fs20 for \f3\fs18 includeModel \f4\fs20 suppresses output of the full script.\ A \f3\fs18 Dictionary \f4\fs20 object containing user-generated metadata may be supplied with the \f3\fs18 metadata \f4\fs20 parameter. If present, this dictionary will be serialized as JSON and attached to the saved tree sequence under a key named \f3\fs18 user_metadata \f4\fs20 , within the \f3\fs18 SLiM \f4\fs20 key. If \f3\fs18 tskit \f4\fs20 is used to read the tree sequence in Python, this metadata will automatically be deserialized and made available at \f3\fs18 ts.metadata["SLiM"]["user_metadata"] \f4\fs20 . This metadata dictionary is not used by SLiM, or by \f3\fs18 pyslim \f4\fs20 , \f3\fs18 tskit \f4\fs20 , or \f3\fs18 msprime \f4\fs20 ; you may use it for any purpose you wish. Note that \f3\fs18 metadata \f4\fs20 may actually be any subclass of \f3\fs18 Dictionary \f4\fs20 , such as a \f3\fs18 DataFrame \f4\fs20 . It can even be a \f3\fs18 Species \f4\fs20 object such as \f3\fs18 sim \f4\fs20 , or a \f3\fs18 LogFile \f4\fs20 instance; however, only the keys and values contained by the object\'92s \f3\fs18 Dictionary \f4\fs20 superclass state will be serialized into the metadata (properties of the subclass will be ignored). This metadata dictionary can be recovered from the saved file using the \f3\fs18 treeSeqMetadata() \f4\fs20 function.\ When saving a single \f3\fs18 .trees \f4\fs20 file, the standard behavior is to overwrite an existing file of the same name; the convenience of this generally outweighs the danger. When saving a trees archive, however, that balance shifts; overwriting an entire directory is potentially quite dangerous. For this reason, \f3\fs18 overwriteDirectory=F \f4\fs20 (the default) specifies that \f3\fs18 treeSeqOutput() \f4\fs20 should not overwrite an existing directory; it will instead raise an error. If \f3\fs18 overwriteDirectory \f4\fs20 is \f3\fs18 T \f4\fs20 , \f3\fs18 treeSeqOutput() \f4\fs20 will overwrite an existing directory of the same name (if the existing directory can be deleted without permissions errors and so forth), but only if the existing directory is empty or contains only files with a \f3\fs18 .trees \f4\fs20 suffix, for safety; if other files are present, an error will still be raised.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(void)treeSeqRememberIndividuals(object\'a0individuals\kerning1\expnd0\expndtw0 , [logical$\'a0permanent\'a0=\'a0T]\expnd0\expndtw0\kerning0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Mark the individuals specified by \f3\fs18 individuals \f4\fs20 to be kept across tree sequence table simplification. This method may only be called if tree sequence recording has been turned on with \f3\fs18 initializeTreeSeq() \f4\fs20 . All currently living individuals are always kept across simplification; this method does not need to be called, and indeed should not be called, for that purpose. Instead, \f3\fs18 treeSeqRememberIndividuals() \f4\fs20 allows any individual, including dead individuals, to be kept in the final tree sequence. Typically this would be used, for example, to keep particular individuals that you wanted to be able to trace ancestry back to in later analysis. However, this is not the typical usage pattern for tree sequence recording; most models will not need to call this method.\ There are two ways to keep individuals across simplification. If \f3\fs18 permanent \f4\fs20 is \f3\fs18 T \f4\fs20 (the default), then the specified individuals will be permanently remembered: their haplosomes will be added to the current sample, and they will always be present in the tree sequence. Permanently remembering a large number of individuals will, of course, markedly increase memory usage and runtime.\ Supplying \f3\fs18 F \f4\fs20 for \f3\fs18 permanent \f4\fs20 will instead mark the individuals only for (temporary) retention: their haplosomes will not be added to the sample, and they will appear in the final tree sequence only if one of their haplosomes is retained across simplification. In other words, the rule of thumb for retained individuals is simple: if a haplosome is kept by simplification, the haplosome\'92s corresponding individual is kept also, \f1\i if \f4\i0 it is retained. Note that permanent remembering takes priority; calling this function with \f3\fs18 permanent=F \f4\fs20 on an individual that has previously been permanently remembered will not remove it from the sample.\ The behavior of simplification for individuals retained with \f3\fs18 permanent=F \f4\fs20 depends upon the value of the \f3\fs18 retainCoalescentOnly \f4\fs20 flag passed to \f3\fs18 initializeTreeSeq() \f4\fs20 ; here we will discuss the behavior of that flag in detail. First of all, haplosomes are \f1\i always \f4\i0 removed by simplification unless they are (a) part of the final generation (i.e., in a living individual when simplification occurs), (b) ancestral to the final generation, (c) a haplosome of a permanently remembered individual, or (d) ancestral to a permanently remembered individual. If \f3\fs18 retainCoalescentOnly \f4\fs20 is \f3\fs18 T \f4\fs20 (the default), they are \f1\i also \f4\i0 always removed if they are not a branch point (i.e., a coalescent node or most recent common ancestor) in the tree sequence. In some cases it may be useful to retain a haplosome and its associated individual when it is simply an intermediate node in the ancestry (i.e., in the middle of a branch). This can be enabled by setting \f3\fs18 retainCoalescentOnly \f4\fs20 to \f3\fs18 F \f4\fs20 in your call to \f3\fs18 initializeTreeSeq() \f4\fs20 . In this case, ancestral haplosomes that are intermediate (\'93unary nodes\'94, in \f3\fs18 tskit \f4\fs20 parlance) and are within an individual that has been retained using the \f3\fs18 permanent=F \f4\fs20 flag here are kept, along with the retained individual itself. Since setting \f3\fs18 retainCoalescentOnly \f4\fs20 to \f3\fs18 F \f4\fs20 will prevent the unary nodes for retained individuals from being pruned, simplification may often be unable to prune very much at all from the tree sequence, and memory usage and runtime may increase rapidly. If you are retaining many individuals, this setting should therefore be used only with caution; it is not necessary if you are purely interested in the most recent common ancestors. See the \f3\fs18 pyslim \f4\fs20 documentation for further discussion of retaining and remembering individuals and the effects of the \f3\fs18 retainCoalescentOnly \f4\fs20 flag.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 The metadata (age, location, etc) that are stored in the resulting tree sequence are those values present at either (a) the final generation, \kerning1\expnd0\expndtw0 if the individual is alive when the tree sequence is output\expnd0\expndtw0\kerning0 , or (b) the last time that the individual was remembered, if not. Calling \f3\fs18 treeSeqRememberIndividuals() \f4\fs20 on an individual that is already remembered will cause the archived information about the remembered individual to be updated to reflect the individual\'92s current state. A case where this is particularly important is for the spatial location of individuals in continuous-space models. SLiM automatically remembers the individuals that comprise the first generation of any new subpopulation created with \f3\fs18 addSubpop() \f4\fs20 , for easy recapitation and other analysis. However, since these first-generation individuals are remembered at the moment they are created, their spatial locations have not yet been set up, and will contain garbage \'96 and those garbage values will be archived in their remembered state. If you need correct spatial locations of first-generation individuals for your post-simulation analysis, you should call \f3\fs18 treeSeqRememberIndividuals() \f4\fs20 explicitly on the first generation, after setting spatial locations, to update the archived information with the correct spatial positions.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)treeSeqSimplify(void)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Triggers an immediate simplification of the tree sequence recording tables. This method may only be called if tree sequence recording has been turned on with \f3\fs18 initializeTreeSeq() \f4\fs20 . A call to this method will free up memory being used by entries that are no longer in the ancestral path of any individual within the current sample (currently living individuals, in other words, plus those explicitly added to the sample with \f3\fs18 treeSeqRememberIndividuals() \f4\fs20 ), but it can also take a significant amount of time. Typically calling this method is not necessary; the automatic simplification performed occasionally by SLiM should be sufficient for most models.\ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 \kerning1\expnd0\expndtw0 5.17 Class Subpopulation\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.17.1 \f2\fs18 Subpopulation \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf0 cloningRate => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The fraction of children in the next generation that will be produced by cloning (as opposed to biparental mating). In non-sexual (i.e. hermaphroditic) simulations, this property is a singleton \f3\fs18 float \f4\fs20 representing the overall subpopulation cloning rate. In sexual simulations, this property is a \f3\fs18 float \f4\fs20 vector with two values: the cloning rate for females (at index \f3\fs18 0 \f4\fs20 ) and for males (at index \f3\fs18 1 \f4\fs20 ). \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 description <\'96> (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A human-readable \f3\fs18 string \f4\fs20 description for the subpopulation. By default, this is the empty string, \f3\fs18 "" \f4\fs20 ; however, it may be set to whatever you wish. When tree-sequence recording is enabled, \f3\fs18 description \f4\fs20 is persisted in the subpopulation\'92s metadata in tree-sequence output.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 firstMaleIndex => (integer$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The index of the first male individual in the subpopulation. The \f3\fs18 individuals \f4\fs20 vector of the subpopulation is sorted into females first and males second; \f3\fs18 firstMaleIndex \f4\fs20 gives the position of the boundary between those sections. The \f3\fs18 firstMaleIndex \f4\fs20 property is also the number of females in the subpopulation, given this design. For non-sexual (i.e. hermaphroditic) simulations, the value of this property is undefined and should not be used.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 fitnessScaling <\'96> (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A \f3\fs18 float \f4\fs20 scaling factor applied to the fitness of all individuals in this subpopulation (i.e., the fitness value computed for each individual will be multiplied by this value). This is primarily of use in nonWF models, where fitness is absolute, rather than in WF models, where fitness is relative (and thus a constant factor multiplied into the fitness of every individual will make no difference); however, it may be used in either type of model. This provides a simple, fast way to modify the fitness of all individuals in a subpopulation; conceptually it is similar to returning the same fitness effect for all individuals in the subpopulation from a \f3\fs18 fitnessEffect() \f4\fs20 callback, but without the complexity and performance overhead of implementing such a callback. To scale the fitness of individuals by different (individual-specific) factors, see the \f3\fs18 fitnessScaling \f4\fs20 property of \f3\fs18 Individual \f4\fs20 .\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 The value of \f3\fs18 fitnessScaling \f4\fs20 is reset to \f3\fs18 1.0 \f4\fs20 every tick, so that any scaling factor set lasts for only a single tick. This reset occurs immediately after fitness values are calculated, in both WF and nonWF models.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 haplosomes => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the haplosomes contained by the subpopulation. All of the haplosomes for the first individual in the \f3\fs18 individuals \f4\fs20 property are provided, followed by all the haplosomes for the second individual, etc., in the same order as \f3\fs18 individuals \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 haplosomesNonNull => (object)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the haplosomes contained by the subpopulation, as with the \f3\fs18 haplosomes \f4\fs20 property, if all of them are not null haplosomes; any null haplosomes present are excluded from the returned vector. This is a convenience shorthand, sometimes useful in models that involve null haplosomes.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier for this subpopulation; for subpopulation \f3\fs18 p3 \f4\fs20 , for example, this is \f3\fs18 3 \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 immigrantSubpopFractions => (float)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The expected value of the fraction of children in the next generation that are immigrants arriving from particular subpopulations. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 immigrantSubpopIDs => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifiers of the particular subpopulations from which immigrants will arrive in the next generation. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 individualCount => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The number of individuals in the subpopulation.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 individuals => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 All of the individuals contained by the subpopulation. See the \f3\fs18 sampleIndividuals() \f4\fs20 and \f3\fs18 subsetIndividuals() \f4\fs20 for fast ways to get a subset of the individuals in a subpopulation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 lifetimeReproductiveOutput => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , \f3\fs18 lifetimeReproductiveOutput \f4\fs20 contains the value of the \f3\fs18 Individual \f4\fs20 property \f3\fs18 reproductiveOutput \f4\fs20 for all individuals in the subpopulation that died in the last viability/survival tick cycle stage (or, for WF models, immediately after reproduction). This allows access to the lifetime reproductive output of individuals in the subpopulation at the end of their lives. If pedigree tracking is not on, this property is unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 lifetimeReproductiveOutputF => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , \f3\fs18 lifetimeReproductiveOutputF \f4\fs20 contains the value of the \f3\fs18 Individual \f4\fs20 property \f3\fs18 reproductiveOutput \f4\fs20 for all female individuals in the subpopulation that died in the last viability/survival tick cycle stage (or, for WF models, immediately after reproduction). This property is undefined if separate sexes have not been enabled, or if pedigree tracking is not on.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 lifetimeReproductiveOutputM => (integer)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 If pedigree tracking is turned on with \f3\fs18 initializeSLiMOptions(keepPedigrees=T) \f4\fs20 , \f3\fs18 lifetimeReproductiveOutputM \f4\fs20 contains the value of the \f3\fs18 Individual \f4\fs20 property \f3\fs18 reproductiveOutput \f4\fs20 for all male individuals in the subpopulation that died in the last viability/survival tick cycle stage (or, for WF models, immediately after reproduction). This property is undefined if separate sexes have not been enabled, or if pedigree tracking is not on.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 name <\'96> (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A human-readable \f3\fs18 string \f4\fs20 name for the subpopulation. By default, this is the subpopulation\'92s symbol as a \f3\fs18 string \f4\fs20 ; for subpopulation \f3\fs18 p3 \f4\fs20 , for example, \f3\fs18 name \f4\fs20 defaults to \f3\fs18 "p3" \f4\fs20 . However, it may be set to whatever you wish except that subpopulation names must be unique across time (two different subpopulations may not both have the name \f3\fs18 "foo" \f4\fs20 , even if they never exist at the same time). A subpopulation\'92s \f3\fs18 name \f4\fs20 may appear as a label in SLiMgui, and it can be useful in generating output, debugging, and other purposes. When tree-sequence recording is enabled, \f3\fs18 name \f4\fs20 is persisted in the subpopulation\'92s metadata in tree-sequence output, and can then be used in Python to identify the subpopulation; if you plan to take advantage of that feature, \f3\fs18 name \f4\fs20 should follow the syntax of Python identifiers: starting with a letter or underscore \f3\fs18 [a-zA-Z_] \f4\fs20 , followed by letters, digits, or underscores \f3\fs18 [a-zA-Z0-9_] \f4\fs20 , without spaces, hyphens, or other characters.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 selfingRate => (float$)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The expected value of the fraction of children in the next generation that will be produced by selfing (as opposed to biparental mating). Selfing is only possible in non-sexual (i.e. hermaphroditic) simulations; for sexual simulations this property always has a value of \f3\fs18 0.0 \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 sexRatio => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 For sexual simulations, the sex ratio for the subpopulation. This is defined, in SLiM, as the fraction of the subpopulation that is male; in other words, it is actually the M:(M+F) ratio. For non-sexual (i.e. hermaphroditic) simulations, this property has an undefined value and should not be used. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 spatialMaps => (object)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The spatial maps that are currently added to the subpopulation.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 spatialBounds => (float)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The spatial boundaries of the subpopulation. The length of the \f3\fs18 spatialBounds \f4\fs20 property depends upon the spatial dimensionality declared with \f3\fs18 initializeSLiMOptions() \f4\fs20 . If the spatial dimensionality is zero (as it is by default), the value of this property is \f3\fs18 float(0) \f4\fs20 (a zero-length \f3\fs18 float \f4\fs20 vector). Otherwise, minimums are supplied for each coordinate used by the dimensionality of the simulation, followed by maximums for each. In other words, if the declared dimensionality is \f3\fs18 "xy" \f4\fs20 , the \f3\fs18 spatialBounds \f4\fs20 property will contain values \f3\fs18 (x0,\'a0y0,\'a0x1,\'a0y1) \f4\fs20 ; bounds for the \f1\i z \f4\i0 coordinate will not be included in that case, since that coordinate is not used in the simulation\'92s dimensionality. This property cannot be set, but the \f3\fs18 setSpatialBounds() \f4\fs20 method may be used to achieve the same thing. \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 species => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 The species to which the target object belongs.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is initially undefined\cf2 \expnd0\expndtw0\kerning0 , and it is an error to try to read it\cf0 \kerning1\expnd0\expndtw0 ; if you wish it to have a defined value, you must arrange that yourself by explicitly setting its value prior to using it elsewhere in your code. The value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use. See also the \f3\fs18 getValue() \f4\fs20 and \f3\fs18 setValue() \f4\fs20 methods\cf2 (provided by the \f3\fs18 Dictionary \f4\fs20 class; see the Eidos manual)\cf0 , for another way of attaching state to subpopulations.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.17.2 \f2\fs18 Subpopulation \f1\fs22 methods\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(object)addCloned(object$\'a0parent, [integer$\'a0count\'a0=\'a01], [logical$\'a0defer\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Generates a new offspring individual from the given parent by clonal reproduction, queues it for addition to the target subpopulation, and returns it. The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage. The subpopulation of \f3\fs18 parent \f4\fs20 will be used to locate applicable \f3\fs18 mutation() \f4\fs20 and \f3\fs18 modifyChild() \f4\fs20 callbacks governing the generation of the offspring individual.\ Beginning in SLiM 4.1, the \f3\fs18 count \f4\fs20 parameter dictates how many offspring will be generated (previously, exactly one offspring was generated). Each offspring is generated independently, based upon the given parameters. The returned vector contains all generated offspring, except those that were rejected by a \f3\fs18 modifyChild() \f4\fs20 callback. If all offspring are rejected, \f3\fs18 object(0) \f4\fs20 is returned, which is a zero-length \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Individual \f4\fs20 ; note that this is a change in behavior from earlier versions, which would return \f3\fs18 NULL \f4\fs20 .\ Beginning in SLiM 4.1, passing \f3\fs18 T \f4\fs20 for \f3\fs18 defer \f4\fs20 requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase. SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if \f3\fs18 defer \f4\fs20 were \f3\fs18 F \f4\fs20 . Haplosome generation can only be deferred if there are no active \f3\fs18 mutation() \f4\fs20 callbacks; otherwise, an error will result. Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a \f3\fs18 modifyChild() \f4\fs20 callback or otherwise). There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of \f3\fs18 F \f4\fs20 for \f3\fs18 defer \f4\fs20 is generally preferable since it has fewer restrictions. When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).\ Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from \f3\fs18 parent \f4\fs20 ; more specifically, the \f3\fs18 x \f4\fs20 property will be inherited in all spatial models (1D/2D/3D), the \f3\fs18 y \f4\fs20 property in 2D/3D models, and the \f3\fs18 z \f4\fs20 property in 3D models. Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1. The parent\'92s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to \f3\fs18 deviatePositions() \f4\fs20 / \f3\fs18 pointDeviated() \f4\fs20 .\ Note that this method is only for use in nonWF models. See \f3\fs18 addCrossed() \f4\fs20 for further general notes on the addition of new offspring individuals.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(object)addCrossed(object$\'a0parent1, object$\'a0parent2, [Nfs$\'a0sex\'a0=\'a0NULL], [integer$\'a0count\'a0=\'a01], [logical$\'a0defer\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Generates a new offspring individual from the given parents by biparental sexual reproduction, queues it for addition to the target subpopulation, and returns it. The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage. Attempting to use a newly generated offspring individual as a mate, or to reference it as a member of the target subpopulation in any other way, will result in an error. In most models the returned individual is not used, but it is provided for maximal generality and flexibility.\ The new offspring individual is generated from \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 by crossing them. In sexual models \f3\fs18 parent1 \f4\fs20 must be female and \f3\fs18 parent2 \f4\fs20 must be male; in hermaphroditic models, \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 are unrestricted. If \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 are the same individual in a hermaphroditic model, that parent self-fertilizes, or \'93selfs\'94, to generate the offspring sexually (note this is not the same as clonal reproduction). Such selfing is considered \'93incidental\'94 by \f3\fs18 addCrossed() \f4\fs20 , however; if the \f3\fs18 preventIncidentalSelfing \f4\fs20 flag of \f3\fs18 initializeSLiMOptions() \f4\fs20 is \f3\fs18 T \f4\fs20 , supplying the same individual for \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 is an error (you must check for and prevent incidental selfing if you set that flag in a nonWF model). If non-incidental selfing is desired, \f3\fs18 addSelfed() \f4\fs20 should be used instead.\ The \f3\fs18 sex \f4\fs20 parameter specifies the sex of the offspring. A value of \f3\fs18 NULL \f4\fs20 means \'93make the default choice\'94; in non-sexual models it is the only legal value for \f3\fs18 sex \f4\fs20 , and does nothing, whereas in sexual models it causes male or female to be chosen with equal probability. A value of \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 for \f3\fs18 sex \f4\fs20 specifies that the offspring should be male or female, respectively. Finally, a \f3\fs18 float \f4\fs20 value from \f3\fs18 0.0 \f4\fs20 to \f3\fs18 1.0 \f4\fs20 for \f3\fs18 sex \f4\fs20 provides the probability that the offspring will be male; a value of \f3\fs18 0.0 \f4\fs20 will produce a female, a value of \f3\fs18 1.0 \f4\fs20 will produce a male, and for intermediate values SLiM will draw the sex of the offspring randomly according to the specified probability. Unless you wish the bias the sex ratio of offspring, the default value of \f3\fs18 NULL \f4\fs20 should generally be used.\ Note that any defined, active, and applicable \f3\fs18 recombination() \f4\fs20 , \f3\fs18 mutation() \f4\fs20 , and \f3\fs18 modifyChild() \f4\fs20 callbacks will be called as a side effect of calling this method, before this method even returns. For \f3\fs18 recombination() \f4\fs20 and \f3\fs18 mutation() \f4\fs20 callbacks, the subpopulation of the parent that is generating a given gamete is used; for \f3\fs18 modifyChild() \f4\fs20 callbacks the situation is more complex. In most biparental mating events, \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 will belong to the same subpopulation, and \f3\fs18 modifyChild() \f4\fs20 callbacks for that subpopulation will be used, just as in WF models. In certain models (such as models of pollen flow and broadcast spawning), however, biparental mating may occur between parents that are not from the same subpopulation; that is legal in nonWF models, and in that case, \f3\fs18 modifyChild() \f4\fs20 callbacks for the subpopulation of \f3\fs18 parent1 \f4\fs20 are used (since that is the maternal parent).\ If the \f3\fs18 modifyChild() \f4\fs20 callback process results in rejection of the proposed child, a new offspring individual is not generated. To force the generation of an offspring individual from a given pair of parents, you could loop until \f3\fs18 addCrossed() \f4\fs20 succeeds, but note that if your \f3\fs18 modifyChild() \f4\fs20 callback rejects all proposed children from those particular parents, your model will then hang, so care must be taken with this approach. Usually, nonWF models do not force generation of offspring in this manner; rejection of a proposed offspring by a \f3\fs18 modifyChild() \f4\fs20 callback typically represents a phenomenon such as post-mating reproductive isolation or lethal genetic incompatibilities that would reduce the expected litter size, so the default behavior is typically desirable.\ Beginning in SLiM 4.1, the \f3\fs18 count \f4\fs20 parameter dictates how many offspring will be generated (previously, exactly one offspring was generated). Each offspring is generated independently, based upon the given parameters. The returned vector contains all generated offspring, except those that were rejected by a \f3\fs18 modifyChild() \f4\fs20 callback. If all offspring are rejected, \f3\fs18 object(0) \f4\fs20 is returned, which is a zero-length \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Individual \f4\fs20 ; note that this is a change in behavior from earlier versions, which would return \f3\fs18 NULL \f4\fs20 .\ Beginning in SLiM 4.1, passing \f3\fs18 T \f4\fs20 for \f3\fs18 defer \f4\fs20 requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase. SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if \f3\fs18 defer \f4\fs20 were \f3\fs18 F \f4\fs20 . Haplosome generation can only be deferred if there are no active \f3\fs18 mutation() \f4\fs20 or \f3\fs18 recombination() \f4\fs20 callbacks; otherwise, an error will result. Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a \f3\fs18 modifyChild() \f4\fs20 callback or otherwise). There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of \f3\fs18 F \f4\fs20 for \f3\fs18 defer \f4\fs20 is generally preferable since it has fewer restrictions. When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).\ Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from \f3\fs18 parent1 \f4\fs20 ; more specifically, the \f3\fs18 x \f4\fs20 property will be inherited in all spatial models (1D/2D/3D), the \f3\fs18 y \f4\fs20 property in 2D/3D models, and the \f3\fs18 z \f4\fs20 property in 3D models. Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1. The parent\'92s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to \f3\fs18 deviatePositions() \f4\fs20 / \f3\fs18 pointDeviated() \f4\fs20 .\ Note that this method is only for use in nonWF models, in which offspring generation is managed manually by the model script; in such models, \f3\fs18 addCrossed() \f4\fs20 must be called only from \f3\fs18 reproduction() \f4\fs20 callbacks, and may not be called at any other time. In WF models, offspring generation is managed automatically by the SLiM core.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(object)addEmpty([Nfs$\'a0sex\'a0=\'a0NULL], [Nl$\'a0haplosome1Null\'a0=\'a0NULL], [Nl$\'a0haplosome2Null\'a0=\'a0NULL], [integer$\'a0count\'a0=\'a01])\ \pard\pardeftab529\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Generates a new offspring individual with empty haplosomes (i.e., containing no mutations), queues it for addition to the target subpopulation, and returns it. The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage. No \f3\fs18 recombination() \f4\fs20 or \f3\fs18 mutation() \f4\fs20 callbacks will be called. The target subpopulation will be used to locate applicable \f3\fs18 modifyChild() \f4\fs20 callbacks governing the generation of the offspring individual (unlike the other \f3\fs18 addX() \f4\fs20 methods, because there is no parental individual to reference). The offspring is considered to have no parents for the purposes of pedigree tracking. The \f3\fs18 sex \f4\fs20 parameter is treated as in \f3\fs18 addCrossed() \f4\fs20 .\ For all chromosome types except \f3\fs18 "A" \f4\fs20 , null haplosomes will be generated as dictated by the sex of the individual and type of the chromosome. For example, for chromosome type \f3\fs18 "X" \f4\fs20 a female would be generated with two empty haplosomes for that chromosome (XX), whereas a male would be generated with one empty haplosome and one null haplosome (X\'96, in SLiM parlance). For chromosome type \f3\fs18 "H" \f4\fs20 an empty haplosome is always generated, not a null haplosome. But for chromosome type \f3\fs18 "A" \f4\fs20 , in particular, more control is afforded. Passing \f3\fs18 NULL \f4\fs20 (the default) or \f3\fs18 F \f4\fs20 for \f3\fs18 haplosome1Null \f4\fs20 will make the first haplosome for every chromosome of type \f3\fs18 "A" \f4\fs20 be a non-null (empty) haplosome, the standard behavior. More interestingly, passing \f3\fs18 T \f4\fs20 for \f3\fs18 haplosome1Null \f4\fs20 would make the first haplosome for every chromosome of type \f3\fs18 "A" \f4\fs20 be a null haplosome. Similarly, passing \f3\fs18 T \f4\fs20 for \f3\fs18 haplosome2Null \f4\fs20 would make the second haplosome for every chromosome of type \f3\fs18 "A" \f4\fs20 be a null haplosome. This option could be useful for situations such as adding new haploids into a haplodiploid model. (Separate control over the haploid or diploid configuration of each chromosome of type \f3\fs18 "A" \f4\fs20 is not presently supported, but would be a simple extension to the design, by allowing \f3\fs18 haplosome1Null \f4\fs20 and \f3\fs18 haplosome2Null \f4\fs20 to provide a whole vector of \f3\fs18 logical \f4\fs20 flags rather than just a singleton value; please request this feature if you require it.)\ Beginning in SLiM 4.1, the \f3\fs18 count \f4\fs20 parameter dictates how many offspring will be generated (previously, exactly one offspring was generated). Each offspring is generated independently, based upon the given parameters. The returned vector contains all generated offspring, except those that were rejected by a \f3\fs18 modifyChild() \f4\fs20 callback. If all offspring are rejected, \f3\fs18 object(0) \f4\fs20 is returned, which is a zero-length \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Individual \f4\fs20 ; note that this is a change in behavior from earlier versions, which would return \f3\fs18 NULL \f4\fs20 .\ Note that this method is only for use in nonWF models. See \f3\fs18 addCrossed() \f4\fs20 for further general notes on the addition of new offspring individuals.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)addMultiRecombinant(object$\'a0pattern, [Nfs$\'a0sex\'a0=\'a0NULL], [No$\'a0parent1\'a0=\'a0NULL], [No$\'a0parent2\'a0=\'a0NULL], [Nl$\'a0randomizeStrands\'a0=\'a0NULL], [integer$\'a0count\'a0=\'a01], [logical$\'a0defer\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Generates a new offspring individual based upon the inheritance pattern specified by \f3\fs18 pattern \f4\fs20 , queues it for addition to the target subpopulation, and returns it. The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage. The \'93pattern dictionary\'94 supplied in \f3\fs18 pattern \f4\fs20 must be of class \f3\fs18 Dictionary \f4\fs20 (or a subclass of \f3\fs18 Dictionary \f4\fs20 ), and more particularly, must be a dictionary of dictionaries structured in a specific way as described below. This method is a multi-chromosome version of the \f3\fs18 addRecombinant() \f4\fs20 method. For single-chromosome models, using \f3\fs18 addRecombinant() \f4\fs20 will be simpler; and it will be easier to understand this extremely complex method if you understand \f3\fs18 addRecombinant() \f4\fs20 first.\ The top-level \'93pattern dictionary\'94 given by \f3\fs18 pattern \f4\fs20 specifies the way in which each chromosome should be handled. It can use \f3\fs18 integer \f4\fs20 keys, in which case each key is the \f3\fs18 id \f4\fs20 of a chromosome, or \f3\fs18 string \f4\fs20 keys, in which case each key is the \f3\fs18 symbol \f4\fs20 of a chromosome. In either case, a chromosome\'92s inheritance pattern is specified by an \'93inheritance dictionary\'94 in \f3\fs18 pattern \f4\fs20 attached to such a chromosome \f3\fs18 id \f4\fs20 or \f3\fs18 symbol \f4\fs20 key. That inheritance dictionary should itself contain up to six keys, with the standard names \f3\fs18 "strand1" \f4\fs20 , \f3\fs18 "strand2" \f4\fs20 , \f3\fs18 "breaks1" \f4\fs20 , \f3\fs18 "strand3" \f4\fs20 , \f3\fs18 "strand4" \f4\fs20 , and \f3\fs18 "breaks2" \f4\fs20 . Any key which is missing in an inheritance dictionary is assumed to have a value of \f3\fs18 NULL \f4\fs20 , and missing keys will be referred to having a value of \f3\fs18 NULL \f4\fs20 here for simplicity. These key-value pairs are used in precisely the same way as the parameters of the same names for \f3\fs18 addRecombinant() \f4\fs20 , to produce the offspring haplosome(s) for each specified chromosome. There is some complication regarding how these six values can be used to produce results like crossing, cloning, and selfing, involving as many as four different \'93parents\'94 for each chromosome; rather than repeating all of that documentation here, please see the \f3\fs18 addRecombinant() \f4\fs20 documentation for more information. When an inheritance dictionary is supplied for a particular chromosome, this method uses the six values that dictionary contains ( \f3\fs18 strand1 \f4\fs20 , \f3\fs18 strand2 \f4\fs20 , \f3\fs18 breaks1 \f4\fs20 , \f3\fs18 strand3 \f4\fs20 , \f3\fs18 strand4 \f4\fs20 , \f3\fs18 breaks2 \f4\fs20 ) in exactly the same way as \f3\fs18 addRecombinant() \f4\fs20 does; \f3\fs18 addMultiRecombinant() \f4\fs20 simply supports multiple chromosomes. In addition to that, however, \f3\fs18 addMultiRecombinant() \f4\fs20 also allows the pattern dictionary to omit the inheritance dictionaries for particular chromosomes; the behavior of \f3\fs18 addMultiRecombinant() \f4\fs20 in that special case will be described below, after discussing all other aspects of the method\'92s implementation.\ The \f3\fs18 sex \f4\fs20 parameter optionally specifies the sex of the offspring. The default value of \f3\fs18 NULL \f4\fs20 for \f3\fs18 sex \f4\fs20 specifies \'93default behavior\'94; in a non-sexual model this is the only legal value, and produces a hermaphroditic offspring. In a sexual model, the \'93default behavior\'94 of \f3\fs18 NULL \f4\fs20 is that the offspring\'92s sex is dictated by the haplosome structure it inherits. For example, if \f3\fs18 pattern \f4\fs20 specifies that the offspring will have two non-null haplosomes for a chromosome of type \f3\fs18 "X" \f4\fs20 , the sex of the offspring must therefore be female, whereas if it will have one non-null \f3\fs18 "X" \f4\fs20 haplosome and one null \f3\fs18 "X" \f4\fs20 haplosome, it must therefore be male. SLiM will scan through \f3\fs18 pattern \f4\fs20 to determine such constraints and enforce them. If the constraints implied by \f3\fs18 pattern \f4\fs20 are not self-consistent (if the offspring would have two non-null \f3\fs18 "X" \f4\fs20 haplosomes but also a non-null \f3\fs18 "Y" \f4\fs20 haplosome, for example), an error will be raised. The constraints defined by each chromosome type, as described in \f3\fs18 initializeChromosome() \f4\fs20 , must always be followed, even when using \f3\fs18 addMultiRecombinant() \f4\fs20 . If \f3\fs18 sex \f4\fs20 is \f3\fs18 NULL \f4\fs20 and \f3\fs18 pattern \f4\fs20 imposes no constraints upon the sex of the offspring, the offspring will be chosen as male or female with equal probability. A value of \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 for \f3\fs18 sex \f4\fs20 specifies that the offspring should be male or female, respectively. A \f3\fs18 float \f4\fs20 value from \f3\fs18 0.0 \f4\fs20 to \f3\fs18 1.0 \f4\fs20 for \f3\fs18 sex \f4\fs20 provides the probability that the offspring will be male; a value of \f3\fs18 0.0 \f4\fs20 will produce a female, a value of \f3\fs18 1.0 \f4\fs20 will produce a male, and for intermediate values SLiM will draw the sex of the offspring randomly according to the specified probability. In these cases where \f3\fs18 sex \f4\fs20 is not \f3\fs18 NULL \f4\fs20 , SLiM will first determine the sex of the individual as just described, and will then scan through \f3\fs18 pattern \f4\fs20 to confirm that it is compatible with the sex that was determined. Again, if there is a conflict an error will be raised; you cannot specify the sex of an individual to be incompatible with the haplosomes that it inherits, and if you specify a sex with a \f3\fs18 string \f4\fs20 or \f3\fs18 float \f4\fs20 value it is up to you to ensure that that is compatible with the specifications in \f3\fs18 pattern \f4\fs20 . (If you need more flexibility, you should probably not use a sexual model at all, but simply use chromosome types \f3\fs18 "A" \f4\fs20 and \f3\fs18 "H" \f4\fs20 in a non-sexual model, track the sex of individuals yourself with a tag value such as \f3\fs18 tagL0 \f4\fs20 , and manipulate haplosomes during reproduction however you wish; SLiM then imposes no constraints.)\ By default, the offspring is considered to have no parents, since there may be more than two \'93parents\'94 in the general case. If specifying parentage is desired, \f3\fs18 parent1 \f4\fs20 and/or \f3\fs18 parent2 \f4\fs20 may be passed explicitly; this will establish those individuals as the parents of the offspring for purposes of pedigree tracking, and for several other purposes described below. If only one of \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 is non- \f3\fs18 NULL \f4\fs20 , that individual will be set as \f1\i both \f4\i0 of the parents of the offspring, mirroring the way that parentage is tracked for other cases such as \f3\fs18 addCloned() \f4\fs20 and \f3\fs18 addSelfed() \f4\fs20 . It is not required for \f3\fs18 parent1 \f4\fs20 or \f3\fs18 parent2 \f4\fs20 to actually be a genetic parent of the offspring at all, although typically they would be. To benefit from the full functionality of \f3\fs18 addMultiRecombinant() \f4\fs20 as described below, it is best to supply \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 when possible.\ The \f3\fs18 randomizeStrands \f4\fs20 parameter is used to control the recombination behavior of \f3\fs18 addMultiRecombinant() \f4\fs20 . An inheritance dictionary can specify two parental strands with crossover breakpoints between them to generate one offspring strand with recombination \'96 for example, with the \f3\fs18 "strand1" \f4\fs20 , \f3\fs18 "strand2" \f4\fs20 , and \f3\fs18 "breaks1" \f4\fs20 keys, as described above, but the same is true of the \f3\fs18 "strand3" \f4\fs20 , \f3\fs18 "strand4" \f4\fs20 , and \f3\fs18 "breaks2" \f4\fs20 keys, and the discussion that follows applies to both cases. If \f3\fs18 randomizeStrands \f4\fs20 is \f3\fs18 F \f4\fs20 , the supplied strands are used as given; for example, \f3\fs18 "strand1" \f4\fs20 will be the initial copy strand when generating the first gamete to form the offspring. This mode should be used if you want explicit control over the initial copy strand; one example would be if your script is explicitly generating all four of the products of a meiosis event. If \f3\fs18 randomizeStrands \f4\fs20 is T, then if \f3\fs18 "strand1" \f4\fs20 and \f3\fs18 "strand2" \f4\fs20 are both non- \f3\fs18 NULL \f4\fs20 , 50% of the time they will be swapped, making \f3\fs18 "strand2" \f4\fs20 the initial copy strand for the first gamete instead. This mode ( \f3\fs18 randomizeStrands \f4\fs20 = \f3\fs18 T \f4\fs20 ) is usually the desired behavior, to avoid an inheritance bias due to a lack of randomization in the initial copy strand, so passing \f3\fs18 T \f4\fs20 for \f3\fs18 randomizeStrands \f4\fs20 is recommended unless you specifically desire otherwise. The default value of \f3\fs18 randomizeStrands \f4\fs20 is \f3\fs18 NULL \f4\fs20 in order to force either \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 to be explicitly chosen whenever it would make a difference; if it is left as \f3\fs18 NULL \f4\fs20 , an error will be raised if generation of the specified offspring involves recombination, since then SLiM needs to know whether the value is \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 . (This unconventional approach has been adopted because the default value was \f3\fs18 F \f4\fs20 prior to SLiM 5, but \f3\fs18 T \f4\fs20 is almost always the correct behavior, as explained above. To try to prevent accidental bugs, this new policy was adopted to force the user to explicitly choose \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 whenever it matters.)\ The value of the \f3\fs18 meanParentAge \f4\fs20 property of the generated offspring is calculated from the mean parent age of each of its two haplosomes (whether they turn out to be null haplosomes or not). The \f3\fs18 addRecombinant() \f4\fs20 documentation provides a simple example; for \f3\fs18 addMultiRecombinant() \f4\fs20 the logic is the same, but potentially extended to more than two offspring haplosomes.\ Callbacks can be involved in offspring generation with \f3\fs18 addMultiRecombinant() \f4\fs20 , but because there are up to four strands with up to four different parents, things are a bit complicated and different from other \f3\fs18 add...() \f4\fs20 methods; the policy described here seems like the best compromise. The target subpopulation for the \f3\fs18 addMultiRecombinant() \f4\fs20 call will be used to locate applicable \f3\fs18 mutation() \f4\fs20 and \f3\fs18 modifyChild() \f4\fs20 callbacks governing the generation of the offspring individual. On the other hand, \f3\fs18 recombination() \f4\fs20 callbacks will be found based upon the subpopulations to which \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 belong (for reasons discussed further in \f3\fs18 drawBreakpoints() \f4\fs20 ); if a parent individual is not supplied, \f3\fs18 recombination() \f4\fs20 callbacks will not be called at all when generating the corresponding offspring haplosome.\ When breakpoints are explicitly supplied to \f3\fs18 addMultiRecombinant() \f4\fs20 with \f3\fs18 breaks1 \f4\fs20 or \f3\fs18 breaks2 \f4\fs20 , gene conversion tracts are not well-supported by this method; the \f3\fs18 breaks1 \f4\fs20 and \f3\fs18 breaks2 \f4\fs20 vectors provide simple crossover breakpoints, which may be used to implement crossovers or simple gene conversion tracts, but complex gene conversion tracts with heteroduplex mismatch repair are not supported in this mode of operation since there is no way to supply the relevant information. If, on the other hand, \f3\fs18 breaks1 \f4\fs20 or \f3\fs18 breaks2 \f4\fs20 is \f3\fs18 NULL \f4\fs20 when generating a haplosome with recombination, then as described above, \f3\fs18 addRecombinant() \f4\fs20 will generate breakpoints internally for that cross, and in this case, complex gene conversion tracts with heteroduplex mismatch repair are supported, since all of the necessary information is available. Similarly, if the inheritance dictionary for a given chromosome is omitted from \f3\fs18 pattern \f4\fs20 entirely and a cross between \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 is inferred (as discussed below), the recombination algorithm used will support gene conversion including heteroduplex mismatch repair.\ Finally, \f3\fs18 count \f4\fs20 is the number of offspring to generate using the given pattern and parameters, and \f3\fs18 defer \f4\fs20 is used for deferral of offspring generation, as described for \f3\fs18 addRecombinant() \f4\fs20 . Any other details omitted from this documentation are all as described for \f3\fs18 addRecombinant() \f4\fs20 .\ Constructing a well-formed pattern dictionary with inheritance dictionaries for every chromosome can be a bit complex and require many lines of code. To ease the process, see the \f3\fs18 Species \f4\fs20 methods \f3\fs18 addPatternForCross() \f4\fs20 , \f3\fs18 addPatternForClone() \f4\fs20 , \f3\fs18 addPatternForNull() \f4\fs20 , and \f3\fs18 addPatternForRecombinant() \f4\fs20 , which help you to build a pattern dictionary one inheritance dictionary at a time. However, several of these methods will probably be used infrequently, because of the final aspect of \f3\fs18 addMultiRecombinant() \f4\fs20 that we have not yet properly discussed.\ As mentioned earlier, not all chromosomes need to be specified with an inheritance dictionary in \f3\fs18 pattern \f4\fs20 ; whenever SLiM\'92s default inheritance behavior is well-defined and is desired for a given chromosome, the inheritance dictionary for that chromosome may be omitted, and will be inferred by \f3\fs18 addMultiRecombinant() \f4\fs20 automatically. This behavior makes it easy to specify a reproduction event that is, for example, like a regular biparental cross involving many chromosomes, but that uses a different reproductive pattern just for one particular chromosome that behaves in a special way. The inferred inheritance dictionary for a given chromosome is based upon the chromosome type, the values of \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 , and the sex of the offspring (which has, by this point, been determined in all cases). The rules for this inference are actually quite simple. If both parents are specified (that is, are both non- \f3\fs18 NULL \f4\fs20 ), the inferred inheritance dictionary is the same as would be produced by a call to \f3\fs18 addPatternForCross() \f4\fs20 with those two parents, given in that order, for that chromosome, for the determined offspring sex. If only one parent is specified (non- \f3\fs18 NULL \f4\fs20 ), the inferred inheritance dictionary is the same as would be produced by a call to \f3\fs18 addPatternForClone() \f4\fs20 with that one parent, for that chromosome, for the determined offspring sex. If selfing is desired for the inferred inheritance dictionary, pass the same individual for both \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 ; the behavior of \f3\fs18 addPatternForCross() \f4\fs20 in that case is essentially to self the individual, as discussed in that method. If the inferred inheritance dictionary for a given chromosome is not well-defined, as discussed in the documentation for \f3\fs18 addPatternForCross() \f4\fs20 and \f3\fs18 addPatternForClone() \f4\fs20 , or if both \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 are \f3\fs18 NULL \f4\fs20 , an error will be raised. In such cases, the inheritance dictionary cannot be inferred, and will need to be given explicitly in \f3\fs18 pattern \f4\fs20 .\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)addRecombinant(No$\'a0strand1, No$\'a0strand2, Ni\'a0breaks1, No$\'a0strand3, No$\'a0strand4, Ni\'a0breaks2, [Nfs$\'a0sex\'a0=\'a0NULL], [No$\'a0parent1\'a0=\'a0NULL], [No$\'a0parent2\'a0=\'a0NULL], [Nl$\'a0randomizeStrands\'a0=\'a0NULL], [integer$\'a0count\'a0=\'a01], [logical$\'a0defer\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Generates a new offspring individual from the given parental haplosomes with the specified crossover breakpoints, queues it for addition to the target subpopulation, and returns it. The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage. This method is an advanced feature; most models will use \f3\fs18 addCrossed() \f4\fs20 , \f3\fs18 addSelfed() \f4\fs20 , or \f3\fs18 addCloned() \f4\fs20 instead. This method may only be used in single-chromosome models; in multi-chromosome models, use \f3\fs18 addMultiRecombinant() \f4\fs20 , a more general version of \f3\fs18 addRecombinant() \f4\fs20 .\ This method supports several possible configurations for \f3\fs18 strand1 \f4\fs20 , \f3\fs18 strand2 \f4\fs20 , and \f3\fs18 breaks1 \f4\fs20 (and the same applies for \f3\fs18 strand3 \f4\fs20 , \f3\fs18 strand4 \f4\fs20 , and \f3\fs18 breaks2 \f4\fs20 ). If \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 are both \f3\fs18 NULL \f4\fs20 , the corresponding haplosome in the generated offspring will be a null haplosome; in this case, \f3\fs18 breaks1 \f4\fs20 must be \f3\fs18 NULL \f4\fs20 or zero-length. If \f3\fs18 strand1 \f4\fs20 is non- \f3\fs18 NULL \f4\fs20 but \f3\fs18 strand2 \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the corresponding haplosome in the generated offspring will be a clonal copy of \f3\fs18 strand1 \f4\fs20 with mutations added, as from \f3\fs18 addCloned() \f4\fs20 ; in this case, \f3\fs18 breaks1 \f4\fs20 must again be \f3\fs18 NULL \f4\fs20 or zero-length. If \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 are both non- \f3\fs18 NULL \f4\fs20 , the corresponding haplosome in the generated offspring will result from recombination between \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 with mutations added, as from \f3\fs18 addCrossed() \f4\fs20 , with \f3\fs18 strand1 \f4\fs20 being the initial copy strand by default (but see below). Copying will switch between strands at each crossover breakpoint. Breakpoints may be supplied in \f3\fs18 breaks1 \f4\fs20 , which need not be sorted or uniqued (SLiM will sort and unique the supplied breakpoints internally). Alternatively, \f3\fs18 breaks1 \f4\fs20 may be \f3\fs18 NULL \f4\fs20 , which requests that \f3\fs18 addRecombinant() \f4\fs20 draw breakpoints automatically to recombine \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 , following SLiM\'92s usual breakpoint-drawing algorithm. (If you do not want any breakpoints, pass \f3\fs18 integer(0) \f4\fs20 , a zero-length \f3\fs18 integer \f4\fs20 vector, for \f3\fs18 breaks1 \f4\fs20 .) Finally, it is not currently legal for \f3\fs18 strand1 \f4\fs20 to be \f3\fs18 NULL \f4\fs20 and \f3\fs18 strand2 \f4\fs20 non- \f3\fs18 NULL \f4\fs20 ; that variant may be assigned some meaning in future. Again, this discussion applies equally to \f3\fs18 strand3 \f4\fs20 , \f3\fs18 strand4 \f4\fs20 , and \f3\fs18 breaks2 \f4\fs20 , \f1\i mutatis mutandis \f4\i0 . Null haplosomes may never be passed as any of the four parental strands; pass \f3\fs18 NULL \f4\fs20 , not a null haplosome, if that strand is not inherited from. When modeling a chromosome that is intrinsically haploid, such as the Y, \f3\fs18 NULL \f4\fs20 must be passed for \f3\fs18 strand3 \f4\fs20 , \f3\fs18 strand4 \f4\fs20 , and \f3\fs18 breaks2 \f4\fs20 ; you cannot supply genetic information for an offspring haplosome that will not exist. Note that when new mutations are generated by \f3\fs18 addRecombinant() \f4\fs20 , their \f3\fs18 subpopID \f4\fs20 property will be the \f3\fs18 id \f4\fs20 of the offspring\'92s subpopulation, since the parental subpopulation is ambiguous in the general case; this behavior differs from the other \f3\fs18 add...() \f4\fs20 methods.\ These semantics allow several uses for \f3\fs18 addRecombinant() \f4\fs20 . When all strands are non- \f3\fs18 NULL \f4\fs20 , it is similar to \f3\fs18 addCrossed() \f4\fs20 except that the recombination breakpoints can be specified explicitly, allowing very precise offspring generation without having to override SLiM\'92s breakpoint generation with a \f3\fs18 recombination() \f4\fs20 callback. When only \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand3 \f4\fs20 are supplied, it is very similar to \f3\fs18 addCloned() \f4\fs20 , creating a clonal offspring, except that the two parental haplosomes need not belong to the same individual (whatever that might mean biologically). Supplying only \f3\fs18 strand1 \f4\fs20 is useful for modeling clonally reproducing haploids, or any chromosome type that is intrinsically haploid, such as the Y chromosome. For a model of clonally reproducing haploids that undergo horizontal gene transfer (HGT), supplying only \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 will allow HGT from \f3\fs18 strand2 \f4\fs20 to replace segments of an otherwise clonal copy of \f3\fs18 strand1 \f4\fs20 , while the second haplosome of the generated offspring will be a null haplosome; this could be useful for modeling bacterial conjugation, for example. Other variations are also possible.\ The \f3\fs18 sex \f4\fs20 parameter optionally specifies the sex of the offspring. The default value of \f3\fs18 NULL \f4\fs20 for \f3\fs18 sex \f4\fs20 specifies \'93default behavior\'94; in a non-sexual model this is the only legal value, and produces a hermaphroditic offspring. In a sexual model, the \'93default behavior\'94 of \f3\fs18 NULL \f4\fs20 is that the offspring\'92s sex is dictated by the haplosome structure it inherits. For example, if the supplied strands indicate that the offspring will have two non-null haplosomes for a chromosome of type \f3\fs18 "X" \f4\fs20 , the sex of the offspring must therefore be female, whereas if it will have one non-null \f3\fs18 "X" \f4\fs20 haplosome and one null \f3\fs18 "X" \f4\fs20 haplosome, it must therefore be male. SLiM will examine the supplied strands to determine such constraints and enforce them. The constraints defined by each chromosome type, as described in \f3\fs18 initializeChromosome() \f4\fs20 , must always be followed, even when using \f3\fs18 addRecombinant() \f4\fs20 . If \f3\fs18 sex \f4\fs20 is \f3\fs18 NULL \f4\fs20 and the sex of the offspring is unconstrained, the offspring will be chosen as male or female with equal probability. A value of \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 for \f3\fs18 sex \f4\fs20 specifies that the offspring should be male or female, respectively. A \f3\fs18 float \f4\fs20 value from \f3\fs18 0.0 \f4\fs20 to \f3\fs18 1.0 \f4\fs20 for \f3\fs18 sex \f4\fs20 provides the probability that the offspring will be male; a value of \f3\fs18 0.0 \f4\fs20 will produce a female, a value of \f3\fs18 1.0 \f4\fs20 will produce a male, and for intermediate values SLiM will draw the sex of the offspring randomly according to the specified probability. In these cases where \f3\fs18 sex \f4\fs20 is not \f3\fs18 NULL \f4\fs20 , SLiM will first determine the sex of the individual as just described, and will then examine the supplied strands to confirm that it is compatible with the sex that was determined. Again, if there is a conflict an error will be raised; you cannot specify the sex of an individual to be incompatible with the haplosomes that it inherits, and if you specify a sex with a \f3\fs18 string \f4\fs20 or \f3\fs18 float \f4\fs20 value it is up to you to ensure that that is compatible with the supplied strands. (If you need more flexibility, you should probably not use a sexual model at all, but simply use chromosome type \f3\fs18 "A" \f4\fs20 or \f3\fs18 "H" \f4\fs20 in a non-sexual model, track the sex of individuals yourself with a tag value such as \f3\fs18 tagL0 \f4\fs20 , and manipulate haplosomes during reproduction however you wish; SLiM then imposes no constraints.)\ By default, the offspring is considered to have no parents, since there may be more than two \'93parents\'94 in the general case. If specifying parentage is desired, \f3\fs18 parent1 \f4\fs20 and/or \f3\fs18 parent2 \f4\fs20 may be passed to explicitly; this will establish those individuals as the parents of the offspring for purposes of pedigree tracking, and for several other purposes described below. If only one of \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 is non- \f3\fs18 NULL \f4\fs20 , that individual will be set as \f1\i both \f4\i0 of the parents of the offspring, mirroring the way that parentage is tracked for other cases such as \f3\fs18 addCloned() \f4\fs20 and \f3\fs18 addSelfed() \f4\fs20 . It is not required for \f3\fs18 parent1 \f4\fs20 or \f3\fs18 parent2 \f4\fs20 to actually be a genetic parent of the offspring at all, although typically they would be. To benefit from the full functionality of \f3\fs18 addRecombinant() \f4\fs20 as described below, it is best to supply \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 when possible.\ The \f3\fs18 randomizeStrands \f4\fs20 parameter is used to control the recombination behavior of \f3\fs18 addRecombinant() \f4\fs20 . As described above, two parental strands with crossover breakpoints between them can be specified to generate one offspring strand with recombination \'96 for example, with the \f3\fs18 strand1 \f4\fs20 , \f3\fs18 strand2 \f4\fs20 , and \f3\fs18 breaks1 \f4\fs20 parameters, but the same is true of the \f3\fs18 strand3 \f4\fs20 , \f3\fs18 strand4 \f4\fs20 , and \f3\fs18 breaks2 \f4\fs20 parameters, and the discussion that follows applies to both cases. If \f3\fs18 randomizeStrands \f4\fs20 is \f3\fs18 F \f4\fs20 , the supplied strands are used as given; for example, \f3\fs18 strand1 \f4\fs20 will be the initial copy strand when generating the first gamete to form the offspring. This mode should be used if you want explicit control over the initial copy strand; one example would be if your script is explicitly generating all four of the products of a meiosis event. If \f3\fs18 randomizeStrands \f4\fs20 is \f3\fs18 T \f4\fs20 , then if \f3\fs18 strand1 \f4\fs20 and \f3\fs18 strand2 \f4\fs20 are both non- \f3\fs18 NULL \f4\fs20 , 50% of the time they will be swapped, making \f3\fs18 strand2 \f4\fs20 the initial copy strand for the first gamete instead. This mode ( \f3\fs18 randomizeStrands \f4\fs20 = \f3\fs18 T \f4\fs20 ) is usually the desired behavior, to avoid an inheritance bias due to a lack of randomization in the initial copy strand, so passing \f3\fs18 T \f4\fs20 for \f3\fs18 randomizeStrands \f4\fs20 is recommended unless you specifically desire otherwise. The default value of \f3\fs18 randomizeStrands \f4\fs20 is \f3\fs18 NULL \f4\fs20 in order to force either \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 to be explicitly chosen whenever it would make a difference; if it is left as \f3\fs18 NULL \f4\fs20 , an error will be raised if generation of the specified offspring involves recombination, since then SLiM needs to know whether the value is \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 . (This unconventional approach has been adopted because the default value was \f3\fs18 F \f4\fs20 prior to SLiM 5, but \f3\fs18 T \f4\fs20 is almost always the correct behavior, as explained above. To try to prevent accidental bugs, this new policy was adopted to force the user to explicitly choose \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 whenever it matters.)\ The value of the \f3\fs18 meanParentAge \f4\fs20 property of the generated offspring is calculated from the mean parent age of each of its two haplosomes (whether they turn out to be null haplosomes or not); that may be an average of two values (if both offspring haplosomes have at least one parent), a single value (if one offspring haplosome has no parent), or no values (if both offspring haplosomes have no parent, in which case \f3\fs18 0.0 \f4\fs20 results). The mean parent age of a given offspring haplosome is the mean of the ages of the parents of the two strands used to generate that offspring haplosome; if one strand is \f3\fs18 NULL \f4\fs20 then the mean parent age for that offspring haplosome is the age of the parent of the non- \f3\fs18 NULL \f4\fs20 strand, while if both strands are \f3\fs18 NULL \f4\fs20 then that offspring haplosome is parentless and is not used in the final calculation. In other words, if one offspring haplosome has two parents with ages A and B, and the other offspring haplosome has one parent with age C, the \f3\fs18 meanParentAge \f4\fs20 of the offspring will be (A+B+C+C)\'a0/\'a04, or equivalently, ((A+B)/2\'a0+\'a0C)\'a0/\'a02, not (A+B+C)\'a0/\'a03.\ Callbacks can be involved in offspring generation with \f3\fs18 addRecombinant() \f4\fs20 , but because there are up to four strands with up to four different parents, things are a bit complicated and different from other \f3\fs18 add...() \f4\fs20 methods; the policy described here seems like the best compromise. The target subpopulation for the \f3\fs18 addRecombinant() \f4\fs20 call will be used to locate applicable \f3\fs18 mutation() \f4\fs20 and \f3\fs18 modifyChild() \f4\fs20 callbacks governing the generation of the offspring individual. On the other hand, \f3\fs18 recombination() \f4\fs20 callbacks will be found based upon the subpopulations to which \f3\fs18 parent1 \f4\fs20 and \f3\fs18 parent2 \f4\fs20 belong (for reasons discussed further in \f3\fs18 drawBreakpoints() \f4\fs20 ); if a parent individual is not supplied, \f3\fs18 recombination() \f4\fs20 callbacks will not be called at all when generating the corresponding offspring haplosome.\ When breakpoints are explicitly supplied to \f3\fs18 addRecombinant() \f4\fs20 with \f3\fs18 breaks1 \f4\fs20 or \f3\fs18 breaks2 \f4\fs20 , gene conversion tracts are not well-supported by this method; the \f3\fs18 breaks1 \f4\fs20 and \f3\fs18 breaks2 \f4\fs20 vectors provide simple crossover breakpoints, which may be used to implement crossovers or simple gene conversion tracts, but complex gene conversion tracts with heteroduplex mismatch repair are not supported in this mode of operation since there is no way to supply the relevant information. If, on the other hand, \f3\fs18 breaks1 \f4\fs20 or \f3\fs18 breaks2 \f4\fs20 is \f3\fs18 NULL \f4\fs20 when generating a haplosome with recombination, then as described above, \f3\fs18 addRecombinant() \f4\fs20 will generate breakpoints internally for that cross, and in this case, complex gene conversion tracts with heteroduplex mismatch repair are supported, since all of the necessary information is available.\ Beginning in SLiM 4.1, the \f3\fs18 count \f4\fs20 parameter dictates how many offspring will be generated (previously, exactly one offspring was generated). Each offspring is generated independently, based upon the given parameters. The returned vector contains all generated offspring, except those that were rejected by a \f3\fs18 modifyChild() \f4\fs20 callback. If all offspring are rejected, \f3\fs18 object(0) \f4\fs20 is returned, which is a zero-length \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Individual \f4\fs20 ; note that this is a change in behavior from earlier versions, which would return \f3\fs18 NULL \f4\fs20 .\ Beginning in SLiM 4.1, passing \f3\fs18 T \f4\fs20 for \f3\fs18 defer \f4\fs20 requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase. SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if \f3\fs18 defer \f4\fs20 were \f3\fs18 F \f4\fs20 . Haplosome generation can only be deferred if there are no active \f3\fs18 mutation() \f4\fs20 callbacks; otherwise, an error will result. Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a \f3\fs18 modifyChild() \f4\fs20 callback or otherwise). There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of \f3\fs18 F \f4\fs20 for \f3\fs18 defer \f4\fs20 is generally preferable since it has fewer restrictions. When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).\ Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from \f3\fs18 parent1 \f4\fs20 ; more specifically, the \f3\fs18 x \f4\fs20 property will be inherited in all spatial models (1D/2D/3D), the \f3\fs18 y \f4\fs20 property in 2D/3D models, and the \f3\fs18 z \f4\fs20 property in 3D models. Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1. The parent\'92s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to \f3\fs18 deviatePositions() \f4\fs20 / \f3\fs18 pointDeviated() \f4\fs20 . If \f3\fs18 parent1 \f4\fs20 is \f3\fs18 NULL \f4\fs20 , \f3\fs18 parent2 \f4\fs20 will be used; if it is also \f3\fs18 NULL \f4\fs20 , no spatial position will be inherited.\ Note that this method is only for use in nonWF models. See \f3\fs18 addCrossed() \f4\fs20 for further general notes on the addition of new offspring individuals.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(object)addSelfed(object$\'a0parent, [integer$\'a0count\'a0=\'a01], [logical$\'a0defer\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \kerning1\expnd0\expndtw0 Generates a new offspring individual from the given parent by selfing, queues it for addition to the target subpopulation, and returns it. The new offspring will not be visible as a member of the target subpopulation until the end of the offspring generation tick cycle stage. The subpopulation of \f3\fs18 parent \f4\fs20 will be used to locate applicable \f3\fs18 mutation() \f4\fs20 , \f3\fs18 recombination() \f4\fs20 , and \f3\fs18 modifyChild() \f4\fs20 callbacks governing the generation of the offspring individual.\ Since selfing requires that \f3\fs18 parent \f4\fs20 act as a source of both a male and a female gamete, this method may be called only in hermaphroditic models; calling it in sexual models will result in an error. This method represents a non-incidental selfing event, so the \f3\fs18 preventIncidentalSelfing \f4\fs20 flag of \f3\fs18 initializeSLiMOptions() \f4\fs20 has no effect on this method (in contrast to the behavior of \f3\fs18 addCrossed() \f4\fs20 , where selfing is assumed to be incidental).\ Beginning in SLiM 4.1, the \f3\fs18 count \f4\fs20 parameter dictates how many offspring will be generated (previously, exactly one offspring was generated). Each offspring is generated independently, based upon the given parameters. The returned vector contains all generated offspring, except those that were rejected by a \f3\fs18 modifyChild() \f4\fs20 callback. If all offspring are rejected, \f3\fs18 object(0) \f4\fs20 is returned, which is a zero-length \f3\fs18 object \f4\fs20 vector of class \f3\fs18 Individual \f4\fs20 ; note that this is a change in behavior from earlier versions, which would return \f3\fs18 NULL \f4\fs20 .\ Beginning in SLiM 4.1, passing \f3\fs18 T \f4\fs20 for \f3\fs18 defer \f4\fs20 requests that the generation of the haplosomes of the produced offspring be deferred until the end of the reproduction phase. SLiM may or may not honor this request; if not, the offspring will be generated synchronously just as if \f3\fs18 defer \f4\fs20 were \f3\fs18 F \f4\fs20 . Haplosome generation can only be deferred if there are no active \f3\fs18 mutation() \f4\fs20 or \f3\fs18 recombination() \f4\fs20 callbacks; otherwise, an error will result. Furthermore, when haplosome generation is deferred the mutations of the haplosomes of the generated offspring may not be accessed until reproduction is complete (whether from a \f3\fs18 modifyChild() \f4\fs20 callback or otherwise). There is little or no advantage to deferring haplosome generation when running single-threaded; in that case, the default of \f3\fs18 F \f4\fs20 for \f3\fs18 defer \f4\fs20 is generally preferable since it has fewer restrictions. When running multi-threaded, deferring haplosome generation allows that task to be done in parallel (which is the reason this option exists).\ Also beginning in SLiM 4.1, in spatial models the spatial position of the offspring will be inherited (i.e., copied) from \f3\fs18 parent \f4\fs20 ; more specifically, the \f3\fs18 x \f4\fs20 property will be inherited in all spatial models (1D/2D/3D), the \f3\fs18 y \f4\fs20 property in 2D/3D models, and the \f3\fs18 z \f4\fs20 property in 3D models. Properties not inherited will be left uninitialized, as they were prior to SLiM 4.1. The parent\'92s spatial position is probably not desirable in itself; the intention here is to make it easy to model the natal dispersal of all the new offspring for a given tick with a single vectorized call to \f3\fs18 deviatePositions() \f4\fs20 / \f3\fs18 pointDeviated() \f4\fs20 .\ Note that this method is only for use in nonWF models. See \f3\fs18 addCrossed() \f4\fs20 for further general notes on the addition of new offspring individuals.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)addSpatialMap(object$\'a0map)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Adds the given \f3\fs18 SpatialMap \f4\fs20 object, \f3\fs18 map \f4\fs20 , to the subpopulation. (The spatial map would have been previously created with a call to \f3\fs18 defineSpatialMap() \f4\fs20 on a different subpopulation; \f3\fs18 addSpatialMap() \f4\fs20 can then be used to add that existing spatial map with other subpopulations, sharing the map between subpopulations.) If the map is already added to the target subpopulation, this method does nothing; if a different map with the same name is already added to the subpopulation, an error results (because map names must be unique within each subpopulation). The map being added must be compatible with the target subpopulation; in particular, the spatial bounds utilized by the map must exactly match the corresponding spatial bounds for the subpopulation, and the dimensionality of the subpopulation must encompass the spatiality of the map. For example, if the map has a spatiality of \f3\fs18 "xz" \f4\fs20 then the subpopulation must have a dimensionality of \f3\fs18 "xyz" \f4\fs20 so that it encompasses both \f3\fs18 "x" \f4\fs20 and \f3\fs18 "z" \f4\fs20 , and the subpopulation\'92s spatial bounds for \f3\fs18 "x" \f4\fs20 and \f3\fs18 "z" \f4\fs20 must match those for the map (but the spatial bounds for \f3\fs18 "y" \f4\fs20 are unimportant, since the map does not use that dimension).\ Adding a map to a subpopulation is not strictly necessary, at present; one may query a \f3\fs18 SpatialMap \f4\fs20 object directly using \f3\fs18 mapValue() \f4\fs20 , regarding points in a subpopulation, without the map actually having been added to that subpopulation. However, it is a good idea to use \f3\fs18 addSpatialMap() \f4\fs20 , both for its compatibility check that prevents unnoticed scripting errors, and because it ensures correct display of the model in SLiMgui.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(float)cachedFitness(Ni\'a0indices)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The fitness values calculated for the individuals at the indices given are returned. If \f3\fs18 NULL \f4\fs20 is passed, fitness values for all individuals in the subpopulation are returned. The fitness values returned are cached values; \f3\fs18 mutationEffect() \f4\fs20 and \f3\fs18 fitnessEffect() \f4\fs20 callbacks are therefore not called as a side effect of this method. It is always an error to call \f3\fs18 cachedFitness() \f4\fs20 from inside a \f3\fs18 mutationEffect() \f4\fs20 or \f3\fs18 fitnessEffect() \f4\fs20 callback, since fitness values are in the middle of being set up. In WF models, it is also an error to call \f3\fs18 cachedFitness() \f4\fs20 from a \f3\fs18 late() \f4\fs20 event, because fitness values for the new offspring generation have not yet been calculated and are undefined. In nonWF models, the population may be a mixture of new and old individuals, so instead, \f3\fs18 NAN \f4\fs20 will be returned as the fitness of any new individuals whose fitness has not yet been calculated. When new subpopulations are first created with \f3\fs18 addSubpop() \f4\fs20 or \f3\fs18 addSubpopSplit() \f4\fs20 , the fitness of all of the newly created individuals is considered to be \f3\fs18 1.0 \f4\fs20 until fitness values are recalculated.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 \'96\'a0(void)configureDisplay([Nf\'a0center\'a0=\'a0NULL], [Nf$\'a0scale\'a0=\'a0NULL], [Ns$\'a0color\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 This method customizes the display of the subpopulation in SLiMgui\'92s Population Visualization graph. When this method is called by a model running outside SLiMgui, it will do nothing except type-checking and bounds-checking its arguments. When called by a model running in SLiMgui, the position, size, and color of the subpopulation\'92s displayed circle can be controlled as specified below.\ The \f3\fs18 center \f4\fs20 parameter sets the coordinates of the center of the subpopulation\'92s displayed circle; it must be a \f3\fs18 float \f4\fs20 vector of length two, such that \f3\fs18 center[0] \f4\fs20 provides the \f1\i x \f4\i0 -coordinate and \f3\fs18 center[1] \f4\fs20 provides the \f1\i y \f4\i0 -coordinate. The square central area of the Population Visualization occupies scaled coordinates in [0,1] for both \f1\i x \f4\i0 and \f1\i y \f4\i0 , so the values in \f3\fs18 center \f4\fs20 must be within those bounds. If a value of \f3\fs18 NULL \f4\fs20 is provided, SLiMgui\'92s default center will be used (which currently arranges subpopulations in a circle).\ The \f3\fs18 scale \f4\fs20 parameter sets a scaling factor to be applied to the radius of the subpopulation\'92s displayed circle. The default radius used by SLiMgui is a function of the subpopulation\'92s number of individuals; this default radius is then multiplied by \f3\fs18 scale \f4\fs20 . If a value of \f3\fs18 NULL \f4\fs20 is provided, the default radius will be used; this is equivalent to supplying a \f3\fs18 scale \f4\fs20 of \f3\fs18 1.0 \f4\fs20 . Typically the same \f3\fs18 scale \f4\fs20 value should be used by all subpopulations, to scale all of their circles up or down uniformly, but that is not required.\ The \f3\fs18 color \f4\fs20 parameter sets the color to be used for the displayed subpopulation\'92s circle. Colors may be specified by name, or with hexadecimal RGB values of the form \f3\fs18 "#RRGGBB" \f4\fs20 (see the Eidos manual). If \f3\fs18 color \f4\fs20 is \f3\fs18 NULL \f4\fs20 or the empty string, \f3\fs18 "" \f4\fs20 , SLiMgui\'92s default (fitness-based) color will be used.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 \'96\'a0(object$)defineSpatialMap(string$\'a0name, string$\'a0spatiality, numeric\'a0values, [logical$\'a0interpolate\'a0=\'a0F], [Nif\'a0valueRange\'a0=\'a0NULL], [Ns\'a0colors\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Defines a spatial map for the subpopulation; see the \f3\fs18 SpatialMap \f4\fs20 documentation regarding this class. The new map is automatically added to the subpopulation; \f3\fs18 addSpatialMap() \f4\fs20 does not need to be called. (That method is for sharing the map with additional subpopulations, beyond the one for which the map was originally defined.) The new \f3\fs18 SpatialMap \f4\fs20 object is returned, and may be retained permanently using \f3\fs18 defineConstant() \f4\fs20 or \f3\fs18 defineGlobal() \f4\fs20 for convenience.\ The name of the map is given by \f3\fs18 name \f4\fs20 , and can be used to identify it. The map uses the spatial dimensions referenced by \f3\fs18 spatiality \f4\fs20 , which must be a subset of the dimensions defined for the simulation in \f3\fs18 initializeSLiMOptions() \f4\fs20 . Spatiality \f3\fs18 "x" \f4\fs20 is permitted for dimensionality \f3\fs18 "x" \f4\fs20 ; spatiality \f3\fs18 "x" \f4\fs20 , \f3\fs18 "y" \f4\fs20 , or \f3\fs18 "xy" \f4\fs20 for dimensionality \f3\fs18 "xy" \f4\fs20 ; and spatiality \f3\fs18 "x" \f4\fs20 , \f3\fs18 "y" \f4\fs20 , \f3\fs18 "z" \f4\fs20 , \f3\fs18 "xy" \f4\fs20 , \f3\fs18 "yz" \f4\fs20 , \f3\fs18 "xz" \f4\fs20 , or \f3\fs18 "xyz" \f4\fs20 for dimensionality \f3\fs18 "xyz" \f4\fs20 . The spatial map is defined by a grid of values supplied in parameter \f3\fs18 values \f4\fs20 . That grid of values is aligned with the spatial bounds of the subpopulation, as described in more detail below; the spatial map is therefore coupled to those spatial bounds, and can only be used in subpopulations that match those particular spatial bounds (to avoid stretching or shrinking the map). The remaining optional parameters are described below.\ Note that the semantics of this method changed in SLiM 3.5; in particular, the \f3\fs18 gridSize \f4\fs20 parameter was removed, and the interpretation of the \f3\fs18 values \f4\fs20 parameter changed as described below. Existing code written prior to SLiM 3.5 will produce an error, due to the removed \f3\fs18 gridSize \f4\fs20 parameter, and must be revised carefully to obtain the same result, even if \f3\fs18 NULL \f4\fs20 had been passed for \f3\fs18 gridSize \f4\fs20 previously.\ Beginning in SLiM 3.5, the \f3\fs18 values \f4\fs20 parameter must be a vector/matrix/array with the number of dimensions appropriate for the declared spatiality of the map; for example, a map with spatiality \f3\fs18 "x" \f4\fs20 would require a (one-dimensional) vector, spatiality \f3\fs18 "xy" \f4\fs20 would require a (two-dimensional) matrix, and a map with spatiality of \f3\fs18 "xyz" \f4\fs20 would require a three-dimensional array. (See the Eidos manual for discussion of vectors, matrices, and arrays.) The data in \f3\fs18 values \f4\fs20 is interpreted in such a way that a two-dimensional matrix of values, with (0, 0) at upper left and values by column, is transformed into the format expected by SLiM, with (0, 0) at lower left and values by row; in other words, the two-dimensional matrix as it prints in the Eidos console will match the appearance of the two-dimensional spatial map as seen in SLiMgui. \f1\i This is a change in behavior from versions prior to SLiM 3.5 \f4\i0 ; it ensures that images loaded from disk with the Eidos class \f3\fs18 Image \f4\fs20 can be used directly as spatial maps, achieving the expected orientation, with no need for transposition or flipping. If the spatial map is a three-dimensional array, it is read as successive \f1\i z \f4\i0 -axis \'93planes\'94, each of which is a two-dimensional matrix that is treated as described above.\ Moving on to the other parameters of \f3\fs18 defineSpatialMap() \f4\fs20 : if \f3\fs18 interpolate \f4\fs20 is \f3\fs18 F \f4\fs20 , values across the spatial map are not interpolated; the value at a given point is equal to the nearest value defined by the grid of values specified. If \f3\fs18 interpolate \f4\fs20 is \f3\fs18 T \f4\fs20 , values across the spatial map will be interpolated (using linear, bilinear, or trilinear interpolation as appropriate) to produce spatially continuous variation in values. In either case, the corners of the value grid are exactly aligned with the corners of the spatial boundaries of the subpopulation as specified by \f3\fs18 setSpatialBounds() \f4\fs20 , and the value grid is then stretched across the spatial extent of the subpopulation in such a manner as to produce equal spacing between the values along each dimension. The setting of \f3\fs18 interpolation \f4\fs20 only affects how values between these grid points are calculated: by nearest-neighbor, or by linear interpolation. Interpolation of spatial maps with periodic boundaries is not handled specially; to ensure that the edges of a periodic spatial map join smoothly, simply ensure that the grid values at the edges of the map are identical, since they will be coincident after periodic wrapping. Note that cubic/bicubic interpolation is generally smoother than linear/bilinear interpolation, with fewer artifacts, but it is substantially slower to calculate; use the \f3\fs18 interpolate() \f4\fs20 method of \f3\fs18 SpatialMap \f4\fs20 to precalculate an interpolated map using cubic/bucubic interpolation.\ The \f3\fs18 valueRange \f4\fs20 and \f3\fs18 colors \f4\fs20 parameters travel together; either both are unspecified, or both are specified. They control how map values will be transformed into colors, by SLiMgui and by the \f3\fs18 mapColor() \f4\fs20 method. The \f3\fs18 valueRange \f4\fs20 parameter establishes the color-mapped range of spatial map values, as a vector of length two specifying a minimum and maximum; this does not need to match the actual range of values in the map. The \f3\fs18 colors \f4\fs20 parameter then establishes the corresponding colors for values within the interval defined by \f3\fs18 valueRange \f4\fs20 : values less than or equal to \f3\fs18 valueRange[0] \f4\fs20 will map to \f3\fs18 colors[0] \f4\fs20 , values greater than or equal to \f3\fs18 valueRange[1] \f4\fs20 will map to the last \f3\fs18 colors \f4\fs20 value, and intermediate values will shade continuously through the specified vector of colors, with interpolation between adjacent colors to produce a continuous spectrum. This is much simpler than it sounds in this description; see the recipes for an illustration of its use.\ Note that at present, SLiMgui will only display spatial maps of spatiality \f3\fs18 "x" \f4\fs20 , \f3\fs18 "y" \f4\fs20 , or \f3\fs18 "xy" \f4\fs20 ; the color-mapping parameters will simply be ignored by SLiMgui for other spatiality values (even if the spatiality is a superset of these values; SLiMgui will not attempt to display an \f3\fs18 "xyz" \f4\fs20 spatial map, for example, since it has no way to choose which 2D slice through the \f1\i xyz \f4\i0 space it ought to display). The \f3\fs18 mapColor() \f4\fs20 method will return translated color strings for any spatial map, however, even if SLiMgui is unable to display the spatial map. If there are multiple spatial maps that SLiMgui is capable of displaying, it choose one for display by default, but other maps may be selected from the action menu on the individuals view (by clicking on the button with the gear icon).\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)deviatePositions(No\'a0individuals, string$\'a0boundary, numeric$\'a0maxDistance, string$\'a0functionType, ...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Deviates the spatial positions of the individuals supplied in \f3\fs18 individuals \f4\fs20 , using the provided boundary condition and dispersal kernel. If \f3\fs18 individuals \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the positions of all individuals in the target subpopulation are deviated. This method is essentially a more efficient shorthand for getting the spatial positions of \f3\fs18 individuals \f4\fs20 from the \f3\fs18 spatialPosition \f4\fs20 property, deviating those positions with \f3\fs18 pointDeviated() \f4\fs20 , and setting the deviated positions back into \f3\fs18 individuals \f4\fs20 with the \f3\fs18 setSpatialPosition() \f4\fs20 method.\ The boundary condition \f3\fs18 boundary \f4\fs20 must be one of \f3\fs18 "none" \f4\fs20 , \f3\fs18 "periodic" \f4\fs20 , \f3\fs18 "reflecting" \f4\fs20 , \f3\fs18 "stopping" \f4\fs20 , \f3\fs18 "reprising" \f4\fs20 , or \f3\fs18 "absorbing" \f4\fs20 , and the spatial kernel type \f3\fs18 functionType \f4\fs20 must be one of \f3\fs18 "f" \f4\fs20 , \f3\fs18 "l" \f4\fs20 , \f3\fs18 "e" \f4\fs20 , \f3\fs18 "n" \f4\fs20 , or \f3\fs18 "t" \f4\fs20 , with the ellipsis parameters \f3\fs18 ... \f4\fs20 supplying kernel configuration parameters appropriate for that kernel type; see \f3\fs18 pointDeviated() \f4\fs20 for further details. As with \f3\fs18 pointDeviated() \f4\fs20 , the ellipsis parameters that follow \f3\fs18 functionType \f4\fs20 may each, independently, be either a singleton or a vector of length equal to \f3\fs18 n \f4\fs20 . This allows each individual\'92s position to be deviated with a different kernel, representing, for example, the movements of individuals with differing dispersal capabilities/propensities. (However, other parameters such as \f3\fs18 boundary \f4\fs20 , \f3\fs18 maxDistance \f4\fs20 , and \f3\fs18 functionType \f4\fs20 must be the same for all of the points, in the present design.)\ The returned vector contains individuals that did not survive the dispersal process. For \f3\fs18 "absorbing" \f4\fs20 boundaries, this will contain the individuals that attempted to disperse beyond the spatial bounds, and in most cases the caller will then kill those individuals \'96 probably by passing them to \f3\fs18 killIndividuals() \f4\fs20 , but perhaps by setting their \f3\fs18 fitnessScaling \f4\fs20 to zero. (The positions of the individuals in the returned vector will be the out-of-bounds positions that were drawn for them; rather than killing those individuals, the caller could conceivably handle them in some other way.) For all other boundary conditions, the returned vector of individuals will be empty and may be ignored by the caller.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)deviatePositionsWithMap(No\'a0individuals, string$\'a0boundary, so$\'a0map, numeric$\'a0maxDistance, string$\'a0functionType, ...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Deviates the spatial positions of the individuals supplied in \f3\fs18 individuals \f4\fs20 , using the provided boundary condition and dispersal kernel. The supplied \f3\fs18 SpatialMap \f4\fs20 object (or a \f3\fs18 string \f4\fs20 specifying a map by name), \f3\fs18 map \f4\fs20 , is used (in addition to the spatial bounds of the subpopulation) to define which positions are considered to be \'93out of bounds\'94, as described below. If \f3\fs18 individuals \f4\fs20 is \f3\fs18 NULL \f4\fs20 , the positions of all individuals in the target subpopulation are deviated. This method is essentially an extension of the \f3\fs18 deviatePositions() \f4\fs20 method, adding bounds-checking using \f3\fs18 map \f4\fs20 ; however, there are some differences as described below.\ The boundary condition \f3\fs18 boundary \f4\fs20 must be either \f3\fs18 "reprising" \f4\fs20 or \f3\fs18 "absorbing" \f4\fs20 . In the simple case where map values are either \f3\fs18 0 \f4\fs20 (bad habitat) or \f3\fs18 1 \f4\fs20 (good habitat), \f3\fs18 "reprising" \f4\fs20 means a new location is drawn conditional on falling within the good habitat; \f3\fs18 "absorbing" \f4\fs20 means individuals falling outside the good habitat are \'93absorbed\'94 (killed, probably). The details are discussed below, when \f3\fs18 map \f4\fs20 is discussed. Note that the boundary condition \f3\fs18 "none" \f4\fs20 is not supported because points must be within the boundaries of the spatial map to be checked. Reflecting and stopping boundaries are not supported because, with the spatial map, if a drawn point is considered out-of-bounds it is not clear where the \'93edge\'94 is, and therefore reflecting off of the edge, or stopping at the edge, are not well-defined. Finally, \f3\fs18 "periodic" \f4\fs20 is not supported because it does not specify any action to be taken when a drawn point is considered to be \'93out of bounds\'94 according to the spatial map; instead, this method automatically applies any periodic boundaries that have been defined and then, using the resulting point, checks the subpopulation\'92s spatial bounds and the spatial map, and applies reprising or absorbing boundaries as requested if the point is \'93out of bounds\'94.\ The spatial map defined by \f3\fs18 map \f4\fs20 must be configured in a specific way. First of all, it must be defined in, or added to, the target subpopulation (and thus, by implication, it must match the spatial bounds of the subpopulation. Second, its spatiality must be equal to the dimensionality of the species; in an \f3\fs18 "xy" \f4\fs20 species, for example, the map must also be \f3\fs18 "xy" \f4\fs20 . Third, the values in the spatial map must represent \'93habitability\'94, in the following sense. The value of \f3\fs18 map \f4\fs20 at a given drawn point is obtained, symbolized here by \f3\fs18 x \f4\fs20 . Next, \f3\fs18 x \f4\fs20 is clamped to the range [ \f3\fs18 0 \f4\fs20 , \f3\fs18 1 \f4\fs20 ]; values less than \f3\fs18 0 \f4\fs20 become \f3\fs18 0 \f4\fs20 , values greater than \f3\fs18 1 \f4\fs20 become \f3\fs18 1 \f4\fs20 . The resulting \f3\fs18 x \f4\fs20 value is then interpreted as the probability that the point is considered \'93within bounds\'94 (as far as the spatial map is concerned; points that are outside the subpopulation\'92s spatial bounds are \f1\i always \f4\i0 considered \'93out of bounds\'94). If \f3\fs18 boundary \f4\fs20 is \f3\fs18 "reprising" \f4\fs20 , \f3\fs18 1-x \f4\fs20 is thus the probability that the point will be redrawn; if boundary is \f3\fs18 "absorbing" \f4\fs20 , \f3\fs18 1-x \f4\fs20 is thus the probability that the individual will be considered \'93absorbed\'94, as discussed below. In this manner, the concept of \'93out of bounds\'94 is treated as a probability by this method, rather than a binary state.\ The spatial kernel type \f3\fs18 functionType \f4\fs20 must be one of \f3\fs18 "f" \f4\fs20 , \f3\fs18 "l" \f4\fs20 , \f3\fs18 "e" \f4\fs20 , \f3\fs18 "n" \f4\fs20 , or \f3\fs18 "t" \f4\fs20 , with the ellipsis parameters \f3\fs18 ... \f4\fs20 supplying kernel configuration parameters appropriate for that kernel type; see \f3\fs18 pointDeviated() \f4\fs20 for further details. As with \f3\fs18 pointDeviated() \f4\fs20 , the ellipsis parameters that follow \f3\fs18 functionType \f4\fs20 may each, independently, be either a singleton or a vector of length equal to \f3\fs18 n \f4\fs20 . This allows each individual\'92s position to be deviated with a different kernel, representing, for example, the movements of individuals with differing dispersal capabilities/propensities. (However, other parameters such as \f3\fs18 boundary \f4\fs20 , \f3\fs18 maxDistance \f4\fs20 , and \f3\fs18 functionType \f4\fs20 must be the same for all of the points, in the present design.)\ The returned vector contains individuals that did not survive the dispersal process. For \f3\fs18 "absorbing" \f4\fs20 boundaries, this will contain the individuals that attempted to disperse to a point considered \'93out of bounds\'94 as described above, and in most cases the caller will then kill those individuals \'96 probably by passing them to \f3\fs18 killIndividuals() \f4\fs20 , but perhaps by setting their \f3\fs18 fitnessScaling \f4\fs20 to zero. (The positions of the individuals in the returned vector will be the out-of-bounds positions that were drawn for them; rather than killing those individuals, the caller could conceivably handle them in some other way.) For all other boundary conditions, the returned vector of individuals will be empty and may be ignored by the caller.\ See also the \f3\fs18 SpatialMap \f4\fs20 methods \f3\fs18 sampleNearbyPoint() \f4\fs20 and \f3\fs18 sampleImprovedNearbyPoint() \f4\fs20 , which are in some ways conceptually similar to this method.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object)haplosomesForChromosomes([Niso\'a0chromosomes\'a0=\'a0NULL], [Ni$\'a0index\'a0=\'a0NULL], [logical$\'a0includeNulls\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing the subpopulation\'92s haplosomes that correspond to the chromosomes passed in \f3\fs18 chromosomes \f4\fs20 (following the order of the \f3\fs18 chromosomes \f4\fs20 property of \f3\fs18 Individual \f4\fs20 ). Chromosomes can be specified by id ( \f3\fs18 integer \f4\fs20 ), by symbol ( \f3\fs18 string \f4\fs20 ) or by the \f3\fs18 Chromosome \f4\fs20 objects themselves; if \f3\fs18 NULL \f4\fs20 is passed (the default), all chromosomes defined for the species are used, in the order in which they were defined.\ This method is equivalent to calling \f3\fs18 haplosomesForChromosomes(chromosomes, index, includeNulls) \f4\fs20 on \f3\fs18 subpop.individuals \f4\fs20 , where \f3\fs18 subpop \f4\fs20 is the target subpopulation. It therefore appends together the specified haplosomes from each individual in the subpopulation to form a single vector. See the documentation for the \f3\fs18 Individual \f4\fs20 method \f3\fs18 haplosomesForChromosomes() \f4\fs20 for further details, such as on the meaning of the \f3\fs18 index \f4\fs20 and \f3\fs18 includeNulls \f4\fs20 parameters.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)outputMSSample(integer$\'a0sampleSize, [logical$\'a0replace\'a0=\'a0T], [string$\'a0requestedSex\'a0=\'a0"*"], [Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F], [logical$\'a0filterMonomorphic\'a0=\'a0F], [Niso$\'a0chromosome\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output a random sample from the subpopulation in MS format. Positions in the output will span the interval [0,1]. A sample of non-null haplosomes (not entire individuals, note) of size \f3\fs18 sampleSize \f4\fs20 from the subpopulation will be output. The sample may be done either with or without replacement, as specified by \f3\fs18 replace \f4\fs20 ; the default is to sample with replacement. A particular sex of individuals may be requested for the sample, for simulations in which sex is enabled, by passing \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 for \f3\fs18 requestedSex \f4\fs20 ; passing \f3\fs18 "*" \f4\fs20 , the default, indicates that haplosomes from individuals should be selected randomly, without respect to sex. If the sampling options provided by this method are not adequate, see the \f3\fs18 outputHaplosomesToMS() \f4\fs20 method of \f3\fs18 Haplosome \f4\fs20 for a more flexible low-level option.\ If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 .\ If \f3\fs18 filterMonomorphic \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), all mutations that are present in the sample will be included in the output. This means that some mutations may be included that are actually monomorphic within the sample (i.e., that exist in \f1\i every \f4\i0 sampled haplosome, and are thus apparently fixed). These may be filtered out with \f3\fs18 filterMonomorphic = T \f4\fs20 if desired; note that this option means that some mutations that do exist in the sampled haplosomes might not be included in the output, simply because they exist in every sampled haplosome.\ The \f3\fs18 chromosome \f4\fs20 parameter identifies the chromosome for which the sample of haplosomes should be taken. The default of \f3\fs18 NULL \f4\fs20 may be used only in single-chromosome models where the choice of chromosome is unambiguous. In multi-chromosome models, chromosome must be non- \f3\fs18 NULL \f4\fs20 ; it must specify the chromosome by id ( \f3\fs18 integer \f4\fs20 ), by symbol ( \f3\fs18 string \f4\fs20 ) or by the \f3\fs18 Chromosome \f4\fs20 object itself.\ See \f3\fs18 outputSample() \f4\fs20 and \f3\fs18 outputVCFSample() \f4\fs20 for other output formats. Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)outputSample(integer$\'a0sampleSize, [logical$\'a0replace\'a0=\'a0T], [string$\'a0requestedSex\'a0=\'a0"*"], [Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F], [Niso$\'a0chromosome\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output a random sample from the subpopulation in SLiM\'92s native format. A sample of non-null haplosomes (not entire individuals, note) of size \f3\fs18 sampleSize \f4\fs20 from the subpopulation will be output. The sample may be done either with or without replacement, as specified by \f3\fs18 replace \f4\fs20 ; the default is to sample with replacement. A particular sex of individuals may be requested for the sample, for simulations in which sex is enabled, by passing \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 for \f3\fs18 requestedSex \f4\fs20 ; passing \f3\fs18 "*" \f4\fs20 , the default, indicates that haplosomes from individuals should be selected randomly, without respect to sex. If the sampling options provided by this method are not adequate, see the \f3\fs18 outputHaplosomes() \f4\fs20 method of \f3\fs18 Haplosome \f4\fs20 for a more flexible low-level option.\ If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 .\ The \f3\fs18 chromosome \f4\fs20 parameter identifies the chromosome for which the sample of haplosomes should be taken. The default of \f3\fs18 NULL \f4\fs20 may be used only in single-chromosome models where the choice of chromosome is unambiguous. In multi-chromosome models, chromosome must be non- \f3\fs18 NULL \f4\fs20 ; it must specify the chromosome by id ( \f3\fs18 integer \f4\fs20 ), by symbol ( \f3\fs18 string \f4\fs20 ) or by the \f3\fs18 Chromosome \f4\fs20 object itself.\ See \f3\fs18 outputMSSample() \f4\fs20 and \f3\fs18 outputVCFSample() \f4\fs20 for other output formats. Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)outputVCFSample(integer$\'a0sampleSize, [logical$\'a0replace\'a0=\'a0T], [string$\'a0requestedSex\'a0=\'a0"*"], [logical$\'a0outputMultiallelics\'a0=\'a0T], [Ns$\'a0filePath\'a0=\'a0NULL], [logical$\'a0append\'a0=\'a0F], [logical$\'a0simplifyNucleotides\'a0=\'a0F], [logical$\'a0outputNonnucleotides\'a0=\'a0T], [logical$\'a0groupAsIndividuals\'a0=\'a0T], [Niso$\'a0chromosome\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Output a random sample from the subpopulation in VCF format. A sample of individuals (not haplosomes, note \'96 unlike the \f3\fs18 outputSample() \f4\fs20 and \f3\fs18 outputMSSample() \f4\fs20 methods) of size \f3\fs18 sampleSize \f4\fs20 from the subpopulation will be output. The sample may be done either with or without replacement, as specified by \f3\fs18 replace \f4\fs20 ; the default is to sample with replacement. A particular sex of individuals may be requested for the sample, for simulations in which sex is enabled, by passing \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 for \f3\fs18 requestedSex \f4\fs20 ; passing \f3\fs18 "*" \f4\fs20 , the default, indicates that individuals should be selected randomly, without respect to sex. If the sampling options provided by this method are not adequate, see the \f3\fs18 outputHaplosomesToVCF() \f4\fs20 method of \f3\fs18 Haplosome \f4\fs20 for a more flexible low-level option.\ If the optional parameter \f3\fs18 filePath \f4\fs20 is \f3\fs18 NULL \f4\fs20 (the default), output will be sent to Eidos\'92s output stream. Otherwise, output will be sent to the filesystem path specified by \f3\fs18 filePath \f4\fs20 , overwriting that file if \f3\fs18 append \f4\fs20 if \f3\fs18 F \f4\fs20 , or appending to the end of it if \f3\fs18 append \f4\fs20 is \f3\fs18 T \f4\fs20 .\ The parameters \f3\fs18 outputMultiallelics \f4\fs20 , \f3\fs18 simplifyNucleotides \f4\fs20 , \f3\fs18 outputNonnucleotides \f4\fs20 , and \f3\fs18 groupAsIndividuals \f4\fs20 affect the format of the output produced.\ The \f3\fs18 chromosome \f4\fs20 parameter identifies the chromosome for which haplosomes of the sampled individuals should be output. The default of \f3\fs18 NULL \f4\fs20 may be used only in single-chromosome models where the choice of chromosome is unambiguous. In multi-chromosome models, chromosome must be non- \f3\fs18 NULL \f4\fs20 ; it must specify the chromosome by id ( \f3\fs18 integer \f4\fs20 ), by symbol ( \f3\fs18 string \f4\fs20 ) or by the \f3\fs18 Chromosome \f4\fs20 object itself. The \f3\fs18 symbol \f4\fs20 property of the chromosome will be output in the \f3\fs18 CHROM \f4\fs20 field of call lines in the VCF output.\ See \f3\fs18 outputMSSample() \f4\fs20 and \f3\fs18 outputSample() \f4\fs20 for other output formats. Output is generally done in a \f3\fs18 late() \f4\fs20 event, so that the output reflects the state of the simulation at the end of a tick.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)pointDeviated(integer$\'a0n, float\'a0point, string$\'a0boundary, numeric$\'a0maxDistance, string$\'a0functionType, ...)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector containing \f3\fs18 n \f4\fs20 points that are derived from \f3\fs18 point \f4\fs20 by adding a deviation drawn from a dispersal kernel (specified by \f3\fs18 maxDistance \f4\fs20 , \f3\fs18 functionType \f4\fs20 , and the ellipsis parameters \f3\fs18 ... \f4\fs20 , as detailed below) and then applying a boundary condition specified by \f3\fs18 boundary \f4\fs20 . This method therefore performs the steps of a simple dispersal algorithm in a single vectorized call. See \f3\fs18 deviatePositions() \f4\fs20 for an even more efficient approach.\ The parameter \f3\fs18 point \f4\fs20 may contain a single point which is deviated and bounded \f3\fs18 n \f4\fs20 independent times, or may contain \f3\fs18 n \f4\fs20 points each of which is deviated and bounded. In any case, each point in \f3\fs18 point \f4\fs20 should match the dimensionality of the model \'96 one element in a 1D model, two elements in a 2D model, or three elements in a 3D model. This method should not be called in a non-spatial model.\ The dispersal kernel is specified similarly to other kernel-based methods, such as \f3\fs18 setInteractionFunction() \f4\fs20 and \f3\fs18 smooth() \f4\fs20 . For \f3\fs18 pointDeviated() \f4\fs20 , \f3\fs18 functionType \f4\fs20 may be \f3\fs18 "f" \f4\fs20 with no ellipsis arguments \f3\fs18 ... \f4\fs20 to use a flat kernel out to \f3\fs18 maxDistance \f4\fs20 ; \f3\fs18 "l" \f4\fs20 with no ellipsis arguments for a kernel that decreases linearly from the center to zero at \f3\fs18 maxDistance \f4\fs20 ; \f3\fs18 "e" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 lambda (rate) parameter for a negative exponential function; \f3\fs18 "n" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 sigma (standard deviation) parameter for a Gaussian function; or \f3\fs18 "t" \f4\fs20 , in which case the ellipsis should supply a \f3\fs18 numeric$ \f4\fs20 degrees of freedom and a \f3\fs18 numeric$ \f4\fs20 scale parameter for a \f1\i t \f4\i0 -distribution function. The Cauchy ( \f3\fs18 "c" \f4\fs20 ) kernel is not supported by \f3\fs18 pointDeviated() \f4\fs20 since it is not well-behaved for this purpose, and the Student\'92s \f1\i t \f4\i0 ( \f3\fs18 "t" \f4\fs20 ) kernel is not allowed in 3D models at present simply because it hasn\'92t been implemented. See the \f3\fs18 InteractionType \f4\fs20 class documentation for more detailed discussion of the available kernel types and their parameters and probability distribution functions. For \f3\fs18 pointDeviated() \f4\fs20 , the ellipsis parameters that follow \f3\fs18 functionType \f4\fs20 may each, independently, be either a singleton or a vector of length equal to \f3\fs18 n \f4\fs20 . This allows each point to be deviated with a different kernel, representing, for example, the movements of individuals with differing dispersal capabilities/propensities. (However, other parameters such as \f3\fs18 boundary \f4\fs20 , \f3\fs18 maxDistance \f4\fs20 , and \f3\fs18 functionType \f4\fs20 must be the same for all of the points, in the present design.)\ The random points returned from this method are drawn from the probability distribution that is radially symmetric and has density proportional to the kernel \'96 in other words, at distance \f1\i r \f4\i0 the density is proportional to the kernel type referred to by \f3\fs18 functionType \f4\fs20 . (Said another way, the shape of the \f1\i cross-section \f4\i0 through the probability density function is given by the kernel.) For instance, the value of the type \f3\fs18 "e" \f4\fs20 (exponential) kernel with rate \f1\i a \f4\i0 at \f1\i r \f4\i0 is proportional to exp(\uc0\u8722 \f1\i ar \f4\i0 ), and so in 2D, the probability density that this method with kernel type \f3\fs18 "e" \f4\fs20 draws from has density proportional to p( \f1\i x \f4\i0 ,\'a0 \f1\i y \f4\i0 )\'a0=\'a0exp(\uc0\u8722 \f1\i a \f4\i0 \'a0sqrt( \f1\i x \f4\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub \'a0+\'a0 \f1\i y \f4\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub )), since \f1\i r \f4\i0 \'a0=\'a0sqrt(x \fs13\fsmilli6667 \super 2 \fs20 \nosupersub \'a0+\'a0y \fs13\fsmilli6667 \super 2 \fs20 \nosupersub ) is the distance. Note that the \f1\i distribution of the distance \f4\i0 is not given by the kernel except in 1D: in the type\'a0 \f3\fs18 "e" \f4\fs20 \'a0example, the distribution of the distance in 1D is exponential, while in 2D it has density proportional to \f1\i r \f4\i0 \'a0exp(\uc0\u8722 \f1\i ar \f4\i0 ) (i.e., Gamma with shape parameter 1). For another example, the value of the type \f3\fs18 "n" \f4\fs20 (Normal) kernel at \f1\i r \f4\i0 with standard deviation 1 is proportional to exp(\uc0\u8722 \f1\i r \f4\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub \'a0/\'a02), and so the density is proportional to\'a0p( \f1\i x \f4\i0 ,\'a0 \f1\i y \f4\i0 )\'a0=\'a0exp(\uc0\u8722 ( \f1\i x \f4\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub \'a0+\'a0 \f1\i y \f4\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub )\'a0/\'a02). This is the standard bivariate Normal, and equivalent to drawing independent Normals for the \f1\i x \f4\i0 and \f1\i y \f4\i0 directions; however, the Normal is the \f1\i only \f4\i0 distribution for which independent draws along each axis will result in a radially symmetric distribution. The distribution of the distance in 2D with type \f3\fs18 "n" \f4\fs20 is proportional to \f1\i r \f4\i0 \'a0exp(\uc0\u8722 \f1\i r \f4\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub \'a0/\'a02), i.e., Rayleigh.\ The boundary condition must be one of \f3\fs18 "none" \f4\fs20 , \f3\fs18 "periodic" \f4\fs20 , \f3\fs18 "reflecting" \f4\fs20 , \f3\fs18 "stopping" \f4\fs20 , or \f3\fs18 "reprising" \f4\fs20 . For \f3\fs18 "none" \f4\fs20 , no boundary condition is enforced; the deviated points are simply returned as is. For \f3\fs18 "periodic" \f4\fs20 , \f3\fs18 "reflecting" \f4\fs20 , and \f3\fs18 "stopping" \f4\fs20 , the boundary condition is enforced just as it is by the \f3\fs18 pointPeriodic() \f4\fs20 , \f3\fs18 pointReflected() \f4\fs20 , and \f3\fs18 pointStopped() \f4\fs20 methods; see their documentation for further details. For \f3\fs18 "reprising" \f4\fs20 , if the deviated point is out of bounds a new deviated point will be chosen, based upon the same original point, until a point inside bounds is obtained. Note that absorbing boundaries (for which being out-of-bounds is lethal) would need to be implemented in script; this method cannot enforce them. (Note, however, that the \f3\fs18 deviatePositions() \f4\fs20 method of \f3\fs18 Subpopulation \f4\fs20 can enforce absorbing boundaries.)\ Note that for the typical usage case, in which \f3\fs18 point \f4\fs20 comes from the \f3\fs18 spatialPosition \f4\fs20 property for a vector of individuals, and the result is then set back onto the same vector of individuals using the \f3\fs18 setSpatialPosition() \f4\fs20 method, the \f3\fs18 deviatePositions() \f4\fs20 method provides an even more efficient alternative.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(logical)pointInBounds(float\'a0point) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 Returns \f3\fs18 T \f4\fs20 if \f3\fs18 point \f4\fs20 is inside the spatial boundaries of the subpopulation, \f3\fs18 F \f4\fs20 otherwise. For example, for a simulation with \f3\fs18 "xy" \f4\fs20 dimensionality, if \f3\fs18 point \f4\fs20 contains exactly two values constituting an ( \f1\i x \f4\i0 , \f1\i y \f4\i0 ) point, the result will be \f3\fs18 T \f4\fs20 if and only if \f3\fs18 ((point[0]>=x0) & (point[0]<=x1) & (point[1]>=y0) & (point[1]<=y1)) \f4\fs20 given spatial bounds \f3\fs18 (x0, y0, x1, y1) \f4\fs20 . This method is useful for implementing absorbing or reprising boundary conditions. This may only be called in simulations for which continuous space has been enabled with \f3\fs18 initializeSLiMOptions() \f4\fs20 .\ The length of \f3\fs18 point \f4\fs20 must be an exact multiple of the dimensionality of the simulation; in other words, \f3\fs18 point \f4\fs20 may contain values comprising more than one point. In this case, a \f3\fs18 logical \f4\fs20 vector will be returned in which each element is \f3\fs18 T \f4\fs20 if the corresponding point in \f3\fs18 point \f4\fs20 is inside the spatial boundaries of the subpopulation, \f3\fs18 F \f4\fs20 otherwise.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)pointPeriodic(float\'a0point)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a revised version of \f3\fs18 point \f4\fs20 that has been brought inside the periodic spatial boundaries of the subpopulation (as specified by the \f3\fs18 periodicity \f4\fs20 parameter of \f3\fs18 initializeSLiMOptions() \f4\fs20 ) by wrapping around periodic spatial boundaries. In brief, if a coordinate of \f3\fs18 point \f4\fs20 lies beyond a periodic spatial boundary, that coordinate is wrapped around the boundary, so that it lies inside the spatial extent by the same magnitude that it previously lay outside, but on the opposite side of the space; in effect, the two edges of the periodic spatial boundary are seamlessly joined. This is done iteratively until all coordinates lie inside the subpopulation\'92s periodic boundaries. Note that non-periodic spatial boundaries are not enforced by this method; they should be enforced using \f3\fs18 pointReflected() \f4\fs20 , \f3\fs18 pointStopped() \f4\fs20 , or some other means of enforcing boundary constraints (which can be used after \f3\fs18 pointPeriodic() \f4\fs20 to bring the remaining coordinates into bounds; coordinates already brought into bounds by \f3\fs18 pointPeriodic() \f4\fs20 will be unaffected by those calls). This method is useful for implementing periodic boundary conditions. This may only be called in simulations for which continuous space and at least one periodic spatial dimension have been enabled with \f3\fs18 initializeSLiMOptions() \f4\fs20 .\ The length of \f3\fs18 point \f4\fs20 must be an exact multiple of the dimensionality of the simulation; in other words, \f3\fs18 point \f4\fs20 may contain values comprising more than one point. In this case, each point will be processed as described above and a new vector containing all of the processed points will be returned.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)pointReflected(float\'a0point)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a revised version of \f3\fs18 point \f4\fs20 that has been brought inside the spatial boundaries of the subpopulation by reflection. In brief, if a coordinate of \f3\fs18 point \f4\fs20 lies beyond a spatial boundary, that coordinate is reflected across the boundary, so that it lies inside the boundary by the same magnitude that it previously lay outside the boundary. This is done iteratively until all coordinates lie inside the subpopulation\'92s boundaries. This method is useful for implementing reflecting boundary conditions. This may only be called in simulations for which continuous space has been enabled with \f3\fs18 initializeSLiMOptions() \f4\fs20 .\ The length of \f3\fs18 point \f4\fs20 must be an exact multiple of the dimensionality of the simulation; in other words, \f3\fs18 point \f4\fs20 may contain values comprising more than one point. In this case, each point will be processed as described above and a new vector containing all of the processed points will be returned.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)pointStopped(float\'a0point)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a revised version of \f3\fs18 point \f4\fs20 that has been brought inside the spatial boundaries of the subpopulation by clamping. In brief, if a coordinate of \f3\fs18 point \f4\fs20 lies beyond a spatial boundary, that coordinate is set to exactly the position of the boundary, so that it lies on the edge of the spatial boundary. This method is useful for implementing stopping boundary conditions. This may only be called in simulations for which continuous space has been enabled with \f3\fs18 initializeSLiMOptions() \f4\fs20 .\ The length of \f3\fs18 point \f4\fs20 must be an exact multiple of the dimensionality of the simulation; in other words, \f3\fs18 point \f4\fs20 may contain values comprising more than one point. In this case, each point will be processed as described above and a new vector containing all of the processed points will be returned.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(float)pointUniform([integer$\'a0n\'a0=\'a01])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a new point (or points, for \f3\fs18 n \f4\fs20 > 1) generated from uniform draws for each coordinate, within the spatial boundaries of the subpopulation. The returned vector will contain \f3\fs18 n \f4\fs20 points, each comprised of a number of coordinates equal to the dimensionality of the simulation, so it will be of total length \f3\fs18 n \f4\fs20 *dimensionality. This may only be called in simulations for which continuous space has been enabled with \f3\fs18 initializeSLiMOptions() \f4\fs20 .\kerning1\expnd0\expndtw0 See \f3\fs18 pointUniformWithMap() \f4\fs20 for an extension to this method which uses a spatial map to govern the probability of a particular point being chosen.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \kerning1\expnd0\expndtw0 \'96\'a0(float)pointUniformWithMap(integer$\'a0n, so$\'a0map)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a new point (or points, for \f3\fs18 n \f4\fs20 > 1) generated from uniform draws for each coordinate, within the spatial boundaries of the subpopulation, and rejection sampled using the spatial map \f3\fs18 map \f4\fs20 as described below. The returned vector will contain \f3\fs18 n \f4\fs20 points, each comprised of a number of coordinates equal to the dimensionality of the simulation, so it will be of total length \f3\fs18 n \f4\fs20 *dimensionality. This may only be called in simulations for which continuous space has been enabled with \f3\fs18 initializeSLiMOptions() \f4\fs20 .\ The spatial map defined by \f3\fs18 map \f4\fs20 must be configured in a specific way. First of all, it must be defined in, or added to, the target subpopulation (and thus, by implication, it must match the spatial bounds of the subpopulation, and its spatiality must be compatible with the subpopulation\'92s dimensionality, as discussed in \f3\fs18 defineSpatialMap() \f4\fs20 and/or \f3\fs18 addSpatialMap() \f4\fs20 ). Second, the values in the spatial map must represent \'93habitability\'94, in the following sense. The value of \f3\fs18 map \f4\fs20 at a given drawn point is obtained, symbolized here by \f3\fs18 x \f4\fs20 . Next, \f3\fs18 x \f4\fs20 is clamped to the range [ \f3\fs18 0 \f4\fs20 , \f3\fs18 1 \f4\fs20 ]; values less than \f3\fs18 0 \f4\fs20 become \f3\fs18 0 \f4\fs20 , values greater than \f3\fs18 1 \f4\fs20 become \f3\fs18 1 \f4\fs20 . The resulting \f3\fs18 x \f4\fs20 value is then interpreted as the probability that the point is considered \'93within bounds\'94 (as far as the spatial map is concerned; points that are outside the subpopulation\'92s spatial bounds are \f1\i always \f4\i0 considered \'93out of bounds\'94). Given this, \f3\fs18 1-x \f4\fs20 is thus the probability that the point will be redrawn because it fell out of bounds. Each point will be redrawn repeatedly until a point considered \'93within bounds\'94 is obtained.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)removeSpatialMap(so$\'a0map)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Removes the \f3\fs18 SpatialMap \f4\fs20 object specified by \f3\fs18 map \f4\fs20 from the subpopulation. The parameter \f3\fs18 map \f4\fs20 may be either a \f3\fs18 SpatialMap \f4\fs20 object, or a \f3\fs18 string \f4\fs20 name for spatial map. The map must have been added to the subpopulation with \f3\fs18 addSpatialMap() \f4\fs20 ; if it has not been, an error results. Removing spatial maps that are no longer in use is optional in most cases. It is generally a good idea because it might decrease SLiM\'92s memory footprint; also, it avoids an error if the subpopulation\'92s spatial bounds are changed (see \f3\fs18 setSpatialBounds() \f4\fs20 ).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)removeSubpopulation(void) \f5 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 Removes this subpopulation from the model. The subpopulation is immediately removed from the list of active subpopulations, and the symbol representing the subpopulation is undefined. The subpopulation object itself remains unchanged until children are next generated (at which point it is deallocated), but it is no longer part of the simulation and should not be used.\ Note that this method is only for use in nonWF models, in which there is a distinction between a subpopulation being empty and a subpopulation being removed from the simulation; an empty subpopulation may be re-colonized by migrants, whereas as a removed subpopulation no longer exists at all. WF models do not make this distinction; when a subpopulation is empty it is automatically removed. WF models should therefore call \f3\fs18 setSubpopulationSize(0) \f4\fs20 instead of this method; \f3\fs18 setSubpopulationSize() \f4\fs20 is the standard way for WF models to change the subpopulation size, including to a size of \f3\fs18 0 \f4\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0\cf2 \expnd0\expndtw0\kerning0 (object)sampleIndividuals(integer$\'a0size, [logical$\'a0replace\'a0=\'a0F], [No$\'a0exclude\'a0=\'a0NULL], [Ns$\'a0sex\'a0=\'a0NULL], [Ni$\'a0tag\'a0=\'a0NULL], [Ni$\'a0minAge\'a0=\'a0NULL], [Ni$\'a0maxAge\'a0=\'a0NULL], [Nl$\'a0migrant\'a0=\'a0NULL]\kerning1\expnd0\expndtw0 , [Nl$\'a0tagL0\'a0=\'a0NULL], [Nl$\'a0tagL1\'a0=\'a0NULL], [Nl$\'a0tagL2\'a0=\'a0NULL], [Nl$\'a0tagL3\'a0=\'a0NULL], [Nl$\'a0tagL4\'a0=\'a0NULL]\expnd0\expndtw0\kerning0 ) \f5 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of individuals, of size less than or equal to parameter \f3\fs18 size \f4\fs20 , sampled from the individuals in the target subpopulation. Sampling is done without replacement if \f3\fs18 replace \f4\fs20 is \f3\fs18 F \f4\fs20 (the default), or with replacement if \f3\fs18 replace \f4\fs20 is \f3\fs18 T \f4\fs20 . The remaining parameters specify constraints upon the pool of individuals that will be considered candidates for the sampling. Parameter \f3\fs18 exclude \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a specific individual that should not be considered a candidate (typically the focal individual in some operation). Parameter \f3\fs18 sex \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a sex ( \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 ) for the individuals to be drawn, in sexual models. Parameter \f3\fs18 tag \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a \f3\fs18 tag \f4\fs20 property value for the individuals to be drawn. Parameters \f3\fs18 minAge \f4\fs20 and \f3\fs18 maxAge \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a minimum or maximum age for the individuals to be drawn, in nonWF models. Parameter \f3\fs18 migrant \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a required value for the \f3\fs18 migrant \f4\fs20 property of the individuals to be drawn (so \f3\fs18 T \f4\fs20 will require that individuals be migrants, \f3\fs18 F \f4\fs20 will require that they not be). Finally, parameters \f3\fs18 tagL0 \f4\fs20 , \f3\fs18 tagL1 \f4\fs20 , \f3\fs18 tagL2 \f4\fs20 , \f3\fs18 tagL3 \f4\fs20 , and \f3\fs18 tagL4 \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a required value ( \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 ) for the corresponding properties ( \f3\fs18 tagL0 \f4\fs20 , \f3\fs18 tagL1 \f4\fs20 , \f3\fs18 tagL2 \f4\fs20 , \f3\fs18 tagL3 \f4\fs20 , and \f3\fs18 tagL4 \f4\fs20 ) of the individuals to be drawn. Note that if any \f3\fs18 tag \f4\fs20 / \f3\fs18 tagL \f4\fs20 parameter is specified as non- \f3\fs18 NULL \f4\fs20 , that \f3\fs18 tag \f4\fs20 / \f3\fs18 tagL \f4\fs20 property must have a defined value for every individual in the subpopulation, otherwise an error may result (although this requirement will not necessarily be checked comprehensively by this method in every invocation). If the candidate pool is smaller than the requested sample size, all eligible candidates will be returned (in randomized order); the result will be a zero-length vector if no eligible candidates exist (unlike \f3\fs18 sample() \f4\fs20 ).\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 This method is similar to getting the \f3\fs18 individuals \f4\fs20 property of the subpopulation, using operator \f3\fs18 [] \f4\fs20 to select only individuals with the desired properties, and then using \f3\fs18 sample() \f4\fs20 to sample from that candidate pool. However, besides being much simpler than the equivalent Eidos code, it is also much faster, and it does not fail if less than the full sample size is available. See \f3\fs18 subsetIndividuals() \f4\fs20 for a similar method that returns a full subset, rather than a sample.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 \'96\'a0(void)setCloningRate(numeric\'a0rate) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the cloning rate of this subpopulation. The rate is changed to \f3\fs18 rate \f4\fs20 , which should be between 0.0 and 1.0, inclusive (see the SLiM manual for further details). Clonal reproduction can be enabled in both non-sexual (i.e. hermaphroditic) and sexual simulations. In non-sexual simulations, \f3\fs18 rate \f4\fs20 must be a singleton value representing the overall clonal reproduction rate for the subpopulation. In sexual simulations, \f3\fs18 rate \f4\fs20 may be either a singleton (specifying the clonal reproduction rate for both sexes) or a vector containing two numeric values (the female and male cloning rates specified separately, at indices \f3\fs18 0 \f4\fs20 and \f3\fs18 1 \f4\fs20 respectively). During mating and offspring generation, the probability that any given offspring individual will be generated by cloning \'96 by asexual reproduction without gametes or meiosis \'96 will be equal to the cloning rate (for its sex, in sexual simulations) set in the parental (not the offspring!) subpopulation. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)setMigrationRates(io\'a0sourceSubpops, numeric\'a0rates) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Set the migration rates to this subpopulation from the subpopulations in \f3\fs18 sourceSubpops \f4\fs20 to the corresponding rates specified in \f3\fs18 rates \f4\fs20 ; in other words, \f3\fs18 rates \f4\fs20 gives the expected fractions of the children in this subpopulation that will subsequently be generated from parents in the subpopulations \f3\fs18 sourceSubpops \f4\fs20 . The \f3\fs18 rates \f4\fs20 parameter may be a singleton value, in which case that rate is used for all subpopulations in \f3\fs18 sourceSubpops \f4\fs20 . This method will only set the migration fractions from the subpopulations given; migration rates from other subpopulations will be left unchanged (explicitly set a zero rate to turn off migration from a given subpopulation). The type of \f3\fs18 sourceSubpops \f4\fs20 may be either \f3\fs18 integer \f4\fs20 , specifying subpopulations by identifier, or \f3\fs18 object \f4\fs20 , specifying subpopulations directly.\ In general it is illegal to try to set the migration rate into a subpopulation from itself; that rate is, by definition, equal to the remainder after all migration from other subpopulations. As a special case for convenience, it is legal to set a rate of \f3\fs18 0.0 \f4\fs20 for all subpopulations in the species, including the target subpopulation. For example, \f3\fs18 subpops.setMigrationRates(allSubpops, 0.0) \f4\fs20 will turn off all migration into the subpopulations in \f3\fs18 subpops \f4\fs20 . The given rate of \f3\fs18 0.0 \f4\fs20 from a subpop into itself is simply ignored, for this specific case only.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)setSelfingRate(numeric$\'a0rate) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the selfing rate of this subpopulation. The rate is changed to \f3\fs18 rate \f4\fs20 , which should be between 0.0 and 1.0, inclusive (see the SLiM manual for further details). Selfing can only be enabled in non-sexual (i.e. hermaphroditic) simulations. During mating and offspring generation, the probability that any given offspring individual will be generated by selfing \'96 by self-fertilization via gametes produced by meiosis by a single parent \'96 will be equal to the selfing rate set in the parental (not the offspring!) subpopulation. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)setSexRatio(float$\'a0sexRatio) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the sex ratio of this subpopulation to \f3\fs18 sexRatio \f4\fs20 . As defined in SLiM, this is actually the fraction of the subpopulation that is male; in other words, the M:(M+F) ratio. This will take effect when children are next generated; it does not change the current subpopulation state. Unlike the selfing rate, the cloning rate, and migration rates, the sex ratio is deterministic: SLiM will generate offspring that exactly satisfy the requested sex ratio (within integer roundoff limits). \f5 \ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)setSpatialBounds(numeric\'a0bounds) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 \expnd0\expndtw0\kerning0 Set the spatial boundaries of the subpopulation to \f3\fs18 bounds \f4\fs20 . This method may be called only for simulations in which continuous space has been enabled with \f3\fs18 initializeSLiMOptions() \f4\fs20 . The length of \f3\fs18 bounds \f4\fs20 must be double the spatial dimensionality, so that it supplies both minimum and maximum values for each coordinate. More specifically, for a dimensionality of \f3\fs18 "x" \f4\fs20 , \f3\fs18 bounds \f4\fs20 should supply \f3\fs18 (x0,\'a0x1) \f4\fs20 values; for dimensionality \f3\fs18 "xy" \f4\fs20 it should supply \f3\fs18 (x0,\'a0y0,\'a0x1,\'a0y1) \f4\fs20 values; and for dimensionality \f3\fs18 "xyz" \f4\fs20 it should supply \f3\fs18 (x0,\'a0y0,\'a0z0,\'a0x1,\'a0y1,\'a0z1) \f4\fs20 (in that order). These boundaries will be used by SLiMgui to calibrate the display of the subpopulation, and will be used by methods such as \f3\fs18 pointInBounds() \f4\fs20 , \f3\fs18 pointReflected() \f4\fs20 , \f3\fs18 pointStopped() \f4\fs20 , and \f3\fs18 pointUniform() \f4\fs20 . The default spatial boundaries for all subpopulations span the interval \f3\fs18 [0,1] \f4\fs20 in each dimension. Spatial dimensions that are periodic (as established with the \f3\fs18 periodicity \f4\fs20 parameter to \f3\fs18 initializeSLiMOptions() \f4\fs20 ) must have a minimum coordinate value of \f3\fs18 0.0 \f4\fs20 (a restriction that allows the handling of periodicity to be somewhat more efficient). The current spatial bounds for the subpopulation may be obtained through the \f3\fs18 spatialBounds \f4\fs20 property.\cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 The spatial bounds of a subpopulation are shared with any \f3\fs18 SpatialMap \f4\fs20 objects added to the subpopulation. For this reason, once a spatial map has been added to a subpopulation, the spatial bounds of the subpopulation can no longer be changed (because it would stretch or shrink the associated spatial map, which does not seem to make physical sense). The bounds for a subpopulation should therefore be configured before any spatial maps are added to it. If those bounds do need to change subsequently, any associated spatial maps must first be removed with \f3\fs18 removeSpatialMap() \f4\fs20 , to ensure model consistency.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0(void)setSubpopulationSize(integer$\'a0size) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 Set the size of this subpopulation to \f3\fs18 size \f4\fs20 individuals (see the SLiM manual for further details). This will take effect when children are next generated; it does not change the current subpopulation state. Setting a subpopulation to a size of 0 does have some immediate effects that serve to disconnect it from the simulation: the subpopulation is removed from the list of active subpopulations, the subpopulation is removed as a source of migration for all other subpopulations, and the symbol representing the subpopulation is undefined. In this case, the subpopulation itself remains unchanged until children are next generated (at which point it is deallocated), but it is no longer part of the simulation and should not be used.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (string)spatialMapColor(string$\'a0name, numeric\'a0value) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 This method has been deprecated, and may be removed in a future release of SLiM. \f4\b0 In SLiM 4.1 and later, use the \f3\fs18 SpatialMap \f4\fs20 method \f3\fs18 mapColor() \f4\fs20 instead, and see that method\'92s documentation. (This method differs only in taking a \f3\fs18 name \f4\fs20 parameter, which is used to look up the spatial map from those that have been added to the subpopulation.)\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(object$)spatialMapImage(string$\'a0name, [Ni$\'a0width\'a0=\'a0NULL], [Ni$\'a0height\'a0=\'a0NULL], [logical$\'a0centers\'a0=\'a0F], [logical$\'a0color\'a0=\'a0T])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f0\b\fs20 \cf2 This method has been deprecated, and may be removed in a future release of SLiM. \f4\b0 In SLiM 4.1 and later, use the \f3\fs18 SpatialMap \f4\fs20 method \f3\fs18 mapImage() \f4\fs20 instead, and see that method\'92s documentation. (This method differs only in taking a \f3\fs18 name \f4\fs20 parameter, which is used to look up the spatial map from those that have been added to the subpopulation.)\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96 \f5 \'a0 \f3 (float)spatialMapValue(so$\'a0map, float\'a0point) \f5 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Looks up the spatial map specified by \f3\fs18 map \f4\fs20 , and uses its mapping machinery (as defined by the \f3\fs18 gridSize \f4\fs20 , \f3\fs18 values \f4\fs20 , and \f3\fs18 interpolate \f4\fs20 parameters to \f3\fs18 defineSpatialMap() \f4\fs20 ) to translate the coordinates of \f3\fs18 point \f4\fs20 into a corresponding map value. The parameter \f3\fs18 map \f4\fs20 may specify the map either as a \f3\fs18 SpatialMap \f4\fs20 object, or by its \f3\fs18 string \f4\fs20 name; in either case, the map must have been added to the subpopulation. The length of \f3\fs18 point \f4\fs20 must be equal to the spatiality of the spatial map; in other words, for a spatial map with spatiality \f3\fs18 "xz" \f4\fs20 , \f3\fs18 point \f4\fs20 must be of length \f3\fs18 2 \f4\fs20 , specifying the \f1\i x \f4\i0 and \f1\i z \f4\i0 coordinates of the point to be evaluated. Interpolation will automatically be used if it was enabled for the spatial map. Point coordinates are clamped into the range defined by the spatial boundaries, even if the spatial boundaries are periodic; use \f3\fs18 pointPeriodic() \f4\fs20 to wrap the point coordinates first if desired. See the documentation for \f3\fs18 defineSpatialMap() \f4\fs20 for information regarding the details of value mapping.\ Beginning in SLiM 3.3, \f3\fs18 point \f4\fs20 may contain more than one point to be looked up. In this case, the length of \f3\fs18 point \f4\fs20 must be an exact multiple of the spatiality of the spatial map; for a spatial map with spatiality \f3\fs18 "xz" \f4\fs20 , for example, the length of \f3\fs18 point \f4\fs20 must be an exact multiple of \f3\fs18 2 \f4\fs20 , and successive pairs of elements from point (elements \f3\fs18 0 \f4\fs20 and \f3\fs18 1 \f4\fs20 , then elements \f3\fs18 2 \f4\fs20 and \f3\fs18 3 \f4\fs20 , etc.) will be taken as the \f1\i x \f4\i0 and \f1\i z \f4\i0 coordinates of the points to be evaluated. This allows \f3\fs18 spatialMapValue() \f4\fs20 to be used in a vectorized fashion.\ The \f3\fs18 mapValue() \f4\fs20 method of \f3\fs18 SpatialMap \f4\fs20 provides the same functionality directly on the \f3\fs18 SpatialMap \f4\fs20 class; \f3\fs18 spatialMapValue() \f4\fs20 is provided on \f3\fs18 Subpopulation \f4\fs20 partly for backward compatibility, but also for convenience in some usage cases.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \'96\'a0\cf2 \expnd0\expndtw0\kerning0 (object)subsetIndividuals([No$\'a0exclude\'a0=\'a0NULL], [Ns$\'a0sex\'a0=\'a0NULL], [Ni$\'a0tag\'a0=\'a0NULL], [Ni$\'a0minAge\'a0=\'a0NULL], [Ni$\'a0maxAge\'a0=\'a0NULL], [Nl$\'a0migrant\'a0=\'a0NULL]\kerning1\expnd0\expndtw0 , [Nl$\'a0tagL0\'a0=\'a0NULL], [Nl$\'a0tagL1\'a0=\'a0NULL], [Nl$\'a0tagL2\'a0=\'a0NULL], [Nl$\'a0tagL3\'a0=\'a0NULL], [Nl$\'a0tagL4\'a0=\'a0NULL]\expnd0\expndtw0\kerning0 ) \f5 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Returns a vector of individuals subset from the individuals in the target subpopulation. The parameters specify constraints upon the subset of individuals that will be returned. Parameter \f3\fs18 exclude \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a specific individual that should not be included (typically the focal individual in some operation). Parameter \f3\fs18 sex \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a sex ( \f3\fs18 "M" \f4\fs20 or \f3\fs18 "F" \f4\fs20 ) for the individuals to be returned, in sexual models. Parameter \f3\fs18 tag \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a \f3\fs18 tag \f4\fs20 property value for the individuals to be returned. Parameters \f3\fs18 minAge \f4\fs20 and \f3\fs18 maxAge \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a minimum or maximum age for the individuals to be returned, in nonWF models. Parameter \f3\fs18 migrant \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a required value for the \f3\fs18 migrant \f4\fs20 property of the individuals to be returned (so \f3\fs18 T \f4\fs20 will require that individuals be migrants, \f3\fs18 F \f4\fs20 will require that they not be). Finally, parameters \f3\fs18 tagL0 \f4\fs20 , \f3\fs18 tagL1 \f4\fs20 , \f3\fs18 tagL2 \f4\fs20 , \f3\fs18 tagL3 \f4\fs20 , and \f3\fs18 tagL4 \f4\fs20 , if non- \f3\fs18 NULL \f4\fs20 , may specify a required value ( \f3\fs18 T \f4\fs20 or \f3\fs18 F \f4\fs20 ) for the corresponding properties ( \f3\fs18 tagL0 \f4\fs20 , \f3\fs18 tagL1 \f4\fs20 , \f3\fs18 tagL2 \f4\fs20 , \f3\fs18 tagL3 \f4\fs20 , and \f3\fs18 tagL4 \f4\fs20 ) of the individuals to be returned. Note that if any \f3\fs18 tag \f4\fs20 / \f3\fs18 tagL \f4\fs20 parameter is specified as non- \f3\fs18 NULL \f4\fs20 , that \f3\fs18 tag \f4\fs20 / \f3\fs18 tagL \f4\fs20 property must have a defined value for every individual in the subpopulation, otherwise an error may result (although this requirement will not necessarily be checked comprehensively by this method in every invocation).\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 This method is shorthand for getting the \f3\fs18 individuals \f4\fs20 property of the subpopulation, and then using operator \f3\fs18 [] \f4\fs20 to select only individuals with the desired properties; besides being much simpler than the equivalent Eidos code, it is also much faster. See \f3\fs18 sampleIndividuals() \f4\fs20 for a similar method that returns a sample taken from a chosen subset of individuals.\ \pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \'96\'a0(void)takeMigrants(object\'a0migrants)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 Immediately moves the individuals in \f3\fs18 migrants \f4\fs20 to the target subpopulation (removing them from their previous subpopulation). Individuals in \f3\fs18 migrants \f4\fs20 that are already in the target subpopulation are unaffected. Note that the indices and order of individuals and haplosomes in both the target and source subpopulations will change unpredictably as a side effect of this method.\ Note that this method is only for use in nonWF models, in which migration is managed manually by the model script. In WF models, migration is managed automatically by the SLiM core based upon the migration rates set for each subpopulation with \f3\fs18 setMigrationRates() \f4\fs20 . \f5 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab720\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 5.18 Class Substitution\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\b0 \cf0 5.18.1 \f2\fs18 Substitution \f1\fs22 properties\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\i0\fs18 \cf2 chromosome => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 The \f3\fs18 Chromosome \f4\fs20 object with which the mutation is associated.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 id => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier for this mutation. Each mutation created during a run receives an immutable identifier that will be unique across the duration of the run, and that identifier is carried over to the \f3\fs18 Substitution \f4\fs20 object when the mutation fixes. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 fixationTick => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The tick in which this mutation fixed. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 mutationType => (object$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The \f3\fs18 MutationType \f4\fs20 from which this mutation was drawn. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 \expnd0\expndtw0\kerning0 nucleotide => (string$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 A \f3\fs18 string \f4\fs20 representing the nucleotide associated with this mutation; this will be \f3\fs18 "A" \f4\fs20 , \f3\fs18 "C" \f4\fs20 , \f3\fs18 "G" \f4\fs20 , or \f3\fs18 "T" \f4\fs20 . If the mutation is not nucleotide-based, this property is unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf2 nucleotideValue => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf2 An \f3\fs18 integer \f4\fs20 representing the nucleotide associated with this mutation; this will be \f3\fs18 0 \f4\fs20 (A), \f3\fs18 1 \f4\fs20 (C), \f3\fs18 2 \f4\fs20 (G), or \f3\fs18 3 \f4\fs20 (T). If the mutation is not nucleotide-based, this property is unavailable.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 \kerning1\expnd0\expndtw0 originTick => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The tick in which this mutation arose. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 position => (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The position in the chromosome of this mutation. \f5 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 selectionCoeff => (float$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The selection coefficient of the mutation, drawn from the distribution of fitness effects of its \f3\fs18 MutationType \f5\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 subpopID <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 The identifier of the subpopulation in which this mutation arose. This value is carried over from the \f3\fs18 Mutation \f4\fs20 object directly; if a \'93tag\'94 value was used in the \f3\fs18 Mutation \f4\fs20 object, that value will carry over to the corresponding \f3\fs18 Substitution \f4\fs20 object. The \f3\fs18 subpopID \f4\fs20 in \f3\fs18 Substitution \f4\fs20 is a read-write property to allow it to be used as a \'93tag\'94 in the same way, if the origin subpopulation identifier is not needed.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f3\fs18 \cf0 tag <\'96> (integer$)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f4\fs20 \cf0 A user-defined \f3\fs18 integer \f4\fs20 value. The value of \f3\fs18 tag \f4\fs20 is carried over automatically from the original \f3\fs18 Mutation \f4\fs20 object. Apart from that, the value of \f3\fs18 tag \f4\fs20 is not used by SLiM; it is free for you to use.\ \pard\pardeftab720\ri720\sb120\sa60\partightenfactor0 \f1\i\fs22 \cf0 5.18.2 \f2\fs18 Substitution \f1\fs22 methods\ \pard\pardeftab397\fi274\ri720\sb40\sa40\partightenfactor0 \f5\i0 \cf0 \ } ================================================ FILE: SLiMgui/SLiMHelpFunctions.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2761 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Optima-Bold;\f1\fnil\fcharset0 Menlo-Regular;\f2\fswiss\fcharset0 Optima-Regular; \f3\fswiss\fcharset0 Optima-Italic;\f4\froman\fcharset0 TimesNewRomanPSMT;\f5\fnil\fcharset0 Menlo-Bold; \f6\fnil\fcharset0 AppleColorEmoji;\f7\froman\fcharset0 TimesNewRomanPS-ItalicMT;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red150\green150\blue150;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c65500\c65500\c65500;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab397 \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.1. Initialization functions\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf0 (integer$)initializeAncestralNucleotides(is\'a0sequence)\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 \expnd0\expndtw0\kerning0 This function, which may be called only in nucleotide-based models, supplies an ancestral nucleotide sequence for the model. The \f1\fs18 sequence \f2\fs20 parameter may be an \f1\fs18 integer \f2\fs20 vector providing nucleotide values (A=0, C=1, G=2, T=3), or a \f1\fs18 string \f2\fs20 vector providing single-character nucleotides ( \f1\fs18 "A" \f2\fs20 , \f1\fs18 "C" \f2\fs20 , \f1\fs18 "G" \f2\fs20 , \f1\fs18 "T" \f2\fs20 ), or a singleton \f1\fs18 string \f2\fs20 providing the sequence as one string ( \f1\fs18 "ACGT..." \f2\fs20 ), or a singleton \f1\fs18 string \f2\fs20 providing the filesystem path of a FASTA file which will be read in to provide the sequence (if the file contains than one sequence, the first sequence will be used). Only A/C/G/T nucleotide values may be provided; other symbols, such as those for amino acids, gaps, or nucleotides of uncertain identity, are not allowed. The two semantic meanings of \f1\fs18 sequence \f2\fs20 that involve a singleton \f1\fs18 string \f2\fs20 value are distinguished heuristically; a singleton \f1\fs18 string \f2\fs20 that contains only the letters ACGT will be assumed to be a nucleotide sequence rather than a filename. The length of the ancestral sequence is returned.\ A utility function, \f1\fs18 randomNucleotides() \f2\fs20 , is provided by SLiM to assist in generating simple random nucleotide sequences. \f3\i \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\i0\fs18 \cf2 \kerning1\expnd0\expndtw0 (object$)initializeChromosome(integer$\'a0id, [Ni$\'a0length\'a0=\'a0NULL], [string$\'a0type\'a0=\'a0"A"], [Ns$\'a0symbol\'a0=\'a0NULL], [Ns$\'a0name\'a0=\'a0NULL], [integer$\'a0mutationRuns\'a0=\'a00])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calling this function, added in SLiM\'a05, initiates the configuration of a chromosome in the species being initialized. The new \f1\fs18 Chromosome \f2\fs20 object is returned, but it is still under construction and will error if used; see below for details. That chromosome is then the \'93focal chromosome\'94 for subsequent genetic initialization functions \'96 specifically, for \f1\fs18 initializeAncestralNucleotides() \f2\fs20 , \f1\fs18 initializeGeneConversion() \f2\fs20 , \f1\fs18 initializeGenomicElement() \f2\fs20 , \f1\fs18 initializeHotspotMap() \f2\fs20 , \f1\fs18 initializeMutationRate() \f2\fs20 , and \f1\fs18 initializeRecombinationRate() \f2\fs20 . If you wish to call \f1\fs18 initializeChromosome() \f2\fs20 at all (which is not required), you must call it \f3\i before \f2\i0 calling any of those genetic initialization functions, so that the focal chromosome is created \f3\i before \f2\i0 being configured further; otherwise, SLiM will assume that you want a default single-chromosome model, and when \f1\fs18 initializeChromosome() \f2\fs20 is called later (contradicting that assumption), an error will result.\ Furthermore, there are some other initialization functions must be called before \f1\fs18 initializeChromosome() \f2\fs20 if they are called at all \'96 specifically, \f1\fs18 initializeSex() \f2\fs20 , \f1\fs18 initializeTreeSeq() \f2\fs20 , \f1\fs18 initializeSpecies() \f2\fs20 , and \f1\fs18 initializeSLiMOptions() \f2\fs20 . This is so that \f1\fs18 initializeChromosome() \f2\fs20 knows the context within which the new chromosome is to be created; if these methods have not been called when \f1\fs18 initializeChromosome() \f2\fs20 is called, the default context is assumed (non-sexual, no tree-sequence recording, single-species, non-nucleotide-based), and an error will result downstream if one of those functions is later called (indicating that those assumptions might be incorrect).\ The parameters to \f1\fs18 initializeChromosome() \f2\fs20 configure the chromosome created. They will be discussed out of order here, because that order of presentation will, I hope, be clearer.\ There are three parameters that in some way identify the chromosome. First, the required \f1\fs18 id \f2\fs20 parameter provides an \f1\fs18 integer \f2\fs20 identifier for the chromosome, which can be used to look up the chromosome later in the simulation; it can be any non-negative \f1\fs18 integer \f2\fs20 value, but must be unique within the species (two chromosomes in the same species cannot have the same \f1\fs18 id \f2\fs20 ). Often it is an empirical chromosome number, for convenience and clarity; if modeling human chromosome 7, for example, you might provide \f1\fs18 7 \f2\fs20 . Second, the \f1\fs18 symbol \f2\fs20 parameter provides a \f1\fs18 string \f2\fs20 identifier for the chromosome, which can also be used to look up the chromosome later in the simulation. If \f1\fs18 NULL \f2\fs20 (the default) is passed for \f1\fs18 symbol \f2\fs20 , the chromosome\'92s default \f1\fs18 symbol \f2\fs20 value will be the \f1\fs18 string \f2\fs20 version of its \f1\fs18 id \f2\fs20 ( \f1\fs18 "7" \f2\fs20 for an \f1\fs18 id \f2\fs20 of \f1\fs18 7 \f2\fs20 , for example). The chromosome\'92s \f1\fs18 symbol \f2\fs20 value will be used to identify the chromosome in output \'96 in VCF output, for example, and in SLiMgui. It must be non-empty (not \f1\fs18 "" \f2\fs20 ), no more than five characters long, and unique within the species. Third, the \f1\fs18 name \f2\fs20 parameter can be any \f1\fs18 string \f2\fs20 value; if \f1\fs18 NULL \f2\fs20 (the default) is passed, the \f1\fs18 name \f2\fs20 value will be \f1\fs18 "" \f2\fs20 . The \f1\fs18 name \f2\fs20 is not used by SLiM, and can be used in any way you wish.\ The \f1\fs18 length \f2\fs20 parameter sets the length, in base positions, of the chromosome, and must either be \f1\fs18 NULL \f2\fs20 , or an \f1\fs18 integer \f2\fs20 greater than or equal to \f1\fs18 1 \f2\fs20 . If \f1\fs18 length \f2\fs20 is \f1\fs18 NULL \f2\fs20 , the length of the chromosome will be calculated after all \f1\fs18 initialize() \f2\fs20 callbacks have been called, as the maximum position referenced by the chromosome\'92s genomic elements, recombination map, mutation rate map, and (in nucleotide-based models) hotspot map; in other words, the chromosome will be sized to encompass all of the things it contains (which is also the behavior of the implicitly defined chromosome if \f1\fs18 initializeChromosome() \f2\fs20 is not called). Otherwise \'96 if \f1\fs18 length \f2\fs20 is specified with an \f1\fs18 integer \f2\fs20 value \'96 the chromosome\'92s length will be fixed at that value, and the last valid base position in the chromosome will be \f1\fs18 length-1 \f2\fs20 . Attempting to add a genomic element or a mutation after the last position will raise an error. Similarly, the last position of the chromosome must match the last position specified for recombination, mutation, and hotspot maps for that chromosome, but not all positions on a chromosome have to actually be used in the model (i.e., not all positions must be covered by a genomic element).\ The \f1\fs18 type \f2\fs20 parameter specifies the type of chromosome to be created. There are numerous options, and they are somewhat complex. They are discussed in more detail in the documentation for class \f1\fs18 Chromosome \f2\fs20 , particularly their specific patterns of inheritance; but they are briefly summarized here for quick reference. Note that \'93\'96\'93 below indicates a null haplosome. First of all, in hermaphroditic models \f1\fs18 type \f2\fs20 will generally be one of:\ \pard\pardeftab720\li907\ri360\sa60\partightenfactor0 \f1\fs18 \cf2 "A" \f2\fs20 (autosome), the default, specifying a diploid autosomal chromosome.\ \f1\fs18 "H" \f2\fs20 (haploid), specifying a haploid autosomal chromosome that recombines in biparental crosses.\ \f1\fs18 "HF" \f2\fs20 (haploid female-inherited), specifying a haploid autosomal chromosome that is inherited by both sexes from the first (female) parent in biparental crosses (also allowed in hermaphroditic models for inheritance that is always from the first parent).\ \f1\fs18 "HM" \f2\fs20 (haploid male-inherited), specifying a haploid autosomal chromosome that is inherited by both sexes from the second (male) parent in biparental crosses (also allowed in hermaphroditic models for inheritance that is always from the second parent).\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 Some sex-chromosome types are supported only in sexual models:\ \pard\pardeftab720\li907\ri360\sa60\partightenfactor0 \f1\fs18 \cf2 "X" \f2\fs20 (X), specifying an X chromosome that is diploid (XX) in females, haploid (X\'96) in males.\ \f1\fs18 "Y" \f2\fs20 (Y), specifying a Y chromosome that is haploid (Y) in males, absent (\'96) in females.\ \f1\fs18 "Z" \f2\fs20 (Z), specifying a Z chromosome that is diploid (ZZ) in males, haploid (\'96Z) in females.\ \f1\fs18 "W" \f2\fs20 (W), specifying a W chromosome that is haploid (W) in females, absent (\'96) in males.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 And there are some haploid chromosome types that are also supported only in sexual models:\ \pard\pardeftab720\li907\ri360\sa60\partightenfactor0 \f1\fs18 \cf2 "FL" \f2\fs20 (female line), specifying a haploid autosomal chromosome that is inherited only by females, from the female parent, and is represented by a null haplosome in males.\ \f1\fs18 "ML" \f2\fs20 (male line), specifying a haploid autosomal chromosome that is inherited only by males, from the male parent, and is represented by a null haplosome in females.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 Finally, two additional values of \f1\fs18 type \f2\fs20 , \f1\fs18 "H-" \f2\fs20 and \f1\fs18 "-Y" \f2\fs20 , are supported for backward compatibility (not intended for use in new models). They are discussed in the \f1\fs18 Chromosome \f2\fs20 documentation.\ The \f1\fs18 mutationRuns \f2\fs20 parameter specifies how many mutation runs the chromosome should use. Internally, SLiM divides haplosomes into a sequence of consecutive mutation runs, allowing more efficient internal computations. The optimal mutation run length is short enough that each mutation run is relatively unlikely to be modified by mutation/recombination events when inherited, but long enough that each mutation run is likely to contain a relatively large number of mutations; these priorities are in tension, so an intermediate balance between them is generally optimal. The optimal number of mutation runs will depend on the model\'92s details, and may also depend upon the machine and even the compiler used to build SLiM. If the \f1\fs18 mutationRuns \f2\fs20 parameter is not \f1\fs18 0 \f2\fs20 , SLiM will use the value given as the number of mutation runs inside \f1\fs18 Haplosome \f2\fs20 objects for the chromosome. If \f1\fs18 mutationRuns \f2\fs20 is \f1\fs18 0 \f2\fs20 (the default), then the behavior depends upon a parameter to the \f1\fs18 initializeSLiMOptions() \f2\fs20 function, \f1\fs18 doMutationRunExperiments \f2\fs20 . If that flag is \f1\fs18 F \f2\fs20 , the behavior here is as if \f1\fs18 mutationRuns=1 \f2\fs20 had been passed: one mutation run will be used, and mutation run experiments will not be conducted. If that flag is \f1\fs18 T \f2\fs20 (the default), then for \f1\fs18 mutationRuns=0 \f2\fs20 SLiM will conduct experiments at runtime, using different mutation run counts, to try to determine the number of mutation runs that produces the best performance. The value that SLiM\'92s experiments determine may not be optimal, however, and in any case there is some overhead associated with conducting these experiments; for maximal performance it can thus be beneficial to determine the true optimal value for the simulation yourself, and set it explicitly using this parameter. Specifying the number of mutation runs is an advanced technique, but in some cases it can improve performance significantly.\ The order in which \f1\fs18 initializeChromosome() \f2\fs20 calls are made is generally unimportant, since the chromosomes assort independently of each other anyway, but SLiM will preserve the order in which they were defined for you (for the \f1\fs18 chromosomes \f2\fs20 property of Species, for display in SLiMgui, for writing out to VCF, and so forth). All of the above types of chromosomes can be defined any number of times; you can have any number of autosomal chromosomes, for example. In a sexual model you could even have multiple defined sex chromosomes \'96 not in the sense of a female being XX, but in the sense of a female being X \fs13\fsmilli6667 \sub 1 \fs20 \nosupersub X \fs13\fsmilli6667 \sub 1 \fs20 \nosupersub X \fs13\fsmilli6667 \sub 2 \fs20 \nosupersub X \fs13\fsmilli6667 \sub 2 \fs20 \nosupersub , where X \fs13\fsmilli6667 \sub 1 \fs20 \nosupersub and X \fs13\fsmilli6667 \sub 2 \fs20 \nosupersub are two different kinds of X chromosome. Similarly, you could define both an X and a Z for a species, if you wish; each would segregate correctly according to the sex of the offspring. In sexual models in SLiM the sex of an offspring is determined randomly or given by the user in script; it is not a function of the sex chromosomes present in the individual, although the sex chromosomes present in the individual will correlate with sex. In other words, SLiM does not know and does not care what sex-determination system the species is using; the chromosomes follow the sex, rather than the sex following the chromosomes. This should allow any sex-determination system to be modeled, even if it is unusual, non-genetic, etc.\ As stated above, the new \f1\fs18 Chromosome \f2\fs20 object is returned by this call, but it is still under construction so most of its methods and properties will error. It will remain in this state until \f1\fs18 initialize() \f2\fs20 callbacks have completed, and will then become active and usable. Until that point, there are only a handful of uses that are guaranteed to be allowed: storing it in a variable; remembering it with \f1\fs18 defineConstant() \f2\fs20 ; using the methods and properties of its superclasses, notably \f1\fs18 Dictionary \f2\fs20 ; setting SLiMgui display-related properties such as \f1\fs18 colorSubstitution \f2\fs20 ; getting and setting its \f1\fs18 tag \f2\fs20 property; and accessing those of its properties that were passed to the \f1\fs18 initializeChromosome() \f2\fs20 call, specifically \f1\fs18 id \f2\fs20 , \f1\fs18 symbol \f2\fs20 , \f1\fs18 name \f2\fs20 , \f1\fs18 type \f2\fs20 , \f1\fs18 length \f2\fs20 , and \f1\fs18 lastPosition \f2\fs20 . Its other properties, and all \f1\fs18 Chromosome \f2\fs20 methods, will raise an error while in this state. This safeguard protects the new \f1\fs18 Chromosome \f2\fs20 object from being used while still in an inconsistent state.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (void)initializeGeneConversion(numeric$\'a0nonCrossoverFraction, numeric$\'a0meanLength, numeric$\'a0simpleConversionFraction, [numeric$\'a0bias\'a0=\'a00], [logical$\'a0redrawLengthsOnFailure\'a0=\'a0F])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calling this function switches the recombination model from a \'93simple crossover\'94 model to a \'93double-stranded break (DSB)\'94 model, and configures the details of the gene conversion tracts that will therefore be modeled. The fraction of DSBs that will be modeled as non-crossover events is given by \f1\fs18 nonCrossoverFraction \f2\fs20 . The mean length of gene conversion tracts (whether associated with crossover or non-crossover events) is given by \f1\fs18 meanLength \f2\fs20 ; the actual extent of a gene conversion tract will be the sum of two independent draws from a geometric distribution with mean \f1\fs18 meanLength/2 \f2\fs20 . The fraction of gene conversion tracts that are modeled as \'93simple\'94 is given by \f1\fs18 simpleConversionFraction \f2\fs20 ; the remainder will be modeled as \'93complex\'94, involving repair of heteroduplex mismatches. Finally, the \f1\fs18 GC \f2\fs20 bias during heteroduplex mismatch repair is given by \f1\fs18 bias \f2\fs20 , with the default of \f1\fs18 0.0 \f2\fs20 indicating no bias, \f1\fs18 1.0 \f2\fs20 indicating an absolute preference for \f1\fs18 G \f2\fs20 / \f1\fs18 C \f2\fs20 mutations over \f1\fs18 A \f2\fs20 / \f1\fs18 T \f2\fs20 mutations, and \f1\fs18 -1.0 \f2\fs20 indicating an absolute preference for \f1\fs18 A \f2\fs20 / \f1\fs18 T \f2\fs20 mutations over \f1\fs18 G \f2\fs20 / \f1\fs18 C \f2\fs20 mutations. A non-zero bias may only be set in nucleotide-based models. This function, and the way that gene conversion is modeled, fundamentally changed in SLiM 3.3.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 Beginning in SLiM 4.1, the \f1\fs18 redrawLengthsOnFailure \f2\fs20 parameter can be used to modify the internal mechanics of layout of gene conversion tracts. If it is \f1\fs18 F \f2\fs20 (the default, and the only behavior supported before SLiM 4.1), then if an attempt to lay out gene conversion tracts fails (because the tracts overlap each other, or overlap the start or end of the chromosome), SLiM will try again by drawing new positions for the tracts \'96 essentially shuffling the tracts around to try to find positions for them that don\'92t overlap. If \f1\fs18 redrawLengthsOnFailure \f2\fs20 is \f1\fs18 T \f2\fs20 , then if an attempt to lay out gene conversion tracts fails, SLiM will try again by drawing new lengths for the tracts, as well as new positions. This makes it more likely that layout will succeed, but risks biasing the realized mean tract length downward from the requested mean length (since layout of long tracts is more likely fail due to overlap). In either case, if SLiM attempts to lay out gene conversion tracts 100 times without success, an error will result. That error indicates that the specified constraints for gene conversion are difficult to satisfy \'96 tracts may commonly be so long that it is difficult or impossible to find an acceptable layout for them within the specified chromosome length. Setting \f1\fs18 redrawLengthsOnFailure \f2\fs20 to \f1\fs18 T \f2\fs20 may mitigate this problem, at the price of biasing the mean tract length downward as discussed.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (object)initializeGenomicElement(io\'a0genomicElementType, [Ni\'a0start\'a0=\'a0NULL], [Ni\'a0end\'a0=\'a0NULL])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 \expnd0\expndtw0\kerning0 Add a genomic element to the chromosome at initialization time. The \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 parameters give the first and last base positions to be spanned by the new genomic element. The new element will be based upon the genomic element type identified by \f1\fs18 genomicElementType \f2\fs20 , which can be either an \f1\fs18 integer \f2\fs20 , representing the ID of the desired element type, or an \f1\fs18 object \f2\fs20 of type \f1\fs18 GenomicElementType \f2\fs20 specified directly.\ Beginning in SLiM 3.3, this function is vectorized: the \f1\fs18 genomicElementType \f2\fs20 , \f1\fs18 start \f2\fs20 , and \f1\fs18 end \f2\fs20 parameters do not have to be singletons. In particular, \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 may be of any length, but must be equal in length; each \f1\fs18 start \f2\fs20 / \f1\fs18 end \f2\fs20 element pair will generate one new genomic element spanning the given base positions. In this case, \f1\fs18 genomicElementType \f2\fs20 may still be a singleton, providing the genomic element type to be used for all of the new genomic elements, or it may be equal in length to \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 , providing an independent genomic element type for each new element. When adding a large number of genomic elements, it will be much faster to add them in order of ascending position with a vectorized call.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 Beginning in SLiM 5, passing \f1\fs18 NULL \f2\fs20 for \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 is allowed by \f1\fs18 initializeGenomicElement() \f2\fs20 , but only in one specific case: if the focal chromosome being configured was explicitly defined with \f1\fs18 initializeChromosome() \f2\fs20 , and that focal chromosome was given an explicit length (rather than a length of \f1\fs18 NULL \f2\fs20 ). In that case, \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 may be \f1\fs18 NULL \f2\fs20 ( \f3\i both \f2\i0 of them, not just one of them), indicating that the genomic element created should span the entire length of the focal chromosome. Since \f1\fs18 NULL \f2\fs20 is now the default value for \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 , this makes this common configuration very simple to set up.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 The return value provides the genomic element(s) created by the call, in the order in which they were specified in the parameters to \f1\fs18 initializeGenomicElement() \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (object$)initializeGenomicElementType(is$\'a0id, io\'a0mutationTypes, numeric\'a0proportions\cf2 \expnd0\expndtw0\kerning0 , [Nf\'a0mutationMatrix\'a0=\'a0NULL]\cf0 \kerning1\expnd0\expndtw0 ) \f4 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf0 Add a genomic element type at initialization time. The \f1\fs18 id \f2\fs20 must not already be used for any genomic element type in the simulation. The \f1\fs18 mutationTypes \f2\fs20 vector identifies the mutation types used by the genomic element, and the \f1\fs18 proportions \f2\fs20 vector should be of equal length, specifying the relative proportion of mutations that will be drawn from the corresponding mutation type (proportions do not need to add up to one; they are interpreted relatively). The \f1\fs18 id \f2\fs20 parameter may be either an \f1\fs18 integer \f2\fs20 giving the ID of the new genomic element type, or a \f1\fs18 string \f2\fs20 giving the name of the new genomic element type (such as \f1\fs18 "g5" \f2\fs20 to specify an ID of 5). The \f1\fs18 mutationTypes \f2\fs20 parameter may be either an \f1\fs18 integer \f2\fs20 vector representing the IDs of the desired mutation types, or an \f1\fs18 object \f2\fs20 vector of \f1\fs18 MutationType \f2\fs20 elements specified directly. The global symbol for the new genomic element type is immediately available; the return value also provides the new object. \f4 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2 \cf2 \expnd0\expndtw0\kerning0 The \f1\fs18 mutationMatrix \f2\fs20 parameter is \f1\fs18 NULL \f2\fs20 by default, and in non-nucleotide-based models it must be \f1\fs18 NULL \f2\fs20 . In nucleotide-based models, on the other hand, it must be non- \f1\fs18 NULL \f2\fs20 , and therefore must be supplied. In that case, \f1\fs18 mutationMatrix \f2\fs20 should take one of two standard forms. For sequence-based mutation rates that depend upon only the single nucleotide at a mutation site, \f1\fs18 mutationMatrix \f2\fs20 should be a 4\'d74 \f1\fs18 float \f2\fs20 matrix, specifying mutation rates for an existing nucleotide state (rows from \f1\fs18 0 \f2\fs20 \'96 \f1\fs18 3 \f2\fs20 representing A/C/G/T) to each of the four possible derived nucleotide states (columns, with the same meaning). The mutation rates in this matrix are absolute rates, per nucleotide per gamete; they will be used by SLiM directly unless they are multiplied by a factor from the hotspot map (see \f1\fs18 initializeHotspotMap() \f2\fs20 ). Rates in \f1\fs18 mutationMatrix \f2\fs20 that involve the mutation of a nucleotide to itself ( \f1\fs18 A \f2\fs20 to \f1\fs18 A \f2\fs20 , \f1\fs18 C \f2\fs20 to \f1\fs18 C \f2\fs20 , etc.) are not used by SLiM and must be \f1\fs18 0.0 \f2\fs20 by convention.\ It is important to note that the order of the rows and columns used in SLiM, A/C/G/T, is not a universal convention; other sources will present substitution-rate/transition-rate matrices using different conventions, and so care must be taken when importing such matrices into SLiM.\ For sequence-based mutation rates that depend upon the trinucleotide sequence centered upon a mutation site (the adjacent bases to the left and right, in other words, as well as the mutating nucleotide itself), \f1\fs18 mutationMatrix \f2\fs20 should be a 64\'d74 \f1\fs18 float \f2\fs20 matrix, specifying mutation rates for the central nucleotide of an existing trinucleotide sequence (rows from \f1\fs18 0 \f2\fs20 \'96 \f1\fs18 63 \f2\fs20 , representing codons as described in the documentation for the \f1\fs18 ancestralNucleotides() \f2\fs20 method of \f1\fs18 Chromosome \f2\fs20 ) to each of the four possible derived nucleotide states (columns from \f1\fs18 0 \f2\fs20 \'96 \f1\fs18 3 \f2\fs20 for A/C/G/T as before). Note that in every case it is the central nucleotide of the trinucleotide sequence that is mutating, but rates can be specified independently based upon the nucleotides in the first and third positions as well, with this type of mutation matrix.\ Several helper functions are defined to construct common types of mutation matrices, such as \f1\fs18 mmJukesCantor() \f2\fs20 to create a mutation matrix for a Jukes\'96Cantor model.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)initializeHotspotMap(numeric\'a0multipliers, [Ni\'a0ends\'a0=\'a0NULL], [string$\'a0sex\'a0=\'a0"*"])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 In nucleotide-based models, set the mutation rate \f3\i multiplier \f2\i0 along the chromosome. Nucleotide-based models define sequence-based mutation rates that are set up with the \f1\fs18 mutationMatrix \f2\fs20 parameter to \f1\fs18 initializeGenomicElementType() \f2\fs20 . If no hotspot map is specified by calling \f1\fs18 initializeHotspotMap() \f2\fs20 , a hotspot map with a multiplier of \f1\fs18 1.0 \f2\fs20 across the whole chromosome is assumed (and so the sequence-based rates are the absolute mutation rates used by SLiM). A hotspot map modifies the sequence-based rates by scaling them up in some regions, with multipliers greater than \f1\fs18 1.0 \f2\fs20 (representing mutational hot spots), and/or scaling them down in some regions, with multipliers less than \f1\fs18 1.0 \f2\fs20 (representing mutational cold spots).\ There are two ways to call this function. If the optional \f1\fs18 ends \f2\fs20 parameter is \f1\fs18 NULL \f2\fs20 (the default), then \f1\fs18 multipliers \f2\fs20 must be a singleton value that specifies a single multiplier to be used along the entire chromosome (typically \f1\fs18 1.0 \f2\fs20 , but not required to be). If, on the other hand, \f1\fs18 ends \f2\fs20 is supplied, then \f1\fs18 multipliers \f2\fs20 and \f1\fs18 ends \f2\fs20 must be the same length, and the values in \f1\fs18 ends \f2\fs20 must be specified in ascending order. In that case, \f1\fs18 multipliers \f2\fs20 and \f1\fs18 ends \f2\fs20 taken together specify the multipliers to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in \f1\fs18 ends \f2\fs20 should extend to the end of the chromosome (i.e. at least to the end of the last genomic element, if not further).\ For example, if the following call is made:\ \pard\pardeftab720\ri720\partightenfactor0 \cf2 \f1\fs18 initializeHotspotMap(c(1.0, 1.2), c(5000, 9999));\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 then the result is that the mutation rate multiplier for bases \f1\fs18 0 \f2\fs20 ... \f1\fs18 5000 \f2\fs20 (inclusive) will be \f1\fs18 1.0 \f2\fs20 (and so the specified sequence-based mutation rates will be used verbatim), and the multiplier for bases \f1\fs18 5001 \f2\fs20 ... \f1\fs18 9999 \f2\fs20 (inclusive) will be \f1\fs18 1.2 \f2\fs20 (and so the sequence-based mutation rates will be multiplied by 1.2 within the region).\ Note that mutations are generated by SLiM only within genomic elements, regardless of the hotspot map. In effect, the hotspot map given is intersected with the coverage area of the genomic elements defined; areas outside of any genomic element are given a multiplier of zero. There is no harm in supplying a hotspot map that specifies multipliers for areas outside of the genomic elements defined; the excess information is simply not used.\ If the optional \f1\fs18 sex \f2\fs20 parameter is \f1\fs18 "*" \f2\fs20 (the default), then the supplied hotspot map will be used for both sexes (which is the only option for hermaphroditic simulations). In sexual simulations \f1\fs18 sex \f2\fs20 may be \f1\fs18 "M" \f2\fs20 or \f1\fs18 "F" \f2\fs20 instead, in which case the supplied hotspot map is used only for that sex (i.e., when generating a gamete from a parent of that sex). In this case, two calls must be made to \f1\fs18 initializeHotspotMap() \f2\fs20 , one for each sex, even if a multiplier of \f1\fs18 1.0 \f2\fs20 is desired for the other sex; no default hotspot map is supplied.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (object$)initializeInteractionType(is$\'a0id, string$\'a0spatiality, [logical$\'a0reciprocal\'a0=\'a0F], [numeric$\'a0maxDistance\'a0=\'a0INF], [string$\'a0sexSegregation\'a0=\'a0"**"]) \f4 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Add an interaction type at initialization time. The \f1\fs18 id \f2\fs20 must not already be used for any interaction type in the simulation. The \f1\fs18 id \f2\fs20 parameter may be either an \f1\fs18 integer \f2\fs20 giving the ID of the new interaction type, or a \f1\fs18 string \f2\fs20 giving the name of the new interaction type (such as \f1\fs18 "i5" \f2\fs20 to specify an ID of 5).\ The \f1\fs18 spatiality \f2\fs20 may be \f1\fs18 "" \f2\fs20 , for non-spatial interactions (i.e., interactions that do not depend upon the distance between individuals); \f1\fs18 "x" \f2\fs20 , \f1\fs18 "y" \f2\fs20 , or \f1\fs18 "z" \f2\fs20 for one-dimensional interactions; \f1\fs18 "xy" \f2\fs20 , \f1\fs18 "xz" \f2\fs20 , or \f1\fs18 "yz" \f2\fs20 for two-dimensional interactions; or \f1\fs18 "xyz" \f2\fs20 for three-dimensional interactions. The dimensions referenced by spatiality must be defined as spatial dimensions with \f1\fs18 initializeSLiMOptions() \f2\fs20 ; if the simulation has dimensionality \f1\fs18 "xy" \f2\fs20 , for example, then interactions in the simulation may have spatiality \f1\fs18 "" \f2\fs20 , \f1\fs18 "x" \f2\fs20 , \f1\fs18 "y" \f2\fs20 , or \f1\fs18 "xy" \f2\fs20 , but may not reference spatial dimension \f3\i z \f2\i0 and thus may not have spatiality \f1\fs18 "xz" \f2\fs20 , \f1\fs18 "yz" \f2\fs20 , or \f1\fs18 "xyz" \f2\fs20 . If no spatial dimensions have been configured, only non-spatial interactions may be defined.\ The \f1\fs18 reciprocal \f2\fs20 flag may be \f1\fs18 T \f2\fs20 , in which case the interaction is guaranteed by the user to be \f3\i reciprocal \f2\i0 : whatever the interaction strength is for exerter B upon receiver A, it will be equal (in magnitude and sign) for exerter A upon receiver B. In principle, this allows the \f1\fs18 InteractionType \f2\fs20 to reduce the amount of computation necessary by up to a factor of two (although it may or may not be used). If \f1\fs18 reciprocal \f2\fs20 is \f1\fs18 F \f2\fs20 , the interaction is not guaranteed to be reciprocal and each interaction will be computed independently. The built-in interaction formulas are all reciprocal, but if you implement an \f1\fs18 interaction() \f2\fs20 callback, you must consider whether the callback you have implemented preserves reciprocality or not. For this reason, the default is \f1\fs18 reciprocal=F \f2\fs20 , so that bugs are not inadvertently introduced by an invalid assumption of reciprocality. See below for a note regarding reciprocality in sexual simulations when using the \f1\fs18 sexSegregation \f2\fs20 flag.\ The \f1\fs18 maxDistance \f2\fs20 parameter supplies the maximum distance over which interactions of this type will be evaluated; at greater distances, the interaction strength is considered to be zero (for efficiency). The default value of \f1\fs18 maxDistance \f2\fs20 , \f1\fs18 INF \f2\fs20 (positive infinity), indicates that there is no maximum interaction distance; note that this can make some interaction queries much less efficient, and is therefore not recommended. In SLiM 3.1 and later, a warning will be issued if a spatial interaction type is defined with no maximum distance to encourage a maximum distance to be defined.\ The \f1\fs18 sexSegregation \f2\fs20 parameter governs the applicability of the interaction to each sex, in sexual simulations. It does not affect distance calculations in any way; it only modifies the way in which interaction strengths are calculated. The default, \f1\fs18 "**" \f2\fs20 , implies that the interaction is felt by both sexes (the first character of the \f1\fs18 string \f2\fs20 value) and is exerted by both sexes (the second character of the \f1\fs18 string \f2\fs20 value). Either or both characters may be \f1\fs18 M \f2\fs20 or \f1\fs18 F \f2\fs20 instead; for example, \f1\fs18 "MM" \f2\fs20 would indicate a male-male interaction, such as male-male competition, whereas \f1\fs18 "FM" \f2\fs20 would indicate an interaction influencing only female receivers that is influenced only by male exerters, such as male mating displays that influence female attraction. This parameter may be set only to \f1\fs18 "**" \f2\fs20 unless sex has been enabled with \f1\fs18 initializeSex() \f2\fs20 . Note that a value of \f1\fs18 sexSegregation \f2\fs20 other than \f1\fs18 "**" \f2\fs20 may imply some degree of non-reciprocality, but it is not necessary to specify \f1\fs18 reciprocal \f2\fs20 to be \f1\fs18 F \f2\fs20 for this reason; SLiM will take the sex-segregation of the interaction into account for you. The value of \f1\fs18 reciprocal \f2\fs20 may therefore be interpreted as meaning: in those cases, if any, in which A interacts with B and B interacts with A, is the interaction strength guaranteed to be the same in both directions? The \f1\fs18 sexSegregation \f2\fs20 parameter is shorthand for setting sex constraints on the interaction type using the \f1\fs18 setConstraints() \f2\fs20 method; see that method for a more extensive set of constraints that may be used.\ By default, the interaction strength is \f1\fs18 1.0 \f2\fs20 for all interactions within \f1\fs18 maxDistance \f2\fs20 . Often it is desirable to change the interaction function using \f1\fs18 setInteractionFunction() \f2\fs20 ; modifying interaction strengths can also be achieved with \f1\fs18 interaction() \f2\fs20 callbacks if necessary. In any case, interactions beyond \f1\fs18 maxDistance \f2\fs20 always have a strength of \f1\fs18 0.0 \f2\fs20 , and the interaction strength of an individual with itself is always \f1\fs18 0.0 \f2\fs20 , regardless of the interaction function or callbacks.\ The global symbol for the new interaction type is immediately available; the return value also provides the new object. Note that in multispecies models, \f1\fs18 initializeInteractionType() \f2\fs20 must be called from a non-species-specific \f1\fs18 interaction() \f2\fs20 callback (declared as \f1\fs18 species all initialize() \f2\fs20 ), since interactions are managed at the community level.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)initializeMutationRate(numeric\'a0rates, [Ni\'a0ends\'a0=\'a0NULL], [string$\'a0sex\'a0=\'a0"*"]) \f4 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Set the mutation rate per base position per gamete. To be precise, this mutation rate is the expected mean number of mutations that will occur per base position per gamete; note that this is different from how the recombination rate is defined (see \f1\fs18 initializeRecombinationRate() \f2\fs20 ). The number of mutations that actually occurs at a given base position when generating an offspring haplosome is, in effect, drawn from a Poisson distribution with that expected mean (but under the hood SLiM uses a mathematically equivalent but much more efficient strategy). It is possible for this Poisson draw to indicate that two or more new mutations have arisen at the same base position, particularly when the mutation rate is very high; in this case, the new mutations will be added to the site one at a time, and as always the mutation stacking policy will be followed.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 There are two ways to call this function. If the optional \f1\fs18 ends \f2\fs20 parameter is \f1\fs18 NULL \f2\fs20 (the default), then \f1\fs18 rates \f2\fs20 must be a singleton value that specifies a single mutation rate to be used along the entire chromosome. If, on the other hand, \f1\fs18 ends \f2\fs20 is supplied, then \f1\fs18 rates \f2\fs20 and \f1\fs18 ends \f2\fs20 must be the same length, and the values in \f1\fs18 ends \f2\fs20 must be specified in ascending order. In that case, \f1\fs18 rates \f2\fs20 and \f1\fs18 ends \f2\fs20 taken together specify the mutation rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in \f1\fs18 ends \f2\fs20 should extend to the end of the chromosome (i.e. at least to the end of the last genomic element, if not further).\ For example, if the following call is made:\ \pard\pardeftab720\ri720\partightenfactor0 \f4 \cf0 \kerning1\expnd0\expndtw0 \f1\fs18 initializeMutationRate(c(1e-7, 2.5e-8), c(5000, 9999));\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 \expnd0\expndtw0\kerning0 then the result is that the mutation rate for bases \f1\fs18 0 \f2\fs20 ... \f1\fs18 5000 \f2\fs20 (inclusive) will be \f1\fs18 1e-7 \f2\fs20 , and the rate for bases \f1\fs18 5001 \f2\fs20 ... \f1\fs18 9999 \f2\fs20 (inclusive) will be \f1\fs18 2.5e-8 \f2\fs20 .\ Note that mutations are generated by SLiM only within genomic elements, regardless of the mutation rate map. In effect, the mutation rate map given is intersected with the coverage area of the genomic elements defined; areas outside of any genomic element are given a mutation rate of zero. There is no harm in supplying a mutation rate map that specifies rates for areas outside of the genomic elements defined; that rate information is simply not used. The \f1\fs18 overallMutationRate \f2\fs20 family of properties on \f1\fs18 Chromosome \f2\fs20 provide the overall mutation rate after genomic element coverage has been taken into account, so it will reflect the rate at which new mutations will actually be generated in the simulation as configured.\ If the optional \f1\fs18 sex \f2\fs20 parameter is \f1\fs18 "*" \f2\fs20 (the default), then the supplied mutation rate map will be used for both sexes (which is the only option for hermaphroditic simulations). In sexual simulations \f1\fs18 sex \f2\fs20 may be \f1\fs18 "M" \f2\fs20 or \f1\fs18 "F" \f2\fs20 instead, in which case the supplied mutation rate map is used only for that sex (i.e., when generating a gamete from a parent of that sex). In this case, two calls must be made to \f1\fs18 initializeMutationRate() \f2\fs20 , one for each sex, even if a rate of zero is desired for the other sex; no default mutation rate map is supplied. \f3\i \ \f2\i0 In nucleotide-based models, \f1\fs18 initializeMutationRate() \f2\fs20 may not be called. Instead, the desired sequence-based mutation rate(s) should be expressed in the \f1\fs18 mutationMatrix \f2\fs20 parameter to \f1\fs18 initializeGenomicElementType() \f2\fs20 . If variation in the mutation rate along the chromosome is desired, \f1\fs18 initializeHotspotMap() \f2\fs20 should be used. \f3\i \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\i0 \cf2 \kerning1\expnd0\expndtw0 The \f1\fs18 initializeMutationRateFromFile() \f2\fs20 function is a useful convenience function if you wish to read the mutation rate map from a file.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)initializeMutationRateFromFile(string$\'a0path, integer$\'a0lastPosition, [float$\'a0scale\'a0=\'a01.0e-08], [string$\'a0sep\'a0=\'a0"\\t"], [string$\'a0dec\'a0=\'a0"."], [string$\'a0sex\'a0=\'a0"*"])\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Set a mutation rate map from data read from the file at \f1\fs18 path \f2\fs20 . This function is essentially a wrapper for \f1\fs18 initializeMutationRate() \f2\fs20 that uses \f1\fs18 readCSV() \f2\fs20 and passes the data through. The file is expected to contain two columns of data. The first column must be \f1\fs18 integer \f2\fs20 start positions for rate map regions; the first region should start at position \f1\fs18 0 \f2\fs20 if the map\'92s positions are \f1\fs18 0 \f2\fs20 -based, or at position \f1\fs18 1 \f2\fs20 if the map\'92s positions are \f1\fs18 1 \f2\fs20 -based; in the latter case, \f1\fs18 1 \f2\fs20 will be subtracted from every position since SLiM uses \f1\fs18 0 \f2\fs20 -based positions. The second column must be \f1\fs18 float \f2\fs20 rates, relative to the scaling factor specified in \f1\fs18 scale \f2\fs20 ; for example, if a given rate is \f1\fs18 1.2 \f2\fs20 and \f1\fs18 scale \f2\fs20 is \f1\fs18 1e-8 \f2\fs20 (the default), the rate used will be \f1\fs18 1.2e-8 \f2\fs20 . No column header line should be present; the file should start immediately with numerical data. The expected separator between columns is a tab character by default, but may be passed in \f1\fs18 sep \f2\fs20 ; the expected decimal separator is a period by default, but may be passed in \f1\fs18 dec \f2\fs20 . Once read, the map is converted into a rate map specified with end positions, rather than start positions, and the position given by \f1\fs18 lastPosition \f2\fs20 is used as the end of the last rate region; it should be the last position of the chromosome.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 See \f1\fs18 readCSV() \f2\fs20 for further details on \f1\fs18 sep \f2\fs20 and \f1\fs18 dec \f2\fs20 , which are passed through to it; and see \f1\fs18 initializeMutationRate() \f2\fs20 for details on how the rate map is validated and used, and how the \f1\fs18 sex \f2\fs20 parameter is used.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 This function is written in Eidos, and its source code can be viewed with \f1\fs18 functionSource() \f2\fs20 , so you can copy and modify its code if you need to modify its functionality.\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (object$)initializeMutationType(is$\'a0id, numeric$\'a0dominanceCoeff, string$\'a0distributionType, ...) \f4 \ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Add a mutation type at initialization time. The \f1\fs18 id \f2\fs20 must not already be used for any mutation type in the simulation. The \f1\fs18 id \f2\fs20 parameter may be either an \f1\fs18 integer \f2\fs20 giving the ID of the new mutation type, or a \f1\fs18 string \f2\fs20 giving the name of the new mutation type (such as \f1\fs18 "m5" \f2\fs20 to specify an ID of 5). The dominanceCoeff parameter supplies the dominance coefficient for the mutation type; \f1\fs18 0.0 \f2\fs20 produces no dominance, \f1\fs18 1.0 \f2\fs20 complete dominance, and values greater than \f1\fs18 1.0 \f2\fs20 , overdominance. The \f1\fs18 distributionType \f2\fs20 may be \f1\fs18 "f" \f2\fs20 , in which case the ellipsis \f1\fs18 ... \f2\fs20 should supply a \f1\fs18 numeric$ \f2\fs20 fixed selection coefficient; \f1\fs18 "e" \f2\fs20 , in which case the ellipsis should supply a \f1\fs18 numeric$ \f2\fs20 mean selection coefficient for an exponential distribution; \f1\fs18 "g" \f2\fs20 , in which case the ellipsis should supply a \f1\fs18 numeric$ \f2\fs20 mean selection coefficient and a \f1\fs18 numeric$ \f2\fs20 alpha shape parameter for a gamma distribution; \f1\fs18 "n" \f2\fs20 , in which case the ellipsis should supply a \f1\fs18 numeric$ \f2\fs20 mean selection coefficient and a \f1\fs18 numeric$ \f2\fs20 sigma (standard deviation) parameter for a normal distribution; \f1\fs18 "p" \f2\fs20 , in which case the ellipsis should supply a \f1\fs18 numeric$ \f2\fs20 mean selection coefficient and a \f1\fs18 numeric$ \f2\fs20 scale parameter for a Laplace distribution; \f1\fs18 "w" \f2\fs20 , in which case the ellipsis should supply a \f1\fs18 numeric$ \f2\fs20 \f4 \uc0\u955 \f2 scale parameter and a \f1\fs18 numeric$ \f2\fs20 k shape parameter for a Weibull distribution; or \f1\fs18 "s" \f2\fs20 , in which case the ellipsis should supply a \f1\fs18 string$ \f2\fs20 Eidos script parameter. The global symbol for the new mutation type is immediately available; the return value also provides the new object.\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 Note that by default in WF models, all mutations of a given mutation type will be converted into \f1\fs18 Substitution \f2\fs20 objects when they reach fixation, for efficiency reasons. If you need to disable this conversion, to keep mutations of a given type active in the simulation even after they have fixed, you can do so by setting the \f1\fs18 convertToSubstitution \f2\fs20 property of \f1\fs18 MutationType \f2\fs20 to \f1\fs18 F \f2\fs20 . In contrast, by default in nonWF models mutations will not be converted into \f1\fs18 Substitution \f2\fs20 objects when they reach fixation; \f1\fs18 convertToSubstitution \f2\fs20 is \f1\fs18 F \f2\fs20 by default in nonWF models. To enable conversion in nonWF models for neutral mutation types with no indirect fitness effects, you should therefore set \f1\fs18 convertToSubstitution \f2\fs20 to \f1\fs18 T \f2\fs20 .\ \pard\pardeftab543\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (object$)initializeMutationTypeNuc(is$\'a0id, numeric$\'a0dominanceCoeff, string$\'a0distributionType, ...)\ \pard\pardeftab543\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Add a nucleotide-based mutation type at initialization time. This function is identical to \f1\fs18 initializeMutationType() \f2\fs20 except that the new mutation type will be nucleotide-based \'96 in other words, mutations belonging to the new mutation type will have an associated nucleotide. This function may be called only in nucleotide-based models (as enabled by the \f1\fs18 nucleotideBased \f2\fs20 parameter to \f1\fs18 initializeSLiMOptions() \f2\fs20 ).\ Nucleotide-based mutations always use a \f1\fs18 mutationStackGroup \f2\fs20 of \f1\fs18 -1 \f2\fs20 and a \f1\fs18 mutationStackPolicy \f2\fs20 of \f1\fs18 "l" \f2\fs20 . This ensures that a new nucleotide mutation always replaces any previously existing nucleotide mutation at a given position, regardless of the mutation types of the nucleotide mutations. These values are set automatically by \f1\fs18 initializeMutationTypeNuc() \f2\fs20 , and may not be changed.\ See the documentation for \f1\fs18 initializeMutationType() \f2\fs20 for all other discussion.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (void)initializeRecombinationRate(numeric\'a0rates, [Ni\'a0ends\'a0=\'a0NULL], [string$\'a0sex\'a0=\'a0"*"]) \f4 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Set the recombination rate per base position per gamete. To be precise, this recombination rate is the probability that a breakpoint will occur between one base and the next base; note that this is different from how the mutation rate is defined (see \f1\fs18 initializeMutationRate() \f2\fs20 ). A recombination rate of 1 centimorgan/Mbp corresponds to a recombination rate of \f1\fs18 1e-8 \f2\fs20 in the units used by SLiM. All rates must be in the interval [ \f1\fs18 0.0 \f2\fs20 , \f1\fs18 0.5 \f2\fs20 ]. A rate of \f1\fs18 0.5 \f2\fs20 implies complete independence between the adjacent bases, which might be used to implement unlinked loci. Whether a breakpoint occurs between two bases is then, in effect, determined by a binomial draw with a single trial and the given rate as probability (but under the hood SLiM uses a mathematically equivalent but much more efficient strategy). The recombinational process in SLiM will never generate more then one crossover between one base and the next (in one generation/haplosome), and a supplied rate of \f1\fs18 0.5 \f2\fs20 will therefore result in an actual probability of \f1\fs18 0.5 \f2\fs20 for a crossover at the relevant position. (Note that this was not true in SLiM 2.x and earlier, however; their implementation of recombination resulted in a crossover probability of about 39.3% for a rate of \f1\fs18 0.5 \f2\fs20 , due to the use of an inaccurate approximation method. Recombination rates lower than about \f1\fs18 0.01 \f2\fs20 would have been essentially exact, since the approximation error became large only as the rate approached \f1\fs18 0.5 \f2\fs20 .)\ There are two ways to call this function. If the optional \f1\fs18 ends \f2\fs20 parameter is \f1\fs18 NULL \f2\fs20 (the default), then \f1\fs18 rates \f2\fs20 must be a singleton value that specifies a single recombination rate to be used along the entire chromosome. If, on the other hand, \f1\fs18 ends \f2\fs20 is supplied, then \f1\fs18 rates \f2\fs20 and \f1\fs18 ends \f2\fs20 must be the same length, and the values in \f1\fs18 ends \f2\fs20 must be specified in ascending order. In that case, \f1\fs18 rates \f2\fs20 and \f1\fs18 ends \f2\fs20 taken together specify the recombination rates to be used along successive contiguous stretches of the chromosome, from beginning to end; the last position specified in \f1\fs18 ends \f2\fs20 should extend to the end of the chromosome (i.e. at least to the end of the last genomic element, if not further).\ If the optional \f1\fs18 sex \f2\fs20 parameter is \f1\fs18 "*" \f2\fs20 (the default), then the supplied recombination rate map will be used for both sexes (which is the only option for hermaphroditic simulations). In sexual simulations \f1\fs18 sex \f2\fs20 may be \f1\fs18 "M" \f2\fs20 or \f1\fs18 "F" \f2\fs20 instead, in which case the supplied recombination map is used only for that sex. In this case, two calls must be made to \f1\fs18 initializeRecombinationRate() \f2\fs20 , one for each sex, even if a rate of zero is desired for the other sex; no default recombination map is supplied.\ The \f1\fs18 initializeRecombinationRateFromFile() \f2\fs20 function is a useful convenience function if you wish to read the recombination rate map from a file.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)initializeRecombinationRateFromFile(string$\'a0path, integer$\'a0lastPosition, [float$\'a0scale\'a0=\'a01.0e-08], [string$\'a0sep\'a0=\'a0"\\t"], [string$\'a0dec\'a0=\'a0"."], [string$\'a0sex\'a0=\'a0"*"])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Set a recombination rate map from data read from the file at \f1\fs18 path \f2\fs20 . This function is essentially a wrapper for \f1\fs18 initializeRecombinationRate() \f2\fs20 that uses \f1\fs18 readCSV() \f2\fs20 and passes the data through. The file is expected to contain two columns of data. The first column must be \f1\fs18 integer \f2\fs20 start positions for rate map regions; the first region should start at position \f1\fs18 0 \f2\fs20 if the map\'92s positions are \f1\fs18 0 \f2\fs20 -based, or at position \f1\fs18 1 \f2\fs20 if the map\'92s positions are \f1\fs18 1 \f2\fs20 -based; in the latter case, \f1\fs18 1 \f2\fs20 will be subtracted from every position since SLiM uses \f1\fs18 0 \f2\fs20 -based positions. The second column must be \f1\fs18 float \f2\fs20 rates, relative to the scaling factor specified in \f1\fs18 scale \f2\fs20 ; for example, if a given rate is \f1\fs18 1.2 \f2\fs20 and \f1\fs18 scale \f2\fs20 is \f1\fs18 1e-8 \f2\fs20 (the default), the rate used will be \f1\fs18 1.2e-8 \f2\fs20 . No column header line should be present; the file should start immediately with numerical data. The expected separator between columns is a tab character by default, but may be passed in \f1\fs18 sep \f2\fs20 ; the expected decimal separator is a period by default, but may be passed in \f1\fs18 dec \f2\fs20 . Once read, the map is converted into a rate map specified with end positions, rather than start positions, and the position given by \f1\fs18 lastPosition \f2\fs20 is used as the end of the last rate region; it should be the last position of the chromosome.\ See \f1\fs18 readCSV() \f2\fs20 for further details on \f1\fs18 sep \f2\fs20 and \f1\fs18 dec \f2\fs20 , which are passed through to it; and see \f1\fs18 initializeRecombinationRate() \f2\fs20 for details on how the rate map is validated and used, and how the \f1\fs18 sex \f2\fs20 parameter is used.\ This function is written in Eidos, and its source code can be viewed with \f1\fs18 functionSource() \f2\fs20 , so you can copy and modify its code if you need to modify its functionality.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)initializeSex([Ns$\'a0chromosomeType\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Enable sex in the simulation. Beginning in SLiM 5, this method should generally be passed \f1\fs18 NULL \f2\fs20 , simply indicating that sex should be enabled: individuals will then be male and female (rather than hermaphroditic), biparental crosses will be required to be between a female first parent and a male second parent, and selfing will not be allowed. In this new configuration style, if a sexual simulation involving sex chromosomes is desired, the new \f1\fs18 initializeChromosome() \f2\fs20 call should be used to configure the chromosome setup for the simulation.\ For backward compatibility, the old style of configuring a sexual simulation is still supported, however. This implicitly defines a single chromosome, without a call to \f1\fs18 initializeChromosome() \f2\fs20 . With this old configuration approach, the \f1\fs18 chromosomeType \f2\fs20 parameter to \f1\fs18 initializeSex() \f2\fs20 gives the type of chromosome that should be simulated; this should be \f1\fs18 "A" \f2\fs20 , \f1\fs18 "X" \f2\fs20 , or \f1\fs18 "Y" \f2\fs20 , and this \f1\fs18 chromosomeType \f2\fs20 value will be used as the symbol ( \f1\fs18 "A" \f2\fs20 , \f1\fs18 "X" \f2\fs20 , or \f1\fs18 "Y" \f2\fs20 ) for the implicit chromosome. These legacy chromosome types correspond to the new chromosome types \f1\fs18 "A" \f2\fs20 , \f1\fs18 "X" \f2\fs20 , and \f1\fs18 "-Y" \f2\fs20 respectively (note that it is \f3\i not \f2\i0 \f1\fs18 "Y" \f2\fs20 ), when using \f1\fs18 initializeChromosome() \f2\fs20 . The implicit chromosome\'92s \f1\fs18 id \f2\fs20 property is always \f1\fs18 1 \f2\fs20 . This old style of chromosome configuration is much less flexible, however, allowing only these three chromosome types, and only allowing a single chromosome to be set up. This backward compatibility mode may be removed for SLiM in the future, and should be considered deprecated; new models should call \f1\fs18 initializeChromosome() \f2\fs20 explicitly instead.\ There is no way to disable sex once it has been enabled; if you don\'92t want to have sex, don\'92t call this function. If you require more flexibility with mating types and reproductive strategies than SLiM\'92s built-in support for sex provides, do not call \f1\fs18 initializeSex() \f2\fs20 ; instead, track the sex or mating type of individuals yourself in script (with the \f1\fs18 tag \f2\fs20 property of \f1\fs18 Individual \f2\fs20 , for example), and manage the consequences of that in your script yourself, in terms of which individuals can mate with which, and exactly how the offspring is produced.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f0\b \cf2 The \f5\fs18 xDominanceCoeff \f0\fs20 parameter has been deprecated and removed. \f2\b0 In SLiM 5 and later, use the \f1\fs18 hemizygousDominanceCoeff \f2\fs20 property of \f1\fs18 MutationType \f2\fs20 instead. \cf3 If the \f1\fs18 chromosomeType \f2\fs20 is \f1\fs18 "X" \f2\fs20 , the optional \f1\fs18 xDominanceCoeff \f2\fs20 parameter can supply the dominance coefficient used when a mutation is present in an XY male, and is thus \'93heterozygous\'94 (but in a different sense than the heterozygosity of an XX female with one copy of the mutation).\cf2 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 (void)initializeSLiMModelType(string$\'a0modelType) \f4 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 \expnd0\expndtw0\kerning0 Configure the type of SLiM model used for the simulation. At present, one of two model types may be selected. If \f1\fs18 modelType \f2\fs20 is \f1\fs18 "WF" \f2\fs20 , SLiM will use a Wright-Fisher (WF) model; this is the model type that has always been supported by SLiM, and is the model type used if \f1\fs18 initializeSLiMModelType() \f2\fs20 is not called. If \f1\fs18 modelType \f2\fs20 is \f1\fs18 "nonWF" \f2\fs20 , SLiM will use a non-Wright-Fisher (nonWF) model instead; this is a new model type supported by SLiM 3.0 and above.\ If \f1\fs18 initializeSLiMModelType() \f2\fs20 is called at all then it must be called before any other initialization function, so that SLiM knows from the outset which features are enabled and which are not.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (void)initializeSLiMOptions([logical$\'a0keepPedigrees\'a0=\'a0F], [string$\'a0dimensionality\'a0=\'a0""], [string$\'a0periodicity\'a0=\'a0""], [logical$\'a0doMutationRunExperiments\'a0=\'a0T], [logical$\'a0preventIncidentalSelfing\'a0=\'a0F]\cf2 \expnd0\expndtw0\kerning0 , [logical$\'a0nucleotideBased\'a0=\'a0F], [logical$\'a0randomizeCallbacks\'a0=\'a0T]\kerning1\expnd0\expndtw0 , [logical$\'a0checkInfiniteLoops\'a0=\'a0T]\cf0 ) \f4 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 \expnd0\expndtw0\kerning0 Configure options for the simulation. If \f1\fs18 initializeSLiMOptions() \f2\fs20 is called at all then it must be called before any other initialization function (except \f1\fs18 initializeSLiMModelType() \f2\fs20 ), so that SLiM knows from the outset which optional features are enabled and which are not.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 If \f1\fs18 keepPedigrees \f2\fs20 is \f1\fs18 T \f2\fs20 , SLiM will keep pedigree information for every individual in the simulation, tracking the identity of its parents and grandparents. This allows individuals to assess their degree of pedigree-based relatedness to other individuals (see \f1\fs18 Individual \f2\fs20 \'92s \f1\fs18 relatedness() \f2\fs20 and \f1\fs18 sharedParentCount() \f2\fs20 methods), as well as allowing a model to find \'93trios\'94 (two parents and an offspring they generated) using the pedigree properties of \f1\fs18 Individual \f2\fs20 . As a side effect of \f1\fs18 keepPedigrees \f2\fs20 being \f1\fs18 T \f2\fs20 , the \f1\fs18 pedigreeID \f2\fs20 , \f1\fs18 pedigreeParentIDs \f2\fs20 , and \f1\fs18 pedigreeGrandparentIDs \f2\fs20 properties of \f1\fs18 Individual \f2\fs20 will have defined values, as will the \f1\fs18 haplosomePedigreeID \f2\fs20 property of \f1\fs18 Haplosome \f2\fs20 . Note that pedigree-based relatedness doesn\'92t necessarily correspond to genetic relatedness, due to effects such as assortment and recombination. Beginning in SLiM 3.5, \f1\fs18 keepPedigrees=T \f2\fs20 also enables tracking of individual reproductive output, available through the \f1\fs18 reproductiveOutput \f2\fs20 property of \f1\fs18 Individual \f2\fs20 and the \f1\fs18 lifetimeReproductiveOutput \f2\fs20 property of \f1\fs18 Subpopulation \f2\fs20 .\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf0 If \f1\fs18 dimensionality \f2\fs20 is not \f1\fs18 "" \f2\fs20 , SLiM will enable its optional \'93continuous space\'94 facility. Three values for \f1\fs18 dimensionality \f2\fs20 are presently supported: \f1\fs18 "x" \f2\fs20 , \f1\fs18 "xy" \f2\fs20 , and \f1\fs18 "xyz" \f2\fs20 , specifying that continuous space should be enabled for one, two, or three dimensions, respectively, using ( \f3\i x \f2\i0 ), ( \f3\i x \f2\i0 , \f3\i y \f2\i0 ), and ( \f3\i x \f2\i0 , \f3\i y \f2\i0 , \f3\i z \f2\i0 ) coordinates respectively. This has a number of side effects. First of all, it means that the specified properties of \f1\fs18 Individual \f2\fs20 ( \f1\fs18 x \f2\fs20 , \f1\fs18 y \f2\fs20 , and/or \f1\fs18 z \f2\fs20 ) will be interpreted by SLiM as spatial positions; in particular, SLiMgui will use those properties to display subpopulations spatially. Second, it allows spatial interactions to be defined, evaluated, and queried using \f1\fs18 initializeInteractionType() \f2\fs20 and \f1\fs18 interaction() \f2\fs20 callbacks. And third, it enables the use of any other properties and methods related to continuous space, such as setting the spatial boundaries of subpopulations, which would otherwise raise an error.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 If \f1\fs18 periodicity \f2\fs20 is not \f1\fs18 "" \f2\fs20 , SLiM will designate the specified spatial dimensions as being periodic \'96 wrapping around at the edges of the spatial boundaries of that dimension. This option may only be used if the \f1\fs18 dimensionality \f2\fs20 parameter to \f1\fs18 initializeSLiMOptions() \f2\fs20 has been used to enable spatiality in the model, and only spatial dimensions that were specified in the dimensionality of the model may be declared to be periodic (but if desired, it is permissible to make just a subset of those dimensions periodic; it is not an all-or-none proposition). For example, if the specified dimensionality is \f1\fs18 "xy" \f2\fs20 , the model\'92s periodicity may be \f1\fs18 "x" \f2\fs20 , \f1\fs18 "y" \f2\fs20 , or \f1\fs18 "xy" \f2\fs20 (or \f1\fs18 "" \f2\fs20 , the default, to specify that there are no periodic dimensions). A one-dimensional periodic model would model a space like the perimeter of a circle. A two-dimensional model periodic in one of those dimensions would model a space like a cylinder without its end caps; if periodic in both dimensions, the modeled space is a torus. The shapes of three-dimensional periodic models are harder to visualize, but are essentially higher-dimensional analogues of these concepts. Periodic boundary conditions are commonly used to model spatial scenarios without \'93edge effects\'94, since there are no edges in the periodic spatial dimensions. The \f1\fs18 pointPeriodic() \f2\fs20 method of \f1\fs18 Subpopulation \f2\fs20 is typically used in conjunction with this option, to actually implement the periodic boundary condition for the specified dimensions.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 The \f1\fs18 doMutationRunExperiments \f2\fs20 parameter specifies whether SLiM should attempt to conduct experiments at runtime to determine the optimal number of mutation runs used in the model. This is a performance optimization. If \f1\fs18 doMutationRunExperiments \f2\fs20 is \f1\fs18 T \f2\fs20 (the default), this optimization is enabled for all chromosomes that do not have an explicitly specified mutation run count; this is generally desirable and may significantly improve performance. If \f1\fs18 doMutationRunExperiments \f2\fs20 is \f1\fs18 F \f2\fs20 , this optimization is disabled and chromosomes that do not have an explicitly specified mutation run count will simply use a single mutation run. See the documentation for \f1\fs18 initializeChromosome() \f2\fs20 for further discussion. Note that this parameter used to be \f1\fs18 [integer$\'a0mutationRuns\'a0=\'a00] \f2\fs20 , specifying the mutation run count directly. That parameter has been moved to \f1\fs18 initializeChromosome() \f2\fs20 , allowing a different mutation run count to be specified for each chromosome in multi-chromosome models.\expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf0 \kerning1\expnd0\expndtw0 If \f1\fs18 preventIncidentalSelfing \f2\fs20 is \f1\fs18 T \f2\fs20 , incidental selfing in hermaphroditic models will be prevented by SLiM. By default (i.e., if \f1\fs18 preventIncidentalSelfing \f2\fs20 is \f1\fs18 F \f2\fs20 ), SLiM chooses the first and second parents in a biparental mating event independently. It is therefore possible for the same individual to be chosen as both the first and second parent, resulting in selfing events even when the selfing rate is zero. In many models this is unimportant, since it happens fairly infrequently and does not have large consequences. This behavior is SLiM\'92s default because it is the simplest option, and produces results that most closely align with simple analytical population genetics models. However, in some models this selfing can be undesirable and problematic. In particular, models that involve very high variance in fitness or very small effective population sizes may see elevated rates of selfing that substantially influence model results. If \f1\fs18 preventIncidentalSelfing \f2\fs20 is set to \f1\fs18 T \f2\fs20 , all such incidental selfing will be prevented (by choosing a new second parent if the first parent was chosen again). Non-incidental selfing, as requested by the selfing rate, will still be permitted. Note that if incidental selfing is prevented, SLiM will hang if it is unable to find a different second parent; there must always be at least two individuals in the population with non-zero fitness, and \f1\fs18 mateChoice() \f2\fs20 and \f1\fs18 modifyChild() \f2\fs20 callbacks must not absolutely prevent those two individuals from producing viable offspring. Enforcement of the prohibition on incidental selfing will occur after \f1\fs18 mateChoice() \f2\fs20 callbacks have been called (and thus the default mating weights provided to \f1\fs18 mateChoice() \f2\fs20 callbacks will \f3\i not \f2\i0 exclude the first parent!), but will occur before \f1\fs18 modifyChild() \f2\fs20 callbacks are called (so those callbacks may assume that the first and second parents are distinct).\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \expnd0\expndtw0\kerning0 If \f1\fs18 nucleotideBased \f2\fs20 is \f1\fs18 T \f2\fs20 , the model will be nucleotide-based. In this case, auto-generated mutations (i.e., mutation types used by genomic element types) must be nucleotide-based, and an ancestral nucleotide sequence must be supplied with \f1\fs18 initializeAncestralNucleotides() \f2\fs20 . Non-nucleotide-based mutations may still be used, but may not be referenced by genomic element types. A mutation rate (or rate map) may not be supplied with \f1\fs18 initializeMutationRate() \f2\fs20 ; instead, a hotspot map may (optionally) be supplied with \f1\fs18 initializeHotspotMap() \f2\fs20 . This choice has many consequences across SLiM. \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 \kerning1\expnd0\expndtw0 If \f1\fs18 randomizeCallbacks \f2\fs20 is \f1\fs18 T \f2\fs20 (the default), the order in which individuals are processed in callbacks will be randomized to make it easier to avoid order-dependency bugs. This flag exists because the order of individuals in each subpopulation is non-random; most notably, females always come before males in the individuals vector, but non-random ordering may also occur with respect to things like migrant versus non-migrant status, origin by selfing versus cloning versus biparental mating, and other factors. When this option is \f1\fs18 F \f2\fs20 , individuals in a subpopulation are processed in the order of the individuals vector in each tick cycle stage, which may lead to order-dependency issues if there is an enabled callback whose behavior is not fully independent between calls. Setting this option to \f1\fs18 T \f2\fs20 will cause individuals within each subpopulation to be processed in a randomized order in each tick cycle stage; specifically, this randomizes the order of calls to \f1\fs18 mutationEffect() \f2\fs20 callbacks in both WF and nonWF models, and calls to \f1\fs18 reproduction() \f2\fs20 and \f1\fs18 survival() \f2\fs20 callbacks in nonWF models. Each subpopulation is still processed separately, in sequential order, so order-dependency issues between subpopulations are still possible if callbacks have effects that are not fully independent. This feature was added in SLiM 4, breaking backward compatibility; to recover the behavior of previous versions of SLiM, pass \f1\fs18 F \f2\fs20 for this option (but then be very careful about order-dependency issues in your script). The default of \f1\fs18 T \f2\fs20 is the safe option, but a small speed penalty is incurred by the randomization of the processing order \'96 for most models the difference will be less than 1%, but in the worst case it may approach 10%. Models that do not have any order-dependency issue may therefore run somewhat faster if this is set to \f1\fs18 F \f2\fs20 . Note that anywhere that your script uses the \f1\fs18 individuals \f2\fs20 property of \f1\fs18 Subpopulation \f2\fs20 , the order of individuals returned will be non-random (regardless of the setting of this option); you should use \f1\fs18 sample() \f2\fs20 to shuffle the order of the individuals vector if necessary to avoid order-dependency issues in your script.\ If \f1\fs18 checkInfiniteLoops \f2\fs20 is \f1\fs18 T \f2\fs20 (the default), SLiM and Eidos will check for infinite loops in various circumstances, such as \f1\fs18 while \f2\fs20 and \f1\fs18 do\'96while \f2\fs20 loops. This check is conducted only when running in SLiMgui; at the command line, checks for infinite loops are never conducted regardless of the value of this flag. When checking is enabled, an error will be raised if any loop executes more than 10 million times, preventing SLiMgui\'92s user interface from freezing. Normally this is desirable, but if you actually want to execute a loop more than 10 million times, this checking will prove inconvenient. In that case, you can pass \f1\fs18 F \f2\fs20 for \f1\fs18 checkInfiniteLoops \f2\fs20 to disable these checks. There is no way to turn these checks on or off for individual loops; it is a global setting.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf0 This function will likely be extended with further options in the future, added on to the end of the argument list. Using named arguments with this call is recommended for readability. Note that turning on optional features may increase the runtime and memory footprint of SLiM.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (void)initializeSpecies([integer$\'a0tickModulo\'a0=\'a01], [integer$\'a0tickPhase\'a0=\'a01], [string$\'a0avatar\'a0=\'a0""], [string$\'a0color\'a0=\'a0""])\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Configure options for the species being initialized. This initialization function may only be called in multispecies models (i.e., models with explicit species declarations); in single-species models, the default values are assumed and cannot be changed.\ The \f1\fs18 tickModulo \f2\fs20 and \f1\fs18 tickPhase \f2\fs20 parameters determine the activation schedule for the species. The \f1\fs18 active \f2\fs20 property of the species will be set to \f1\fs18 T \f2\fs20 (thus activating the species) every \f1\fs18 tickModulo \f2\fs20 ticks, beginning in tick \f1\fs18 tickPhase \f2\fs20 . (However, when the species is activated in a given tick, the \f1\fs18 skipTick() \f2\fs20 method may still be called in a \f1\fs18 first() \f2\fs20 event to deactivate it.) See the \f1\fs18 active \f2\fs20 property of \f1\fs18 Species \f2\fs20 for more details.\ The \f1\fs18 avatar \f2\fs20 parameter, if not \f1\fs18 "" \f2\fs20 , sets a \f1\fs18 string \f2\fs20 value used to represent the species graphically, particularly in SLiMgui but perhaps in other contexts also. The \f1\fs18 avatar \f2\fs20 should generally be a single character \'96 usually an emoji corresponding to the species, such as \f1\fs18 " \f6\fs14 \uc0\u55358 \u56714 \f1\fs18 " \f2\fs20 for foxes or \f1\fs18 " \f6\fs14 \uc0\u55357 \u56365 \f1\fs18 " \f2\fs20 for mice. If \f1\fs18 avatar \f2\fs20 is the empty string, \f1\fs18 "" \f2\fs20 , SLiMgui will choose a default avatar.\ The \f1\fs18 color \f2\fs20 parameter, if not \f1\fs18 "" \f2\fs20 , sets a \f1\fs18 string \f2\fs20 color value used to represent the species in SLiMgui. Colors may be specified by name, or with hexadecimal RGB values of the form \f1\fs18 "#RRGGBB" \f2\fs20 (see the Eidos manual for details). If \f1\fs18 color \f2\fs20 is the empty string, \f1\fs18 "" \f2\fs20 , SLiMgui will choose a default color.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \expnd0\expndtw0\kerning0 (void)initializeTreeSeq([logical$\'a0recordMutations\'a0=\'a0T], [Nif$\'a0simplificationRatio\'a0=\'a0NULL], [Ni$\'a0simplificationInterval\'a0=\'a0NULL], [logical$\'a0checkCoalescence\'a0=\'a0F], [logical$\'a0runCrosschecks\'a0=\'a0F], [logical$\'a0\kerning1\expnd0\expndtw0 retainCoalescentOnly\expnd0\expndtw0\kerning0 \'a0=\'a0T]\kerning1\expnd0\expndtw0 , [Ns$\'a0timeUnit\'a0=\'a0NULL]\expnd0\expndtw0\kerning0 ) \f4 \cf0 \kerning1\expnd0\expndtw0 \ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Configure options for tree sequence recording. Calling this function turns on tree sequence recording, as a side effect, for later reconstruction of the simulation\'92s evolutionary dynamics; if you do not want tree sequence recording to be enabled, do not call this function. Note that tree-sequence recording internally uses SLiM\'92s \'93pedigree tracking\'94 feature to uniquely identify individuals and haplosomes; however, if you want to use pedigree tracking in your script you must still enable it yourself with \f1\fs18 initializeSLiMOptions(keepPedigrees=T) \f2\fs20 . A separate tree sequence will be recorded for each chromosome in the simulation, as configured with \f1\fs18 initializeChromosome() \f2\fs20 .\ The \f1\fs18 recordMutations \f2\fs20 flag controls whether information about individual mutations is recorded or not. Such recording takes time and memory, and so can be turned off if only the tree sequence itself is needed, but it is turned on by default since mutation recording is generally useful.\ The \f1\fs18 simplificationRatio \f2\fs20 and \f1\fs18 simplificationInterval \f2\fs20 parameters control how often automatic simplification of the recorded tree sequence occurs. This is a speed\'96memory tradeoff: more frequent simplification (lower \f1\fs18 simplificationRatio \f2\fs20 or smaller \f1\fs18 simplificationInterval \f2\fs20 ) means the stored tree sequences will use less memory, but at a cost of somewhat longer run times. Conversely, a larger \f1\fs18 simplificationRatio \f2\fs20 or \f1\fs18 simplificationInterval \f2\fs20 means that SLiM will wait longer between simplifications. There are three ways these parameters can be used. With the first option, with a non- \f1\fs18 NULL \f2\fs20 \f1\fs18 simplificationRatio \f2\fs20 and a \f1\fs18 NULL \f2\fs20 value for \f1\fs18 simplificationInterval \f2\fs20 , SLiM will try to find an optimal tick interval for simplification such that the ratio of the memory used by the tree sequence tables, (before:after) simplification, is close to the requested ratio. The default of \f1\fs18 10 \f2\fs20 (used if both \f1\fs18 simplificationRatio \f2\fs20 and \f1\fs18 simplificationInterval \f2\fs20 are \f1\fs18 NULL \f2\fs20 ) thus requests that SLiM try to find a tick interval such that the maximum size of the stored tree sequences is ten times the size after simplification. \f1\fs18 INF \f2\fs20 may be supplied to indicate that automatic simplification should never occur; \f1\fs18 0 \f2\fs20 may be supplied to indicate that automatic simplification should be performed at the end of every tick. Alternatively \'96 the second option \'96 \f1\fs18 simplificationRatio \f2\fs20 may be \f1\fs18 NULL \f2\fs20 and \f1\fs18 simplificationInterval \f2\fs20 may be set to the interval, in ticks, between simplifications. This may provide more reliable performance, but the interval must be chosen carefully to avoid exceeding the available memory. The \f1\fs18 simplificationInterval \f2\fs20 value may be a very large number to specify that simplification should never occur (not \f1\fs18 INF \f2\fs20 , though, since it is an \f1\fs18 integer \f2\fs20 value), or \f1\fs18 1 \f2\fs20 to simplify every tick. Finally \'96 the third option \'96 both parameters may be non- \f1\fs18 NULL \f2\fs20 , in which case \f1\fs18 simplificationRatio \f2\fs20 is used as described above, while \f1\fs18 simplificationInterval \f2\fs20 provides the \f3\i initial \f2\i0 interval first used by SLiM (and then subsequently increased or decreased to try to match the requested simplification ratio). The default initial interval, used when \f1\fs18 simplificationInterval \f2\fs20 is \f1\fs18 NULL \f2\fs20 , is usually \f1\fs18 20 \f2\fs20 ; this is chosen to be relatively frequent, and thus unlikely to lead to a memory overflow, but it can result in rather slow spool-up for models where the equilibrium simplification interval, as determined by the simplification ratio, is much longer. It can therefore be helpful to set a larger initial interval so that the early part of the model run is not excessively bogged down in simplification.\ The \f1\fs18 checkCoalescence \f2\fs20 parameter controls whether a check for full coalescence is conducted after each simplification. If a model will call \f1\fs18 treeSeqCoalesced() \f2\fs20 to check for coalescence during its execution, \f1\fs18 checkCoalescence \f2\fs20 should be set to \f1\fs18 T \f2\fs20 . Since the coalescence checks entail a performance penalty, the default of \f1\fs18 F \f2\fs20 is preferable otherwise. See the documentation for \f1\fs18 treeSeqCoalesced() \f2\fs20 for further discussion.\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 The \f1\fs18 runCrosschecks \f2\fs20 parameter controls whether cross-checks between SLiM\'92s internal data structures and the tree-sequence recording data structures will be conducted. These two sets of data structures record much the same thing (mutations in haplosomes), but using completely different representations, so such cross-checks can be useful to confirm that the two data structures do indeed represent the same conceptual state. This slows down the model considerably, however, and would normally be turned on only for debugging purposes, so it is turned off by default.\ \pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0 \cf2 The \f1\fs18 retainCoalescentOnly \f2\fs20 parameter controls how, exactly, simplification of the tree-sequence data is performed in SLiM (both for auto-simplification and for calls to \f1\fs18 treeSeqSimplify() \f2\fs20 ). More specifically, this parameter controls the behavior of simplification for individuals and haplosomes that have been \'93retained\'94 by calling \f1\fs18 treeSeqRememberIndividuals() \f2\fs20 with the parameter \f1\fs18 permanent=F \f2\fs20 . The default of \f1\fs18 retainCoalescentOnly=T \f2\fs20 helps to keep the number of retained individuals relatively small, which is helpful if your simulation regularly flags many individuals for retaining. In this case, changing \f1\fs18 retainCoalescentOnly \f2\fs20 to \f1\fs18 F \f2\fs20 may dramatically increase memory usage and runtime, in a similar way to permanently remembering all the individuals. See the documentation of \f1\fs18 treeSeqRememberIndividuals() \f2\fs20 for further discussion.\ The \f1\fs18 timeUnit \f2\fs20 parameter controls the time unit stated in the tree sequence when it is saved (which can be accessed through \f1\fs18 tskit \f2\fs20 APIs); it has no effect on the running simulation whatsoever. The default value, \f1\fs18 NULL \f2\fs20 , means that a time unit of \f1\fs18 "ticks" \f2\fs20 will be used for all model types. (In SLiM 3.7 / 3.7.1, \f1\fs18 NULL \f2\fs20 implied a time unit of \f1\fs18 "generations" \f2\fs20 for WF models, but \f1\fs18 "ticks" \f2\fs20 for nonWF models; given the new multispecies timescale parameters in SLiM 4, a default of \f1\fs18 "ticks" \f2\fs20 makes sense in all cases since now even in WF models one tick might not equal one biological generation.) It may be helpful to set \f1\fs18 timeUnit \f2\fs20 to \f1\fs18 "generations" \f2\fs20 explicitly when modeling non-overlapping generations in which one tick equals one generation, to tell \f1\fs18 tskit \f2\fs20 that the time unit does in fact represent biological generations; doing so may avoid warnings from \f1\fs18 tskit \f2\fs20 or \f1\fs18 msprime \f2\fs20 regarding the time unit, in cases such as recapitation where the simulation timescale is important.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.2. Nucleotide utilities\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\b0\fs18 \cf2 \expnd0\expndtw0\kerning0 (is)codonsToAminoAcids(integer\'a0codons, [li$\'a0long\'a0=\'a0F], [logical$\'a0paste\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 \kerning1\expnd0\expndtw0 Returns the amino acid sequence corresponding to the codon sequence in \f1\fs18 codons \f2\fs20 . Codons should be represented with values in [ \f1\fs18 0 \f2\fs20 , \f1\fs18 63 \f2\fs20 ] where AAA is \f1\fs18 0 \f2\fs20 , AAC is \f1\fs18 1 \f2\fs20 , AAG is \f1\fs18 2 \f2\fs20 , and TTT is \f1\fs18 63 \f2\fs20 ; see \f1\fs18 ancestralNucleotides() \f2\fs20 for discussion of this encoding. If \f1\fs18 long \f2\fs20 is \f1\fs18 F \f2\fs20 (the default), the standard single-letter codes for amino acids will be used (where Serine is \f1\fs18 "S" \f2\fs20 , etc.); if \f1\fs18 long \f2\fs20 is \f1\fs18 T \f2\fs20 , the standard three-letter codes will be used instead (where Serine is \f1\fs18 "Ser" \f2\fs20 , etc.). Beginning in SLiM 3.5, if \f1\fs18 long \f2\fs20 is \f1\fs18 0 \f2\fs20 , \f1\fs18 integer \f2\fs20 codes will be used as follows (and \f1\fs18 paste \f2\fs20 will be ignored):\ \pard\tx3780\pardeftab720\li1080\sa60\partightenfactor0 \cf2 stop (TAA, TAG, TGA) \f1\fs18 0 \f2\fs20 \uc0\u8232 Alanine \f1\fs18 1 \f2\fs20 \uc0\u8232 Arginine \f1\fs18 2 \f2\fs20 \uc0\u8232 Asparagine \f1\fs18 3 \f2\fs20 \uc0\u8232 Aspartic acid (Aspartate) \f1\fs18 4 \f2\fs20 \uc0\u8232 Cysteine \f1\fs18 5 \f2\fs20 \uc0\u8232 Glutamine \f1\fs18 6 \f2\fs20 \uc0\u8232 Glutamic acid (Glutamate) \f1\fs18 7 \f2\fs20 \uc0\u8232 Glycine \f1\fs18 8 \f2\fs20 \uc0\u8232 Histidine \f1\fs18 9 \f2\fs20 \uc0\u8232 Isoleucine \f1\fs18 10 \f2\fs20 \uc0\u8232 Leucine \f1\fs18 11 \f2\fs20 \uc0\u8232 Lysine \f1\fs18 12 \f2\fs20 \uc0\u8232 Methionine \f1\fs18 13 \f2\fs20 \uc0\u8232 Phenylalanine \f1\fs18 14 \f2\fs20 \uc0\u8232 Proline \f1\fs18 15 \f2\fs20 \uc0\u8232 Serine \f1\fs18 16 \f2\fs20 \uc0\u8232 Threonine \f1\fs18 17 \f2\fs20 \uc0\u8232 Tryptophan \f1\fs18 18 \f2\fs20 \uc0\u8232 Tyrosine \f1\fs18 19 \f2\fs20 \uc0\u8232 Valine \f1\fs18 20 \f2\fs20 \ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \cf2 There does not seem to be a widely used standard for integer coding of amino acids, so SLiM just numbers them alphabetically, making stop codons \f1\fs18 0 \f2\fs20 . If you want a different coding, you can make your own 64-element vector and use it to convert codons to whatever integer codes you need. Other \f1\fs18 integer \f2\fs20 values of \f1\fs18 long \f2\fs20 are reserved for future use (to support other codings), and will currently produce an error.\ When \f1\fs18 long \f2\fs20 is \f1\fs18 T \f2\fs20 or \f1\fs18 F \f2\fs20 and \f1\fs18 paste \f2\fs20 is \f1\fs18 T \f2\fs20 (the default), the amino acid sequence returned will be a singleton \f1\fs18 string \f2\fs20 , such as \f1\fs18 "LYATI" \f2\fs20 (when \f1\fs18 long \f2\fs20 is \f1\fs18 F \f2\fs20 ) or \f1\fs18 "Leu-Tyr-Ala-Thr-Ile" \f2\fs20 (when \f1\fs18 long \f2\fs20 is \f1\fs18 T \f2\fs20 ). When \f1\fs18 long \f2\fs20 is \f1\fs18 T \f2\fs20 or \f1\fs18 F \f2\fs20 and \f1\fs18 paste \f2\fs20 is \f1\fs18 F \f2\fs20 , the amino acid sequence will instead be returned as a \f1\fs18 string \f2\fs20 vector, with one element per amino acid, such as \f1\fs18 "L" "Y" "A" "T" "I" \f2\fs20 (when \f1\fs18 long \f2\fs20 is \f1\fs18 F \f2\fs20 ) or \f1\fs18 "Leu" "Tyr" "Ala" "Thr" "Ile" \f2\fs20 (when \f1\fs18 long \f2\fs20 is \f1\fs18 T \f2\fs20 ). Using the \f1\fs18 paste=T \f2\fs20 option is considerably faster than using \f1\fs18 paste() \f2\fs20 in script.\expnd0\expndtw0\kerning0 \ This function interprets the supplied codon sequence as the \f3\i sense \f2\i0 strand (i.e., the strand that is \f3\i not \f2\i0 transcribed, and which mirrors the mRNA\'92s sequence). This uses the standard DNA codon table directly. For example, if the nucleotide sequence is CAA TTC, that will correspond to a codon vector of \f1\fs18 16 61 \f2\fs20 , and will result in the amino acid sequence Gln-Phe ( \f1\fs18 "QF" \f2\fs20 ).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (is)codonsToNucleotides(integer\'a0codons, [string$\'a0format\'a0=\'a0"string"])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Returns the nucleotide sequence corresponding to the codon sequence supplied in \f1\fs18 codons \f2\fs20 . Codons should be represented with values in [ \f1\fs18 0 \f2\fs20 , \f1\fs18 63 \f2\fs20 ] where AAA is \f1\fs18 0 \f2\fs20 , AAC is \f1\fs18 1 \f2\fs20 , AAG is \f1\fs18 2 \f2\fs20 , and TTT is \f1\fs18 63 \f2\fs20 ; see \f1\fs18 ancestralNucleotides() \f2\fs20 for discussion of this encoding.\ The \f1\fs18 format \f2\fs20 parameter controls the format of the returned sequence. It may be \f1\fs18 "string" \f2\fs20 to obtain the sequence as a singleton \f1\fs18 string \f2\fs20 (e.g., \f1\fs18 "TATACG" \f2\fs20 ), \f1\fs18 "char" \f2\fs20 to obtain it as a \f1\fs18 string \f2\fs20 vector of single characters (e.g., \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 , \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 , \f1\fs18 "C" \f2\fs20 , \f1\fs18 "G" \f2\fs20 ), or \f1\fs18 "integer" \f2\fs20 to obtain it as an \f1\fs18 integer \f2\fs20 vector (e.g., \f1\fs18 3 \f2\fs20 , \f1\fs18 0 \f2\fs20 , \f1\fs18 3 \f2\fs20 , \f1\fs18 0 \f2\fs20 , \f1\fs18 1 \f2\fs20 , \f1\fs18 2 \f2\fs20 ), using SLiM\'92s standard code of A= \f1\fs18 0 \f2\fs20 , C= \f1\fs18 1 \f2\fs20 , G= \f1\fs18 2 \f2\fs20 , T= \f1\fs18 3 \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)mm16To256(float\'a0mutationMatrix16)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Returns a 64\'d74 mutation matrix that is functionally identical to the supplied 4\'d74 mutation matrix in \f1\fs18 mutationMatrix16 \f2\fs20 . The mutation rate for each of the 64 trinucleotides will depend only upon the central nucleotide of the trinucleotide, and will be taken from the corresponding entry for the same nucleotide in \f1\fs18 mutationMatrix16 \f2\fs20 . This function can be used to easily construct a simple trinucleotide-based mutation matrix which can then be modified so that specific trinucleotides sustain a mutation rate that does not depend only upon their central nucleotide.\ See the documentation for \f1\fs18 initializeGenomicElementType() \f2\fs20 for further discussion of how these 64\'d74 mutation matrices are interpreted and used.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)mmJukesCantor(float$\'a0alpha)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Returns a mutation matrix representing a Jukes\'96Cantor (1969) model with mutation rate \f1\fs18 alpha \f2\fs20 to each possible alternative nucleotide at a site. This 2\'d72 matrix is suitable for use with \f1\fs18 initializeGenomicElementType() \f2\fs20 . Note that the actual mutation rate produced by this matrix is \f1\fs18 3*alpha \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)mmKimura(float$\'a0alpha, float$\'a0beta)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Returns a mutation matrix representing a Kimura (1980) model with transition rate \f1\fs18 alpha \f2\fs20 and transversion rate \f1\fs18 beta \f2\fs20 . This 2\'d72 matrix is suitable for use with \f1\fs18 initializeGenomicElementType() \f2\fs20 . Note that the actual mutation rate produced by this model is \f1\fs18 alpha+2*beta \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)nucleotideCounts(is\'a0sequence)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 A convenience function that returns an \f1\fs18 integer \f2\fs20 vector of length four, providing the number of occurrences of A / C / G / T nucleotides, respectively, in the supplied nucleotide sequence. The parameter sequence may be a singleton \f1\fs18 string \f2\fs20 (e.g., \f1\fs18 "TATA" \f2\fs20 ), a \f1\fs18 string \f2\fs20 vector of single characters (e.g., \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 , \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 ), or an \f1\fs18 integer \f2\fs20 vector (e.g., 3, \f1\fs18 0 \f2\fs20 , \f1\fs18 3 \f2\fs20 , \f1\fs18 0 \f2\fs20 ), using SLiM\'92s standard code of A= \f1\fs18 0 \f2\fs20 , C= \f1\fs18 1 \f2\fs20 , G= \f1\fs18 2 \f2\fs20 , T= \f1\fs18 3 \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)nucleotideFrequencies(is\'a0sequence)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 A convenience function that returns a \f1\fs18 float \f2\fs20 vector of length four, providing the frequencies of occurrences of A / C / G / T nucleotides, respectively, in the supplied nucleotide sequence. The parameter sequence may be a singleton \f1\fs18 string \f2\fs20 (e.g., \f1\fs18 "TATA" \f2\fs20 ), a \f1\fs18 string \f2\fs20 vector of single characters (e.g., \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 , \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 ), or an \f1\fs18 integer \f2\fs20 vector (e.g., 3, \f1\fs18 0 \f2\fs20 , \f1\fs18 3 \f2\fs20 , \f1\fs18 0 \f2\fs20 ), using SLiM\'92s standard code of A= \f1\fs18 0 \f2\fs20 , C= \f1\fs18 1 \f2\fs20 , G= \f1\fs18 2 \f2\fs20 , T= \f1\fs18 3 \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (integer)nucleotidesToCodons(is\'a0sequence)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Returns the codon sequence corresponding to the nucleotide sequence in \f1\fs18 sequence \f2\fs20 . The codon sequence is an \f1\fs18 integer \f2\fs20 vector with values from \f1\fs18 0 \f2\fs20 to \f1\fs18 63 \f2\fs20 , based upon successive nucleotide triplets in the nucleotide sequence. The codon value for a given nucleotide triplet XYZ is 16X\'a0+\'a04Y\'a0+\'a0Z, where X, Y, and Z have the usual values A= \f1\fs18 0 \f2\fs20 , C= \f1\fs18 1 \f2\fs20 , G= \f1\fs18 2 \f2\fs20 , T= \f1\fs18 3 \f2\fs20 . For example, the triplet AAA has a codon value of \f1\fs18 0 \f2\fs20 , AAC is \f1\fs18 1 \f2\fs20 , AAG is \f1\fs18 2 \f2\fs20 , AAT is \f1\fs18 3 \f2\fs20 , ACA is \f1\fs18 4 \f2\fs20 , and on upward to TTT which is \f1\fs18 63 \f2\fs20 . If the nucleotide sequence AACACATTT is passed in, the codon vector \f1\fs18 1 4 63 \f2\fs20 will therefore be returned. These codon values can be useful in themselves; they can also be passed to \f1\fs18 codonsToAminoAcids() \f2\fs20 to translate them into the corresponding amino acid sequence if desired.\ The nucleotide sequence in \f1\fs18 sequence \f2\fs20 may be supplied in any of three formats: a \f1\fs18 string \f2\fs20 vector with single-letter nucleotides (e.g., \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 , \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 ), a singleton \f1\fs18 string \f2\fs20 of nucleotide letters (e.g., \f1\fs18 "TATA" \f2\fs20 ), or an \f1\fs18 integer \f2\fs20 vector of nucleotide values (e.g., \f1\fs18 3 \f2\fs20 , \f1\fs18 0 \f2\fs20 , \f1\fs18 3 \f2\fs20 , \f1\fs18 0 \f2\fs20 ) using SLiM\'92s standard code of A= \f1\fs18 0 \f2\fs20 , C= \f1\fs18 1 \f2\fs20 , G= \f1\fs18 2 \f2\fs20 , T= \f1\fs18 3 \f2\fs20 . If the choice of format is not driven by other considerations, such as ease of manipulation, then the singleton \f1\fs18 string \f2\fs20 format will certainly be the most memory-efficient for long sequences, and will probably also be the fastest. The nucleotide sequence provided must be a multiple of three in length, so that it translates to an integral number of codons.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (is)randomNucleotides(integer$\'a0length, [Nif\'a0basis\'a0=\'a0NULL], [string$\'a0format\'a0=\'a0"string"])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 \expnd0\expndtw0\kerning0 Generates a new random nucleotide sequence with \f1\fs18 length \f2\fs20 bases. The four nucleotides ACGT are equally probable if \f1\fs18 basis \f2\fs20 is \f1\fs18 NULL \f2\fs20 (the default); otherwise, \f1\fs18 basis \f2\fs20 may be a 4-element \f1\fs18 integer \f2\fs20 or \f1\fs18 float \f2\fs20 vector providing relative fractions for A, C, G, and T respectively (these need not sum to \f1\fs18 1.0 \f2\fs20 , as they will be normalized). More complex generative models such as Markov processes are not supported intrinsically in SLiM at this time, but arbitrary generated sequences may always be loaded from files on disk.\ The \f1\fs18 format \f2\fs20 parameter controls the format of the returned sequence. It may be \f1\fs18 "string" \f2\fs20 to obtain the generated sequence as a singleton \f1\fs18 string \f2\fs20 (e.g., \f1\fs18 "TATA" \f2\fs20 ), \f1\fs18 "char" \f2\fs20 to obtain it as a \f1\fs18 string \f2\fs20 vector of single characters (e.g., \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 , \f1\fs18 "T" \f2\fs20 , \f1\fs18 "A" \f2\fs20 ), or \f1\fs18 "integer" \f2\fs20 to obtain it as an \f1\fs18 integer \f2\fs20 vector (e.g., \f1\fs18 3 \f2\fs20 , \f1\fs18 0 \f2\fs20 , \f1\fs18 3, 0 \f2\fs20 ), using SLiM\'92s standard code of A= \f1\fs18 0 \f2\fs20 , C= \f1\fs18 1 \f2\fs20 , G= \f1\fs18 2 \f2\fs20 , T= \f1\fs18 3 \f2\fs20 . For passing directly to \f1\fs18 initializeAncestralNucleotides() \f2\fs20 , format \f1\fs18 "string" \f2\fs20 (a singleton string) will certainly be the most memory-efficient, and probably also the fastest. Memory efficiency can be a significant consideration; the nucleotide sequence for a chromosome of length 10 \fs13\fsmilli6667 \super 9 \fs20 \nosupersub will occupy approximately 1 GB of memory when stored as a singleton string (with one byte per nucleotide), and much more if stored in the other formats. However, the other formats can be easier to work with in Eidos, and so may be preferable for relatively short chromosomes if you are manipulating the generated sequence.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 \kerning1\expnd0\expndtw0 3.3. Population genetics utilities \f2\b0\fs20 \cf2 \expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (float$)calcDxy(object\'a0haplosomes1, object\'a0haplosomes2, [No\'a0muts\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL], [logical$\'a0normalize\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the estimated \f3\i D \f2\i0\fs13\fsmilli6667 \sub xy \fs20 \nosupersub between two \f1\fs18 Haplosome \f2\fs20 vectors for the set of mutations given in \f1\fs18 muts \f2\fs20 . \f3\i D \f2\i0\fs13\fsmilli6667 \sub xy \fs20 \nosupersub is the expected number of differences between two sequences, typically drawn from two different subpopulations whose haplosomes are given in \f1\fs18 haplosomes1 \f2\fs20 and \f1\fs18 haplosomes2 \f2\fs20 . It is therefore a metric of genetic divergence, comparable in some respects to \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub ; see Cruickshank and Hahn (2014, Molecular Ecology) for a discussion of \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub versus \f3\i D \f2\i0\fs13\fsmilli6667 \sub xy \fs20 \nosupersub . This method implements \f3\i D \f2\i0\fs13\fsmilli6667 \sub xy \fs20 \nosupersub as defined by Nei (1987) in Molecular Evolutionary Genomics (eq. 10.20), with optimizations for computational efficiency based upon an assumption that that multiallelic loci are rare (this is compatible with the infinite-sites model).\ The calculation can be narrowed to apply to only a window \'96 a subrange of the full haplosomes \'96 by passing the interval bounds [ \f1\fs18 start \f2\fs20 , \f1\fs18 end \f2\fs20 ] for the desired window. In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window. The default behavior, with \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 of \f1\fs18 NULL \f2\fs20 , provides the haplosome-wide \f3\i D \f2\i0\fs13\fsmilli6667 \sub xy \fs20 \nosupersub .\ If \f1\fs18 normalize \f2\fs20 is \f1\fs18 F \f2\fs20 (the default), the returned \f1\fs18 float \f2\fs20 value is simply the expected number of differences, following Nei. Often, however, it will be desirable to normalize that value by dividing by the length of the sequence considered, yielding the expected number of differences \f3\i per site \f2\i0 , a metric that then does not depend upon the sequence length; passing \f1\fs18 normalize=T \f2\fs20 will return that normalized value, and that is probably what most users of this function will want.\ The implementation of \f1\fs18 calcDxy() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , treats every mutation in \f1\fs18 muts \f2\fs20 as independent in its calculations (similar to \f1\fs18 calcPi() \f2\fs20 ); in other words, if mutations are stacked, the \f3\i D \f2\i0\fs13\fsmilli6667 \sub xy \fs20 \nosupersub value calculated is \f3\i by mutation \f2\i0 , not \f3\i by site \f2\i0 . Similarly, if multiple \f1\fs18 Mutation \f2\fs20 objects exist in different haplosomes at the same site (whether representing different genetic states, or multiple mutational lineages for the same genetic state), each \f1\fs18 Mutation \f2\fs20 object is treated separately for purposes of the calculation, just as if they were at different sites. One could regard these choices as embodying an infinite-sites interpretation of the segregating mutations. In most biologically realistic models, such genetic states will be quite rare, and so the impact of these choices will be negligible; however, in some models these distinctions may be important. See \f1\fs18 calcPairHeterozygosity() \f2\fs20 for further discussion.\ All haplosomes and mutations must be associated with the same chromosome. If \f1\fs18 muts \f2\fs20 is \f1\fs18 NULL \f2\fs20 (the default), all mutations in the population associated with the same chromosome as the given haplosomes will be used.\ This function was written by Vitor Sudbrack (currently affiliated with University of Lausanne).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcFST(object\'a0haplosomes1, object\'a0haplosomes2, [No\'a0muts\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub between two \f1\fs18 Haplosome \f2\fs20 vectors \'96 typically, but not necessarily, the haplosomes that constitute two different subpopulations (which we will assume for the purposes of this discussion). In general, higher \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub indicates greater genetic divergence between subpopulations. The haplosomes may be associated with more than one chromosome, in a multi-chromosome model; if so, \f1\fs18 haplosomes1 \f2\fs20 and \f1\fs18 haplosomes2 \f2\fs20 must be associated with the same set of chromosomes, defining the focal set of chromosomes for the calculation.\ The calculation is done using only the mutations in \f1\fs18 muts \f2\fs20 ; if \f1\fs18 muts \f2\fs20 is \f1\fs18 NULL \f2\fs20 , all mutations associated with the focal chromosomes are used. The \f1\fs18 muts \f2\fs20 parameter can be used to calculate the \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub only for a particular mutation type (by passing only mutations of that type), for example; it can focus the calculation on particular mutations of interest. The mutations in \f1\fs18 muts \f2\fs20 must always be associated with the focal chromosomes.\ If there is a single focal chromosome, the calculation can be narrowed to apply to only a window \'96 a subrange of the focal chromosome \'96 by passing the interval bounds [ \f1\fs18 start \f2\fs20 , \f1\fs18 end \f2\fs20 ] for the desired window. In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window. The default behavior, with \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 of \f1\fs18 NULL \f2\fs20 , provides the chromosome-wide \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub , which is often used to assess the overall level of genetic divergence between sister species or allopatric subpopulations.\ The code for \f1\fs18 calcFST() \f2\fs20 is, roughly, an Eidos implementation of Wright\'92s definition of \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub (but see below for further discussion and clarification):\ \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \expnd0\expndtw0\kerning0 \nosupersub = 1 - \f3\i H \f2\i0\fs13\fsmilli6667 \kerning1\expnd0\expndtw0 \sub S \fs20 \expnd0\expndtw0\kerning0 \nosupersub / \f3\i H \f2\i0\fs13\fsmilli6667 \kerning1\expnd0\expndtw0 \sub T \fs20 \expnd0\expndtw0\kerning0 \nosupersub \ \kerning1\expnd0\expndtw0 where \f3\i H \fs13\fsmilli6667 \sub S \f2\i0\fs20 \nosupersub is the average heterozygosity in the two subpopulations, and \f3\i H \fs13\fsmilli6667 \sub T \f2\i0\fs20 \nosupersub is the total heterozygosity when both subpopulations are combined. In this implementation, the two haplosome vectors are weighted equally, not weighted by their size. In SLiM 3, the implementation followed Wright\'92s definition closely, and returned the \f3\i average of ratios \f2\i0 : \f1\fs18 mean(1.0 - H_s/H_t) \f2\fs20 , in the Eidos code. In SLiM 4, it returns the \f3\i ratio of averages \f2\i0 instead: \f1\fs18 1.0 - mean(H_s)/mean(H_t) \f2\fs20 . In other words, the \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub value reported by SLiM 4 is an average across the specified mutations in the two sets of haplosomes, where \f1\fs18 H_s \f2\fs20 and \f1\fs18 H_t \f2\fs20 are first averaged across all specified mutations prior to taking the ratio of the two. This ratio of averages is less biased than the average of ratios, and and is generally considered to be best practice (see, e.g., Bhatia et al., 2013). This means that the behavior of \f1\fs18 calcFST() \f2\fs20 differs between SLiM 3 and SLiM 4.\ As can be seen from its equation, the \f3\i F \f2\i0\fs13\fsmilli6667 \sub ST \fs20 \nosupersub is undefined if \f3\i H \fs13\fsmilli6667 \sub T \f2\i0\fs20 \nosupersub is zero, which occurs if no mutations are present in the haplosomes provided (given the optionally specified window and set of mutations). In that case, \f1\fs18 calcFST() \f2\fs20 will return \f1\fs18 NAN \f2\fs20 . It is up to the caller to detect this with \f1\fs18 isNAN() \f2\fs20 and handle it as necessary.\ The implementation of \f1\fs18 calcFST() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , treats every mutation in \f1\fs18 muts \f2\fs20 as independent in the heterozygosity calculations; in other words, if mutations are stacked, the heterozygosity calculated is \f3\i by mutation \f2\i0 , not \f3\i by site \f2\i0 . Similarly, if multiple \f1\fs18 Mutation \f2\fs20 objects exist in different haplosomes at the same site (whether representing different genetic states, or multiple mutational lineages for the same genetic state), each \f1\fs18 Mutation \f2\fs20 object is treated separately for purposes of the heterozygosity calculation, just as if they were at different sites. One could regard these choices as embodying an infinite-sites interpretation of the segregating mutations. In most biologically realistic models, such genetic states will be quite rare, and so the impact of these choices will be negligible; however, in some models these distinctions may be important.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcHeterozygosity(object\'a0haplosomes, [No\'a0muts\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the heterozygosity for a vector of haplosomes (containing at least one element), based upon the frequencies of mutations in the haplosomes. The result is the \f3\i expected \f2\i0 heterozygosity, for the individuals to which the haplosomes belong, assuming that they are under Hardy-Weinberg equilibrium; this can be compared to the \f3\i observed \f2\i0 heterozygosity of an individual, as calculated by \f1\fs18 calcPairHeterozygosity() \f2\fs20 . Often \f1\fs18 haplosomes \f2\fs20 will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used. By default, with \f1\fs18 muts=NULL \f2\fs20 , the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for \f1\fs18 muts \f2\fs20 .\ In multi-chromosome models, all of the haplosomes and mutations passed in \f1\fs18 haplosomes \f2\fs20 and \f1\fs18 muts \f2\fs20 must all be associated with the same single chromosome. If you wish to calculate heterozygosity across multiple chromosomes, you can simply write a \f1\fs18 for \f2\fs20 loop that calculates it for each chromosome and combines the results; but it is not entirely clear how to weight the chromosomes to produce a single number, especially when sex chromosomes and other chromosomes of variable ploidy might be represented in \f1\fs18 haplosomes \f2\fs20 , so it is not done automatically by this function.\ The calculation can be narrowed to apply to only a window \'96 a subrange of the full chromosome \'96 by passing the interval bounds [ \f1\fs18 start \f2\fs20 , \f1\fs18 end \f2\fs20 ] for the desired window. In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window. The default behavior, with \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 of \f1\fs18 NULL \f2\fs20 , provides the haplosome-wide heterozygosity.\ The implementation of \f1\fs18 calcHeterozygosity() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , treats every mutation as independent in the heterozygosity calculations. One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations. In most biologically realistic models, such genetic states will be quite rare, and so the impact of this choice will be negligible; however, in some models this distinction may be important. See \f1\fs18 calcPairHeterozygosity() \f2\fs20 for further discussion.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcInbreedingLoad(object\'a0haplosomes, [Nio$\'a0mutType\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates inbreeding load (the haploid number of lethal equivalents, or \f3\i B \f2\i0 ) for a vector of haplosomes (containing at least one element) passed in \f1\fs18 haplosomes \f2\fs20 . The calculation can be limited to a focal mutation type passed in \f1\fs18 mutType \f2\fs20 (which may be either an \f1\fs18 integer \f2\fs20 representing the ID of the desired mutation type, or a \f1\fs18 MutationType \f2\fs20 object specified directly); if \f1\fs18 mutType \f2\fs20 is \f1\fs18 NULL \f2\fs20 (the default), all of the mutations for the focal species will be considered. In any case, only deleterious mutations (those with a negative selection coefficient) will be included in the final calculation.\ The inbreeding load is a measure of the quantity of recessive deleterious variation that is heterozygous in a population and can contribute to fitness declines under inbreeding. This function implements the following equation from Morton et al. (1956), which assumes no epistasis and random mating:\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f3\i\fs22 \cf2 B \f2\i0 = sum( \f3\i qs \f2\i0 ) \uc0\u8722 sum( \f3\i q \f2\i0\fs14\fsmilli7333 \super 2 \f3\i\fs22 \nosupersub s \f2\i0 ) \uc0\u8722 2sum( \f3\i q \f2\i0 (1\uc0\u8722 \f3\i q \f2\i0 ) \f3\i sh \f2\i0 )\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \fs20 \cf2 where \f3\i q \f2\i0 is the frequency of a given deleterious allele, \f3\i s \f2\i0 is the absolute value of the selection coefficient, and \f3\i h \f2\i0 is its dominance coefficient. Note that the implementation, viewable with \f1\fs18 functionSource() \f2\fs20 , sets a maximum | \f3\i s \f2\i0 | of \f1\fs18 1.0 \f2\fs20 (i.e., a lethal allele); | \f3\i s \f2\i0 | can sometimes be greater than \f1\fs18 1.0 \f2\fs20 when \f3\i s \f2\i0 is drawn from a distribution, but in practice an allele with \f3\i s \f2\i0 \'a0<\'a0 \f1\fs18 -1.0 \f2\fs20 has the same lethal effect as when \f3\i s \f2\i0 \'a0=\'a0 \f1\fs18 -1.0 \f2\fs20 . Also note that this implementation will not work when the model changes the dominance coefficients of mutations using \f1\fs18 mutationEffect() \f2\fs20 callbacks, since it relies on the \f1\fs18 dominanceCoeff \f2\fs20 property of \f1\fs18 MutationType \f2\fs20 . Finally, note that, to estimate the diploid number of lethal equivalents (2 \f3\i B \f2\i0 ), the result from this function can simply be multiplied by two.\ This function was contributed by Chris Kyriazis; thanks, Chris!\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)calcLD_D(object$\'a0mut1, [No\'a0mut2\'a0=\'a0NULL], [No\'a0haplosomes\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the linkage disequilibrium (LD) coefficient \f3\i D \f2\i0 between a focal mutation \f1\fs18 mut1 \f2\fs20 and one or more mutations in \f1\fs18 mut2 \f2\fs20 , evaluated across a set of haplosomes given by \f1\fs18 haplosomes \f2\fs20 . The result is a \f1\fs18 float \f2\fs20 vector that matches the size and order of \f1\fs18 mut2 \f2\fs20 . The implementation of this function, viewable with \f1\fs18 functionSource() \f2\fs20 , calculates \f3\i D \f2\i0 as defined by Hill and Robertson (1968, p. 226). The coefficient \f3\i D \f2\i0 is within [\uc0\u8722 \f3\i p \f2\i0 (1\uc0\u8722 \f3\i p \f2\i0 ), \f3\i p \f2\i0 (1\uc0\u8722 \f3\i p \f2\i0 )], where \f3\i p \f2\i0 is the frequency of the more common mutation (that is, \f3\i p \f2\i0 \'a0=\'a0max( \f3\i f \f2\i0\fs13\fsmilli6667 \sub 1 \fs20 \nosupersub , \f3\i f \f2\i0\fs13\fsmilli6667 \sub 2 \fs20 \nosupersub ) where \f3\i f \f2\i0\fs13\fsmilli6667 \sub 1 \fs20 \nosupersub and \f3\i f \f2\i0\fs13\fsmilli6667 \sub 2 \fs20 \nosupersub are the frequencies of the two mutations for which \f3\i D \f2\i0 is being calculated); for the normalized LD metric \f3\i r \f2\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub , which is within [0, 1], see \f1\fs18 calcLD_Rsquared() \f2\fs20 . Departures of \f3\i D \f2\i0 from zero indicate LD; more specifically, \f3\i D \f2\i0 \'a0>\'a00 indicates that the mutations occur together more often than expected by chance (positive linkage), whereas \f3\i D \f2\i0 \'a0<\'a00 indicates they occur together less often than expected by chance (negative linkage).\ All mutations in \f1\fs18 mut2 \f2\fs20 must be associated with the same chromosome as \f1\fs18 mut1 \f2\fs20 ; this function does not currently calculate LD between mutations associated with different chromosomes. If \f1\fs18 mut2 \f2\fs20 is \f1\fs18 NULL \f2\fs20 (the default), all such mutations in the population (including \f1\fs18 mut1 \f2\fs20 itself) will be used. Similarly, all haplosomes must be associated with the same chromosome as \f1\fs18 mut1 \f2\fs20 . If the \f1\fs18 haplosomes \f2\fs20 parameter is \f1\fs18 NULL \f2\fs20 (the default), all such haplosomes in the population will be used.\ This function was written by Vitor Sudbrack (currently affiliated with University of Lausanne).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float)calcLD_Rsquared(object$\'a0mut1, [No\'a0mut2\'a0=\'a0NULL], [No\'a0haplosomes\'a0=\'a0NULL], [logical$\'a0squared\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the linkage disequilibrium (LD) squared correlation coefficient \f3\i r \f2\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub between a focal mutation \f1\fs18 mut1 \f2\fs20 and one or more mutations in \f1\fs18 mut2 \f2\fs20 , evaluated across a set of haplosomes given by \f1\fs18 haplosomes \f2\fs20 . The result is a \f1\fs18 float \f2\fs20 vector that matches the size and order of \f1\fs18 mut2 \f2\fs20 . The implementation of this function, viewable with \f1\fs18 functionSource() \f2\fs20 , calculates \f3\i r \f2\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub as defined by Hill and Robertson (1968, p. 227). The squared correlation coefficient \f3\i r \f2\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub is a normalized measure of LD within [0, 1] (for the unnormalized LD coefficient \f3\i D \f2\i0 , see \f1\fs18 calcLD_D() \f2\fs20 ). When \f3\i r \f2\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub \'a0=\'a00, there is no statistical association between the mutations; they co-occur as expected by chance. A value of \f3\i r \f2\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub \'a0=\'a01 indicates complete correlation: the mutations either always appear together or never appear together, depending on the sign of the underlying correlation coefficient \f3\i r \f2\i0 . To obtain the raw (signed) \f3\i r \f2\i0 value instead of \f3\i r \f2\i0\fs13\fsmilli6667 \super 2 \fs20 \nosupersub , you can pass \f1\fs18 squared=F \f2\fs20 instead of the default of \f1\fs18 T \f2\fs20 .\ All mutations in \f1\fs18 mut2 \f2\fs20 must be associated with the same chromosome as \f1\fs18 mut1 \f2\fs20 ; this function does not currently calculate LD between mutations associated with different chromosomes. If \f1\fs18 mut2 \f2\fs20 is \f1\fs18 NULL \f2\fs20 (the default), all such mutations in the population (including \f1\fs18 mut1 \f2\fs20 itself) will be used. Similarly, all haplosomes must be associated with the same chromosome as \f1\fs18 mut1 \f2\fs20 . If the \f1\fs18 haplosomes \f2\fs20 parameter is \f1\fs18 NULL \f2\fs20 (the default), all such haplosomes in the population will be used.\ This function was written by Vitor Sudbrack (currently affiliated with University of Lausanne).\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcMeanFroh(object\'a0individuals, [integer$\'a0minimumLength\'a0=\'a01000000], [Niso$\'a0chromosome\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the mean value of the \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub statistic across the individuals passed in \f1\fs18 individuals \f2\fs20 . This statistic is a measure of individual autozygosity, likely resulting from inbreeding, and is calculated based upon \'93runs of homozygosity\'94, or ROH, in the genome of an individual. Broadly speaking, \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub is the proportion of an individual\'92s genome that is spanned by ROH longer than a given threshold length. However, it should be noted that there are many different ways of calculating \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub , producing different results. For example, the threshold length might be a given constant, or might be determined statistically from the characteristics of the population. Furthermore, some heterozygous sites might be discarded (to compensate for genotyping errors), a minimum SNP density might be required within a sliding window for an ROH to be diagnosed, and so forth \'96 it can get quite complex, as seen in the software PLINK (Purcell et al., 2007) and GARLIC (Szpiech, Blant and Pemberton, 2017). The method used by \f1\fs18 calcMeanFroh() \f2\fs20 is the simplest possible method, assessing ROH for each individual directly from the simulated mutations without filtering or modification, and applying a given constant threshold length. If a more sophisticated \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub algorithm is desired, one could modify the implementation of \f1\fs18 calcMeanFroh() \f2\fs20 , which is viewable with \f1\fs18 functionSource() \f2\fs20 , or one could output VCF data from SLiM and analyze it with other tools, perhaps calling out from the running SLiM script with \f1\fs18 system() \f2\fs20 .\ The threshold ROH length used by \f1\fs18 calcMeanFroh() \f2\fs20 is supplied by the parameter \f1\fs18 minimumLength \f2\fs20 . It defaults to \f1\fs18 1e6 \f2\fs20 , or 1 Mbp, since that is a length commonly used in the literature, but can be adjusted as desired.\ The \f1\fs18 chromosome \f2\fs20 parameter can be supplied to focus the \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub calculation on a specific chromosome; otherwise, the calculation spans all chromosomes for which the individual is actually diploid (without a null haplosome). If \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub cannot be calculated for an individual (due to the presence of null haplosomes for every intrinsically diploid chromosome being analyzed), that individual is omitted from the mean \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub calculation; for example, if an X chromosome is the focal chromosome being analyzed, all males will be omitted from the mean \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub calculation. If all individuals are omitted from the mean \f3\i F \f2\i0\fs13\fsmilli6667 \sub roh \fs20 \nosupersub calculation for this reason, \f1\fs18 NAN \f2\fs20 is returned.\ This function was developed with advice from Ryan Chaffee. Thanks, Ryan!\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcPairHeterozygosity(object$\'a0haplosome1, object$\'a0haplosome2, [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL], [logical$\'a0infiniteSites\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the heterozygosity for a pair of haplosomes; these will typically be two homologous haplosomes of the same diploid individual, but any two haplosomes associated with the same chromosome may be supplied.\ The calculation can be narrowed to apply to only a window \'96 a subrange of the full chromosome \'96 by passing the interval bounds [ \f1\fs18 start \f2\fs20 , \f1\fs18 end \f2\fs20 ] for the desired window. In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window. The default behavior, with \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 of \f1\fs18 NULL \f2\fs20 , provides the haplosome-wide heterozygosity.\ The implementation \f1\fs18 calcPairHeterozygosity() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , treats every mutation as independent in the heterozygosity calculations by default (i.e., with \f1\fs18 infiniteSites=T \f2\fs20 ). If mutations are stacked, the heterozygosity calculated therefore depends upon the number of \f3\i unshared mutations \f2\i0 , not the number of \f3\i differing sites \f2\i0 . Similarly, if multiple \f1\fs18 Mutation \f2\fs20 objects exist in different haplosomes at the same site (whether representing different genetic states, or multiple mutational lineages for the same genetic state), each \f1\fs18 Mutation \f2\fs20 object is treated separately for purposes of the heterozygosity calculation, just as if they were at different sites. One could regard these choices as embodying an infinite-sites interpretation of the segregating mutations. In most biologically realistic models, such genetic states will be quite rare, and so the impact of this choice will be negligible; however, in some models this distinction may be important. The behavior of \f1\fs18 calcPairHeterozygosity() \f2\fs20 can be switched to calculate based upon the number of differing sites, rather than the number of unshared mutations, by passing \f1\fs18 infiniteSites=F \f2\fs20 .\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcPi(object\'a0haplosomes, [No\'a0muts\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates \f7\i \uc0\u960 \f2\i0 (nucleotide diversity, a metric of genetic diversity) for a vector of haplosomes (containing at least two elements), based upon the mutations in the haplosomes. \f7\i \uc0\u960 \f2\i0 is computed by calculating the mean number of pairwise differences at each site, summing across all sites, and dividing by the number of sites. Therefore, it is interpretable as the number of differences per site expected between two randomly chosen sequences. The mathematical formulation (as an estimator of the population parameter \f7\i \uc0\u952 \f2\i0 ) is based on work in Nei and Li (1979), Nei and Tajima (1981), and Tajima (1983; equation A3). The exact formula used here is common in textbooks (e.g., equations 9.1\'969.5 in Li 1997, equation 3.3 in Hahn 2018, or equation 2.2 in Coop 2020).\ Often \f1\fs18 haplosomes \f2\fs20 will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used. By default, with \f1\fs18 muts=NULL \f2\fs20 , the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for \f1\fs18 muts \f2\fs20 .\ The calculation can be narrowed to apply to only a window \'96 a subrange of the full chromosome \'96 by passing the interval bounds [ \f1\fs18 start \f2\fs20 , \f1\fs18 end \f2\fs20 ] for the desired window. In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window. The default behavior, with \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 of \f1\fs18 NULL \f2\fs20 , provides the haplosome-wide value of \f7\i \uc0\u960 \f2\i0 .\ The implementation of \f1\fs18 calcPi() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , treats every mutation as independent in the heterozygosity calculations. One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations, as with \f1\fs18 calcHeterozygosity() \f2\fs20 . Indeed, finite-sites models of \f7\i \uc0\u960 \f2\i0 have been derived (Tajima 1996) though are not used here. In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important. See \f1\fs18 calcPairHeterozygosity() \f2\fs20 for further discussion. This function was written by Nick Bailey (currently affiliated with CNRS and the Laboratory of Biometry and Evolutionary Biology at University Lyon 1), with helpful input from Peter Ralph and Chase Nelson.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (numeric)calcSFS([Ni$\'a0binCount\'a0=\'a0NULL], [No\'a0haplosomes\'a0=\'a0NULL], [No\'a0muts\'a0=\'a0NULL], [string$\'a0metric\'a0=\'a0"density"], [logical$\'a0fold\'a0=\'a0F])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates the site frequency spectrum, or SFS, for the mutations specified by \f1\fs18 muts \f2\fs20 , within the haplosomes specified by \f1\fs18 haplosomes \f2\fs20 . The site frequency spectrum or SFS (sometimes called the allele frequency spectrum, although some authors distinguish between the two) is essentially a histogram of the frequencies of the mutations within the haplosomes; the first bin spans the lowest range of frequencies (down to a frequency of \f1\fs18 0.0 \f2\fs20 , or a count of \f1\fs18 1 \f2\fs20 ), whereas the last bin spans the highest range of frequencies (up to a frequency of \f1\fs18 1.0 \f2\fs20 , or a count equal to number of haplosomes minus one). The idea was introduced by Watterson (1975), and will be discussed in any population genetics textbook (e.g., A. Cutter, 2019, pp. 50\'9652). This histogram can be returned as a \f1\fs18 float \f2\fs20 vector of density values for each bin by specifying \f1\fs18 "density" \f2\fs20 for \f1\fs18 metric \f2\fs20 (the default), or as an \f1\fs18 integer \f2\fs20 vector of count values for each bin by specifying \f1\fs18 "count" \f2\fs20 .\ There are two modes of operation for \f1\fs18 calcSFS() \f2\fs20 . If a specific number of bins is passed for \f1\fs18 binCount \f2\fs20 , then the frequency range \f1\fs18 [0.0, 1.0] \f2\fs20 is subdivided into \f1\fs18 binCount \f2\fs20 intervals of equal width, and the mutations are tallied into those bins according to their frequencies within the haplosomes to produce the histogram. In this mode, there will be exactly \f1\fs18 binCount \f2\fs20 elements in the returned vector. Note that either \f1\fs18 "density" \f2\fs20 or \f1\fs18 "count" \f2\fs20 can be chosen in this mode; you can return the frequency bin tallies as either densities or counts.\ In the other mode of operation, chosen with a \f1\fs18 binCount \f2\fs20 value of \f1\fs18 NULL \f2\fs20 , the bins instead represent the count of the number of occurrences for each mutation, and range from a count of \f1\fs18 1 \f2\fs20 (the bin for mutations that occur only once in the haplosomes, sometimes called \'93singletons\'94) up to a count of \f1\fs18 N-1 \f2\fs20 where \f1\fs18 N \f2\fs20 is the number of haplosomes. (Note that mutations occurring in all \f1\fs18 N \f2\fs20 haplosomes are not included in the tally, since they would not be empirically observable.) In this mode, there will be exactly \f1\fs18 N-1 \f2\fs20 elements in the returned vector. Again, either \f1\fs18 "density" \f2\fs20 or \f1\fs18 "count" \f2\fs20 can be chosen in this mode; you can return the count bin tallies as either densities or counts (it\'92s a bit confusing, but we\'92re talking about two different kinds of \'93counts\'94, the count of the number of times a mutation occurs in the haplosomes versus the count of the number of mutations that were tallied into a particular count bin).\ The \f1\fs18 haplosomes \f2\fs20 parameter can be either a vector of \f1\fs18 Haplosome \f2\fs20 objects or \f1\fs18 NULL \f2\fs20 . If \f1\fs18 NULL \f2\fs20 is passed, \f1\fs18 calcSFS() \f2\fs20 will calculate the SFS across the whole species, using all non-null haplosomes present (and thus there must be only a single species in the model, since an SFS cannot be calculated across multiple species). Otherwise, \f1\fs18 haplosomes \f2\fs20 can contain any set of haplosomes desired, such as from the individuals of one subpopulation, several subpopulations, or an entire species. However, they must all belong to the same species, and null haplosomes will be automatically and silently excluded from the set.\ The \f1\fs18 muts \f2\fs20 parameter can be either a vector of \f1\fs18 Mutation \f2\fs20 objects or \f1\fs18 NULL \f2\fs20 . If \f1\fs18 NULL \f2\fs20 is passed, \f1\fs18 calcSFS() \f2\fs20 will calculate the SFS across all mutations belonging to the focal species (as determined from the species of the haplosomes). Otherwise, \f1\fs18 muts \f2\fs20 can contain any set of mutations desired, such as mutations belonging to a specific mutation type, mutations within a specific range of positions along the chromosome, or all of the mutations in the focal species.\ The \f1\fs18 binCount \f2\fs20 and \f1\fs18 metric \f2\fs20 parameters have already been discussed. Finally, the \f1\fs18 fold \f2\fs20 parameter, if \f1\fs18 T \f2\fs20 , \'93folds\'94 the calculated SFS, adding the first and last bins, the second and next-to-last bins, etc., until the center is reached. Folding is common when working with empirical data, where one often doesn\'92t know the \'93polarity\'94 \'96 which allele at a site is ancestral and which is derived. Folding solves this problem, because the polarity then doesn\'92t matter; the tally for a given mutation ends up in the same bin regardless. If the number of bins is even, folding can be performed without ambiguity; the final number of bins is exactly half the original number of bins, and each final bin is the sum of two original bins. If the number of bins is odd, the correct treatment of the central bin is somewhat ambiguous. In \f1\fs18 calcFST() \f2\fs20 , the central bin is added to itself \'96 doubled \'96 and the number of bins is equal to half the original number of bins rounded up. If you would prefer to exclude the central bin altogether \'96 another population treatment \'96 then when the original number of bins is odd, you can simply discard the final value in the returned vector (and, if you wish to work with densities rather than counts, re-normalize the result to sum to 1.0).\ The implementation of \f1\fs18 calcSFS() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , tallies each mutation separately, even if more than one mutation occurs at the same position (or is even stacked with another mutation). One could regard this choice as embodying an infinite-sites interpretation of the SFS, perhaps; in any case, it follows SLiM\'92s behavior in other population-genetics utility functions. In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important.\ This function is compatible with multi-chromosome models, in the following sense. When \f1\fs18 binCount \f2\fs20 is specified with an \f1\fs18 integer \f2\fs20 value, mutations are binned according to their frequencies, as described above. In a multi-chromosome model, the haplosomes and mutations used by \f1\fs18 calcSFS() \f2\fs20 may be associated with more than one chromosome, and the frequency assessed for each mutation is its frequency specifically within the haplosomes associated with its chromosome (as you would expect). Mutations occurring in different chromosomes can therefore be tallied together into the same frequency bins, and combined into a single SFS; this produces a meaningful SFS. (If you want an SFS for just a single chromosome, then of course you can pass just those haplosomes and mutations to \f1\fs18 calcSFS() \f2\fs20 .) When \f1\fs18 binCount \f2\fs20 is \f1\fs18 NULL \f2\fs20 , on the other hand, mutations are binned according to their counts, as described above. In a multi-chromosome model, it would not make sense to bin counts together from different chromosomes, since those counts might not be on the same scale \'96 the number of haplosomes associated with the various chromosomes might not be equal. In this case, \f1\fs18 calcSFS() \f2\fs20 will raise an error if haplosomes from more than one chromosome are supplied, or if haplosomes is \f1\fs18 NULL \f2\fs20 (since it doesn\'92t know which chromosome to choose). If you wish to tally according to counts, with \f1\fs18 binCount=NULL \f2\fs20 , you must pass in a vector of haplosomes associated with a single chromosome. (If you know what you are doing and wish to combine counts across multiple chromosomes, you can simply call \f1\fs18 calcSFS() \f2\fs20 once per chromosome, and combine the resulting vectors by adding them together.)\ Thanks to Ryan Chaffee and Chase Nelson for helpful input.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcTajimasD(object\'a0haplosomes, [No\'a0muts\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates Tajima\'92s \f3\i D \f2\i0 (a test of neutrality based on the allele frequency spectrum) for a vector of haplosomes (containing at least four elements), based upon the mutations in the haplosomes. The mathematical formulation is given in Tajima 1989 (equation 38) and remains unchanged (e.g., equations 2.30 in Durrett 2008, 8.4 in Hahn 2018, and 4.44 in Coop 2020). Often \f1\fs18 haplosomes \f2\fs20 will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used. By default, with \f1\fs18 muts=NULL \f2\fs20 , the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for \f1\fs18 muts \f2\fs20 .\ The calculation can be narrowed to apply to only a window \'96 a subrange of the full chromosome \'96 by passing the interval bounds [ \f1\fs18 start \f2\fs20 , \f1\fs18 end \f2\fs20 ] for the desired window. In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window. The default behavior, with \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 of \f1\fs18 NULL \f2\fs20 , provides the haplosome-wide Tajima\'92s \f3\i D \f2\i0 .\ If the genetic diversity contained within the haplosomes is insufficient for the calculation, \f1\fs18 calcTajimasD() \f2\fs20 may return \f1\fs18 NAN \f2\fs20 . It is up to the caller to detect this with \f1\fs18 isNAN() \f2\fs20 and handle it as necessary.\ The implementation of \f1\fs18 calcTajimasD() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , treats every mutation as independent in the heterozygosity calculations. One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations, as with \f1\fs18 calcHeterozygosity() \f2\fs20 . Indeed, Tajima\'92s \f3\i D \f2\i0 can be modified with finite-sites models of \f7\i \uc0\u960 \f2\i0 and \f7\i \uc0\u952 \f2\i0 (Misawa and Tajima 1997) though these are not used here. In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important. See \f1\fs18 calcPairHeterozygosity() \f2\fs20 for further discussion. This function was written by Nick Bailey (currently affiliated with CNRS and the Laboratory of Biometry and Evolutionary Biology at University Lyon 1), with helpful input from Peter Ralph.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcVA(object\'a0individuals, io$\'a0mutType)\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates \f3\i V \f2\i0\fs13\fsmilli6667 \sub A \fs20 \nosupersub , the additive genetic variance, among a vector of individuals (containing at least two elements) passed in \f1\fs18 individuals \f2\fs20 , in a particular mutation type \f1\fs18 mutType \f2\fs20 that represents quantitative trait loci (QTLs) influencing a quantitative phenotypic trait. The \f1\fs18 mutType \f2\fs20 parameter may be either an \f1\fs18 integer \f2\fs20 representing the ID of the desired mutation type, or a \f1\fs18 MutationType \f2\fs20 object specified directly.\ This function assumes that mutations of type \f1\fs18 mutType \f2\fs20 encode their effect size upon the quantitative trait in their \f1\fs18 selectionCoeff \f2\fs20 property, as is fairly standard in SLiM. The implementation of \f1\fs18 calcVA() \f2\fs20 , which is viewable with \f1\fs18 functionSource() \f2\fs20 , is quite simple; if effect sizes are stored elsewhere (such as with \f1\fs18 setValue() \f2\fs20 ), a new user-defined function following the pattern of \f1\fs18 calcVA() \f2\fs20 can easily be written.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (float$)calcWattersonsTheta(object\'a0haplosomes, [No\'a0muts\'a0=\'a0NULL], [Ni$\'a0start\'a0=\'a0NULL], [Ni$\'a0end\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Calculates Watterson\'92s theta (a metric of genetic diversity comparable to heterozygosity) for a vector of haplosomes (containing at least one element), based upon the mutations in the haplosomes. Often \f1\fs18 haplosomes \f2\fs20 will be all of the haplosomes in a subpopulation, or in the entire population, but any haplosome vector may be used. By default, with \f1\fs18 muts=NULL \f2\fs20 , the calculation is based upon all mutations in the simulation; the calculation can instead be based upon a subset of mutations, such as mutations of a specific mutation type, by passing the desired vector of mutations for \f1\fs18 muts \f2\fs20 .\ The calculation can be narrowed to apply to only a window \'96 a subrange of the full chromosome \'96 by passing the interval bounds [ \f1\fs18 start \f2\fs20 , \f1\fs18 end \f2\fs20 ] for the desired window. In this case, the vector of mutations used for the calculation will be subset to include only mutations within the specified window. The default behavior, with \f1\fs18 start \f2\fs20 and \f1\fs18 end \f2\fs20 of \f1\fs18 NULL \f2\fs20 , provides the haplosome-wide Watterson\'92s theta.\ The implementation of \f1\fs18 calcWattersonsTheta() \f2\fs20 , viewable with \f1\fs18 functionSource() \f2\fs20 , treats every mutation as independent in the heterozygosity calculations. One could regard this choice as embodying an infinite-sites interpretation of the segregating mutations, as with \f1\fs18 calcHeterozygosity() \f2\fs20 . In most biologically realistic models, such genetic states will be quite rare, and so the impact of this assumption will be negligible; however, in some models this distinction may be important. See \f1\fs18 calcPairHeterozygosity() \f2\fs20 for further discussion.\ \pard\pardeftab397\ri720\sb360\sa60\partightenfactor0 \f0\b\fs22 \cf0 3.4. Other utilities \f2\b0\fs20 \cf2 \expnd0\expndtw0\kerning0 \ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (float)summarizeIndividuals(object\'a0individuals, integer\'a0dim, numeric\'a0spatialBounds, string$\'a0operation, [Nlif$\'a0empty\'a0=\'a00.0], [logical$\'a0perUnitArea\'a0=\'a0F], [Ns$\'a0spatiality\'a0=\'a0NULL])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Returns a vector, matrix, or array that summarizes spatial patterns of information related to the individuals in \f1\fs18 individuals \f2\fs20 . In essence, those individuals are assigned into \f3\i bins \f2\i0 according to their spatial position, and then a summary value for each bin is calculated based upon the individuals each bin contains. The individuals might be binned in one dimension (resulting in a vector of summary values), in two dimensions (resulting in a matrix), or in three dimensions (resulting in an array). Typically the spatiality of the result (the dimensions into which the individuals are binned) will match the dimensionality of the model, as indicated by the default value of \f1\fs18 NULL \f2\fs20 for the optional \f1\fs18 spatiality \f2\fs20 parameter; for example, a two-dimensional ( \f1\fs18 "xy" \f2\fs20 ) model would by default produce a two-dimensional matrix as a summary. However, a spatiality that is more restrictive than the model dimensionality may be passed; for example, in a two-dimensional ( \f1\fs18 "xy" \f2\fs20 ) model a \f1\fs18 spatiality \f2\fs20 of \f1\fs18 "y" \f2\fs20 could be passed to summarize individuals into a vector, rather than a matrix, assigning them to bins based only upon their \f3\i y \f2\i0 position (i.e., the value of their \f1\fs18 y \f2\fs20 property). Whatever spatiality is chosen, the parameter \f1\fs18 dim \f2\fs20 provides the dimensions of the desired result, in the same form that the \f1\fs18 dim() \f2\fs20 function does: first the number of rows, then the number of columns, and then the number of planes, as needed (see the Eidos manual for discussion of matrices, arrays, and \f1\fs18 dim() \f2\fs20 ). The length of \f1\fs18 dims \f2\fs20 must match the requested spatiality; for spatiality \f1\fs18 "xy" \f2\fs20 , for example, \f1\fs18 dims \f2\fs20 might be \f1\fs18 c(50,100) \f2\fs20 to request that the returned matrix have \f1\fs18 50 \f2\fs20 rows and \f1\fs18 100 \f2\fs20 columns. The result vector/matrix/array is in the correct orientation to be directly usable as a spatial map, by passing it to the \f1\fs18 defineSpatialMap() \f2\fs20 method of \f1\fs18 Subpopulation \f2\fs20 . For further discussion of dimensionality and spatiality, see \f1\fs18 initializeInteractionType() \f2\fs20 and \f1\fs18 InteractionType \f2\fs20 .\ The \f1\fs18 spatialBounds \f2\fs20 parameter defines the spatial boundaries within which the individuals are binned. Typically this is the spatial bounds of a particular subpopulation, within which the individuals reside; for individuals in \f1\fs18 p1 \f2\fs20 , for example, you would likely pass \f1\fs18 p1.spatialBounds \f2\fs20 for this. However, this is not required; individuals may come from any or all subpopulations in the model, and \f1\fs18 spatialBounds \f2\fs20 may be any bounds of non-zero area (if an individual falls outside of the given spatial bounds, it is excluded, as if it were not in \f1\fs18 individuals \f2\fs20 at all). If you have multiple subpopulations that conceptually reside within the same overall coordinate space, for example, that can be accommodated here. The bounds are supplied in the dimensionality of the model, in the same form as for \f1\fs18 Subpopulation \f2\fs20 ; for an \f1\fs18 "xy" \f2\fs20 model, for example, they are supplied as a four-element vector of the form \f1\fs18 c(x0, y0, x1, y1) \f2\fs20 even if the summary is being produced with spatiality \f1\fs18 "y" \f2\fs20 . To produce the result, a grid with dimensions defined by \f1\fs18 dims \f2\fs20 is conceptually stretched out across the given spatial bounds, such that the \f3\i centers \f2\i0 of the edge and corner grid squares are aligned with the limits of the spatial bounds. This matches the way that \f1\fs18 defineSpatialMap() \f2\fs20 defines its maps.\ The particular summary produced depends upon the parameters \f1\fs18 operation \f2\fs20 and \f1\fs18 empty \f2\fs20 . Consider a single grid square represented by a single element in the result. That grid square contains zero or more of the individuals in \f1\fs18 individuals \f2\fs20 . If it contains zero individuals \f3\i and \f2\i0 \f1\fs18 empty \f2\fs20 is not \f1\fs18 NULL \f2\fs20 , the \f1\fs18 empty \f2\fs20 value is used for the result, regardless of \f1\fs18 operation \f2\fs20 , providing specific, separate control over the treatment of empty grid squares. If \f1\fs18 empty \f2\fs20 is \f1\fs18 NULL \f2\fs20 , this separate control over the treatment of empty grid squares is declined; empty grid squares will be handled through the standard mechanism described next. In all other cases for the given grid square \'96 when it contains more than zero individuals, or when \f1\fs18 empty \f2\fs20 is \f1\fs18 NULL \f2\fs20 \'96 \f1\fs18 operation \f2\fs20 is executed as an Eidos \f3\i lambda \f2\i0 , a small snippet of code, supplied as a singleton \f1\fs18 string \f2\fs20 , that is executed in a manner similar to a function call. Within the execution of the \f1\fs18 operation \f2\fs20 lambda, a constant named \f1\fs18 individuals \f2\fs20 is defined to be the focal individuals being evaluated \'96 all of the individuals within that grid square. This lambda should evaluate to a singleton \f1\fs18 logical \f2\fs20 , \f1\fs18 integer \f2\fs20 , or \f1\fs18 float \f2\fs20 value, comprising the result value for the grid square; these types will all be coerced to \f1\fs18 float \f2\fs20 ( \f1\fs18 T \f2\fs20 being \f1\fs18 1 \f2\fs20 and \f1\fs18 F \f2\fs20 being \f1\fs18 0 \f2\fs20 ).\ Two examples may illustrate the use of \f1\fs18 empty \f2\fs20 and \f1\fs18 operation \f2\fs20 . To produce a summary indicating presence/absence, simply use the default of \f1\fs18 0.0 \f2\fs20 for \f1\fs18 empty \f2\fs20 , and \f1\fs18 "1.0; \f2\fs20 \f1\fs18 " \f2\fs20 (or \f1\fs18 "1;" \f2\fs20 , or \f1\fs18 "T;" \f2\fs20 ) for \f1\fs18 operation \f2\fs20 . This will produce \f1\fs18 0.0 \f2\fs20 for empty grid squares, and \f1\fs18 1.0 \f2\fs20 for those that contain at least one individual. Note that the use of \f1\fs18 empty \f2\fs20 is essential here, because \f1\fs18 operation \f2\fs20 doesn\'92t even check whether individuals are present or not. To produce a summary with a count of the number of individuals in each grid square, again use the default of \f1\fs18 0.0 \f2\fs20 for \f1\fs18 empty \f2\fs20 , but now use an \f1\fs18 operation \f2\fs20 of \f1\fs18 "individuals.size();" \f2\fs20 , counting the number of individuals in each grid square. In this case, \f1\fs18 empty \f2\fs20 could be \f1\fs18 NULL \f2\fs20 instead and \f1\fs18 operation \f2\fs20 would still produce the correct result; but using \f1\fs18 empty \f2\fs20 makes \f1\fs18 summarizeIndividuals() \f2\fs20 more efficient since it allows the execution of \f1\fs18 operation \f2\fs20 to be skipped for those squares.\ Lambdas are not limited in their complexity; they can use \f1\fs18 if \f2\fs20 , \f1\fs18 for \f2\fs20 , etc., and can call methods and functions. A typical \f1\fs18 operation \f2\fs20 to compute the mean phenotype in a quantitative genetic model that stores phenotype values in \f1\fs18 tagF \f2\fs20 , for example, would be \f1\fs18 "mean(individuals.tagF);" \f2\fs20 , and this is still quite simple compared to what is possible. However, keep in mind that the lambda will be evaluated for every grid cell (or at least those that are non-empty), so efficiency can be a concern, and you may wish to pre-calculate values shared by all of the lambda calls, making them available to your lambda code using \f1\fs18 defineGlobal() \f2\fs20 or \f1\fs18 defineConstant() \f2\fs20 .\ There is one last twist, if \f1\fs18 perUnitArea \f2\fs20 is \f1\fs18 T \f2\fs20 : values are divided by the area (or length, in 1D, or volume, in 3D) that their corresponding grid cell comprises, so that each value is in units of \'93per unit area\'94 (or \'93per unit length\'94, or \'93per unit volume\'94). The total area of the grid is defined by the spatial bounds, and the area of a given grid cell is defined by the portion of the spatial bounds that is within that cell. This is not the same for all grid cells; grid cells that fall partially outside \f1\fs18 spatialBounds \f2\fs20 (because, remember, the \f3\i centers \f2\i0 of the edge/corner grid cells are aligned with the limits of \f1\fs18 spatialBounds \f2\fs20 ) will have a smaller area inside the bounds. For an \f1\fs18 "xy" \f2\fs20 spatiality summary, for example, corner cells have only a quarter of their area inside \f1\fs18 spatialBounds \f2\fs20 , while edge elements have half of their area inside \f1\fs18 spatialBounds \f2\fs20 ; for purposes of \f1\fs18 perUnitArea \f2\fs20 , then, their respective areas are \'bc and \'bd the area of an interior grid cell. By default, \f1\fs18 perUnitArea \f2\fs20 is \f1\fs18 F \f2\fs20 , and no scaling is performed. Whether you want \f1\fs18 perUnitArea \f2\fs20 to be \f1\fs18 F \f2\fs20 or \f1\fs18 T \f2\fs20 depends upon whether the summary you are producing is, conceptually, \'93per unit area\'94, such as density (individuals per unit area) or local competition strength (total interaction strength per unit area), or is not, such as \'93mean individual age\'94, or \'93maximum \f1\fs18 tag \f2\fs20 value\'94. For the previous example of counting individuals with an operation of \f1\fs18 "individuals.size();" \f2\fs20 , a value of \f1\fs18 F \f2\fs20 for \f1\fs18 perUnitArea \f2\fs20 (the default) will produce a simple \f3\i count \f2\i0 of individuals in each grid square, whereas with \f1\fs18 T \f2\fs20 it would produce the \f3\i density \f2\i0 of individuals in each grid square.\ \pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0 \f1\fs18 \cf2 (object$)treeSeqMetadata(string$\'a0filePath, [logical$\'a0userData\'a0=\'a0T])\ \pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0 \f2\fs20 \cf2 Returns a \f1\fs18 Dictionary \f2\fs20 containing top-level metadata from the \f1\fs18 .trees \f2\fs20 (tree-sequence) file at \f1\fs18 filePath \f2\fs20 . If \f1\fs18 userData \f2\fs20 is \f1\fs18 T \f2\fs20 (the default), the top-level metadata under the \f1\fs18 SLiM/user_metadata \f2\fs20 key is returned; this is the same metadata that can optionally be supplied to \f1\fs18 treeSeqOutput() \f2\fs20 in its \f1\fs18 metadata \f2\fs20 parameter, so it makes it easy to recover metadata that you attached to the tree sequence when it was saved. If \f1\fs18 userData \f2\fs20 is \f1\fs18 F \f2\fs20 , the entire top-level metadata \f1\fs18 Dictionary \f2\fs20 object is returned; this can be useful for examining the values of other keys under the \f1\fs18 SLiM \f2\fs20 key, or values inside the top-level dictionary itself that might have been placed there by \f1\fs18 msprime \f2\fs20 or other software.\ This function can be used to read in parameter values or other saved state ( \f1\fs18 tag \f2\fs20 property values, for example), in order to resuscitate the complete state of a simulation that was written to a \f1\fs18 .trees \f2\fs20 file. It could be used for more esoteric purposes too, such as to search through \f1\fs18 .trees \f2\fs20 files in a directory (with the help of the Eidos function \f1\fs18 filesAtPath() \f2\fs20 ) to find those files that satisfy some metadata criterion.\ } ================================================ FILE: SLiMgui/SLiMPDFDocument.h ================================================ // // SLiMPDFDocument.h // SLiM // // Created by Ben Haller on 4/20/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import @interface SLiMPDFDocument : NSDocument { NSData *pdfData; // we hold onto the NSData we are given as our "model" NSDate *modificationDate; // we have to track the mod date of our file to tell when it changes } @end ================================================ FILE: SLiMgui/SLiMPDFDocument.mm ================================================ // // SLiMPDFDocument.mm // SLiM // // Created by Ben Haller on 4/20/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "SLiMPDFDocument.h" #import "SLiMPDFWindowController.h" @implementation SLiMPDFDocument - (instancetype)init { if (self = [super init]) { } return self; } - (void)dealloc { [super dealloc]; } - (SLiMPDFWindowController *)slimPDFWindowController { NSArray *controllers = [self windowControllers]; if ([controllers count] > 0) { NSWindowController *mainController = [controllers objectAtIndex:0]; if ([mainController isKindOfClass:[SLiMPDFWindowController class]]) return (SLiMPDFWindowController *)mainController; } return nil; } - (void)givePDFData:(NSData *)data toController:(SLiMPDFWindowController *)controller { // Get our window controller to display the PDF data we have if (pdfData && [pdfData length]) { PDFDocument *doc = [[PDFDocument alloc] initWithData:pdfData]; [controller setDisplayPDFDocument:doc]; [doc release]; } else { [controller setDisplayPDFDocument:nil]; } } - (void)makeWindowControllers { NSArray *myControllers = [self windowControllers]; // If this document displaced a transient document, it will already have been assigned // a window controller. If that is not the case, create one. if ([myControllers count] == 0) { SLiMPDFWindowController *controller = [[[SLiMPDFWindowController alloc] init] autorelease]; [self addWindowController:controller]; [self givePDFData:pdfData toController:controller]; } } - (void)windowControllerDidLoadNib:(NSWindowController *)aController { // Note this method is not called, because we override -makeWindowControllers [super windowControllerDidLoadNib:aController]; } - (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError * _Nullable *)outError { if ([super readFromURL:url ofType:typeName error:outError]) { NSString *filePath = [url path]; NSFileManager *fm = [NSFileManager defaultManager]; [modificationDate release]; modificationDate = [[[fm attributesOfItemAtPath:filePath error:nil] fileModificationDate] retain]; return YES; } return NO; } - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { if ([typeName isEqualToString:@"com.adobe.pdf"]) { if (pdfData != data) { [pdfData release]; pdfData = [data retain]; } SLiMPDFWindowController *controller = [self slimPDFWindowController]; if (controller) [self givePDFData:pdfData toController:controller]; return YES; } if (outError) *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:nil]; return NO; } - (void)revertAfterChange:(id)sender { NSURL *fileURL = [self fileURL]; NSString *filePath = [fileURL path]; NSFileManager *fm = [NSFileManager defaultManager]; NSDate *newModDate = [[fm attributesOfItemAtPath:filePath error:nil] fileModificationDate]; if ([newModDate timeIntervalSinceDate:modificationDate] > 0.0) { //NSLog(@" reverting..."); NSURL *url = [self fileURL]; [self revertToContentsOfURL:url ofType:@"com.adobe.pdf" error:NULL]; } } - (void)presentedItemDidChange { //NSLog(@"presentedItemDidChange"); [self performSelectorOnMainThread:@selector(revertAfterChange:) withObject:nil waitUntilDone:NO]; } + (NSArray *)writableTypes { return [NSArray array]; } + (BOOL)isNativeType:(NSString *)type { return NO; } - (void)copy:(id)sender { if (pdfData && [pdfData length]) { NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; [pasteboard declareTypes:@[NSPDFPboardType] owner:self]; [pasteboard setData:pdfData forType:NSPDFPboardType]; } } - (BOOL)validateUserInterfaceItem:(id)item { // I would have expected that returning an empty array for writeableTypes would cause Save As... // to be disabled, but that is apparently not the case. I can't find any better way than this. if ([item action] == @selector(saveDocumentAs:)) return NO; if ([item action] == @selector(copy:)) return (pdfData && [pdfData length]); //NSLog(@"sel = %@", NSStringFromSelector([item action])); return [super validateUserInterfaceItem:item]; } + (BOOL)autosavesInPlace { return NO; } + (BOOL)autosavesDrafts { return NO; } + (BOOL)preservesVersions { return NO; } @end ================================================ FILE: SLiMgui/SLiMPDFView.h ================================================ // // SLiMPDFView.h // SLiM // // Created by Ben Haller on 4/20/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import @interface SLiMPDFView : NSView { } @property (nonatomic, retain) PDFDocument *document; @end ================================================ FILE: SLiMgui/SLiMPDFView.mm ================================================ // // SLiMPDFView.m // SLiM // // Created by Ben Haller on 4/20/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "SLiMPDFView.h" @implementation SLiMPDFView - (instancetype)initWithFrame:(NSRect)frameRect { if (self = [super initWithFrame:frameRect]) { } return self; } - (void)dealloc { [_document release]; _document = nil; [super dealloc]; } - (void)drawRect:(NSRect)dirtyRect { NSRect bounds = [self bounds]; [[NSColor darkGrayColor] set]; NSRectFill(bounds); if (_document) { // thanks to https://ryanbritton.com/2015/09/correctly-drawing-pdfs-in-cocoa/ for this code CGSize drawingSize = {bounds.size.width, bounds.size.height}; // Document Loading CGPDFDocumentRef pdfDocument = [_document documentRef]; CGPDFPageRef page = CGPDFDocumentGetPage(pdfDocument, 1); // Start by getting the crop box since only its contents should be drawn CGRect cropBox = CGPDFPageGetBoxRect(page, kCGPDFCropBox); // Account for rotation of the page to figure out the size to create the context. Like images, // rotation can be represented by one of two ways in a PDF: the contents can be pre-rotated // in which case nothing needs to be done or the document can have its rotation value set and // it means we need to apply the rotation as an affine transformation when drawing NSInteger rotationAngle = CGPDFPageGetRotationAngle(page); CGFloat angleInRadians = -rotationAngle * (M_PI / 180); CGAffineTransform transform = CGAffineTransformMakeRotation(angleInRadians); CGRect rotatedCropRect = CGRectApplyAffineTransform(cropBox, transform); // Figure out the closest size we can draw the PDF at that's no larger than drawingSize CGRect bestFit = CGRectMake(0, 0, 0, 0); double rotatedCropRectAspect = rotatedCropRect.size.width / rotatedCropRect.size.height; double drawingSizeAspect = drawingSize.width / drawingSize.height; if (rotatedCropRectAspect > drawingSizeAspect) { bestFit.size.width = drawingSize.width; bestFit.size.height = round(rotatedCropRect.size.height * (drawingSize.width / rotatedCropRect.size.width)); bestFit.origin.y = round((drawingSize.height - bestFit.size.height) / 2); } else { bestFit.size.width = round(rotatedCropRect.size.width * (drawingSize.height / rotatedCropRect.size.height)); bestFit.size.height = drawingSize.height; bestFit.origin.x = round((drawingSize.width - bestFit.size.width) / 2); } CGFloat scale = CGRectGetHeight(bestFit) / CGRectGetHeight(rotatedCropRect); // Get the drawing context CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; // Create the affine transformation matrix to align the PDF's CropBox to our drawing context. //transform = CGPDFPageGetDrawingTransform(page, kCGPDFCropBox, CGRectMake(0, 0, CGRectGetWidth(bestFit), CGRectGetHeight(bestFit)), 0, true); transform = CGPDFPageGetDrawingTransform(page, kCGPDFCropBox, bestFit, 0, true); if (scale > 1) { // Since CGPDFPageGetDrawingTransform won't scale up, we need to do it manually transform = CGAffineTransformTranslate(transform, CGRectGetMidX(cropBox), CGRectGetMidY(cropBox)); transform = CGAffineTransformScale(transform, scale, scale); transform = CGAffineTransformTranslate(transform, -CGRectGetMidX(cropBox), -CGRectGetMidY(cropBox)); } CGContextConcatCTM(context, transform); // Clip the drawing to the CropBox CGContextAddRect(context, cropBox); CGContextClip(context); [[NSColor whiteColor] set]; NSRectFill(cropBox); CGContextDrawPDFPage(context, page); } } - (BOOL)isOpaque { return YES; } - (void)setDocument:(PDFDocument *)document { [document retain]; [_document release]; _document = document; [self setNeedsDisplay:YES]; } - (NSMenu *)menu { NSMenu *menu = [[NSMenu alloc] initWithTitle:@"pdf_menu"]; NSMenuItem *menuItem; menuItem = [menu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@""]; [menuItem setTarget:[[[self window] windowController] document]]; return [menu autorelease]; } @end ================================================ FILE: SLiMgui/SLiMPDFWindow.xib ================================================ ================================================ FILE: SLiMgui/SLiMPDFWindowController.h ================================================ // // SLiMPDFWindowController.h // SLiM // // Created by Ben Haller on 4/20/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #import #import "SLiMPDFView.h" @interface SLiMPDFWindowController : NSWindowController { IBOutlet SLiMPDFView *pdfView; } @property (nonatomic, retain) PDFDocument *displayPDFDocument; @end ================================================ FILE: SLiMgui/SLiMPDFWindowController.mm ================================================ // // SLiMPDFWindowController.mm // SLiM // // Created by Ben Haller on 4/20/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "SLiMPDFWindowController.h" @implementation SLiMPDFWindowController - (instancetype)init { if (self = [super initWithWindowNibName:@"SLiMPDFWindow"]) { } return self; } - (void)dealloc { pdfView = nil; [self setDisplayPDFDocument:nil]; [super dealloc]; } - (void)windowDidLoad { [super windowDidLoad]; [pdfView setDocument:_displayPDFDocument]; } - (void)setDisplayPDFDocument:(PDFDocument *)doc { if (doc != _displayPDFDocument) { [doc retain]; [_displayPDFDocument release]; _displayPDFDocument = doc; [pdfView setDocument:_displayPDFDocument]; } } @end ================================================ FILE: SLiMgui/SLiMWindowController.h ================================================ // // SLiMWindowController.h // SLiM // // Created by Ben Haller on 1/21/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import #include "eidos_rng.h" #include "species.h" #include "slim_gui.h" #import "ChromosomeView.h" #import "PopulationView.h" #import "GraphView.h" #import "CocoaExtra.h" #import "EidosTextView.h" #import "EidosTextViewDelegate.h" #import "EidosConsoleTextView.h" #import "EidosConsoleWindowController.h" #import "EidosConsoleWindowControllerDelegate.h" #include class Community; @interface SLiMWindowController : NSWindowController { @public NSString *scriptString; // the script string that we are running on right now; not the same as the script textview! Community *community; // the simulation instance for this window Species *focalSpecies; // NOT OWNED: a pointer to the focal species in community; do not use, call focalDisplaySpecies() std::string focalSpeciesName; // the name of the focal species, for persistence across recycles SLiMgui *slimgui; // the SLiMgui Eidos class instance for this window // state variables that are globals in Eidos and SLiM; we swap these in and out as needed, to provide each sim with its own context bool sim_RNG_initialized; #ifndef _OPENMP Eidos_RNG_State sim_RNG_SINGLE; #else std::vector sim_RNG_PERTHREAD; // pointers to per-thread allocations, for "first touch" optimization #endif slim_pedigreeid_t sim_next_pedigree_id; slim_mutationid_t sim_next_mutation_id; bool sim_suppress_warnings; std::string sim_working_dir; // the current working dir that we will return to when executing SLiM/Eidos code std::string sim_requested_working_dir; // the last working dir set by the user with the SLiMgui button/menu; we return to it on recycle // play-related variables; note that continuousPlayOn covers both profiling and non-profiling runs, whereas profilePlayOn // and nonProfilePlayOn cover those cases individually; this is for simplicity in enable bindings in the nib BOOL invalidSimulation, continuousPlayOn, profilePlayOn, nonProfilePlayOn, tickPlayOn, reachedSimulationEnd, hasImported; slim_tick_t targetTick; NSDate *continuousPlayStartDate; uint64_t continuousPlayTicksCompleted; int partialUpdateCount; SLiMPlaySliderToolTipWindow *playSpeedToolTipWindow; // display-related variables NSMutableDictionary *genomicElementColorRegistry; BOOL zoomedChromosomeShowsRateMaps; BOOL zoomedChromosomeShowsGenomicElements; BOOL zoomedChromosomeShowsMutations; BOOL zoomedChromosomeShowsFixedSubstitutions; BOOL reloadingSubpopTableview; BOOL reloadingSpeciesBar; // outlets IBOutlet NSButton *buttonForDrawer; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" IBOutlet NSDrawer *drawer; #pragma GCC diagnostic pop IBOutlet NSTableView *mutTypeTableView; IBOutlet NSTableColumn *mutTypeIDColumn; IBOutlet NSTableColumn *mutTypeDominanceColumn; IBOutlet NSTableColumn *mutTypeDFETypeColumn; IBOutlet NSTableColumn *mutTypeDFEParamsColumn; IBOutlet NSTableView *genomicElementTypeTableView; IBOutlet NSTableColumn *genomicElementTypeIDColumn; IBOutlet NSTableColumn *genomicElementTypeColorColumn; IBOutlet NSTableColumn *genomicElementTypeMutationTypesColumn; IBOutlet NSTableView *interactionTypeTableView; IBOutlet NSTableColumn *interactionTypeIDColumn; IBOutlet NSTableColumn *interactionTypeMaxDistanceColumn; IBOutlet NSTableColumn *interactionTypeIFTypeColumn; IBOutlet NSTableColumn *interactionTypeIFParamsColumn; IBOutlet NSTableView *scriptBlocksTableView; IBOutlet NSTableColumn *scriptBlocksIDColumn; IBOutlet NSTableColumn *scriptBlocksStartColumn; IBOutlet NSTableColumn *scriptBlocksEndColumn; IBOutlet NSTableColumn *scriptBlocksTypeColumn; IBOutlet NSSplitView *overallSplitView; IBOutlet NSView *overallTopView; IBOutlet NSLayoutConstraint *overallTopViewConstraint1; IBOutlet NSLayoutConstraint *overallTopViewConstraint2; IBOutlet NSLayoutConstraint *overallTopViewConstraint3; IBOutlet NSLayoutConstraint *overallTopViewConstraint4; IBOutlet NSButton *playOneStepButton; IBOutlet NSButton *playButton; IBOutlet NSButton *profileButton; IBOutlet NSButton *recycleButton; IBOutlet NSSlider *playSpeedSlider; IBOutlet NSTextField *tickTextField; IBOutlet NSProgressIndicator *tickProgressIndicator; IBOutlet NSTextField *cycleTextField; IBOutlet NSSplitView *bottomSplitView; IBOutlet EidosTextView *scriptTextView; IBOutlet NSTextField *scriptStatusTextField; IBOutlet EidosTextView *outputTextView; IBOutlet NSButton *consoleButton; IBOutlet NSButton *browserButton; IBOutlet NSSegmentedControl *speciesBar; IBOutlet NSLayoutConstraint *speciesBarBottomConstraint; IBOutlet NSTableView *subpopTableView; IBOutlet NSTableColumn *subpopIDColumn; IBOutlet NSTableColumn *subpopSizeColumn; IBOutlet NSTableColumn *subpopSelfingRateColumn; IBOutlet NSTableColumn *subpopMaleCloningRateColumn; IBOutlet NSTableColumn *subpopFemaleCloningRateColumn; IBOutlet NSTableColumn *subpopSexRatioColumn; IBOutlet PopulationView *populationView; IBOutlet PopulationErrorView *populationErrorView; IBOutlet ChromosomeView *chromosomeOverview; IBOutlet ChromosomeView *chromosomeZoomed; IBOutlet NSButton *showRecombinationIntervalsButton; IBOutlet NSButton *showGenomicElementsButton; IBOutlet NSButton *showMutationsButton; IBOutlet NSButton *showFixedSubstitutionsButton; IBOutlet SLiMMenuButton *graphCommandsButton; IBOutlet NSMenu *graphCommandsMenu; // Graph window ivars IBOutlet NSWindow *graphWindow; // outlet for GraphWindow.xib; note this does not stay wired up, it is just used transiently NSWindow *graphWindowMutationFreqSpectrum; NSWindow *graphWindowMutationFreqTrajectories; NSWindow *graphWindowMutationLossTimeHistogram; NSWindow *graphWindowMutationFixationTimeHistogram; NSWindow *graphWindowFitnessOverTime; NSWindow *graphWindowPopulationVisualization; // don't forget to add new graph windows in -dealloc, -updateAfterTick, and -windowWillClose: int openedGraphCount; // used for new graph window positioning // Other linked windows, such as the haplotype snapshot NSMutableArray *linkedWindows; // Profile Report window ivars IBOutlet NSWindow *profileWindow; // outlet for ProfileReport.xib; note this does not stay wired up, it is just used transiently IBOutlet NSTextView *profileTextView; // ditto // Misc bool observingKeyPaths; SLiMFunctionGraphToolTipWindow *functionGraphToolTipWindow; // for previews of muttype DFEs or interaction type IFs } + (NSColor *)blackContrastingColorForIndex:(int)index; + (NSColor *)whiteContrastingColorForIndex:(int)index; - (instancetype)init; - (instancetype)initWithWindow:(NSWindow *)window __attribute__((unavailable)); - (instancetype)initWithWindowNibName:(NSString *)windowNibName __attribute__((unavailable)); - (instancetype)initWithWindowNibName:(NSString *)windowNibName owner:(id)owner __attribute__((unavailable)); - (instancetype)initWithWindowNibPath:(NSString *)windowNibPath owner:(id)owner __attribute__((unavailable)); - (instancetype)initWithCoder:(NSCoder *)coder __attribute__((unavailable)); - (void)setScriptStringAndInitializeSimulation:(NSString *)string; - (Species *)focalDisplaySpecies; - (std::vector)selectedSubpopulations; - (void)updatePopulationViewHiding; - (NSColor *)colorForGenomicElementType:(GenomicElementType *)elementType withID:(slim_objectid_t)elementTypeID; - (void)updateRecycleHighlightForChangeCount:(int)changeCount; - (void)displayStartupMessage; // // Properties // @property (nonatomic) BOOL invalidSimulation; @property (nonatomic) BOOL continuousPlayOn; @property (nonatomic) BOOL profilePlayOn; @property (nonatomic) BOOL nonProfilePlayOn; @property (nonatomic) BOOL tickPlayOn; @property (nonatomic) BOOL reachedSimulationEnd; @property (nonatomic, readonly) NSColor *colorForWindowLabels; @property (nonatomic, retain) IBOutlet EidosConsoleWindowController *consoleController; // // Actions // - (IBAction)speciesBarChanged:(id)sender; - (IBAction)graphMutationFrequencySpectrum:(id)sender; - (IBAction)graphMutationFrequencyTrajectories:(id)sender; - (IBAction)graphMutationLossTimeHistogram:(id)sender; - (IBAction)graphMutationFixationTimeHistogram:(id)sender; - (IBAction)graphFitnessOverTime:(id)sender; - (IBAction)graphPopulationVisualization:(id)sender; - (IBAction)playOneStep:(id)sender; - (IBAction)play:(id)sender; - (IBAction)profile:(id)sender; - (IBAction)recycle:(id)sender; - (IBAction)playSpeedChanged:(id)sender; - (IBAction)tickChanged:(id)sender; - (IBAction)checkScript:(id)sender; - (IBAction)prettyprintScript:(id)sender; - (IBAction)showScriptHelp:(id)sender; - (IBAction)toggleConsoleVisibility:(id)sender; - (IBAction)toggleBrowserVisibility:(id)sender; - (IBAction)clearOutput:(id)sender; - (IBAction)dumpPopulationToOutput:(id)sender; - (IBAction)changeWorkingDirectory:(id)sender; - (IBAction)showRecombinationIntervalsButtonToggled:(id)sender; - (IBAction)showGenomicElementsButtonToggled:(id)sender; - (IBAction)showMutationsButtonToggled:(id)sender; - (IBAction)showFixedSubstitutionsButtonToggled:(id)sender; - (IBAction)drawerButtonToggled:(id)sender; // This action has been disabled; see comments on the implementation. //- (IBAction)importPopulation:(id)sender; // wired through firstResponder because these are menu items - (IBAction)exportScript:(id)sender; // wired through firstResponder because these are menu items - (IBAction)exportOutput:(id)sender; // wired through firstResponder because these are menu items // Eidos SLiMgui method forwards - (void)eidos_openDocument:(NSString *)path; - (void)eidos_pauseExecution; @end ================================================ FILE: SLiMgui/SLiMWindowController.mm ================================================ // // SLiMWindowController.m // SLiM // // Created by Ben Haller on 1/21/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "SLiMWindowController.h" #import "SLiMDocument.h" #import "AppDelegate.h" #import "GraphView_MutationFrequencySpectra.h" #import "GraphView_MutationLossTimeHistogram.h" #import "GraphView_MutationFixationTimeHistogram.h" #import "GraphView_FitnessOverTime.h" #import "GraphView_PopulationVisualization.h" #import "GraphView_MutationFrequencyTrajectory.h" #import "EidosHelpController.h" #import "EidosPrettyprinter.h" #import "EidosCocoaExtra.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "eidos_type_interpreter.h" #include "slim_test.h" #include "slim_gui.h" #include "community.h" #include "interaction_type.h" #include #include #include #include #include #include #include @implementation SLiMWindowController // // KVC / KVO / properties // @synthesize invalidSimulation, continuousPlayOn, profilePlayOn, nonProfilePlayOn, reachedSimulationEnd, tickPlayOn; + (NSSet *)keyPathsForValuesAffectingColorForWindowLabels { return [NSSet setWithObjects:@"invalidSimulation", nil]; } - (NSColor *)getColorForWindowLabels { if (invalidSimulation) return [NSColor colorWithCalibratedWhite:0.5 alpha:1.0]; else return [NSColor blackColor]; } - (void)setContinuousPlayOn:(BOOL)newFlag { if (continuousPlayOn != newFlag) { continuousPlayOn = newFlag; [_consoleController setInterfaceEnabled:!(continuousPlayOn || tickPlayOn)]; } } - (void)setTickPlayOn:(BOOL)newFlag { if (tickPlayOn != newFlag) { tickPlayOn = newFlag; [_consoleController setInterfaceEnabled:!(continuousPlayOn || tickPlayOn)]; } } // // Core class methods // #pragma mark - #pragma mark Core class methods + (NSColor *)blackContrastingColorForIndex:(int)index { static NSColor **colorArray = NULL; if (!colorArray) { colorArray = (NSColor **)malloc(8 * sizeof(NSColor *)); colorArray[0] = [[NSColor colorWithCalibratedHue:0.65 saturation:0.65 brightness:1.0 alpha:1.0] retain]; colorArray[1] = [[NSColor colorWithCalibratedHue:0.55 saturation:1.0 brightness:1.0 alpha:1.0] retain]; colorArray[2] = [[NSColor colorWithCalibratedHue:0.40 saturation:1.0 brightness:0.9 alpha:1.0] retain]; colorArray[3] = [[NSColor colorWithCalibratedHue:0.16 saturation:1.0 brightness:1.0 alpha:1.0] retain]; colorArray[4] = [[NSColor colorWithCalibratedHue:0.08 saturation:0.65 brightness:1.0 alpha:1.0] retain]; colorArray[5] = [[NSColor colorWithCalibratedHue:0.00 saturation:0.65 brightness:1.0 alpha:1.0] retain]; colorArray[6] = [[NSColor colorWithCalibratedHue:0.80 saturation:0.65 brightness:1.0 alpha:1.0] retain]; colorArray[7] = [[NSColor colorWithCalibratedHue:0.00 saturation:0.0 brightness:0.8 alpha:1.0] retain]; } return ((index >= 0) && (index <= 6)) ? colorArray[index] : colorArray[7]; } + (NSColor *)whiteContrastingColorForIndex:(int)index { static NSColor **colorArray = NULL; if (!colorArray) { colorArray = (NSColor **)malloc(7 * sizeof(NSColor *)); colorArray[0] = [[NSColor colorWithCalibratedHue:0.65 saturation:0.75 brightness:1.0 alpha:1.0] retain]; colorArray[1] = [[NSColor colorWithCalibratedHue:0.55 saturation:1.0 brightness:1.0 alpha:1.0] retain]; colorArray[2] = [[NSColor colorWithCalibratedHue:0.40 saturation:1.0 brightness:0.8 alpha:1.0] retain]; colorArray[3] = [[NSColor colorWithCalibratedHue:0.08 saturation:0.75 brightness:1.0 alpha:1.0] retain]; colorArray[4] = [[NSColor colorWithCalibratedHue:0.00 saturation:0.85 brightness:1.0 alpha:1.0] retain]; colorArray[5] = [[NSColor colorWithCalibratedHue:0.80 saturation:0.85 brightness:1.0 alpha:1.0] retain]; colorArray[6] = [[NSColor colorWithCalibratedHue:0.00 saturation:0.0 brightness:0.5 alpha:1.0] retain]; } return ((index >= 0) && (index <= 5)) ? colorArray[index] : colorArray[6]; } - (instancetype)init { if (self = [super initWithWindowNibName:@"SLiMWindow"]) { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // observe preferences that we care about [defaults addObserver:self forKeyPath:defaultsSyntaxHighlightScriptKey options:0 context:NULL]; [defaults addObserver:self forKeyPath:defaultsSyntaxHighlightOutputKey options:0 context:NULL]; [defaults addObserver:self forKeyPath:defaultsDisplayFontSizeKey options:0 context:NULL]; observingKeyPaths = YES; // Observe notifications to keep our variable browser toggle button up to date [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(browserWillShow:) name:EidosVariableBrowserWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(browserWillHide:) name:EidosVariableBrowserWillHideNotification object:nil]; // set default viewing state; this might come from user defaults on a per-script basis eventually... zoomedChromosomeShowsRateMaps = NO; zoomedChromosomeShowsGenomicElements = NO; zoomedChromosomeShowsMutations = YES; zoomedChromosomeShowsFixedSubstitutions = NO; // We set the working directory for new windows to ~/Desktop/, since it makes no sense for them to use the location of the app. // Each running simulation will track its own working directory, and the user can set it with a button in the SLiMgui window. // BCH 4/2/2020: Per request from PLR, we will now use the Desktop as the default directory only if we were launched by Finder // or equivalent; if we were launched by a shell, we will use the working directory given us by that shell. See issue #76 bool launchedFromShell = [(AppDelegate *)[NSApp delegate] launchedFromShell]; if (launchedFromShell) sim_working_dir = [(AppDelegate *)[NSApp delegate] SLiMguiCurrentWorkingDirectory]; else sim_working_dir = Eidos_ResolvedPath("~/Desktop"); // Check that our chosen working directory actually exists; if not, use ~ struct stat buffer; if (stat(sim_working_dir.c_str(), &buffer) != 0) sim_working_dir = Eidos_ResolvedPath("~"); sim_requested_working_dir = sim_working_dir; // return to the working dir on recycle unless the user overrides it } return self; } - (void)cleanup { //NSLog(@"[SLiMWindowController cleanup]"); [[NSNotificationCenter defaultCenter] removeObserver:self]; [NSObject cancelPreviousPerformRequestsWithTarget:self]; // _tickPlay:, _continuousPlay:, _continuousProfile:, etc. // Disconnect delegate relationships [overallSplitView setDelegate:nil]; overallSplitView = nil; [bottomSplitView setDelegate:nil]; bottomSplitView = nil; [scriptTextView setDelegate:nil]; scriptTextView = nil; [outputTextView setDelegate:nil]; outputTextView = nil; [_consoleController setDelegate:nil]; // Remove observers if (observingKeyPaths) { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults removeObserver:self forKeyPath:defaultsSyntaxHighlightScriptKey context:NULL]; [defaults removeObserver:self forKeyPath:defaultsSyntaxHighlightOutputKey context:NULL]; [defaults removeObserver:self forKeyPath:defaultsDisplayFontSizeKey context:NULL]; observingKeyPaths = NO; } [[NSNotificationCenter defaultCenter] removeObserver:self]; [NSObject cancelPreviousPerformRequestsWithTarget:playSpeedToolTipWindow selector:@selector(orderOut:) object:nil]; [playSpeedToolTipWindow orderOut:nil]; [playSpeedToolTipWindow release]; playSpeedToolTipWindow = nil; // Free resources [scriptString release]; scriptString = nil; if (community) { delete community; community = nullptr; focalSpecies = nullptr; } if (slimgui) { delete slimgui; slimgui = nullptr; } if (sim_RNG_initialized) { #ifndef _OPENMP _Eidos_FreeOneRNG(sim_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) { Eidos_RNG_State *rng_state = sim_RNG_PERTHREAD[threadIndex]; _Eidos_FreeOneRNG(*rng_state); sim_RNG_PERTHREAD[threadIndex] = nullptr; free(rng_state); } sim_RNG_PERTHREAD.resize(0); #endif sim_RNG_initialized = false; //NSLog(@"-[SLiMWindowController cleanup]: freed sim_RNG"); } [self setInvalidSimulation:YES]; [continuousPlayStartDate release]; continuousPlayStartDate = nil; [genomicElementColorRegistry release]; genomicElementColorRegistry = nil; // All graph windows attached to this controller need to be closed, since they refer back to us; // closing them will come back via windowWillClose: and make them release and nil themselves [self sendAllLinkedViewsSelector:@selector(cleanup)]; [self sendAllLinkedWindowsSelector:@selector(close)]; [graphWindowMutationFreqSpectrum autorelease]; [graphWindowMutationFreqTrajectories autorelease]; [graphWindowMutationLossTimeHistogram autorelease]; [graphWindowMutationFixationTimeHistogram autorelease]; [graphWindowFitnessOverTime autorelease]; [graphWindowPopulationVisualization autorelease]; graphWindowMutationFreqSpectrum = nil; graphWindowMutationFreqTrajectories = nil; graphWindowMutationLossTimeHistogram = nil; graphWindowMutationFixationTimeHistogram = nil; graphWindowFitnessOverTime = nil; graphWindowPopulationVisualization = nil; [linkedWindows autorelease]; linkedWindows = nil; // We also need to close and release our console window and its associated variable browser window. // We don't track the console or var browser in windowWillClose: since we want those windows to // continue to exist even when they are hidden, unlike graph windows. [[_consoleController browserController] hideWindow]; [_consoleController hideWindow]; [self setConsoleController:nil]; // Also close and let go of our mini-graph tooltip window [self hideMiniGraphToolTipWindow]; [functionGraphToolTipWindow autorelease]; functionGraphToolTipWindow = nil; } - (void)dealloc { //NSLog(@"[SLiMWindowController dealloc]"); [self cleanup]; if ([self document]) [self setDocument:nil]; [super dealloc]; } - (void)setDocument:(id)document { [super setDocument:document]; // The document currently has our model information, but we keep the model, so we need to pull it over if ([self document]) [self setScriptStringAndInitializeSimulation:[[self document] documentScriptString]]; } - (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName { NSString *superString = [super windowTitleForDocumentDisplayName:displayName]; SLiMDocument *document = [self document]; NSString *recipeName = [document recipeName]; NSURL *docURL = [document fileURL]; // If we have a recipe name, *and* we are an unsaved (i.e. untitled) document, then we want to customize the window title if (recipeName && !docURL && [displayName hasPrefix:[document defaultDraftName]]) { NSRange spaceRange = [recipeName rangeOfString:@" "]; if (spaceRange.location != NSNotFound) { NSString *recipeNumber = [recipeName substringToIndex:spaceRange.location]; if ([recipeNumber length]) return [NSString stringWithFormat:@"%@ [Recipe %@]", superString, recipeNumber]; } } return superString; } - (void)browserWillShow:(NSNotification *)note { if ([note object] == [_consoleController browserController]) [browserButton setState:NSOnState]; } - (void)browserWillHide:(NSNotification *)note { if ([note object] == [_consoleController browserController]) [browserButton setState:NSOffState]; } - (void)displayStartupMessage { NSDictionary *statusAttrs = [NSDictionary eidosTextAttributesWithColor:[NSColor textColor] size:11.0]; NSString *statusString = [NSString stringWithFormat:@"SLiM %s, %@ build.", SLIM_VERSION_STRING, #if DEBUG @"debug" #else @"release" #endif ]; #ifdef _OPENMP statusString = [statusString stringByAppendingFormat:@" Running SLiM in parallel with %d threads maximum.", gEidosMaxThreads]; #endif NSMutableAttributedString *statusAttrString = [[[NSMutableAttributedString alloc] initWithString:statusString attributes:statusAttrs] autorelease]; [statusAttrString addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [statusAttrString length])]; [scriptStatusTextField setAttributedStringValue:statusAttrString]; } - (void)showTerminationMessage:(NSString *)terminationMessage { NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleCritical]; [alert setMessageText:@"Simulation Runtime Error"]; [alert setInformativeText:[NSString stringWithFormat:@"%@\nThis error has invalidated the simulation; it cannot be run further. Once the script is fixed, you can recycle the simulation and try again.", terminationMessage]]; [alert addButtonWithTitle:@"OK"]; [alert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { [alert autorelease]; [terminationMessage autorelease]; }]; // Depending on the circumstances of the error, we might be able to select a range in our input file to show what caused the error if (![[self document] changedSinceRecycle]) [scriptTextView selectErrorRange]; // Show the error in the status bar also NSString *trimmedError = [terminationMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSDictionary *errorAttrs = [NSDictionary eidosTextAttributesWithColor:[NSColor redColor] size:11.0]; NSMutableAttributedString *errorAttrString = [[[NSMutableAttributedString alloc] initWithString:trimmedError attributes:errorAttrs] autorelease]; [errorAttrString addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [errorAttrString length])]; [scriptStatusTextField setAttributedStringValue:errorAttrString]; } - (void)checkForSimulationTermination { std::string &&terminationMessage = gEidosTermination.str(); if (!terminationMessage.empty()) { const char *cstr = terminationMessage.c_str(); NSString *str = [NSString stringWithUTF8String:cstr]; gEidosTermination.clear(); gEidosTermination.str(""); [self performSelector:@selector(showTerminationMessage:) withObject:[str retain] afterDelay:0.0]; // showTerminationMessage: will release the string // Now we need to clean up so we are in a displayable state. Note that we don't even attempt to dispose // of the old simulation object; who knows what state it is in, touching it might crash. community = nullptr; focalSpecies = nullptr; slimgui = nullptr; if (sim_RNG_initialized) { #ifndef _OPENMP _Eidos_FreeOneRNG(sim_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) { Eidos_RNG_State *rng_state = sim_RNG_PERTHREAD[threadIndex]; _Eidos_FreeOneRNG(*rng_state); sim_RNG_PERTHREAD[threadIndex] = nullptr; free(rng_state); } sim_RNG_PERTHREAD.resize(0); #endif sim_RNG_initialized = false; //NSLog(@"-[SLiMWindowController checkForSimulationTermination]: freed sim_RNG"); } [self setReachedSimulationEnd:YES]; [self setInvalidSimulation:YES]; } } - (void)startNewSimulationFromScript { if (community) { delete community; community = nullptr; focalSpecies = nullptr; } if (slimgui) { delete slimgui; slimgui = nullptr; } // Reset the number of threads to be used in parallel regions, if it has been changed by parallelSetNumThreads(); // note that we do not save/restore the value across context switches between models, as we do RNGs and such, // since we don't support end users running SLiMgui multithreaded anyhow gEidosNumThreads = gEidosMaxThreads; gEidosNumThreadsOverride = false; omp_set_num_threads(gEidosMaxThreads); // Free the old simulation RNG and make a new one, to have clean state if (sim_RNG_initialized) { #ifndef _OPENMP _Eidos_FreeOneRNG(sim_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) { Eidos_RNG_State *rng_state = sim_RNG_PERTHREAD[threadIndex]; _Eidos_FreeOneRNG(*rng_state); sim_RNG_PERTHREAD[threadIndex] = nullptr; free(rng_state); } sim_RNG_PERTHREAD.resize(0); #endif sim_RNG_initialized = false; //NSLog(@"-[SLiMWindowController startNewSimulationFromScript]: freed sim_RNG"); } #ifndef _OPENMP _Eidos_InitializeOneRNG(sim_RNG_SINGLE); #else sim_RNG_PERTHREAD.resize(gEidosMaxThreads); #pragma omp parallel default(none) shared(gEidos_RNG_PERTHREAD) num_threads(gEidosMaxThreads) { // Each thread allocates and initializes its own Eidos_RNG_State, for "first touch" optimization int threadnum = omp_get_thread_num(); Eidos_RNG_State *rng_state = (Eidos_RNG_State *)calloc(1, sizeof(Eidos_RNG_State)); _Eidos_InitializeOneRNG(*rng_state); sim_RNG_PERTHREAD[threadnum] = rng_state; } #endif sim_RNG_initialized = true; //NSLog(@"-[SLiMWindowController startNewSimulationFromScript]: initialized sim_RNG"); // The Eidos RNG may be set up already; if so, get rid of it. When we are not running, we keep the // Eidos RNG in an initialized state, to catch errors with the swapping of RNG state. Nobody should // use it when we have not swapped in our own RNG. if (gEidos_RNG_Initialized) { #ifndef _OPENMP _Eidos_FreeOneRNG(gEidos_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) { Eidos_RNG_State *rng_state = gEidos_RNG_PERTHREAD[threadIndex]; _Eidos_FreeOneRNG(*rng_state); gEidos_RNG_PERTHREAD[threadIndex] = nullptr; free(rng_state); } // note that we do not resize the gEidos_RNG_PERTHREAD vector; it stays at its final size #endif gEidos_RNG_Initialized = false; //NSLog(@"-[SLiMWindowController startNewSimulationFromScript]: freed gEidos_RNG"); } // Swap in our RNG #ifndef _OPENMP std::swap(sim_RNG_SINGLE, gEidos_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) std::swap(sim_RNG_PERTHREAD[threadIndex], gEidos_RNG_PERTHREAD[threadIndex]); #endif std::swap(sim_RNG_initialized, gEidos_RNG_Initialized); //NSLog(@"-[SLiMWindowController startNewSimulationFromScript]: swapped IN simRNG (sim_RNG_initialized == %@, gEidos_RNG_Initialized == %@)", sim_RNG_initialized ? @"YES" : @"NO", gEidos_RNG_Initialized ? @"YES" : @"NO"); std::istringstream infile([scriptString UTF8String]); try { community = new Community(); community->InitializeFromFile(infile); community->InitializeRNGFromSeed(nullptr); community->FinishInitialization(); // Swap out our RNG #ifndef _OPENMP std::swap(sim_RNG_SINGLE, gEidos_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) std::swap(sim_RNG_PERTHREAD[threadIndex], gEidos_RNG_PERTHREAD[threadIndex]); #endif std::swap(sim_RNG_initialized, gEidos_RNG_Initialized); //NSLog(@"-[SLiMWindowController startNewSimulationFromScript]: swapped OUT simRNG (sim_RNG_initialized == %@, gEidos_RNG_Initialized == %@)", sim_RNG_initialized ? @"YES" : @"NO", gEidos_RNG_Initialized ? @"YES" : @"NO"); // We also reset various Eidos/SLiM instance state; each SLiMgui window is independent sim_next_pedigree_id = 0; sim_next_mutation_id = 0; sim_suppress_warnings = false; // The current working directory was set up in -init to be ~/Desktop, and should not be reset here; if the // user has changed it, that change ought to stick across recycles. So this bounces us back to the last dir chosen. sim_working_dir = sim_requested_working_dir; [self setReachedSimulationEnd:NO]; [self setInvalidSimulation:NO]; hasImported = NO; } catch (...) { // BCH 12/25/2022: adding this to swap out our RNG after a raise, seems better... #ifndef _OPENMP std::swap(sim_RNG_SINGLE, gEidos_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) std::swap(sim_RNG_PERTHREAD[threadIndex], gEidos_RNG_PERTHREAD[threadIndex]); #endif std::swap(sim_RNG_initialized, gEidos_RNG_Initialized); //NSLog(@"-[SLiMWindowController startNewSimulationFromScript]: swapped OUT simRNG (sim_RNG_initialized == %@, gEidos_RNG_Initialized == %@)", sim_RNG_initialized ? @"YES" : @"NO", gEidos_RNG_Initialized ? @"YES" : @"NO"); if (community) community->simulation_valid_ = false; [self setReachedSimulationEnd:YES]; [self checkForSimulationTermination]; } if (community) { // make a new SLiMgui instance to represent SLiMgui in Eidos slimgui = new SLiMgui(*community, self); // set up the "slimgui" symbol for it immediately // BCH 11/7/2025: note this symbol is now protected in SLiM_ConfigureContext() community->simulation_constants_->InitializeConstantSymbolEntry(slimgui->SymbolTableEntry()); } } - (void)setScriptStringAndInitializeSimulation:(NSString *)string { [scriptString release]; scriptString = [string retain]; [self startNewSimulationFromScript]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; if (object == defaults) { if ([keyPath isEqualToString:defaultsSyntaxHighlightScriptKey]) { if ([defaults boolForKey:defaultsSyntaxHighlightScriptKey]) [scriptTextView setSyntaxColoring:kEidosSyntaxColoringEidos]; else [scriptTextView setSyntaxColoring:kEidosSyntaxColoringNone]; } if ([keyPath isEqualToString:defaultsSyntaxHighlightOutputKey]) { if ([defaults boolForKey:defaultsSyntaxHighlightOutputKey]) [outputTextView setSyntaxColoring:kEidosSyntaxColoringOutput]; else [outputTextView setSyntaxColoring:kEidosSyntaxColoringNone]; } if ([keyPath isEqualToString:defaultsDisplayFontSizeKey]) { int fontSize = (int)[defaults integerForKey:defaultsDisplayFontSizeKey]; [scriptTextView setDisplayFontSize:fontSize]; [outputTextView setDisplayFontSize:fontSize]; [[_consoleController scriptTextView] setDisplayFontSize:fontSize]; [[_consoleController outputTextView] setDisplayFontSize:fontSize]; } } } - (GraphView *)graphViewForGraphWindow:(NSWindow *)window { return (GraphView *)[window contentView]; } - (void)sendAllLinkedViewsSelector:(SEL)selector { [[self graphViewForGraphWindow:graphWindowMutationFreqSpectrum] performSelector:selector]; [[self graphViewForGraphWindow:graphWindowMutationFreqTrajectories] performSelector:selector]; [[self graphViewForGraphWindow:graphWindowMutationLossTimeHistogram] performSelector:selector]; [[self graphViewForGraphWindow:graphWindowMutationFixationTimeHistogram] performSelector:selector]; [[self graphViewForGraphWindow:graphWindowFitnessOverTime] performSelector:selector]; [[self graphViewForGraphWindow:graphWindowPopulationVisualization] performSelector:selector]; for (NSWindow *window : linkedWindows) { NSView *contentView = [window contentView]; if ([contentView respondsToSelector:selector]) [contentView performSelector:selector]; } } - (void)sendAllLinkedWindowsSelector:(SEL)selector { [graphWindowMutationFreqSpectrum performSelector:selector]; [graphWindowMutationFreqTrajectories performSelector:selector]; [graphWindowMutationLossTimeHistogram performSelector:selector]; [graphWindowMutationFixationTimeHistogram performSelector:selector]; [graphWindowFitnessOverTime performSelector:selector]; [graphWindowPopulationVisualization performSelector:selector]; [linkedWindows makeObjectsPerformSelector:selector]; } - (void)updateOutputTextView { // QtSLiM separates these output streams, but we just glom them together std::string newOutput = gSLiMOut.str(); newOutput += gSLiMError.str(); if (!newOutput.empty()) { const char *cstr = newOutput.c_str(); NSString *str = [NSString stringWithUTF8String:cstr]; // So, ideally we would stay pinned at the bottom if the user had scrolled to the bottom, but would stay // at the user's chosen scroll position above the bottom if they chose such a position. Unfortunately, // this doesn't seem to work. I'm not quite sure why. Particularly when large amounts of output get // added quickly, the scroller doesn't seem to catch up, and then it reads here as not being at the // bottom, and so we become unpinned even though we used to be pinned. I'm going to just give up, for // now, and always scroll to the bottom when new output comes out. That's what many other such apps // do anyway; it's a little annoying if you're trying to read old output, but so it goes. //NSScrollView *enclosingScrollView = [outputTextView enclosingScrollView]; BOOL scrolledToBottom = YES; //(![enclosingScrollView hasVerticalScroller] || [[enclosingScrollView verticalScroller] doubleValue] == 1.0); NSTextStorage *ts = [outputTextView textStorage]; NSUInteger tsOriginalLength = [ts length]; NSUInteger strLength = [str length]; [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange(tsOriginalLength, 0) withString:str]; [ts addAttribute:NSFontAttributeName value:[NSFont fontWithName:@"Menlo" size:[outputTextView displayFontSize]] range:NSMakeRange(tsOriginalLength, strLength)]; [ts endEditing]; if ([[NSUserDefaults standardUserDefaults] boolForKey:defaultsSyntaxHighlightOutputKey]) [outputTextView recolorAfterChanges]; // if the user was scrolled to the bottom, we keep them there; otherwise, we let them stay where they were if (scrolledToBottom) [outputTextView scrollRangeToVisible:NSMakeRange([[outputTextView string] length], 0)]; // clear any error flags set on the stream and empty out its string so it is ready to receive new output gSLiMOut.clear(); gSLiMOut.str(""); gSLiMError.clear(); gSLiMError.str(""); } } - (void)updateTickCounter { //NSLog(@"updating after tick %d, tickTextField %p", community->tick_, tickTextField); Species *displaySpecies = [self focalDisplaySpecies]; if (displaySpecies) { if (community->tick_ == 0) { [tickTextField setStringValue:@"initialize()"]; [cycleTextField setStringValue:@"initialize()"]; } else { [tickTextField setIntegerValue:community->tick_]; [cycleTextField setIntegerValue:displaySpecies->cycle_]; } } else { [tickTextField setStringValue:@""]; [cycleTextField setStringValue:@""]; } } - (void)updatePopulationViewHiding { std::vector selectedSubpopulations = [self selectedSubpopulations]; BOOL canDisplayPopulationView = [populationView tileSubpopulations:selectedSubpopulations]; // Swap between populationView and populationErrorView as needed to show text messages in the pop view area if (canDisplayPopulationView) { if ([populationView isHidden]) { [populationView setHidden:NO]; [populationErrorView setHidden:YES]; } } else { if (![populationView isHidden]) { [populationView setHidden:YES]; [populationErrorView setHidden:NO]; } } } - (bool)modelMightBeNonWF { // We don't have any really solid way to tell what the model will do until it is executed, given the dynamic nature of // Eidos, but this method tries to apply some heuristics to the question to provide a guess that will usually be correct. if (![self invalidSimulation] && community) if (community->ModelType() == SLiMModelType::kModelTypeNonWF) return YES; NSString *string = [scriptTextView string]; if ([string containsString:@"initializeSLiMModelType(\"nonWF\")"] || [string containsString:@"initializeSLiMModelType(\'nonWF\')"]) return YES; return NO; } - (void)updateSpeciesBar { Species *displaySpecies = [self focalDisplaySpecies]; // Update the species bar as needed; we do this only after initialization, to avoid a hide/show on recycle of multispecies models if (community && displaySpecies && (community->Tick() >= 1)) { BOOL speciesBarVisibleNow = ![speciesBar isHidden]; bool speciesBarShouldBeVisible = (community->all_species_.size() > 1); if (speciesBarVisibleNow && !speciesBarShouldBeVisible) { [speciesBar setEnabled:NO]; [speciesBar setHidden:YES]; [speciesBarBottomConstraint setConstant:0]; reloadingSpeciesBar = true; [speciesBar setSegmentCount:0]; reloadingSpeciesBar = false; } else if (!speciesBarVisibleNow && speciesBarShouldBeVisible) { [speciesBar setHidden:NO]; [speciesBarBottomConstraint setConstant:10]; if (([speciesBar segmentCount] == 0) && (community->all_species_.size() > 0)) { // add tabs for species when shown int selectedSpeciesIndex = 0; int speciesCount = (int)community->all_species_.size(), speciesIndex = 0; bool avatarsOnly = (speciesCount > 2); reloadingSpeciesBar = true; [speciesBar setSegmentCount:speciesCount]; for (Species *species : community->all_species_) { NSString *tabLabel = [NSString stringWithUTF8String:species->avatar_.c_str()]; if (!avatarsOnly) { tabLabel = [tabLabel stringByAppendingString:@" "]; tabLabel = [tabLabel stringByAppendingString:[NSString stringWithUTF8String:species->name_.c_str()]]; } [speciesBar setLabel:tabLabel forSegment:speciesIndex]; [speciesBar setWidth:0 forSegment:speciesIndex]; NSString *tooltipString = [@"Species " stringByAppendingString:[NSString stringWithUTF8String:species->name_.c_str()]]; [[speciesBar cell] setToolTip:tooltipString forSegment:speciesIndex]; // do this on the cell because the corresponding NSSegmentedControl API wasn't added until 10.13 if (focalSpeciesName.length() && (species->name_.compare(focalSpeciesName) == 0)) selectedSpeciesIndex = speciesIndex; speciesIndex++; } reloadingSpeciesBar = false; //qDebug() << "selecting index" << selectedSpeciesIndex << "for name" << QString::fromStdString(focalSpeciesName); [speciesBar setSelectedSegment:selectedSpeciesIndex]; } [speciesBar setEnabled:YES]; } } else { // Whenever we're invalid or uninitialized, we hide the species bar and disable and remove all the tabs [speciesBar setEnabled:NO]; [speciesBar setHidden:YES]; [speciesBarBottomConstraint setConstant:0]; reloadingSpeciesBar = true; [speciesBar setSegmentCount:0]; reloadingSpeciesBar = false; } } - (void)updateAfterTickFull:(BOOL)fullUpdate { // fullUpdate is used to suppress some expensive updating to every third update if (!fullUpdate) { if (++partialUpdateCount >= 3) { partialUpdateCount = 0; fullUpdate = YES; } } // Update the species bar and then fetch the focal species after that update, which might change it [self updateSpeciesBar]; Species *displaySpecies = [self focalDisplaySpecies]; // Flush any buffered output to files every full update, so that the user sees changes to the files without too much delay // NOTE THAT THE WORKING DIRECTORY HAS BEEN CHANGED BACK AT THIS POINT! if (fullUpdate) if (!Eidos_FlushFiles()) raise(SIGTRAP); // could be improved, but for SLiMguiLegacy this is OK // Check whether the simulation has terminated due to an error; if so, show an error message with a delayed perform [self checkForSimulationTermination]; // The rest of the code here needs to be careful about the invalid state; we do want to update our controls when invalid, but sim is nil. if (fullUpdate) { // FIXME it would be good for this updating to be minimal; reloading the tableview every time, etc., is quite wasteful... [self updateOutputTextView]; // Reloading the subpop tableview is tricky, because we need to preserve the selection across the reload, while also noting that the selection is forced // to change when a subpop goes extinct. The current selection is noted in the gui_selected_ ivar of each subpop. So what we do here is reload the tableview // while suppressing our usual update of our selection state, and then we try to re-impose our selection state on the new tableview content. If a subpop // went extinct, we will fail to notice the selection change; but that is OK, since we force an update of populationView and chromosomeZoomed below anyway. reloadingSubpopTableview = YES; [subpopTableView reloadData]; if (!displaySpecies) { [subpopTableView deselectAll:nil]; } else { Population &population = displaySpecies->population_; int subpopCount = (int)population.subpops_.size(); auto popIter = population.subpops_.begin(); NSMutableIndexSet *indicesToSelect = [NSMutableIndexSet indexSet]; for (int i = 0; i < subpopCount; ++i) { if (popIter->second->gui_selected_) [indicesToSelect addIndex:i]; popIter++; } [subpopTableView selectRowIndexes:indicesToSelect byExtendingSelection:NO]; } reloadingSubpopTableview = NO; [subpopTableView setNeedsDisplay]; } // Now update our other UI, some of which depends upon the state of subpopTableView [populationView setNeedsDisplay:YES]; [chromosomeZoomed setNeedsDisplayInInterior]; [self updatePopulationViewHiding]; if (fullUpdate) [self updateTickCounter]; // Update stuff that only needs updating when the script is re-parsed, not after every tick if (!displaySpecies || community->mutation_types_changed_) { [mutTypeTableView reloadData]; [mutTypeTableView setNeedsDisplay]; if (community) community->mutation_types_changed_ = false; } if (!displaySpecies || community->genomic_element_types_changed_) { [genomicElementTypeTableView reloadData]; [genomicElementTypeTableView setNeedsDisplay]; if (community) community->genomic_element_types_changed_ = false; } if (!displaySpecies || community->interaction_types_changed_) { [interactionTypeTableView reloadData]; [interactionTypeTableView setNeedsDisplay]; if (community) community->interaction_types_changed_ = false; } if (!displaySpecies || community->scripts_changed_) { [scriptBlocksTableView reloadData]; [scriptBlocksTableView setNeedsDisplay]; if (community) community->scripts_changed_ = false; } if (!displaySpecies || community->chromosome_changed_) { [chromosomeOverview setNeedsDisplay:YES]; [chromosomeZoomed setNeedsDisplay:YES]; if (community) community->chromosome_changed_ = false; } // Update graph windows as well; this will usually trigger a setNeedsDisplay:YES but may do other updating work as well if (fullUpdate) [self sendAllLinkedViewsSelector:@selector(updateAfterTick)]; } - (void)chromosomeSelectionChanged:(NSNotification *)note { [self sendAllLinkedViewsSelector:@selector(controllerSelectionChanged)]; } - (void)replaceHeaderForColumn:(NSTableColumn *)column withImageNamed:(NSString *)imageName scaledToSize:(int)imageSize withSexSymbol:(IndividualSex)sexSymbol { NSImage *headerImage = [NSImage imageNamed:imageName]; NSImage *scaledHeaderImage = [headerImage copy]; NSTextAttachment *ta = [[NSTextAttachment alloc] init]; [scaledHeaderImage setSize:NSMakeSize(imageSize, imageSize)]; [(NSCell *)[ta attachmentCell] setImage:scaledHeaderImage]; NSMutableAttributedString *attrStr = [[NSAttributedString attributedStringWithAttachment:ta] mutableCopy]; if (sexSymbol != IndividualSex::kUnspecified) { NSImage *sexSymbolImage = [NSImage imageNamed:(sexSymbol == IndividualSex::kMale ? @"male_symbol" : @"female_symbol")]; NSImage *scaledSexSymbolImage = [sexSymbolImage copy]; NSTextAttachment *sexSymbolAttachment = [[NSTextAttachment alloc] init]; [scaledSexSymbolImage setSize:NSMakeSize(14, 14)]; [(NSCell *)[sexSymbolAttachment attachmentCell] setImage:scaledSexSymbolImage]; NSAttributedString *sexSymbolAttrStr = [NSAttributedString attributedStringWithAttachment:sexSymbolAttachment]; [attrStr appendAttributedString:sexSymbolAttrStr]; [scaledSexSymbolImage release]; [sexSymbolAttachment release]; } NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; [paragraphStyle setAlignment:NSTextAlignmentCenter]; [attrStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [attrStr length])]; [[column headerCell] setAttributedStringValue:attrStr]; [attrStr release]; [paragraphStyle release]; [scaledHeaderImage release]; [ta release]; } - (void)windowDidLoad { [super windowDidLoad]; // Tell Cocoa that we can go full-screen [[self window] setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; // The splitview sets the size of overallTopView based upon the position of the splitter. The overallTopView then // wants to manually manage the size of its subview, as a way of compensating for half-pixel frame positions that // mess up our OpenGL subviews. (We have the constraints in the nib so that working in the nib has fully defined // constraints, even though we don't want them at runtime.) See SLiMLayoutRoundoffView in CocoaExtra.mm. Note // that a side effect of this is that the window doesn't know its minimum width, because the necessary chain of // constraints is broken by overallTopView; we have to set a minimum width for the window in the nib to make that // work. Which seems to work, although it's an unfortunate hack. The minimum height is governed by constraints // on the top-level views under the main splitview, however, and thus the minimum height set on the window in the // nib is ignored. [overallTopView removeConstraint:overallTopViewConstraint1]; [overallTopView removeConstraint:overallTopViewConstraint2]; [overallTopView removeConstraint:overallTopViewConstraint3]; [overallTopView removeConstraint:overallTopViewConstraint4]; NSView *subviewOfOverallTopView = [[overallTopView subviews] objectAtIndex:0]; [subviewOfOverallTopView setAutoresizingMask:NSViewNotSizable]; [subviewOfOverallTopView setTranslatesAutoresizingMaskIntoConstraints:YES]; // Fix our splitview's position restore, which NSSplitView sometimes screws up [overallSplitView eidosRestoreAutosavedPositionsWithName:@"SLiMgui Overall Splitter"]; [bottomSplitView eidosRestoreAutosavedPositionsWithName:@"SLiMgui Splitter"]; // THEN set the autosave names on the splitviews; this prevents NSSplitView from getting confused [overallSplitView setAutosaveName:@"SLiMgui Overall Splitter"]; [bottomSplitView setAutosaveName:@"SLiMgui Splitter"]; // Change column headers in the subpopulation table to images [self replaceHeaderForColumn:subpopSelfingRateColumn withImageNamed:@"change_selfing_ratio" scaledToSize:14 withSexSymbol:IndividualSex::kUnspecified]; [self replaceHeaderForColumn:subpopFemaleCloningRateColumn withImageNamed:@"change_cloning_rate" scaledToSize:14 withSexSymbol:IndividualSex::kFemale]; [self replaceHeaderForColumn:subpopMaleCloningRateColumn withImageNamed:@"change_cloning_rate" scaledToSize:14 withSexSymbol:IndividualSex::kMale]; [self replaceHeaderForColumn:subpopSexRatioColumn withImageNamed:@"change_sex_ratio" scaledToSize:15 withSexSymbol:IndividualSex::kUnspecified]; // Set up syntax coloring based on the user's defaults NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [scriptTextView setSyntaxColoring:([defaults boolForKey:defaultsSyntaxHighlightScriptKey] ? kEidosSyntaxColoringEidos : kEidosSyntaxColoringNone)]; [outputTextView setSyntaxColoring:([defaults boolForKey:defaultsSyntaxHighlightOutputKey] ? kEidosSyntaxColoringOutput : kEidosSyntaxColoringNone)]; // fire the observer message for font size, to make it correct; seems like this used to happen automatically but now doesn't? [self observeValueForKeyPath:defaultsDisplayFontSizeKey ofObject:defaults change:nullptr context:nullptr]; // Set the script textview to show its string, with correct formatting [scriptTextView setString:scriptString]; [scriptTextView recolorAfterChanges]; // Set up our chromosome views to show the proper stuff [chromosomeOverview setOverview:true]; [chromosomeOverview setShouldDrawGenomicElements:YES]; [chromosomeOverview setShouldDrawMutations:NO]; [chromosomeOverview setShouldDrawFixedSubstitutions:NO]; [chromosomeOverview setShouldDrawRateMaps:NO]; [chromosomeZoomed setOverview:false]; [chromosomeZoomed setShouldDrawGenomicElements:zoomedChromosomeShowsGenomicElements]; [chromosomeZoomed setShouldDrawMutations:zoomedChromosomeShowsMutations]; [chromosomeZoomed setShouldDrawFixedSubstitutions:zoomedChromosomeShowsFixedSubstitutions]; [chromosomeZoomed setShouldDrawRateMaps:zoomedChromosomeShowsRateMaps]; [showRecombinationIntervalsButton setState:(zoomedChromosomeShowsRateMaps ? NSOnState : NSOffState)]; [showGenomicElementsButton setState:(zoomedChromosomeShowsGenomicElements ? NSOnState : NSOffState)]; [showMutationsButton setState:(zoomedChromosomeShowsMutations ? NSOnState : NSOffState)]; [showFixedSubstitutionsButton setState:(zoomedChromosomeShowsFixedSubstitutions ? NSOnState : NSOffState)]; [[NSNotificationCenter defaultCenter] removeObserver:self name:SLiMChromosomeSelectionChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(chromosomeSelectionChanged:) name:SLiMChromosomeSelectionChangedNotification object:chromosomeOverview]; // Set up our menu buttons with their menus [graphCommandsButton setSlimMenu:graphCommandsMenu]; // Configure our drawer; note that the minimum height here actually forces a minimum height on our window too [drawer setMinContentSize:NSMakeSize(280, 450)]; [drawer setMaxContentSize:NSMakeSize(450, 570)]; [drawer setContentSize:NSMakeSize(280, 570)]; [drawer setLeadingOffset:0.0]; [drawer setTrailingOffset:20.0]; [drawer openOnEdge:NSMaxXEdge]; [drawer close]; // Update all our UI to reflect the current state of the simulation [self updateAfterTickFull:YES]; // Load our console window nib [[NSBundle mainBundle] loadNibNamed:@"EidosConsoleWindow" owner:self topLevelObjects:NULL]; } - (Species *)focalDisplaySpecies { // SLiMgui focuses on one species at a time in its main window display; this method should be called to obtain that species. // This funnel method checks for various invalid states and returns nil; callers should check for a nil return as needed. if (![self invalidSimulation] && community && community->simulation_valid_) { // If we have a focal species set already, it must be valid (the community still exists), so return it if (focalSpecies) return focalSpecies; // If not, we'll choose a species from the species list if there are any const std::vector &all_species = community->AllSpecies(); if (all_species.size() >= 1) { // If we have a species name remembered, try to choose that species again if (focalSpeciesName.length()) { for (Species *species : all_species) { if (species->name_.compare(focalSpeciesName) == 0) { focalSpecies = species; return focalSpecies; } } } // Failing that, choose the first declared species and remember its name focalSpecies = all_species[0]; focalSpeciesName = focalSpecies->name_; } } return nullptr; } - (std::vector)selectedSubpopulations { Species *displaySpecies = [self focalDisplaySpecies]; std::vector selectedSubpops; if (displaySpecies) { Population &population = displaySpecies->population_; int subpopCount = (int)population.subpops_.size(); auto popIter = population.subpops_.begin(); for (int i = 0; i < subpopCount; ++i) { Subpopulation *subpop = popIter->second; if (subpop->gui_selected_) selectedSubpops.emplace_back(subpop); popIter++; } } return selectedSubpops; } - (NSColor *)colorForGenomicElementType:(GenomicElementType *)elementType withID:(slim_objectid_t)elementTypeID { if (elementType && !elementType->color_.empty()) { return [NSColor colorWithCalibratedRed:elementType->color_red_ green:elementType->color_green_ blue:elementType->color_blue_ alpha:1.0]; } else { if (!genomicElementColorRegistry) genomicElementColorRegistry = [[NSMutableDictionary alloc] init]; NSNumber *key = [NSNumber numberWithInteger:elementTypeID]; NSColor *elementColor = [genomicElementColorRegistry objectForKey:key]; if (!elementColor) { int elementCount = (int)[genomicElementColorRegistry count]; elementColor = [SLiMWindowController blackContrastingColorForIndex:elementCount]; [genomicElementColorRegistry setObject:elementColor forKey:key]; } return elementColor; } } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL sel = [menuItem action]; if (sel == @selector(playOneStep:)) return !(invalidSimulation || continuousPlayOn || tickPlayOn || reachedSimulationEnd); if (sel == @selector(play:)) { [menuItem setTitle:(nonProfilePlayOn ? @"Stop" : @"Play")]; return !(invalidSimulation || tickPlayOn || reachedSimulationEnd || profilePlayOn); } if (sel == @selector(profile:)) { [menuItem setTitle:(profilePlayOn ? @"Stop" : @"Profile")]; return !(invalidSimulation || tickPlayOn || reachedSimulationEnd || nonProfilePlayOn); } if (sel == @selector(recycle:)) return !(continuousPlayOn || tickPlayOn); if (sel == @selector(graphMutationFrequencySpectrum:)) return !(invalidSimulation); if (sel == @selector(graphMutationFrequencyTrajectories:)) return !(invalidSimulation); if (sel == @selector(graphMutationLossTimeHistogram:)) return !(invalidSimulation); if (sel == @selector(graphMutationFixationTimeHistogram:)) return !(invalidSimulation); if (sel == @selector(graphFitnessOverTime:)) return !(invalidSimulation); if (sel == @selector(checkScript:)) return !(continuousPlayOn || tickPlayOn); if (sel == @selector(prettyprintScript:)) return !(continuousPlayOn || tickPlayOn); if (sel == @selector(exportScript:)) return !(continuousPlayOn || tickPlayOn); if (sel == @selector(exportOutput:)) return !(continuousPlayOn || tickPlayOn); if (sel == @selector(clearOutput:)) return !(invalidSimulation); if (sel == @selector(dumpPopulationToOutput:)) return !(invalidSimulation); if (sel == @selector(changeWorkingDirectory:)) return !(invalidSimulation); if (sel == @selector(toggleConsoleVisibility:)) return [_consoleController validateMenuItem:menuItem]; if (sel == @selector(toggleBrowserVisibility:)) return [_consoleController validateMenuItem:menuItem]; return YES; } - (void)updateRecycleHighlightForChangeCount:(int)changeCount { if (changeCount) [recycleButton slimSetTintColor:[NSColor colorWithCalibratedHue:0.33 saturation:0.4 brightness:1.0 alpha:1.0]]; else [recycleButton slimSetTintColor:nil]; } // // Actions // #pragma mark - #pragma mark Actions - (IBAction)speciesBarChanged:(id)sender { // We don't want to react to automatic tab changes as we are adding or removing tabs from the species bar if (reloadingSpeciesBar) return; int speciesIndex = (int)[speciesBar selectedSegment]; const std::vector &allSpecies = community->AllSpecies(); if ((speciesIndex < 0) || (speciesIndex >= (int)allSpecies.size())) { //qDebug() << "selectedSpeciesChanged() index" << speciesIndex << "out of range"; return; } focalSpecies = allSpecies[speciesIndex]; focalSpeciesName = focalSpecies->name_; //qDebug() << "selectedSpeciesChanged(): changed to species name" << QString::fromStdString(focalSpeciesName); // do a full update to show the state for the new species [self updateAfterTickFull:YES]; } - (void)positionNewGraphWindow:(NSWindow *)window { NSRect windowFrame = [window frame]; NSRect mainWindowFrame = [[self window] frame]; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" BOOL drawerIsOpen = ([drawer state] == NSDrawerOpenState); #pragma GCC diagnostic pop int oldOpenedGraphCount = openedGraphCount++; // try along the bottom first { NSRect candidateFrame = windowFrame; candidateFrame.origin.x = mainWindowFrame.origin.x + oldOpenedGraphCount * (windowFrame.size.width + 5); candidateFrame.origin.y = mainWindowFrame.origin.y - (candidateFrame.size.height + 5); if ([NSScreen visibleCandidateWindowFrame:candidateFrame]) { [window setFrameOrigin:candidateFrame.origin]; return; } } // try on the left side { NSRect candidateFrame = windowFrame; candidateFrame.origin.x = mainWindowFrame.origin.x - (candidateFrame.size.width + 5); candidateFrame.origin.y = (mainWindowFrame.origin.y + mainWindowFrame.size.height - candidateFrame.size.height) - oldOpenedGraphCount * (windowFrame.size.height + 5); if ([NSScreen visibleCandidateWindowFrame:candidateFrame]) { [window setFrameOrigin:candidateFrame.origin]; return; } } // unless the drawer is open, let's try on the right side if (!drawerIsOpen) { NSRect candidateFrame = windowFrame; candidateFrame.origin.x = mainWindowFrame.origin.x + mainWindowFrame.size.width + 5; candidateFrame.origin.y = (mainWindowFrame.origin.y + mainWindowFrame.size.height - candidateFrame.size.height) - oldOpenedGraphCount * (windowFrame.size.height + 5); if ([NSScreen visibleCandidateWindowFrame:candidateFrame]) { [window setFrameOrigin:candidateFrame.origin]; return; } } // try along the top { NSRect candidateFrame = windowFrame; candidateFrame.origin.x = mainWindowFrame.origin.x + oldOpenedGraphCount * (windowFrame.size.width + 5); candidateFrame.origin.y = mainWindowFrame.origin.y + mainWindowFrame.size.height + 5; if ([NSScreen visibleCandidateWindowFrame:candidateFrame]) { [window setFrameOrigin:candidateFrame.origin]; return; } } // if none of those worked, we just leave the window where it got placed out of the nib } - (NSWindow *)graphWindowWithTitle:(NSString *)windowTitle viewClass:(Class)viewClass { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status [[NSBundle mainBundle] loadNibNamed:@"GraphWindow" owner:self topLevelObjects:NULL]; // Set the graph window title [graphWindow setTitle:windowTitle]; // We substitute in a GraphView subclass here, in place in place of the contentView NSView *oldContentView = [graphWindow contentView]; NSRect contentRect = [oldContentView frame]; GraphView *graphView = [[viewClass alloc] initWithFrame:contentRect withController:self]; [graphWindow setContentView:graphView]; [graphView release]; // Position the graph window prior to showing it [self positionNewGraphWindow:graphWindow]; // We use one nib for all graph types, so we transfer the outlet to a separate ivar NSWindow *returnWindow = graphWindow; graphWindow = nil; return returnWindow; } - (IBAction)graphMutationFrequencySpectrum:(id)sender { if (!graphWindowMutationFreqSpectrum) graphWindowMutationFreqSpectrum = [[self graphWindowWithTitle:@"Mutation Frequency Spectrum" viewClass:[GraphView_MutationFrequencySpectra class]] retain]; [graphWindowMutationFreqSpectrum orderFront:nil]; } - (IBAction)graphMutationFrequencyTrajectories:(id)sender { if (!graphWindowMutationFreqTrajectories) graphWindowMutationFreqTrajectories = [[self graphWindowWithTitle:@"Mutation Frequency Trajectories" viewClass:[GraphView_MutationFrequencyTrajectory class]] retain]; [graphWindowMutationFreqTrajectories orderFront:nil]; } - (IBAction)graphMutationLossTimeHistogram:(id)sender { if (!graphWindowMutationLossTimeHistogram) graphWindowMutationLossTimeHistogram = [[self graphWindowWithTitle:@"Mutation Loss Time" viewClass:[GraphView_MutationLossTimeHistogram class]] retain]; [graphWindowMutationLossTimeHistogram orderFront:nil]; } - (IBAction)graphMutationFixationTimeHistogram:(id)sender { if (!graphWindowMutationFixationTimeHistogram) graphWindowMutationFixationTimeHistogram = [[self graphWindowWithTitle:@"Mutation Fixation Time" viewClass:[GraphView_MutationFixationTimeHistogram class]] retain]; [graphWindowMutationFixationTimeHistogram orderFront:nil]; } - (IBAction)graphFitnessOverTime:(id)sender { if (!graphWindowFitnessOverTime) graphWindowFitnessOverTime = [[self graphWindowWithTitle:@"Fitness ~ Time" viewClass:[GraphView_FitnessOverTime class]] retain]; [graphWindowFitnessOverTime orderFront:nil]; } - (IBAction)graphPopulationVisualization:(id)sender { if (!graphWindowPopulationVisualization) graphWindowPopulationVisualization = [[self graphWindowWithTitle:@"Population Visualization" viewClass:[GraphView_PopulationVisualization class]] retain]; [graphWindowPopulationVisualization orderFront:nil]; } #if (SLIMPROFILING == 1) - (void)displayProfileResults { [[NSBundle mainBundle] loadNibNamed:@"ProfileReport" owner:self topLevelObjects:NULL]; // Set up the report window attributes [profileWindow setTitle:[NSString stringWithFormat:@"Profile Report for %@", [[self window] title]]]; [profileTextView setTextContainerInset:NSMakeSize(5.0, 10.0)]; // Build the report attributed string NSMutableAttributedString *content = [[[NSMutableAttributedString alloc] init] autorelease]; NSFont *optima18b = [NSFont fontWithName:@"Optima-Bold" size:18.0]; NSFont *optima14b = [NSFont fontWithName:@"Optima-Bold" size:14.0]; NSFont *optima13 = [NSFont fontWithName:@"Optima" size:13.0]; NSFont *optima13i = [NSFont fontWithName:@"Optima-Italic" size:13.0]; NSFont *optima8 = [NSFont fontWithName:@"Optima" size:8.0]; NSFont *optima3 = [NSFont fontWithName:@"Optima" size:3.0]; NSFont *menlo11 = [NSFont fontWithName:@"Menlo" size:11.0]; NSDictionary *optima14b_d = @{NSFontAttributeName : optima14b}; NSDictionary *optima13_d = @{NSFontAttributeName : optima13}; NSDictionary *optima13i_d = @{NSFontAttributeName : optima13i}; NSDictionary *optima8_d = @{NSFontAttributeName : optima8}; NSDictionary *optima3_d = @{NSFontAttributeName : optima3}; NSDictionary *menlo11_d = @{NSFontAttributeName : menlo11}; NSDate *profileStartDate = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)community->profile_start_date]; NSDate *profileEndDate = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)community->profile_end_date]; NSString *startDateString = [NSDateFormatter localizedStringFromDate:profileStartDate dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterMediumStyle]; NSString *endDateString = [NSDateFormatter localizedStringFromDate:profileEndDate dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterMediumStyle]; double elapsedWallClockTime = (std::chrono::duration_cast(community->profile_end_clock - community->profile_start_clock).count()) / (double)1000000L; double elapsedCPUTimeInSLiM = community->profile_elapsed_CPU_clock / (double)CLOCKS_PER_SEC; double elapsedWallClockTimeInSLiM = Eidos_ElapsedProfileTime(community->profile_elapsed_wall_clock); slim_tick_t elapsedSLiMTicks = community->profile_end_tick - community->profile_start_tick; [content eidosAppendString:@"Profile Report\n" attributes:@{NSFontAttributeName : optima18b}]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:[NSString stringWithFormat:@"Model: %@\n", [[self window] title]] attributes:optima13_d]; [content eidosAppendString:@"\n" attributes:optima8_d]; [content eidosAppendString:[NSString stringWithFormat:@"Run start: %@\n", startDateString] attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"Run end: %@\n", endDateString] attributes:optima13_d]; [content eidosAppendString:@"\n" attributes:optima8_d]; #ifdef _OPENMP [content eidosAppendString:[NSString stringWithFormat:@"Maximum parallel threads: %d\n", gEidosMaxThreads] attributes:optima13_d]; [content eidosAppendString:@"\n" attributes:optima8_d]; #endif [content eidosAppendString:[NSString stringWithFormat:@"Elapsed wall clock time: %0.2f s\n", (double)elapsedWallClockTime] attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"Elapsed wall clock time inside SLiM core (corrected): %0.2f s\n", (double)elapsedWallClockTimeInSLiM] attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"Elapsed CPU time inside SLiM core (uncorrected): %0.2f s\n", (double)elapsedCPUTimeInSLiM] attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"Elapsed ticks: %d%@\n", (int)elapsedSLiMTicks, (community->profile_start_tick == 0) ? @" (including initialize)" : @""] attributes:optima13_d]; [content eidosAppendString:@"\n" attributes:optima8_d]; [content eidosAppendString:[NSString stringWithFormat:@"Profile block external overhead: %0.2f ticks (%0.4g s)\n", gEidos_ProfileOverheadTicks, gEidos_ProfileOverheadSeconds] attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"Profile block internal lag: %0.2f ticks (%0.4g s)\n", gEidos_ProfileLagTicks, gEidos_ProfileLagSeconds] attributes:optima13_d]; [content eidosAppendString:@"\n" attributes:optima8_d]; size_t total_usage = community->profile_total_memory_usage_Community.totalMemoryUsage + community->profile_total_memory_usage_AllSpecies.totalMemoryUsage; size_t average_usage = total_usage / community->total_memory_tallies_; size_t last_usage = community->profile_last_memory_usage_Community.totalMemoryUsage + community->profile_last_memory_usage_AllSpecies.totalMemoryUsage; [content eidosAppendString:[NSString stringWithFormat:@"Average tick SLiM memory use: %@\n", [NSString stringForByteCount:average_usage]] attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"Final tick SLiM memory use: %@\n", [NSString stringForByteCount:last_usage]] attributes:optima13_d]; // // Cycle stage breakdown // if (elapsedWallClockTimeInSLiM > 0.0) { bool isWF = (community->ModelType() == SLiMModelType::kModelTypeWF); double elapsedStage0Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[0]); double elapsedStage1Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[1]); double elapsedStage2Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[2]); double elapsedStage3Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[3]); double elapsedStage4Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[4]); double elapsedStage5Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[5]); double elapsedStage6Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[6]); double elapsedStage7Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[7]); double elapsedStage8Time = Eidos_ElapsedProfileTime(community->profile_stage_totals_[8]); double percentStage0 = (elapsedStage0Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage1 = (elapsedStage1Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage2 = (elapsedStage2Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage3 = (elapsedStage3Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage4 = (elapsedStage4Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage5 = (elapsedStage5Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage6 = (elapsedStage6Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage7 = (elapsedStage7Time / elapsedWallClockTimeInSLiM) * 100.0; double percentStage8 = (elapsedStage8Time / elapsedWallClockTimeInSLiM) * 100.0; int fw = 4; fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage0Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage1Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage2Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage3Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage4Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage5Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage6Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage7Time)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedStage8Time)); [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"Cycle stage breakdown\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage0Time, percentStage0] attributes:menlo11_d]; [content eidosAppendString:@" : initialize() callback execution\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage1Time, percentStage1] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 0 – first() event execution\n" : @" : stage 0 – first() event execution\n") attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage2Time, percentStage2] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 1 – early() event execution\n" : @" : stage 1 – offspring generation\n") attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage3Time, percentStage3] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 2 – offspring generation\n" : @" : stage 2 – early() event execution\n") attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage4Time, percentStage4] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 3 – generation swap\n" : @" : stage 3 – fitness calculation\n") attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage5Time, percentStage5] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 4 – bookkeeping (fixed mutation removal, etc.)\n" : @" : stage 4 – viability/survival selection\n") attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage6Time, percentStage6] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 5 – late() event execution\n" : @" : stage 5 – bookkeeping (fixed mutation removal, etc.)\n") attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage7Time, percentStage7] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 6 – fitness calculation\n" : @" : stage 6 – late() event execution\n") attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%5.2f%%)", fw, elapsedStage8Time, percentStage8] attributes:menlo11_d]; [content eidosAppendString:(isWF ? @" : stage 7 – tree sequence auto-simplification\n" : @" : stage 7 – tree sequence auto-simplification\n") attributes:optima13_d]; } // // Callback type breakdown // if (elapsedWallClockTimeInSLiM > 0.0) { double elapsedTime_first = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosEventFirst]); double elapsedTime_early = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosEventEarly]); double elapsedTime_late = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosEventLate]); double elapsedTime_initialize = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosInitializeCallback]); double elapsedTime_mutationEffect = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosMutationEffectCallback]); double elapsedTime_fitnessEffect = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosFitnessEffectCallback]); double elapsedTime_interaction = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosInteractionCallback]); double elapsedTime_matechoice = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosMateChoiceCallback]); double elapsedTime_modifychild = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosModifyChildCallback]); double elapsedTime_recombination = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosRecombinationCallback]); double elapsedTime_mutation = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosMutationCallback]); double elapsedTime_reproduction = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosReproductionCallback]); double elapsedTime_survival = Eidos_ElapsedProfileTime(community->profile_callback_totals_[(int)SLiMEidosBlockType::SLiMEidosSurvivalCallback]); double percent_first = (elapsedTime_first / elapsedWallClockTimeInSLiM) * 100.0; double percent_early = (elapsedTime_early / elapsedWallClockTimeInSLiM) * 100.0; double percent_late = (elapsedTime_late / elapsedWallClockTimeInSLiM) * 100.0; double percent_initialize = (elapsedTime_initialize / elapsedWallClockTimeInSLiM) * 100.0; double percent_fitness = (elapsedTime_mutationEffect / elapsedWallClockTimeInSLiM) * 100.0; double percent_fitnessglobal = (elapsedTime_fitnessEffect / elapsedWallClockTimeInSLiM) * 100.0; double percent_interaction = (elapsedTime_interaction / elapsedWallClockTimeInSLiM) * 100.0; double percent_matechoice = (elapsedTime_matechoice / elapsedWallClockTimeInSLiM) * 100.0; double percent_modifychild = (elapsedTime_modifychild / elapsedWallClockTimeInSLiM) * 100.0; double percent_recombination = (elapsedTime_recombination / elapsedWallClockTimeInSLiM) * 100.0; double percent_mutation = (elapsedTime_mutation / elapsedWallClockTimeInSLiM) * 100.0; double percent_reproduction = (elapsedTime_reproduction / elapsedWallClockTimeInSLiM) * 100.0; double percent_survival = (elapsedTime_survival / elapsedWallClockTimeInSLiM) * 100.0; int fw = 4, fw2 = 4; fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_first)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_early)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_late)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_initialize)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_mutationEffect)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_fitnessEffect)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_interaction)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_matechoice)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_modifychild)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_recombination)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_mutation)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_reproduction)); fw = std::max(fw, 3 + DisplayDigitsForIntegerPart(elapsedTime_survival)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_first)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_early)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_late)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_initialize)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_fitness)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_fitnessglobal)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_interaction)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_matechoice)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_modifychild)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_recombination)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_mutation)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_reproduction)); fw2 = std::max(fw2, 3 + DisplayDigitsForIntegerPart(percent_survival)); [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"Callback type breakdown\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; // Note these are out of numeric order, but in cycle stage order if (community->ModelType() == SLiMModelType::kModelTypeWF) { [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_initialize, fw2, percent_initialize] attributes:menlo11_d]; [content eidosAppendString:@" : initialize() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_first, fw2, percent_first] attributes:menlo11_d]; [content eidosAppendString:@" : first() events\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_early, fw2, percent_early] attributes:menlo11_d]; [content eidosAppendString:@" : early() events\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_matechoice, fw2, percent_matechoice] attributes:menlo11_d]; [content eidosAppendString:@" : mateChoice() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_recombination, fw2, percent_recombination] attributes:menlo11_d]; [content eidosAppendString:@" : recombination() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_mutation, fw2, percent_mutation] attributes:menlo11_d]; [content eidosAppendString:@" : mutation() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_modifychild, fw2, percent_modifychild] attributes:menlo11_d]; [content eidosAppendString:@" : modifyChild() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_late, fw2, percent_late] attributes:menlo11_d]; [content eidosAppendString:@" : late() events\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_mutationEffect, fw2, percent_fitness] attributes:menlo11_d]; [content eidosAppendString:@" : mutationEffect() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_fitnessEffect, fw2, percent_fitnessglobal] attributes:menlo11_d]; [content eidosAppendString:@" : fitnessEffect() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_interaction, fw2, percent_interaction] attributes:menlo11_d]; [content eidosAppendString:@" : interaction() callbacks\n" attributes:optima13_d]; } else { [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_initialize, fw2, percent_initialize] attributes:menlo11_d]; [content eidosAppendString:@" : initialize() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_first, fw2, percent_first] attributes:menlo11_d]; [content eidosAppendString:@" : first() events\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_reproduction, fw2, percent_reproduction] attributes:menlo11_d]; [content eidosAppendString:@" : reproduction() events\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_recombination, fw2, percent_recombination] attributes:menlo11_d]; [content eidosAppendString:@" : recombination() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_mutation, fw2, percent_mutation] attributes:menlo11_d]; [content eidosAppendString:@" : mutation() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_modifychild, fw2, percent_modifychild] attributes:menlo11_d]; [content eidosAppendString:@" : modifyChild() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_early, fw2, percent_early] attributes:menlo11_d]; [content eidosAppendString:@" : early() events\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_mutationEffect, fw2, percent_fitness] attributes:menlo11_d]; [content eidosAppendString:@" : mutationEffect() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_fitnessEffect, fw2, percent_fitnessglobal] attributes:menlo11_d]; [content eidosAppendString:@" : fitnessEffect() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_survival, fw2, percent_survival] attributes:menlo11_d]; [content eidosAppendString:@" : survival() callbacks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_late, fw2, percent_late] attributes:menlo11_d]; [content eidosAppendString:@" : late() events\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%*.2f s (%*.2f%%)", fw, elapsedTime_interaction, fw2, percent_interaction] attributes:menlo11_d]; [content eidosAppendString:@" : interaction() callbacks\n" attributes:optima13_d]; } } // // Script block profiles // if (elapsedWallClockTimeInSLiM > 0.0) { { std::vector &script_blocks = community->AllScriptBlocks(); // Convert the profile counts in all script blocks into self counts (excluding the counts of nodes below them) for (SLiMEidosBlock *script_block : script_blocks) if (script_block->type_ != SLiMEidosBlockType::SLiMEidosUserDefinedFunction) // exclude function blocks; not user-visible script_block->root_node_->ConvertProfileTotalsToSelfCounts(); } { [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"Script block profiles (as a fraction of corrected wall clock time)\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; std::vector &script_blocks = community->AllScriptBlocks(); bool firstBlock = true, hiddenInconsequentialBlocks = false; for (SLiMEidosBlock *script_block : script_blocks) { if (script_block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) continue; const EidosASTNode *profile_root = script_block->root_node_; double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts()); // relies on ConvertProfileTotalsToSelfCounts() being called above! double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0; if ((total_block_time >= 0.01) || (percent_block_time >= 0.01)) { if (!firstBlock) [content eidosAppendString:@"\n\n" attributes:menlo11_d]; firstBlock = false; const std::string &script_std_string = profile_root->token_->token_string_; NSString *script_string = [NSString stringWithUTF8String:script_std_string.c_str()]; NSMutableAttributedString *scriptAttrString = [[NSMutableAttributedString alloc] initWithString:script_string attributes:menlo11_d]; [self colorScript:scriptAttrString withProfileCountsFromNode:profile_root elapsedTime:elapsedWallClockTimeInSLiM baseIndex:profile_root->token_->token_UTF16_start_]; [content eidosAppendString:[NSString stringWithFormat:@"%0.2f s (%0.2f%%):\n", total_block_time, percent_block_time] attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content appendAttributedString:scriptAttrString]; } else hiddenInconsequentialBlocks = YES; } if (hiddenInconsequentialBlocks) { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:@"(blocks using < 0.01 s and < 0.01% of total wall clock time are not shown)" attributes:optima13i_d]; } } { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"Script block profiles (as a fraction of within-block wall clock time)\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; std::vector &script_blocks = community->AllScriptBlocks(); bool firstBlock = true, hiddenInconsequentialBlocks = false; for (SLiMEidosBlock *script_block : script_blocks) { if (script_block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) continue; const EidosASTNode *profile_root = script_block->root_node_; double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts()); // relies on ConvertProfileTotalsToSelfCounts() being called above! double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0; if ((total_block_time >= 0.01) || (percent_block_time >= 0.01)) { if (!firstBlock) [content eidosAppendString:@"\n\n" attributes:menlo11_d]; firstBlock = false; const std::string &script_std_string = profile_root->token_->token_string_; NSString *script_string = [NSString stringWithUTF8String:script_std_string.c_str()]; NSMutableAttributedString *scriptAttrString = [[NSMutableAttributedString alloc] initWithString:script_string attributes:menlo11_d]; if (total_block_time > 0.0) [self colorScript:scriptAttrString withProfileCountsFromNode:profile_root elapsedTime:total_block_time baseIndex:profile_root->token_->token_UTF16_start_]; [content eidosAppendString:[NSString stringWithFormat:@"%0.2f s (%0.2f%%):\n", total_block_time, percent_block_time] attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content appendAttributedString:scriptAttrString]; } else hiddenInconsequentialBlocks = YES; } if (hiddenInconsequentialBlocks) { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:@"(blocks using < 0.01 s and < 0.01% of total wall clock time are not shown)" attributes:optima13i_d]; } } } // // User-defined functions (if any) // if (elapsedWallClockTimeInSLiM > 0.0) { EidosFunctionMap &function_map = community->FunctionMap(); std::vector userDefinedFunctions; for (auto functionPairIter = function_map.begin(); functionPairIter != function_map.end(); ++functionPairIter) { const EidosFunctionSignature *signature = functionPairIter->second.get(); if (signature->body_script_ && signature->user_defined_) { signature->body_script_->AST()->ConvertProfileTotalsToSelfCounts(); userDefinedFunctions.emplace_back(signature); } } if (userDefinedFunctions.size()) { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"User-defined functions (as a fraction of corrected wall clock time)\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; bool firstBlock = true, hiddenInconsequentialBlocks = false; for (const EidosFunctionSignature *signature : userDefinedFunctions) { const EidosASTNode *profile_root = signature->body_script_->AST(); double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts()); // relies on ConvertProfileTotalsToSelfCounts() being called above! double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0; if ((total_block_time >= 0.01) || (percent_block_time >= 0.01)) { if (!firstBlock) [content eidosAppendString:@"\n\n" attributes:menlo11_d]; firstBlock = false; const std::string &script_std_string = profile_root->token_->token_string_; NSString *script_string = [NSString stringWithUTF8String:script_std_string.c_str()]; NSMutableAttributedString *scriptAttrString = [[NSMutableAttributedString alloc] initWithString:script_string attributes:menlo11_d]; const std::string &&signature_string = signature->SignatureString(); NSString *signatureString = [NSString stringWithUTF8String:signature_string.c_str()]; [self colorScript:scriptAttrString withProfileCountsFromNode:profile_root elapsedTime:elapsedWallClockTimeInSLiM baseIndex:profile_root->token_->token_UTF16_start_]; [content eidosAppendString:[NSString stringWithFormat:@"%0.2f s (%0.2f%%):\n", total_block_time, percent_block_time] attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:[NSString stringWithFormat:@"%@\n", signatureString] attributes:menlo11_d]; [content appendAttributedString:scriptAttrString]; } else hiddenInconsequentialBlocks = YES; } if (hiddenInconsequentialBlocks) { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:@"(functions using < 0.01 s and < 0.01% of total wall clock time are not shown)" attributes:optima13i_d]; } } if (userDefinedFunctions.size()) { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"User-defined functions (as a fraction of within-block wall clock time)\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; bool firstBlock = true, hiddenInconsequentialBlocks = false; for (const EidosFunctionSignature *signature : userDefinedFunctions) { const EidosASTNode *profile_root = signature->body_script_->AST(); double total_block_time = Eidos_ElapsedProfileTime(profile_root->TotalOfSelfCounts()); // relies on ConvertProfileTotalsToSelfCounts() being called above! double percent_block_time = (total_block_time / elapsedWallClockTimeInSLiM) * 100.0; if ((total_block_time >= 0.01) || (percent_block_time >= 0.01)) { if (!firstBlock) [content eidosAppendString:@"\n\n" attributes:menlo11_d]; firstBlock = false; const std::string &script_std_string = profile_root->token_->token_string_; NSString *script_string = [NSString stringWithUTF8String:script_std_string.c_str()]; NSMutableAttributedString *scriptAttrString = [[NSMutableAttributedString alloc] initWithString:script_string attributes:menlo11_d]; const std::string &&signature_string = signature->SignatureString(); NSString *signatureString = [NSString stringWithUTF8String:signature_string.c_str()]; if (total_block_time > 0.0) [self colorScript:scriptAttrString withProfileCountsFromNode:profile_root elapsedTime:total_block_time baseIndex:profile_root->token_->token_UTF16_start_]; [content eidosAppendString:[NSString stringWithFormat:@"%0.2f s (%0.2f%%):\n", total_block_time, percent_block_time] attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:[NSString stringWithFormat:@"%@\n", signatureString] attributes:menlo11_d]; [content appendAttributedString:scriptAttrString]; } else hiddenInconsequentialBlocks = YES; } if (hiddenInconsequentialBlocks) { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; [content eidosAppendString:@"(functions using < 0.01 s and < 0.01% of total wall clock time are not shown)" attributes:optima13i_d]; } } } #if SLIM_USE_NONNEUTRAL_CACHES // // MutationRun metrics, presented per Species // for (Species *focal_species : community->all_species_) { [content eidosAppendString:@"\n" attributes:menlo11_d]; [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"MutationRun usage" attributes:optima14b_d]; if (community->all_species_.size() > 1) { [content eidosAppendString:@" (" attributes:optima14b_d]; [content eidosAppendString:[NSString stringWithUTF8String:focal_species->avatar_.c_str()] attributes:optima14b_d]; [content eidosAppendString:@" " attributes:optima14b_d]; [content eidosAppendString:[NSString stringWithUTF8String:focal_species->name_.c_str()] attributes:optima14b_d]; [content eidosAppendString:@")" attributes:optima14b_d]; } [content eidosAppendString:@"\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; if (!focal_species->HasGenetics()) { [content eidosAppendString:@"(omitted for no-genetics species)" attributes:optima13i_d]; continue; } { int64_t regime_tallies[3]; int64_t regime_tallies_total = (int)focal_species->profile_nonneutral_regime_history_.size(); for (int regime = 0; regime < 3; ++regime) regime_tallies[regime] = 0; for (int32_t regime : focal_species->profile_nonneutral_regime_history_) if ((regime >= 1) && (regime <= 3)) regime_tallies[regime - 1]++; else regime_tallies_total--; for (int regime = 0; regime < 3; ++regime) { [content eidosAppendString:[NSString stringWithFormat:@"%6.2f%%", (regime_tallies[regime] / (double)regime_tallies_total) * 100.0] attributes:menlo11_d]; [content eidosAppendString:[NSString stringWithFormat:@" of ticks : regime %d (%@)\n", regime + 1, (regime == 0 ? @"no mutationEffect() callbacks" : (regime == 1 ? @"constant neutral mutationEffect() callbacks only" : @"unpredictable mutationEffect() callbacks present"))] attributes:optima13_d]; } [content eidosAppendString:@"\n" attributes:optima8_d]; } [content eidosAppendString:[NSString stringWithFormat:@"%lld", (long long int)focal_species->profile_max_mutation_index_] attributes:menlo11_d]; [content eidosAppendString:@" maximum simultaneous mutations\n" attributes:optima13_d]; const std::vector &chromosomes = focal_species->Chromosomes(); for (Chromosome *focal_chromosome : chromosomes) { [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"Chromosome " attributes:optima13i_d]; [content eidosAppendString:[NSString stringWithUTF8String:focal_chromosome->Symbol().c_str()] attributes:optima13i_d]; [content eidosAppendString:@":\n" attributes:optima13i_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; { int64_t power_tallies[20]; // we only go up to 1024 mutruns right now, but this gives us some headroom int64_t power_tallies_total = (int)focal_chromosome->profile_mutcount_history_.size(); for (int power = 0; power < 20; ++power) power_tallies[power] = 0; for (int32_t count : focal_chromosome->profile_mutcount_history_) { int power = (int)round(log2(count)); power_tallies[power]++; } for (int power = 0; power < 20; ++power) { if (power_tallies[power] > 0) { [content eidosAppendString:[NSString stringWithFormat:@"%6.2f%%", (power_tallies[power] / (double)power_tallies_total) * 100.0] attributes:menlo11_d]; [content eidosAppendString:[NSString stringWithFormat:@" of ticks : %d mutation runs per haplosome\n", (int)(round(pow(2.0, power)))] attributes:optima13_d]; } } } [content eidosAppendString:@"\n" attributes:optima8_d]; [content eidosAppendString:[NSString stringWithFormat:@"%lld", (long long int)focal_chromosome->profile_mutation_total_usage_] attributes:menlo11_d]; [content eidosAppendString:@" mutations referenced, summed across all ticks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%lld", (long long int)focal_chromosome->profile_nonneutral_mutation_total_] attributes:menlo11_d]; [content eidosAppendString:@" mutations considered potentially nonneutral\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%0.2f%%", ((focal_chromosome->profile_mutation_total_usage_ - focal_chromosome->profile_nonneutral_mutation_total_) / (double)focal_chromosome->profile_mutation_total_usage_) * 100.0] attributes:menlo11_d]; [content eidosAppendString:@" of mutations excluded from fitness calculations\n" attributes:optima13_d]; [content eidosAppendString:@"\n" attributes:optima8_d]; [content eidosAppendString:[NSString stringWithFormat:@"%lld", (long long int)focal_chromosome->profile_mutrun_total_usage_] attributes:menlo11_d]; [content eidosAppendString:@" mutation runs referenced, summed across all ticks\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%lld", (long long int)focal_chromosome->profile_unique_mutrun_total_] attributes:menlo11_d]; [content eidosAppendString:@" unique mutation runs maintained among those\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%6.2f%%", (focal_chromosome->profile_mutrun_nonneutral_recache_total_ / (double)focal_chromosome->profile_unique_mutrun_total_) * 100.0] attributes:menlo11_d]; [content eidosAppendString:@" of mutation run nonneutral caches rebuilt per tick\n" attributes:optima13_d]; [content eidosAppendString:[NSString stringWithFormat:@"%6.2f%%", ((focal_chromosome->profile_mutrun_total_usage_ - focal_chromosome->profile_unique_mutrun_total_) / (double)focal_chromosome->profile_mutrun_total_usage_) * 100.0] attributes:menlo11_d]; [content eidosAppendString:@" of mutation runs shared among haplosomes\n" attributes:optima13_d]; } } #endif { // // Memory usage metrics // SLiMMemoryUsage_Community &mem_tot_C = community->profile_total_memory_usage_Community; SLiMMemoryUsage_Species &mem_tot_S = community->profile_total_memory_usage_AllSpecies; SLiMMemoryUsage_Community &mem_last_C = community->profile_last_memory_usage_Community; SLiMMemoryUsage_Species &mem_last_S = community->profile_last_memory_usage_AllSpecies; int64_t div = community->total_memory_tallies_; double ddiv = community->total_memory_tallies_; double average_total = (mem_tot_C.totalMemoryUsage + mem_tot_S.totalMemoryUsage) / ddiv; double final_total = mem_last_C.totalMemoryUsage + mem_last_S.totalMemoryUsage; [content eidosAppendString:@"\n" attributes:optima13_d]; [content eidosAppendString:@"SLiM memory usage (average / final tick)\n" attributes:optima14b_d]; [content eidosAppendString:@"\n" attributes:optima3_d]; // Chromosome [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.chromosomeObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.chromosomeObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : Chromosome objects (%0.2f / %lld)\n", mem_tot_S.chromosomeObjects_count / ddiv, (long long int)mem_last_S.chromosomeObjects_count] attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.chromosomeMutationRateMaps / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.chromosomeMutationRateMaps total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : mutation rate maps\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.chromosomeRecombinationRateMaps / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.chromosomeRecombinationRateMaps total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : recombination rate maps\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.chromosomeAncestralSequence / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.chromosomeAncestralSequence total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : ancestral nucleotides\n" attributes:optima13_d]; // Community [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.communityObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.communityObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : Community object\n" attributes:optima13_d]; // Haplosome [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.haplosomeObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.haplosomeObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : Haplosome objects (%0.2f / %lld)\n", mem_tot_S.haplosomeObjects_count / ddiv, (long long int)mem_last_S.haplosomeObjects_count] attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.haplosomeExternalBuffers / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.haplosomeExternalBuffers total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : external MutationRun* buffers\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.haplosomeUnusedPoolSpace / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.haplosomeUnusedPoolSpace total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : unused pool space\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.haplosomeUnusedPoolBuffers / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.haplosomeUnusedPoolBuffers total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : unused pool buffers\n" attributes:optima13_d]; // GenomicElement [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.genomicElementObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.genomicElementObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : GenomicElement objects (%0.2f / %lld)\n", mem_tot_S.genomicElementObjects_count / ddiv, (long long int)mem_last_S.genomicElementObjects_count] attributes:optima13_d]; // GenomicElementType [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.genomicElementTypeObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.genomicElementTypeObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : GenomicElementType objects (%0.2f / %lld)\n", mem_tot_S.genomicElementTypeObjects_count / ddiv, (long long int)mem_last_S.genomicElementTypeObjects_count] attributes:optima13_d]; // Individual [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.individualObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.individualObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : Individual objects (%0.2f / %lld)\n", mem_tot_S.individualObjects_count / ddiv, (long long int)mem_last_S.individualObjects_count] attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.individualHaplosomeVectors / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.individualHaplosomeVectors total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : external Haplosome* buffers\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.individualJunkyardAndHaplosomes / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.individualJunkyardAndHaplosomes total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : individuals awaiting reuse\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.individualUnusedPoolSpace / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.individualUnusedPoolSpace total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : unused pool space\n" attributes:optima13_d]; // InteractionType [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.interactionTypeObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.interactionTypeObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : InteractionType objects (%0.2f / %lld)\n", mem_tot_C.interactionTypeObjects_count / ddiv, (long long int)mem_last_C.interactionTypeObjects_count] attributes:optima13_d]; if (mem_tot_C.interactionTypeObjects_count || mem_last_C.interactionTypeObjects_count) { [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.interactionTypeKDTrees / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.interactionTypeKDTrees total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : k-d trees\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.interactionTypePositionCaches / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.interactionTypePositionCaches total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : position caches\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.interactionTypeSparseVectorPool / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.interactionTypeSparseVectorPool total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : sparse arrays\n" attributes:optima13_d]; } // Mutation [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.mutationObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.mutationObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : Mutation objects (%0.2f / %lld)\n", mem_tot_S.mutationObjects_count / ddiv, (long long int)mem_last_S.mutationObjects_count] attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.mutationRefcountBuffer / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.mutationRefcountBuffer total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : refcount buffer\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.mutationUnusedPoolSpace / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.mutationUnusedPoolSpace total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : unused pool space\n" attributes:optima13_d]; // MutationRun [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.mutationRunObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.mutationRunObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : MutationRun objects (%0.2f / %lld)\n", mem_tot_S.mutationRunObjects_count / ddiv, (long long int)mem_last_S.mutationRunObjects_count] attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.mutationRunExternalBuffers / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.mutationRunExternalBuffers total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : external MutationIndex buffers\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.mutationRunNonneutralCaches / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.mutationRunNonneutralCaches total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : nonneutral mutation caches\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.mutationRunUnusedPoolSpace / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.mutationRunUnusedPoolSpace total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : unused pool space\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.mutationRunUnusedPoolBuffers / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.mutationRunUnusedPoolBuffers total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : unused pool buffers\n" attributes:optima13_d]; // MutationType [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.mutationTypeObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.mutationTypeObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : MutationType objects (%0.2f / %lld)\n", mem_tot_S.mutationTypeObjects_count / ddiv, (long long int)mem_last_S.mutationTypeObjects_count] attributes:optima13_d]; // Species [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.speciesObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.speciesObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : Species objects (%0.2f / %lld)\n", mem_tot_S.speciesObjects_count / ddiv, (long long int)mem_last_S.speciesObjects_count] attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.speciesTreeSeqTables / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.speciesTreeSeqTables total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : tree-sequence tables\n" attributes:optima13_d]; // Subpopulation [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.subpopulationObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.subpopulationObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : Subpopulation objects (%0.2f / %lld)\n", mem_tot_S.subpopulationObjects_count / ddiv, (long long int)mem_last_S.subpopulationObjects_count] attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.subpopulationFitnessCaches / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.subpopulationFitnessCaches total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : fitness caches\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.subpopulationParentTables / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.subpopulationParentTables total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : parent tables\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.subpopulationSpatialMaps / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.subpopulationSpatialMaps total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : spatial maps\n" attributes:optima13_d]; if (mem_tot_S.subpopulationSpatialMapsDisplay || mem_last_S.subpopulationSpatialMapsDisplay) { [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.subpopulationSpatialMapsDisplay / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.subpopulationSpatialMapsDisplay total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : spatial map display (SLiMgui only)\n" attributes:optima13_d]; } // Substitution [content eidosAppendString:@"\n" attributes:optima8_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_S.substitutionObjects / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_S.substitutionObjects total:final_total attributes:menlo11_d]]; [content eidosAppendString:[NSString stringWithFormat:@" : Substitution objects (%0.2f / %lld)\n", mem_tot_S.substitutionObjects_count / ddiv, (long long int)mem_last_S.substitutionObjects_count] attributes:optima13_d]; // Eidos [content eidosAppendString:@"\n" attributes:optima8_d]; [content eidosAppendString:@"Eidos:\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.eidosASTNodePool / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.eidosASTNodePool total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : EidosASTNode pool\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.eidosSymbolTablePool / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.eidosSymbolTablePool total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : EidosSymbolTable pool\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.eidosValuePool / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.eidosValuePool total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : EidosValue pool\n" attributes:optima13_d]; [content eidosAppendString:@" " attributes:menlo11_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_tot_C.fileBuffers / div total:average_total attributes:menlo11_d]]; [content eidosAppendString:@" / " attributes:optima13_d]; [content appendAttributedString:[NSAttributedString attributedStringForByteCount:mem_last_C.fileBuffers total:final_total attributes:menlo11_d]]; [content eidosAppendString:@" : File buffers" attributes:optima13_d]; } // Set the attributed string into the profile report text view NSTextStorage *ts = [profileTextView textStorage]; [ts beginEditing]; [ts setAttributedString:content]; [ts endEditing]; // Show the window and give it ownership over itself [profileWindow makeKeyAndOrderFront:nil]; [profileWindow retain]; // it is set to release itself when closed // We use one nib for all profile reports, so clear our outlet; the profile now lives as its own independent window profileWindow = nil; profileTextView = nil; } - (void)colorScript:(NSMutableAttributedString *)script withProfileCountsFromNode:(const EidosASTNode *)node elapsedTime:(double)elapsedTime baseIndex:(int32_t)baseIndex { // First color the range for this node eidos_profile_t count = node->profile_total_; if (count > 0) { int32_t start = 0, end = 0; node->FullUTF16Range(&start, &end); start -= baseIndex; end -= baseIndex; NSRange range = NSMakeRange(start, end - start + 1); NSColor *backgroundColor = [NSColor slimColorForFraction:Eidos_ElapsedProfileTime(count) / elapsedTime]; [script addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range]; } // Then let child nodes color for (const EidosASTNode *child : node->children_) [self colorScript:script withProfileCountsFromNode:child elapsedTime:elapsedTime baseIndex:baseIndex]; } #endif // (SLIMPROFILING == 1) - (BOOL)runSimOneTick { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status // This method should always be used when calling out to run the simulation, because it swaps the correct random number // generator stuff in and out bracketing the call to RunOneTick(). This bracketing would need to be done around // any other call out to the simulation that caused it to use random numbers, too, such as subsample output. BOOL stillRunning = YES; [self eidosConsoleWindowControllerWillExecuteScript:_consoleController]; #if (SLIMPROFILING == 1) if (profilePlayOn) { // We put the wall clock measurements on the inside since we want those to be maximally accurate, // as profile report percentages are fractions of the total elapsed wall clock time. std::clock_t startCPUClock = std::clock(); SLIM_PROFILE_BLOCK_START(); stillRunning = community->RunOneTick(); SLIM_PROFILE_BLOCK_END(community->profile_elapsed_wall_clock); std::clock_t endCPUClock = std::clock(); community->profile_elapsed_CPU_clock += (endCPUClock - startCPUClock); } else #endif { stillRunning = community->RunOneTick(); } [self eidosConsoleWindowControllerDidExecuteScript:_consoleController]; // We also want to let graphViews know when each tick has finished, in case they need to pull data from the sim. Note this // happens after every tick, not just when we are updating the UI, so drawing and setNeedsDisplay: should not happen here. [self sendAllLinkedViewsSelector:@selector(controllerTickFinished)]; return stillRunning; } - (IBAction)playOneStep:(id)sender { if (!invalidSimulation) { [_consoleController invalidateSymbolTableAndFunctionMap]; [self setReachedSimulationEnd:![self runSimOneTick]]; [_consoleController validateSymbolTableAndFunctionMap]; [self updateAfterTickFull:YES]; } } - (void)_continuousPlay:(id)sender { // NOTE this code is parallel to the code in _continuousProfile: if (!invalidSimulation) { NSDate *startDate = [NSDate date]; double speedSliderValue = [playSpeedSlider doubleValue]; double intervalSinceStarting = -[continuousPlayStartDate timeIntervalSinceNow]; // Calculate frames per second; this equation must match the equation in playSpeedChanged: double maxTicksPerSecond = 1000000000.0; // bounded, to allow -eidos_pauseExecution to interrupt us if (speedSliderValue < 0.99999) maxTicksPerSecond = (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * 839; //NSLog(@"speedSliderValue == %f, maxTicksPerSecond == %f", speedSliderValue, maxTicksPerSecond); // We keep a local version of reachedSimulationEnd, because calling setReachedSimulationEnd: every tick // can actually be a large drag for simulations that run extremely quickly – it can actually exceed the time // spent running the simulation itself! Moral of the story, KVO is wicked slow. BOOL reachedEnd = reachedSimulationEnd; do { if (continuousPlayTicksCompleted / intervalSinceStarting >= maxTicksPerSecond) break; @autoreleasepool { reachedEnd = ![self runSimOneTick]; } continuousPlayTicksCompleted++; } while (!reachedEnd && (-[startDate timeIntervalSinceNow] < 0.02)); [self setReachedSimulationEnd:reachedEnd]; if (!reachedSimulationEnd) { [self updateAfterTickFull:(-[startDate timeIntervalSinceNow] > 0.04)]; // if too much time has elapsed, do a full update [self performSelector:@selector(_continuousPlay:) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; } else { // stop playing [self updateAfterTickFull:YES]; [playButton setState:NSOffState]; [self play:nil]; // bounce our icon; if we are not the active app, to signal that the run is done [NSApp requestUserAttention:NSInformationalRequest]; } } } - (void)_continuousProfile:(id)sender { // NOTE this code is parallel to the code in _continuousPlay: if (!invalidSimulation) { NSDate *startDate = [NSDate date]; // We keep a local version of reachedSimulationEnd, because calling setReachedSimulationEnd: every tick // can actually be a large drag for simulations that run extremely quickly – it can actually exceed the time // spent running the simulation itself! Moral of the story, KVO is wicked slow. BOOL reachedEnd = reachedSimulationEnd; if (!reachedEnd) { do { @autoreleasepool { reachedEnd = ![self runSimOneTick]; } continuousPlayTicksCompleted++; } while (!reachedEnd && (-[startDate timeIntervalSinceNow] < 0.02)); [self setReachedSimulationEnd:reachedEnd]; } if (!reachedSimulationEnd) { [self updateAfterTickFull:(-[startDate timeIntervalSinceNow] > 0.04)]; // if too much time has elapsed, do a full update [self performSelector:@selector(_continuousProfile:) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; } else { // stop profiling [self updateAfterTickFull:YES]; [profileButton setState:NSOffState]; [self profile:nil]; // bounce our icon; if we are not the active app, to signal that the run is done [NSApp requestUserAttention:NSInformationalRequest]; } } } - (void)forceImmediateMenuUpdate { // So, the situation is that the simulation has stopped playing because the end of the simulation was reached. If that happens while the user // is tracking a menu in the menu bar, the menus do not get updating (enabling, titles) until the user ends menu tracking. This is sort of a // bug in Cocoa, I guess; it assumes that state that influences menu bar items does not change while the user is tracking menus, which is false // in our case, but is true for most applications, I suppose, maybe. Anyway, we need to force an immediate update in this situation. NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu]; NSInteger numberOfSubmenus = [mainMenu numberOfItems]; for (int itemIndex = 0; itemIndex < numberOfSubmenus; ++itemIndex) { NSMenuItem *menuItem = [mainMenu itemAtIndex:itemIndex]; NSMenu *submenu = [menuItem submenu]; [submenu update]; } // Same for our context menus [graphCommandsMenu update]; } - (void)placeSubview:(NSView *)topView aboveSubview:(NSView *)bottomView { NSView *topSuperview = [topView superview]; NSView *bottomSuperview = [bottomView superview]; if (topSuperview == bottomSuperview) { NSMutableArray *subviews = [NSMutableArray arrayWithArray:[topSuperview subviews]]; [topView retain]; [subviews removeObject:topView]; NSUInteger bottomIndex = [subviews indexOfObjectIdenticalTo:bottomView]; [subviews insertObject:topView atIndex:bottomIndex + 1]; [topView release]; [topSuperview setSubviews:subviews]; } } - (void)playOrProfile:(BOOL)isPlayAction { BOOL isProfileAction = !isPlayAction; // to avoid having to think in negatives #if DEBUG if (isProfileAction) { [profileButton setState:NSOffState]; NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleWarning]; [alert setMessageText:@"Release build required"]; [alert setInformativeText:@"In order to obtain accurate timing information that is relevant to the actual runtime of a model, profiling requires that you are running a Release build of SLiMgui. If you are running SLiMgui from within Xcode, please choose the Release build configuration from the Edit Scheme panel."]; [alert addButtonWithTitle:@"OK"]; [alert setShowsSuppressionButton:NO]; [alert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { [alert autorelease]; }]; return; } #endif #if (SLIMPROFILING != 1) if (isProfileAction) { [profileButton setState:NSOffState]; NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleWarning]; [alert setMessageText:@"Profiling disabled"]; [alert setInformativeText:@"Profiling has been disabled in this build of SLiMgui. If you are running SLiMgui from within Xcode, please change the definition of SLIMPROFILING to 1 in the SLiMgui target."]; [alert addButtonWithTitle:@"OK"]; [alert setShowsSuppressionButton:NO]; [alert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { [alert autorelease]; }]; return; } #endif if (!continuousPlayOn) { // keep the button on; this works for the button itself automatically, but when the menu item is chosen this is needed if (isProfileAction) { [profileButton setState:NSOnState]; [profileButton slimSetTintColor:[NSColor colorWithCalibratedHue:0.0 saturation:0.5 brightness:1.0 alpha:1.0]]; } else { [playButton setState:NSOnState]; [self placeSubview:playButton aboveSubview:profileButton]; } // log information needed to track our play speed [continuousPlayStartDate release]; continuousPlayStartDate = [[NSDate date] retain]; continuousPlayTicksCompleted = 0; [self setContinuousPlayOn:YES]; isProfileAction ? [self setProfilePlayOn:YES] : [self setNonProfilePlayOn:YES]; // invalidate the console symbols, and don't validate them until we are done [_consoleController invalidateSymbolTableAndFunctionMap]; #if (SLIMPROFILING == 1) // prepare profiling information if necessary if (isProfileAction) community->StartProfiling(); #endif // start playing/profiling if (isPlayAction) [self performSelector:@selector(_continuousPlay:) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; else [self performSelector:@selector(_continuousProfile:) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; } else { #if (SLIMPROFILING == 1) // close out profiling information if necessary if (isProfileAction && community && !invalidSimulation) community->StopProfiling(); #endif // keep the button off; this works for the button itself automatically, but when the menu item is chosen this is needed if (isProfileAction) { [profileButton setState:NSOffState]; [profileButton slimSetTintColor:nil]; } else { [playButton setState:NSOffState]; [self placeSubview:profileButton aboveSubview:playButton]; } // stop our recurring perform request if (isPlayAction) [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_continuousPlay:) object:nil]; else [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_continuousProfile:) object:nil]; [self setContinuousPlayOn:NO]; isProfileAction ? [self setProfilePlayOn:NO] : [self setNonProfilePlayOn:NO]; [_consoleController validateSymbolTableAndFunctionMap]; [self updateAfterTickFull:YES]; // Work around a bug that when the simulation ends during menu tracking, menus do not update until menu tracking finishes if ([self reachedSimulationEnd]) [self forceImmediateMenuUpdate]; #if (SLIMPROFILING == 1) // If we just finished profiling, display a report if (isProfileAction && community && !invalidSimulation) [self displayProfileResults]; #endif } } - (IBAction)play:(id)sender { [self playOrProfile:YES]; } - (IBAction)profile:(id)sender { [self playOrProfile:NO]; } - (void)_tickPlay:(id)sender { // FIXME would be nice to have a way to stop this prematurely, if an incorrect tick is entered or whatever... BCH 2 Nov. 2017 if (!invalidSimulation) { NSDate *startDate = [NSDate date]; // We keep a local version of reachedSimulationEnd, because calling setReachedSimulationEnd: every tick // can actually be a large drag for simulations that run extremely quickly – it can actually exceed the time // spent running the simulation itself! Moral of the story, KVO is wicked slow. BOOL reachedEnd = reachedSimulationEnd; do { if (community->tick_ >= targetTick) break; @autoreleasepool { reachedEnd = ![self runSimOneTick]; } } while (!reachedEnd && (-[startDate timeIntervalSinceNow] < 0.02)); [self setReachedSimulationEnd:reachedEnd]; if (!reachedSimulationEnd && !(community->tick_ >= targetTick)) { [self updateAfterTickFull:(-[startDate timeIntervalSinceNow] > 0.04)]; // if too much time has elapsed, do a full update [self performSelector:@selector(_tickPlay:) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; } else { // stop playing [self updateAfterTickFull:YES]; [self tickChanged:nil]; // bounce our icon; if we are not the active app, to signal that the run is done [NSApp requestUserAttention:NSInformationalRequest]; } } } - (IBAction)tickChanged:(id)sender { if (!tickPlayOn) { NSString *tickString = [tickTextField stringValue]; // Special-case initialize(); we can never advance to it, since it is first, so we just validate it if ([tickString isEqualToString:@"initialize()"]) { if (community->tick_ != 0) { NSBeep(); [self updateTickCounter]; } return; } // Get the integer value from the textfield, since it is not "initialize()". I would like the method used here to be // -longLongValue, but that method does not presently exist on NSControl. [tickString longLongValue] does not // work properly with the formatter; the formatter adds commas and longLongValue doesn't understand them. Whatever. targetTick = SLiMClampToTickType([tickTextField integerValue]); // make sure the requested tick is in range if (community->tick_ >= targetTick) { if (community->tick_ > targetTick) NSBeep(); return; } // update UI [tickProgressIndicator startAnimation:nil]; [self setTickPlayOn:YES]; // invalidate the console symbols, and don't validate them until we are done [_consoleController invalidateSymbolTableAndFunctionMap]; // get the first responder out of the tick textfield [[self window] performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0.0]; // start playing [self performSelector:@selector(_tickPlay:) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; } else { // stop our recurring perform request [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_tickPlay:) object:nil]; [self setTickPlayOn:NO]; [tickProgressIndicator stopAnimation:nil]; [_consoleController validateSymbolTableAndFunctionMap]; // Work around a bug that when the simulation ends during menu tracking, menus do not update until menu tracking finishes if ([self reachedSimulationEnd]) [self forceImmediateMenuUpdate]; } } - (IBAction)recycle:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status [_consoleController invalidateSymbolTableAndFunctionMap]; [self clearOutput:nil]; [self setScriptStringAndInitializeSimulation:[scriptTextView string]]; [_consoleController validateSymbolTableAndFunctionMap]; [self updateAfterTickFull:YES]; // A bit of playing with undo. We want to break undo coalescing at the point of recycling, so that undo and redo stop // at the moment that we recycled. Then we reset a change counter that we use to know if we have changed relative to // the recycle point, so we can highlight the recycle button to show that the executing script is out of date. [scriptTextView breakUndoCoalescing]; [[self document] resetSLiMChangeCount]; // Update the status field so that if the selection is in an initialize...() function the right signature is shown. This call would // be more technically correct in -updateAfterTick, but I don't want the tokenization overhead there, it's too heavyweight. The // only negative consequence of not having it there is that when the user steps out of initialization time into tick 1, an // initialize...() function signature may persist in the status bar that should have been changed to "unrecognized call" - no biggie. // BCH 31 May 2016: commenting this out since the status bar is no longer dependent on the simulation state. We want the // initialize() functions to show their prototypes in the status bar whether we are in the zero tick or not. //[self updateStatusFieldFromSelection]; [self sendAllLinkedViewsSelector:@selector(controllerRecycled)]; } - (IBAction)playSpeedChanged:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status // We want our speed to be from the point when the slider changed, not from when play started [continuousPlayStartDate release]; continuousPlayStartDate = [[NSDate date] retain]; continuousPlayTicksCompleted = 1; // this prevents a new tick from executing every time the slider moves a pixel // This method is called whenever playSpeedSlider changes, continuously; we want to show the chosen speed in a tooltip-ish window double speedSliderValue = [playSpeedSlider doubleValue]; // Calculate frames per second; this equation must match the equation in _continuousPlay: double maxTicksPerSecond = INFINITY; if (speedSliderValue < 0.99999) maxTicksPerSecond = (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * (speedSliderValue + 0.06) * 839; // Make a tooltip label string NSString *fpsString= @"∞ fps"; if (!isinf(maxTicksPerSecond)) { if (maxTicksPerSecond < 1.0) fpsString = [NSString stringWithFormat:@"%.2f fps", maxTicksPerSecond]; else if (maxTicksPerSecond < 10.0) fpsString = [NSString stringWithFormat:@"%.1f fps", maxTicksPerSecond]; else fpsString = [NSString stringWithFormat:@"%.0f fps", maxTicksPerSecond]; //NSLog(@"fps string: %@", fpsString); } // Calculate the tooltip origin; this is adjusted for the metrics on OS X 10.12 NSRect sliderRect = [playSpeedSlider alignmentRectForFrame:[playSpeedSlider frame]]; NSPoint tipPoint = sliderRect.origin; tipPoint.x += 5 + round((sliderRect.size.width - 11.0) * speedSliderValue); tipPoint.y += 10; tipPoint = [[playSpeedSlider superview] convertPoint:tipPoint toView:nil]; NSRect tipRect = NSMakeRect(tipPoint.x, tipPoint.y, 0, 0); tipPoint = [[playSpeedSlider window] convertRectToScreen:tipRect].origin; // Make the tooltip window, configure it, and display it if (!playSpeedToolTipWindow) playSpeedToolTipWindow = [SLiMPlaySliderToolTipWindow new]; [playSpeedToolTipWindow setLabel:fpsString]; [playSpeedToolTipWindow setTipPoint:tipPoint]; [playSpeedToolTipWindow orderFront:nil]; // Schedule a hide of the tooltip; this runs only once we're out of the tracking loop, conveniently [NSObject cancelPreviousPerformRequestsWithTarget:playSpeedToolTipWindow selector:@selector(orderOut:) object:nil]; [playSpeedToolTipWindow performSelector:@selector(orderOut:) withObject:nil afterDelay:0.01]; } - (BOOL)checkScriptSuppressSuccessResponse:(BOOL)suppressSuccessResponse { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status // Note this does *not* check out scriptString, which represents the state of the script when the Community object was created // Instead, it checks the current script in the script TextView – which is not used for anything until the recycle button is clicked. const char *cstr = [[scriptTextView string] UTF8String]; NSString *errorDiagnostic = nil; if (!cstr) { errorDiagnostic = [@"The script string could not be read, possibly due to an encoding problem." retain]; } else { SLiMEidosScript script(cstr); try { script.Tokenize(); script.ParseSLiMFileToAST(); } catch (...) { std::string &&error_diagnostic = Eidos_GetTrimmedRaiseMessage(); errorDiagnostic = [[NSString stringWithUTF8String:error_diagnostic.c_str()] retain]; } } BOOL checkDidSucceed = !errorDiagnostic; if (!checkDidSucceed || !suppressSuccessResponse) { // use our ConsoleWindowController delegate method to play the appropriate sound [self eidosConsoleWindowController:_consoleController checkScriptDidSucceed:checkDidSucceed]; if (!checkDidSucceed) { // On failure, we show an alert describing the error, and highlight the relevant script line NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleWarning]; [alert setMessageText:@"Script error"]; [alert setInformativeText:errorDiagnostic]; [alert addButtonWithTitle:@"OK"]; [alert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { [alert autorelease]; }]; [scriptTextView selectErrorRange]; // Show the error in the status bar also NSString *trimmedError = [errorDiagnostic stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSDictionary *errorAttrs = [NSDictionary eidosTextAttributesWithColor:[NSColor redColor] size:11.0]; NSMutableAttributedString *errorAttrString = [[[NSMutableAttributedString alloc] initWithString:trimmedError attributes:errorAttrs] autorelease]; [errorAttrString addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:2.0] range:NSMakeRange(0, [errorAttrString length])]; [scriptStatusTextField setAttributedStringValue:errorAttrString]; } else { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // On success, we optionally show a success alert sheet if (![defaults boolForKey:EidosDefaultsSuppressScriptCheckSuccessPanelKey]) { NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleInformational]; [alert setMessageText:@"No script errors"]; [alert setInformativeText:@"No errors found."]; [alert addButtonWithTitle:@"OK"]; [alert setShowsSuppressionButton:YES]; [alert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { if ([[alert suppressionButton] state] == NSOnState) [defaults setBool:YES forKey:EidosDefaultsSuppressScriptCheckSuccessPanelKey]; [alert autorelease]; }]; } } } [errorDiagnostic release]; return checkDidSucceed; } - (IBAction)checkScript:(id)sender { [self checkScriptSuppressSuccessResponse:NO]; } - (IBAction)prettyprintScript:(id)sender { if ([scriptTextView isEditable]) { if ([self checkScriptSuppressSuccessResponse:YES]) { // We know the script is syntactically correct, so we can tokenize and parse it without worries const char *cstr = [[scriptTextView string] UTF8String]; SLiMEidosScript script(cstr); // SLiMEidosScript does not override Tokenize(), but it could... script.Tokenize(false, true); // get whitespace and comment tokens // Then generate a new script string that is prettyprinted const std::vector &tokens = script.Tokens(); NSMutableString *pretty = [NSMutableString string]; if ([EidosPrettyprinter prettyprintTokens:tokens fromScript:script intoString:pretty]) { if ([scriptTextView shouldChangeTextInRange:NSMakeRange(0, [[scriptTextView string] length]) replacementString:pretty]) { [scriptTextView setString:pretty]; [scriptTextView didChangeText]; } else { NSBeep(); } } else NSBeep(); } } else { NSBeep(); } } - (IBAction)showScriptHelp:(id)sender { [_consoleController showScriptHelp:sender]; } - (IBAction)toggleConsoleVisibility:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status [_consoleController toggleConsoleVisibility:sender]; } - (IBAction)toggleBrowserVisibility:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status [[_consoleController browserController] toggleBrowserVisibility:self]; } - (IBAction)clearOutput:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status [outputTextView setString:@""]; // Just in case we have any buffered output, clear the output stream gSLiMOut.clear(); gSLiMOut.str(""); gSLiMError.clear(); gSLiMError.str(""); } - (IBAction)dumpPopulationToOutput:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status try { // BCH 3/6/2022: Note that the species cycle has been added here for SLiM 4, in keeping with SLiM's native output formats. Species *displaySpecies = [self focalDisplaySpecies]; slim_tick_t species_cycle = displaySpecies->Cycle(); // dump the population: output spatial positions and ages and tags if available, but not ancestral sequence or substitutions Individual::PrintIndividuals_SLiM(SLIM_OUTSTREAM, nullptr, 0, *displaySpecies, true, true, false, false, true, false, /* p_focal_chromosome */ nullptr); // dump fixed substitutions also; so the dump in SLiMgui is like outputFull() + outputFixedMutations() SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "#OUT: " << community->tick_ << " " << species_cycle << " F " << std::endl; SLIM_OUTSTREAM << "Mutations:" << std::endl; for (unsigned int i = 0; i < displaySpecies->population_.substitutions_.size(); i++) { SLIM_OUTSTREAM << i << " "; displaySpecies->population_.substitutions_[i]->PrintForSLiMOutput_Tag(SLIM_OUTSTREAM); } // now send SLIM_OUTSTREAM to the output textview [self updateOutputTextView]; } catch (...) { } } - (IBAction)changeWorkingDirectory:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status NSOpenPanel *op = [[NSOpenPanel openPanel] retain]; [op setTitle:@"Choose Working Directory"]; [op setNameFieldLabel:@"Directory:"]; [op setMessage:@"Choose a current working directory for this model:"]; [op setExtensionHidden:NO]; [op setCanChooseFiles:NO]; [op setCanChooseDirectories:YES]; [op setCanCreateDirectories:YES]; // try to make the panel start in the current working directory (not the last requested dir, the actual dir) std::string cwd = sim_working_dir; NSString *cwd_string = [NSString stringWithUTF8String:cwd.c_str()]; NSString *expanded_path = [cwd_string stringByStandardizingPath]; NSURL *url = [NSURL fileURLWithPath:expanded_path]; [op setDirectoryURL:url]; [op beginSheetModalForWindow:[self window] completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { NSURL *fileURL = [op URL]; const char *filePath = [fileURL fileSystemRepresentation]; sim_working_dir = filePath; sim_requested_working_dir = sim_working_dir; } }]; } - (IBAction)showRecombinationIntervalsButtonToggled:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status BOOL newValue = ([showRecombinationIntervalsButton state] == NSOnState); if (newValue != zoomedChromosomeShowsRateMaps) { zoomedChromosomeShowsRateMaps = newValue; [chromosomeZoomed setShouldDrawRateMaps:zoomedChromosomeShowsRateMaps]; [chromosomeZoomed setNeedsDisplayInInterior]; } } - (IBAction)showGenomicElementsButtonToggled:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status BOOL newValue = ([showGenomicElementsButton state] == NSOnState); if (newValue != zoomedChromosomeShowsGenomicElements) { zoomedChromosomeShowsGenomicElements = newValue; [chromosomeZoomed setShouldDrawGenomicElements:zoomedChromosomeShowsGenomicElements]; [chromosomeZoomed setNeedsDisplayInInterior]; } } - (IBAction)showMutationsButtonToggled:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status BOOL newValue = ([showMutationsButton state] == NSOnState); if (newValue != zoomedChromosomeShowsMutations) { zoomedChromosomeShowsMutations = newValue; [chromosomeZoomed setShouldDrawMutations:zoomedChromosomeShowsMutations]; [chromosomeZoomed setNeedsDisplayInInterior]; } } - (IBAction)showFixedSubstitutionsButtonToggled:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status BOOL newValue = ([showFixedSubstitutionsButton state] == NSOnState); if (newValue != zoomedChromosomeShowsFixedSubstitutions) { zoomedChromosomeShowsFixedSubstitutions = newValue; [chromosomeZoomed setShouldDrawFixedSubstitutions:zoomedChromosomeShowsFixedSubstitutions]; [chromosomeZoomed setNeedsDisplayInInterior]; } } - (IBAction)drawerButtonToggled:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status [drawer toggle:sender]; } - (IBAction)exportScript:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status NSSavePanel *sp = [[NSSavePanel savePanel] retain]; [sp setTitle:@"Export Script"]; [sp setNameFieldLabel:@"Export As:"]; [sp setMessage:@"Export the simulation script to a file:"]; [sp setExtensionHidden:NO]; [sp setCanSelectHiddenExtension:NO]; [sp setAllowedFileTypes:@[@"txt"]]; [sp beginSheetModalForWindow:[self window] completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [[scriptTextView string] writeToURL:[sp URL] atomically:YES encoding:NSUTF8StringEncoding error:NULL]; [sp autorelease]; } }]; } - (IBAction)exportOutput:(id)sender { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status NSSavePanel *sp = [[NSSavePanel savePanel] retain]; [sp setTitle:@"Export Output"]; [sp setNameFieldLabel:@"Export As:"]; [sp setMessage:@"Export the simulation output to a file:"]; [sp setExtensionHidden:NO]; [sp setCanSelectHiddenExtension:NO]; [sp setAllowedFileTypes:@[@"txt"]]; [sp beginSheetModalForWindow:[self window] completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { NSString *currentOutputString = [outputTextView string]; [currentOutputString writeToURL:[sp URL] atomically:YES encoding:NSUTF8StringEncoding error:NULL]; [sp autorelease]; } }]; } // // Eidos SLiMgui method forwards // #pragma mark - #pragma mark Eidos SLiMgui method forwards - (void)finish_eidos_pauseExecution:(id)sender { // this gets called by performSelectorOnMainThread: after _continuousPlay: has broken out of its loop // if the simulation has already ended, or is invalid, or is not in continuous play, it does nothing if (!invalidSimulation && !reachedSimulationEnd && continuousPlayOn && nonProfilePlayOn && !profilePlayOn && !tickPlayOn) { [self play:nil]; // this will simulate a press of the play button to stop continuous play // bounce our icon; if we are not the active app, to signal that the run is done [NSApp requestUserAttention:NSInformationalRequest]; } } - (void)eidos_openDocument:(NSString *)path { NSURL *pathURL = [NSURL fileURLWithPath:path]; [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:pathURL display:YES completionHandler:(^ void (NSDocument *typelessDoc, BOOL already_open, NSError *error) { })]; } - (void)eidos_pauseExecution { if (!invalidSimulation && !reachedSimulationEnd && continuousPlayOn && nonProfilePlayOn && !profilePlayOn && !tickPlayOn) { continuousPlayTicksCompleted = UINT64_MAX - 1; // this will break us out of the loop in _continuousPlay: at the end of this tick [self performSelectorOnMainThread:@selector(finish_eidos_pauseExecution:) withObject:nil waitUntilDone:NO]; // this will actually stop continuous play } } // // EidosConsoleWindowControllerDelegate methods // #pragma mark - #pragma mark EidosConsoleWindowControllerDelegate - (EidosContext *)eidosConsoleWindowControllerEidosContext:(EidosConsoleWindowController *)eidosConsoleController { return community; } - (void)eidosConsoleWindowControllerAppendWelcomeMessageAddendum:(EidosConsoleWindowController *)eidosConsoleController { EidosConsoleTextView *textView = [_consoleController textView]; NSTextStorage *ts = [textView textStorage]; NSDictionary *outputAttrs = [NSDictionary eidosOutputAttrsWithSize:[textView displayFontSize]]; NSString *bundleVersionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; NSString *bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; NSString *versionString = [NSString stringWithFormat:@"%@ (build %@)", bundleVersionString, bundleVersion]; NSAttributedString *launchString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"Connected to SLiMguiLegacy simulation.\nSLiM version %@.\n", versionString] attributes:outputAttrs]; // SLIM VERSION NSAttributedString *dividerString = [[NSAttributedString alloc] initWithString:@"\n-----------------------------------------------------\n\n" attributes:outputAttrs]; [ts beginEditing]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:launchString]; [ts replaceCharactersInRange:NSMakeRange([ts length], 0) withAttributedString:dividerString]; [ts endEditing]; [launchString release]; [dividerString release]; // We have some one-time work that we do when the first window opens; this is here instead of // applicationWillFinishLaunching: because we don't want to mess up gEidos_RNG static BOOL beenHere = NO; if (!beenHere) { beenHere = YES; EidosHelpController *sharedHelp = [EidosHelpController sharedController]; std::vector context_properties = EidosClass::RegisteredClassProperties(false, true); std::vector context_methods = EidosClass::RegisteredClassMethods(false, true); const std::vector *zg_functions = Community::ZeroTickFunctionSignatures(); const std::vector *slim_functions = Community::SLiMFunctionSignatures(); std::vector all_slim_functions; all_slim_functions.insert(all_slim_functions.end(), zg_functions->begin(), zg_functions->end()); all_slim_functions.insert(all_slim_functions.end(), slim_functions->begin(), slim_functions->end()); [sharedHelp addTopicsFromRTFFile:@"SLiMHelpFunctions" underHeading:@"6. SLiM Functions" functions:&all_slim_functions methods:nullptr properties:nullptr]; [sharedHelp addTopicsFromRTFFile:@"SLiMHelpClasses" underHeading:@"7. SLiM Classes" functions:nullptr methods:&context_methods properties:&context_properties]; [sharedHelp addTopicsFromRTFFile:@"SLiMHelpCallbacks" underHeading:@"8. SLiM Events and Callbacks" functions:nullptr methods:nullptr properties:nullptr]; // Check for completeness of the help documentation, since it's easy to forget to add new functions/properties/methods to the doc [sharedHelp checkDocumentationOfFunctions:&all_slim_functions]; for (EidosClass *class_object : EidosClass::RegisteredClasses(false, true)) { const std::string &element_type = class_object->ClassNameForDisplay(); if (!Eidos_string_hasPrefix(element_type, "_")) // internal classes are undocumented [sharedHelp checkDocumentationOfClass:class_object]; } [sharedHelp checkDocumentationForDuplicatePointers]; // Run startup tests, iff the option key is down; NOTE THAT THIS CAUSES MASSIVE LEAKING DUE TO RAISES INSIDE EIDOS! if ([NSEvent modifierFlags] & NSEventModifierFlagOption) { // We will be executing scripts, so bracket that with our delegate method call [self eidosConsoleWindowControllerWillExecuteScript:_consoleController]; // Run the tests RunSLiMTests(); // Done executing scripts [self eidosConsoleWindowControllerDidExecuteScript:_consoleController]; } } } - (EidosSymbolTable *)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController symbolsFromBaseSymbols:(EidosSymbolTable *)baseSymbols { if (community && !invalidSimulation) return community->SymbolsFromBaseSymbols(baseSymbols); return baseSymbols; } - (EidosFunctionMap *)functionMapForEidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController { if (community && !invalidSimulation) return &community->FunctionMap(); return nullptr; } - (void)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController addOptionalFunctionsToMap:(EidosFunctionMap *)functionMap { Community::AddZeroTickFunctionsToMap(*functionMap); Community::AddSLiMFunctionsToMap(*functionMap); } - (EidosSyntaxHighlightType)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController tokenStringIsSpecialIdentifier:(const std::string &)token_string { if (token_string.compare(gStr_community) == 0) return EidosSyntaxHighlightType::kHighlightAsIdentifier; if (token_string.compare(gStr_sim) == 0) return EidosSyntaxHighlightType::kHighlightAsIdentifier; if (token_string.compare(gStr_slimgui) == 0) return EidosSyntaxHighlightType::kHighlightAsIdentifier; // Request that SLiM's callback keywords be highlighted as "context keywords", which gives them a special color // I'm not sure that I'm crazy about this; feels like it makes the color scheme too jumbled. // Leaving it out for now. BCH 2 Nov. 2017 /* if (token_string.compare("initialize") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("first") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("early") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("late") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("mutationEffect") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("fitnessEffect") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("mateChoice") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("modifyChild") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("interaction") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("recombination") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("mutation") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("survival") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; if (token_string.compare("reproduction") == 0) return EidosSyntaxHighlightType::kHighlightAsContextKeyword; */ int len = (int)token_string.length(); if (len >= 2) { unichar first_ch = token_string[0]; if ((first_ch == 'p') || (first_ch == 'g') || (first_ch == 'm') || (first_ch == 's') || (first_ch == 'i')) { for (int ch_index = 1; ch_index < len; ++ch_index) { unichar idx_ch = token_string[ch_index]; if ((idx_ch < '0') || (idx_ch > '9')) return EidosSyntaxHighlightType::kNoSyntaxHighlight; } return EidosSyntaxHighlightType::kHighlightAsIdentifier; } } return EidosSyntaxHighlightType::kNoSyntaxHighlight; } - (NSString *)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController helpTextForClickedText:(NSString *)clickedText { // A few strings which, when option-clicked, should result in more targeted searches. // @"initialize" is deliberately omitted here so that the initialize...() methods also come up. if ([clickedText isEqualToString:@"first"]) return @"Eidos events"; if ([clickedText isEqualToString:@"early"]) return @"Eidos events"; if ([clickedText isEqualToString:@"late"]) return @"Eidos events"; if ([clickedText isEqualToString:@"mutationEffect"]) return @"mutationEffect() callbacks"; if ([clickedText isEqualToString:@"fitnessEffect"]) return @"fitnessEffect() callbacks"; if ([clickedText isEqualToString:@"interaction"]) return @"interaction() callbacks"; if ([clickedText isEqualToString:@"mateChoice"]) return @"mateChoice() callbacks"; if ([clickedText isEqualToString:@"modifyChild"]) return @"modifyChild() callbacks"; if ([clickedText isEqualToString:@"recombination"]) return @"recombination() callbacks"; if ([clickedText isEqualToString:@"mutation"]) return @"mutation() callbacks"; if ([clickedText isEqualToString:@"survival"]) return @"survival() callbacks"; if ([clickedText isEqualToString:@"reproduction"]) return @"reproduction() callbacks"; return nil; } - (void)eidosConsoleWindowController:(EidosConsoleWindowController *)eidosConsoleController checkScriptDidSucceed:(BOOL)succeeded { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; if (succeeded) { if ([defaults boolForKey:defaultsPlaySoundParseSuccessKey]) [[NSSound soundNamed:@"Bottle"] play]; } else { if ([defaults boolForKey:defaultsPlaySoundParseFailureKey]) [[NSSound soundNamed:@"Ping"] play]; } } - (void)eidosConsoleWindowControllerWillExecuteScript:(EidosConsoleWindowController *)eidosConsoleController { // Whenever we are about to execute script, we swap in our random number generator; at other times, gEidos_RNG_SINGLE is NULL. // The goal here is to keep each SLiM window independent in its random number sequence. if (gEidos_RNG_Initialized) NSLog(@"eidosConsoleWindowControllerWillExecuteScript: gEidos_rng already set up!"); #ifndef _OPENMP std::swap(sim_RNG_SINGLE, gEidos_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) std::swap(sim_RNG_PERTHREAD[threadIndex], gEidos_RNG_PERTHREAD[threadIndex]); #endif std::swap(sim_RNG_initialized, gEidos_RNG_Initialized); //NSLog(@"-[SLiMWindowController eidosConsoleWindowControllerWillExecuteScript]: swapped IN simRNG (sim_RNG_initialized == %@, gEidos_RNG_Initialized == %@)", sim_RNG_initialized ? @"YES" : @"NO", gEidos_RNG_Initialized ? @"YES" : @"NO"); // We also swap in the pedigree id and mutation id counters; each SLiMgui window is independent gSLiM_next_pedigree_id = sim_next_pedigree_id; gSLiM_next_mutation_id = sim_next_mutation_id; gEidosSuppressWarnings = sim_suppress_warnings; // Set the current directory to its value for this window errno = 0; int retval = chdir(sim_working_dir.c_str()); if (retval == -1) NSLog(@"eidosConsoleWindowControllerWillExecuteScript: Unable to set the working directory to %s (error %d)", sim_working_dir.c_str(), errno); } - (void)eidosConsoleWindowControllerDidExecuteScript:(EidosConsoleWindowController *)eidosConsoleController { // Swap our random number generator back out again; see -eidosConsoleWindowControllerWillExecuteScript // Note that gEidos_RNG_Initialized can be false; this gets called even when an error has invalidated the simulation #ifndef _OPENMP std::swap(sim_RNG_SINGLE, gEidos_RNG_SINGLE); #else for (int threadIndex = 0; threadIndex < gEidosMaxThreads; ++threadIndex) std::swap(sim_RNG_PERTHREAD[threadIndex], gEidos_RNG_PERTHREAD[threadIndex]); #endif std::swap(sim_RNG_initialized, gEidos_RNG_Initialized); //NSLog(@"-[SLiMWindowController eidosConsoleWindowControllerDidExecuteScript]: swapped OUT simRNG (sim_RNG_initialized == %@, gEidos_RNG_Initialized == %@)", sim_RNG_initialized ? @"YES" : @"NO", gEidos_RNG_Initialized ? @"YES" : @"NO"); // Swap out our pedigree id and mutation id counters; see -eidosConsoleWindowControllerWillExecuteScript // Setting to -100000 here is not necessary, but will maybe help find bugs... sim_next_pedigree_id = gSLiM_next_pedigree_id; gSLiM_next_pedigree_id = -100000; sim_next_mutation_id = gSLiM_next_mutation_id; gSLiM_next_mutation_id = -100000; sim_suppress_warnings = gEidosSuppressWarnings; gEidosSuppressWarnings = false; // Get the current working directory; each SLiM window has its own cwd, which may have been changed in script since ...WillExecuteScript: sim_working_dir = Eidos_CurrentDirectory(); // Return to the app's working directory when not running SLiM/Eidos code std::string &app_cwd = [(AppDelegate *)[NSApp delegate] SLiMguiCurrentWorkingDirectory]; errno = 0; int retval = chdir(app_cwd.c_str()); if (retval == -1) NSLog(@"eidosConsoleWindowControllerDidExecuteScript: Unable to set the working directory to %s (error %d)", app_cwd.c_str(), errno); } - (void)eidosConsoleWindowControllerConsoleWindowWillClose:(EidosConsoleWindowController *)eidosConsoleController { [consoleButton setState:NSOffState]; } // // EidosTextViewDelegate methods // #pragma mark - #pragma mark EidosTextViewDelegate // This is necessary because we are both a EidosTextViewDelegate (for the views we directly contain) and an // EidosConsoleWindowControllerDelegate (for the console window we own), and the delegate protocols are similar // but not identical. This protocol just forwards on to the EidosConsoleWindowControllerDelegate methods. - (EidosSymbolTable *)eidosTextView:(EidosTextView *)eidosTextView symbolsFromBaseSymbols:(EidosSymbolTable *)baseSymbols { // Here we use the symbol table from the console window, rather than calling the console window controller delegate // method, which would derive a new symbol table – not what we want here return [_consoleController symbols]; } - (EidosFunctionMap *)functionMapForEidosTextView:(EidosTextView *)eidosTextView { return [self functionMapForEidosConsoleWindowController:nullptr]; } - (void)eidosTextView:(EidosTextView *)eidosTextView addOptionalFunctionsToMap:(EidosFunctionMap *)functionMap { [self eidosConsoleWindowController:nullptr addOptionalFunctionsToMap:functionMap]; } - (EidosSyntaxHighlightType)eidosTextView:(EidosTextView *)eidosTextView tokenStringIsSpecialIdentifier:(const std::string &)token_string; { return [self eidosConsoleWindowController:nullptr tokenStringIsSpecialIdentifier:token_string]; } - (NSString *)eidosTextView:(EidosTextView *)eidosTextView helpTextForClickedText:(NSString *)clickedText { return [self eidosConsoleWindowController:nullptr helpTextForClickedText:clickedText]; } - (BOOL)eidosTextView:(EidosTextView *)eidosTextView completionContextWithScriptString:(NSString *)completionScriptString selection:(NSRange)selection typeTable:(EidosTypeTable **)typeTable functionMap:(EidosFunctionMap **)functionMap callTypeTable:(EidosCallTypeTable **)callTypeTable keywords:(NSMutableArray *)keywords argumentNameCompletions:(std::vector *)argNameCompletions { // Code completion in the console window and other ancillary EidosTextViews should use the standard code completion // machinery in EidosTextView. In the script view, however, we want things to behave somewhat differently. In // other contexts, we want the variables and functions available to depend solely upon the current state of the // simulation; whatever is actually available is what code completion provides. In the script view, however, we // want to be smarter than that. Initialization functions should be available when the user is completing // inside an initialize() callback, and not available otherwise, regardless of the current simulation state. // Similarly, variables associated with particular types of callbacks should always be available within those // callbacks; variables defined in script blocks other than the focal block should not be visible in code // completion; defined constants should be available everywhere; and it should be assumed that variables with // names like pX, mX, gX, and sX have their usual types even if they are not presently defined. This delegate // method accomplishes all of those things, by replacing the standard EidosTextView completion handling. if (eidosTextView == scriptTextView) { std::string script_string([completionScriptString UTF8String]); SLiMEidosScript script(script_string); #if EIDOS_DEBUG_COMPLETION std::cout << "SLiM script:\n" << script_string << std::endl << std::endl; #endif // Parse an "interpreter block" bounded by an EOF rather than a "script block" that requires braces script.Tokenize(true, false); // make bad tokens as needed, do not keep nonsignificant tokens script.ParseSLiMFileToAST(true); // make bad nodes as needed (i.e. never raise, and produce a correct tree) #if EIDOS_DEBUG_COMPLETION std::ostringstream parse_stream; script.PrintAST(parse_stream); std::cout << "SLiM AST:\n" << parse_stream.str() << std::endl << std::endl; #endif // Substitute a type table of class SLiMTypeTable and add any defined symbols to it. We use SLiMTypeTable so that // variables like pX, gX, mX, and sX have a known object type even if they are not presently defined in the simulation. *typeTable = new SLiMTypeTable(); EidosSymbolTable *symbols = [_consoleController symbols]; if (symbols) symbols->AddSymbolsToTypeTable(*typeTable); // Use the script text view's facility for using type-interpreting to get a "definitive" function map. This way // all functions that are defined, even if below the completion point, end up in the function map. *functionMap = [scriptTextView functionMapForScriptString:[scriptTextView string] includingOptionalFunctions:NO]; Community::AddSLiMFunctionsToMap(**functionMap); // Now we scan through the children of the root node, each of which is the root of a SLiM script block. The last // script block is the one we are actually completing inside, but we also want to do a quick scan of any other // blocks we find, solely to add entries for any defineConstant() and defineGlobal() calls we can decode. const EidosASTNode *script_root = script.AST(); if (script_root && (script_root->children_.size() > 0)) { EidosASTNode *completion_block = script_root->children_.back(); // If the last script block has a range that ends before the start of the selection, then we are completing after the end // of that block, at the outer level of the script. Detect that case and fall through to the handler for it at the end. int32_t completion_block_end = completion_block->token_->token_end_; if ((int)selection.location > completion_block_end) { // Selection is after end of completion_block completion_block = nullptr; } if (completion_block) { for (EidosASTNode *script_block_node : script_root->children_) { // skip species/ticks specifiers, which are identifier token nodes at the top level of the AST with one child if ((script_block_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (script_block_node->children_.size() == 1)) continue; // script_block_node can have various children, such as an sX identifier, start and end ticks, a block type // identifier like late(), and then the root node of the compound statement for the script block. We want to // decode the parts that are important to us, without the complication of making SLiMEidosBlock objects. EidosASTNode *block_statement_root = nullptr; SLiMEidosBlockType block_type = SLiMEidosBlockType::SLiMEidosEventEarly; // assume early() by default for (EidosASTNode *block_child : script_block_node->children_) { EidosToken *child_token = block_child->token_; if (child_token->token_type_ == EidosTokenType::kTokenIdentifier) { const std::string &child_string = child_token->token_string_; if (child_string.compare(gStr_first) == 0) block_type = SLiMEidosBlockType::SLiMEidosEventFirst; else if (child_string.compare(gStr_early) == 0) block_type = SLiMEidosBlockType::SLiMEidosEventEarly; else if (child_string.compare(gStr_late) == 0) block_type = SLiMEidosBlockType::SLiMEidosEventLate; else if (child_string.compare(gStr_initialize) == 0) block_type = SLiMEidosBlockType::SLiMEidosInitializeCallback; else if (child_string.compare(gStr_fitnessEffect) == 0) block_type = SLiMEidosBlockType::SLiMEidosFitnessEffectCallback; else if (child_string.compare(gStr_mutationEffect) == 0) block_type = SLiMEidosBlockType::SLiMEidosMutationEffectCallback; else if (child_string.compare(gStr_interaction) == 0) block_type = SLiMEidosBlockType::SLiMEidosInteractionCallback; else if (child_string.compare(gStr_mateChoice) == 0) block_type = SLiMEidosBlockType::SLiMEidosMateChoiceCallback; else if (child_string.compare(gStr_modifyChild) == 0) block_type = SLiMEidosBlockType::SLiMEidosModifyChildCallback; else if (child_string.compare(gStr_recombination) == 0) block_type = SLiMEidosBlockType::SLiMEidosRecombinationCallback; else if (child_string.compare(gStr_mutation) == 0) block_type = SLiMEidosBlockType::SLiMEidosMutationCallback; else if (child_string.compare(gStr_survival) == 0) block_type = SLiMEidosBlockType::SLiMEidosSurvivalCallback; else if (child_string.compare(gStr_reproduction) == 0) block_type = SLiMEidosBlockType::SLiMEidosReproductionCallback; // Check for an sX designation on a script block and, if found, add a symbol for it else if ((block_child == script_block_node->children_[0]) && (child_string.length() >= 2)) { if (child_string[0] == 's') { bool all_numeric = true; for (size_t idx = 1; idx < child_string.length(); ++idx) if (!isdigit(child_string[idx])) all_numeric = false; if (all_numeric) { EidosGlobalStringID constant_id = EidosStringRegistry::GlobalStringIDForString(child_string); (*typeTable)->SetTypeForSymbol(constant_id, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_SLiMEidosBlock_Class}); } } } } else if (child_token->token_type_ == EidosTokenType::kTokenLBrace) { block_statement_root = block_child; } else if (child_token->token_type_ == EidosTokenType::kTokenFunction) { // We handle function blocks a bit differently; see below block_type = SLiMEidosBlockType::SLiMEidosUserDefinedFunction; if (block_child->children_.size() >= 4) block_statement_root = block_child->children_[3]; } } // Now we know the type of the node, and the root node of its compound statement; extract what we want if (block_statement_root) { // The species/community symbols are defined in all blocks except initialize() blocks; we need to add // and remove them dynamically so that each block has it defined or not defined as necessary. Since // the completion block is last, the symbols will be correctly defined at the end of this process. if (block_type == SLiMEidosBlockType::SLiMEidosInitializeCallback) { std::vector symbol_ids = (*typeTable)->AllSymbolIDs(); for (EidosGlobalStringID symbol_id : symbol_ids) { EidosTypeSpecifier typeSpec = (*typeTable)->GetTypeForSymbol(symbol_id); if ((typeSpec.type_mask == kEidosValueMaskObject) && ((typeSpec.object_class == gSLiM_Community_Class) || (typeSpec.object_class == gSLiM_Species_Class))) (*typeTable)->RemoveTypeForSymbol(symbol_id); } } else { (*typeTable)->SetTypeForSymbol(gID_community, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Community_Class}); if (community) { for (Species *species : community->AllSpecies()) { EidosGlobalStringID species_symbol = species->self_symbol_.first; (*typeTable)->SetTypeForSymbol(species_symbol, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Species_Class}); } } else { // We don't have a community object, so we don't have a vector of species; this is usually because of a failed parse // In this case, we try to keep things functional by just assuming the single-species case and defining "sim" (*typeTable)->SetTypeForSymbol(gID_sim, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Species_Class}); } } // The slimgui symbol is always available within a block, but not at the top level (*typeTable)->SetTypeForSymbol(gID_slimgui, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_SLiMgui_Class}); // Do the same for the zero-tick functions, which should be defined in initialization() blocks and // not in other blocks; we add and remove them dynamically so they are defined as appropriate. We ought // to do this for other block-specific stuff as well (like the stuff below), but it is unlikely to matter. // Note that we consider the zero-gen functions to always be defined inside function blocks, since the // function might be called from the zero gen (we have no way of knowing definitively). if ((block_type == SLiMEidosBlockType::SLiMEidosInitializeCallback) || (block_type == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)) Community::AddZeroTickFunctionsToMap(**functionMap); else Community::RemoveZeroTickFunctionsFromMap(**functionMap); if (script_block_node == completion_block) { // This is the block we're actually completing in the context of; it is also the last block in the script // snippet that we're working with. We want to first define any callback-associated variables for the block. // Note that self is not defined inside functions, even though they are SLiMEidosBlocks; we pretend we are Eidos. if (block_type == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) (*typeTable)->RemoveTypeForSymbol(gID_self); else (*typeTable)->SetTypeForSymbol(gID_self, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_SLiMEidosBlock_Class}); switch (block_type) { case SLiMEidosBlockType::SLiMEidosEventFirst: break; case SLiMEidosBlockType::SLiMEidosEventEarly: break; case SLiMEidosBlockType::SLiMEidosEventLate: break; case SLiMEidosBlockType::SLiMEidosInitializeCallback: (*typeTable)->RemoveSymbolsOfClass(gSLiM_Subpopulation_Class); // subpops defined upstream from us still do not exist for us break; case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosMutationEffectCallback: (*typeTable)->SetTypeForSymbol(gID_mut, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Mutation_Class}); (*typeTable)->SetTypeForSymbol(gID_homozygous, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_effect, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosInteractionCallback: (*typeTable)->SetTypeForSymbol(gID_distance, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_strength, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_receiver, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_exerter, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosMateChoiceCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_sourceSubpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gEidosID_weights, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); break; case SLiMEidosBlockType::SLiMEidosModifyChildCallback: (*typeTable)->SetTypeForSymbol(gID_child, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_parent1, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_isCloning, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_isSelfing, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_parent2, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_sourceSubpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosRecombinationCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_haplosome1, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Haplosome_Class}); (*typeTable)->SetTypeForSymbol(gID_haplosome2, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Haplosome_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_breakpoints, EidosTypeSpecifier{kEidosValueMaskInt, nullptr}); break; case SLiMEidosBlockType::SLiMEidosMutationCallback: (*typeTable)->SetTypeForSymbol(gID_mut, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Mutation_Class}); (*typeTable)->SetTypeForSymbol(gID_parent, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_element, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_GenomicElement_Class}); (*typeTable)->SetTypeForSymbol(gID_haplosome, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Haplosome_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_originalNuc, EidosTypeSpecifier{kEidosValueMaskInt, nullptr}); break; case SLiMEidosBlockType::SLiMEidosSurvivalCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); (*typeTable)->SetTypeForSymbol(gID_surviving, EidosTypeSpecifier{kEidosValueMaskLogical, nullptr}); (*typeTable)->SetTypeForSymbol(gID_fitness, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); (*typeTable)->SetTypeForSymbol(gID_draw, EidosTypeSpecifier{kEidosValueMaskFloat, nullptr}); break; case SLiMEidosBlockType::SLiMEidosReproductionCallback: (*typeTable)->SetTypeForSymbol(gID_individual, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Individual_Class}); (*typeTable)->SetTypeForSymbol(gID_subpop, EidosTypeSpecifier{kEidosValueMaskObject, gSLiM_Subpopulation_Class}); break; case SLiMEidosBlockType::SLiMEidosUserDefinedFunction: { // Similar to the local variables that are defined for callbacks above, here we need to define the parameters to the // function, by parsing the relevant AST nodes; this is parallel to EidosTypeInterpreter::TypeEvaluate_FunctionDecl() EidosASTNode *function_declaration_node = script_block_node->children_[0]; const EidosASTNode *param_list_node = function_declaration_node->children_[2]; const std::vector ¶m_nodes = param_list_node->children_; std::vector used_param_names; for (EidosASTNode *param_node : param_nodes) { const std::vector ¶m_children = param_node->children_; int param_children_count = (int)param_children.size(); if ((param_children_count == 2) || (param_children_count == 3)) { EidosTypeSpecifier ¶m_type = param_children[0]->typespec_; const std::string ¶m_name = param_children[1]->token_->token_string_; // Check param_name; it needs to not be used by another parameter if (std::find(used_param_names.begin(), used_param_names.end(), param_name) != used_param_names.end()) continue; if (param_children_count >= 2) { // param_node has 2 or 3 children (type, identifier, [default]); we don't care about default values (*typeTable)->SetTypeForSymbol(EidosStringRegistry::GlobalStringIDForString(param_name), param_type); } } } break; } case SLiMEidosBlockType::SLiMEidosNoBlockType: break; // never hit } } if (script_block_node == completion_block) { // Make a type interpreter and add symbols to our type table using it // We use SLiMTypeInterpreter because we want to pick up definitions of SLiM constants SLiMTypeInterpreter typeInterpreter(block_statement_root, **typeTable, **functionMap, **callTypeTable); typeInterpreter.TypeEvaluateInterpreterBlock_AddArgumentCompletions(argNameCompletions, script_string.length()); // result not used return YES; } else { // This is not the block we're completing in. We want to add symbols for any constant-defining calls // in this block; apart from that, this block cannot affect the completion block, due to scoping. // However, constant-defining calls might use the types of variables, like defineConstant("foo", bar) // where the type of foo comes from the type of bar; so we need to keep track of all symbols even // though they will fall out of scope. We therefore use a separate local type table with a reference // upward to our main type table. EidosTypeTable scopedTypeTable(**typeTable); // Make a type interpreter and add symbols to our type table using it // We use SLiMTypeInterpreter because we want to pick up definitions of SLiM constants SLiMTypeInterpreter typeInterpreter(block_statement_root, scopedTypeTable, **functionMap, **callTypeTable); typeInterpreter.SetExternalTypeTable(*typeTable); // defined constants/variables should also go into the global scope typeInterpreter.TypeEvaluateInterpreterBlock(); // result not used } } } } } // We drop through to here if we have a bad or empty script root, or if the final script block (completion_block) didn't // have a compound statement (meaning its starting brace has not yet been typed), or if we're completing outside of any // existing script block. In these sorts of cases, we want to return completions for the outer level of a SLiM script. // This means that standard Eidos language keywords like "while", "next", etc. are not legal, but SLiM script block // keywords like "first", "early", "late", "mutationEffect", "fitnessEffect", "interaction", "mateChoice", "modifyChild", // "recombination", "mutation", "survival", and "reproduction" are. We also add "species" and "ticks" here for // multispecies models. [keywords removeAllObjects]; [keywords addObjectsFromArray:@[ @"initialize() {\n\n}\n", @"first() {\n\n}\n", @"early() {\n\n}\n", @"late() {\n\n}\n", @"mutationEffect() {\n\n}\n", @"fitnessEffect() {\n\n}\n", @"interaction() {\n\n}\n", @"mateChoice() {\n\n}\n", @"modifyChild() {\n\n}\n", @"recombination() {\n\n}\n", @"mutation() {\n\n}\n", @"survival() {\n\n}\n", @"reproduction() {\n\n}\n", @"function (void)name(void) {\n\n}\n", @"species", @"ticks"]]; // At the outer level, functions are also not legal (*functionMap)->clear(); // And no variables exist except SLiM objects like pX, gX, mX, sX and species symbols std::vector symbol_ids = (*typeTable)->AllSymbolIDs(); for (EidosGlobalStringID symbol_id : symbol_ids) { EidosTypeSpecifier typeSpec = (*typeTable)->GetTypeForSymbol(symbol_id); if ((typeSpec.type_mask != kEidosValueMaskObject) || (typeSpec.object_class == gSLiM_Community_Class) || (typeSpec.object_class == gSLiM_SLiMgui_Class)) (*typeTable)->RemoveTypeForSymbol(symbol_id); } return YES; } return NO; } // // NSWindow delegate methods // #pragma mark - #pragma mark NSWindow delegate - (void)windowWillClose:(NSNotification *)notification { // We are the delegate of our own window, and of all of our graph windows, too NSWindow *closingWindow = [notification object]; if (closingWindow == [self window]) { //NSLog(@"SLiMWindowController window closing..."); [closingWindow setDelegate:nil]; [self cleanup]; // NSWindowController takes care of the rest; we don't need to release ourselves, or ask our document to close, or anything } else if (closingWindow == graphWindowMutationFreqSpectrum) { //NSLog(@"graphWindowMutationFreqSpectrum window closing..."); [graphWindowMutationFreqSpectrum autorelease]; graphWindowMutationFreqSpectrum = nil; } else if (closingWindow == graphWindowMutationFreqTrajectories) { //NSLog(@"graphWindowMutationFreqTrajectories window closing..."); [graphWindowMutationFreqTrajectories autorelease]; graphWindowMutationFreqTrajectories = nil; } else if (closingWindow == graphWindowMutationLossTimeHistogram) { //NSLog(@"graphWindowMutationLossTimeHistogram window closing..."); [graphWindowMutationLossTimeHistogram autorelease]; graphWindowMutationLossTimeHistogram = nil; } else if (closingWindow == graphWindowMutationFixationTimeHistogram) { //NSLog(@"graphWindowMutationFixationTimeHistogram window closing..."); [graphWindowMutationFixationTimeHistogram autorelease]; graphWindowMutationFixationTimeHistogram = nil; } else if (closingWindow == graphWindowFitnessOverTime) { //NSLog(@"graphWindowFitnessOverTime window closing..."); [graphWindowFitnessOverTime autorelease]; graphWindowFitnessOverTime = nil; } else if (closingWindow == graphWindowPopulationVisualization) { //NSLog(@"graphWindowPopulationVisualization window closing..."); [graphWindowPopulationVisualization autorelease]; graphWindowPopulationVisualization = nil; } else if ([linkedWindows containsObject:closingWindow]) { //NSLog(@"linked window window closing..."); [linkedWindows removeObject:closingWindow]; } // If all of our subsidiary graph windows have been closed, we are effectively back at square one regarding window placement if (!graphWindowMutationFreqSpectrum && !graphWindowMutationFreqTrajectories && !graphWindowMutationLossTimeHistogram && !graphWindowMutationFixationTimeHistogram && !graphWindowFitnessOverTime && !graphWindowPopulationVisualization) openedGraphCount = 0; } - (void)windowDidResize:(NSNotification *)notification { //[[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status NSWindow *resizingWindow = [notification object]; NSView *contentView = [resizingWindow contentView]; if ([contentView isKindOfClass:[GraphView class]]) [(GraphView *)contentView graphWindowResized]; if (resizingWindow == [self window]) [self updatePopulationViewHiding]; } - (void)windowDidMove:(NSNotification *)notification { //[[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status } // // NSTextView delegate methods // #pragma mark - #pragma mark NSTextView delegate - (void)updateStatusFieldFromSelection { NSAttributedString *attributedSignature = [scriptTextView attributedSignatureForScriptString:[scriptTextView string] selection:[scriptTextView selectedRange]]; NSString *signatureString = [attributedSignature string]; // Here we do a little quick-and-dirty patching in order to show signatures when inside callback definitions if ([signatureString hasSuffix:@"unrecognized call"]) { const EidosCallSignature *sig = nullptr; if ([signatureString hasPrefix:@"initialize()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("initialize", nullptr, kEidosValueMaskVOID))); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"first()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("first", nullptr, kEidosValueMaskVOID))); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"early()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("early", nullptr, kEidosValueMaskVOID))); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"late()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("late", nullptr, kEidosValueMaskVOID))); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"mutationEffect()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("mutationEffect", nullptr, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddObject_S("mutationType", gSLiM_MutationType_Class)->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"fitnessEffect()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("fitnessEffect", nullptr, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"interaction()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("interaction", nullptr, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddObject_S("interactionType", gSLiM_InteractionType_Class)->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"mateChoice()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("mateChoice", nullptr, kEidosValueMaskNULL | kEidosValueMaskFloat | kEidosValueMaskObject, gSLiM_Individual_Class))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"modifyChild()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("modifyChild", nullptr, kEidosValueMaskLogical | kEidosValueMaskSingleton))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"recombination()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("recombination", nullptr, kEidosValueMaskLogical | kEidosValueMaskSingleton))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)->AddIntString_OSN("chromosome", gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"mutation()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("mutation", nullptr, kEidosValueMaskLogical | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Mutation_Class))->AddObject_OSN("mutationType", gSLiM_MutationType_Class, gStaticEidosValueNULLInvisible)->AddObject_OSN("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"survival()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("survival", nullptr, kEidosValueMaskNULL | kEidosValueMaskLogical | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Subpopulation_Class))->AddObject_OS("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } else if ([signatureString hasPrefix:@"reproduction()"]) { static EidosCallSignature_CSP callbackSig = nullptr; if (!callbackSig) callbackSig = EidosCallSignature_CSP((new EidosFunctionSignature("reproduction", nullptr, kEidosValueMaskVOID))->AddObject_OSN("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULLInvisible)->AddString_OSN("sex", gStaticEidosValueNULLInvisible)); sig = callbackSig.get(); } if (sig) attributedSignature = [NSAttributedString eidosAttributedStringForCallSignature:sig size:11.0]; } [scriptStatusTextField setAttributedStringValue:attributedSignature]; } - (void)textViewDidChangeSelection:(NSNotification *)notification { NSTextView *textView = (NSTextView *)[notification object]; if (textView == scriptTextView) { [self updateStatusFieldFromSelection]; } } // // NSTableView delegate methods // #pragma mark - #pragma mark NSTableView delegate - (BOOL)tableView:(NSTableView *)tableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { return NO; } - (void)tableViewSelectionDidChange:(NSNotification *)aNotification { Species *displaySpecies = [self focalDisplaySpecies]; if (displaySpecies) { NSTableView *aTableView = [aNotification object]; if (aTableView == subpopTableView && !reloadingSubpopTableview) // see comment in -updateAfterTick after reloadingSubpopTableview { Population &population = displaySpecies->population_; int subpopCount = (int)population.subpops_.size(); auto popIter = population.subpops_.begin(); for (int i = 0; i < subpopCount; ++i) { popIter->second->gui_selected_ = [subpopTableView isRowSelected:i]; popIter++; } // If the selection has changed, that means that our private mutation tallies need to be recomputed population.InvalidateMutationReferencesCache(); // force a retally population.TallyMutationReferencesAcrossPopulation_SLiMgui(); // It's a bit hard to tell for sure whether we need to update or not, since a selected subpop might have been removed from the tableview; // selection changes should not happen often, so we can just always update, I think. [populationView setNeedsDisplay:YES]; [self updatePopulationViewHiding]; [chromosomeZoomed setNeedsDisplayInInterior]; } } } // // NSTableView datasource methods // #pragma mark - #pragma mark NSTableView datasource - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView { Species *displaySpecies = [self focalDisplaySpecies]; if (displaySpecies) { if (aTableView == subpopTableView) { return displaySpecies->population_.subpops_.size(); } else if (aTableView == mutTypeTableView) { return community->AllMutationTypes().size(); } else if (aTableView == genomicElementTypeTableView) { return community->AllGenomicElementTypes().size(); } else if (aTableView == interactionTypeTableView) { return community->AllInteractionTypes().size(); } else if (aTableView == scriptBlocksTableView) { return community->AllScriptBlocks().size(); } } return 0; } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { Species *displaySpecies = [self focalDisplaySpecies]; if (displaySpecies) { if (aTableView == subpopTableView) { Population &population = displaySpecies->population_; int subpopCount = (int)population.subpops_.size(); if (rowIndex < subpopCount) { auto popIter = population.subpops_.begin(); std::advance(popIter, rowIndex); slim_objectid_t subpop_id = popIter->first; Subpopulation *subpop = popIter->second; if (aTableColumn == subpopIDColumn) { return [NSString stringWithFormat:@"p%lld", (long long int)subpop_id]; } else if (aTableColumn == subpopSizeColumn) { return [NSString stringWithFormat:@"%lld", (long long int)subpop->parent_subpop_size_]; } else if (community->ModelType() == SLiMModelType::kModelTypeNonWF) { // in nonWF models selfing/cloning/sex rates/ratios are emergent, calculated from collected metrics double total_offspring = subpop->gui_offspring_cloned_M_ + subpop->gui_offspring_crossed_ + subpop->gui_offspring_empty_ + subpop->gui_offspring_selfed_; if (subpop->sex_enabled_) total_offspring += subpop->gui_offspring_cloned_F_; // avoid double-counting clones when we are modeling hermaphrodites if (aTableColumn == subpopSelfingRateColumn) { if (!subpop->sex_enabled_ && (total_offspring > 0)) return [NSString stringWithFormat:@"%.2f", subpop->gui_offspring_selfed_ / total_offspring]; } else if (aTableColumn == subpopFemaleCloningRateColumn) { if (total_offspring > 0) return [NSString stringWithFormat:@"%.2f", subpop->gui_offspring_cloned_F_ / total_offspring]; } else if (aTableColumn == subpopMaleCloningRateColumn) { if (total_offspring > 0) return [NSString stringWithFormat:@"%.2f", subpop->gui_offspring_cloned_M_ / total_offspring]; } else if (aTableColumn == subpopSexRatioColumn) { if (subpop->sex_enabled_ && (subpop->parent_subpop_size_ > 0)) return [NSString stringWithFormat:@"%.2f", 1.0 - subpop->parent_first_male_index_ / (double)subpop->parent_subpop_size_]; } return @"—"; } else // community->ModelType() == SLiMModelType::kModelTypeWF { if (aTableColumn == subpopSelfingRateColumn) { if (subpop->sex_enabled_) return @"—"; else return [NSString stringWithFormat:@"%.2f", subpop->selfing_fraction_]; } else if (aTableColumn == subpopFemaleCloningRateColumn) { return [NSString stringWithFormat:@"%.2f", subpop->female_clone_fraction_]; } else if (aTableColumn == subpopMaleCloningRateColumn) { return [NSString stringWithFormat:@"%.2f", subpop->male_clone_fraction_]; } else if (aTableColumn == subpopSexRatioColumn) { if (subpop->sex_enabled_) return [NSString stringWithFormat:@"%.2f", subpop->parent_sex_ratio_]; else return @"—"; } } } } else if (aTableView == mutTypeTableView) { const std::map &mutationTypes = community->AllMutationTypes(); int mutationTypeCount = (int)mutationTypes.size(); if (rowIndex < mutationTypeCount) { auto mutTypeIter = mutationTypes.begin(); std::advance(mutTypeIter, rowIndex); slim_objectid_t mutTypeID = mutTypeIter->first; MutationType *mutationType = mutTypeIter->second; if (aTableColumn == mutTypeIDColumn) { NSString *idString = [NSString stringWithFormat:@"m%lld", (long long int)mutTypeID]; if (community->all_species_.size() > 1) idString = [idString stringByAppendingFormat:@" %@", [NSString stringWithUTF8String:mutationType->species_.avatar_.c_str()]]; return idString; } else if (aTableColumn == mutTypeDominanceColumn) { return [NSString stringWithFormat:@"%.3f", mutationType->dominance_coeff_]; } else if (aTableColumn == mutTypeDFETypeColumn) { switch (mutationType->dfe_type_) { case DFEType::kFixed: return @"fixed"; case DFEType::kGamma: return @"gamma"; case DFEType::kExponential: return @"exp"; case DFEType::kNormal: return @"normal"; case DFEType::kWeibull: return @"Weibull"; case DFEType::kLaplace: return @"Laplace"; case DFEType::kScript: return @"script"; } } else if (aTableColumn == mutTypeDFEParamsColumn) { NSMutableString *paramString = [[NSMutableString alloc] init]; if (mutationType->dfe_type_ == DFEType::kScript) { // DFE type 's' has parameters of type string for (unsigned int paramIndex = 0; paramIndex < mutationType->dfe_strings_.size(); ++paramIndex) { const char *dfe_string = mutationType->dfe_strings_[paramIndex].c_str(); NSString *ns_dfe_string = [NSString stringWithUTF8String:dfe_string]; [paramString appendFormat:@"\"%@\"", ns_dfe_string]; if (paramIndex < mutationType->dfe_strings_.size() - 1) [paramString appendString:@", "]; } } else { // All other DFEs have parameters of type double for (unsigned int paramIndex = 0; paramIndex < mutationType->dfe_parameters_.size(); ++paramIndex) { NSString *paramSymbol = @""; switch (mutationType->dfe_type_) { case DFEType::kFixed: paramSymbol = @"s"; break; case DFEType::kGamma: paramSymbol = (paramIndex == 0 ? @"s̄" : @"α"); break; case DFEType::kExponential: paramSymbol = @"s̄"; break; case DFEType::kNormal: paramSymbol = (paramIndex == 0 ? @"s̄" : @"σ"); break; case DFEType::kWeibull: paramSymbol = (paramIndex == 0 ? @"λ" : @"k"); break; case DFEType::kLaplace: paramSymbol = (paramIndex == 0 ? @"s̄" : @"b"); break; case DFEType::kScript: break; } [paramString appendFormat:@"%@=%.3f", paramSymbol, mutationType->dfe_parameters_[paramIndex]]; if (paramIndex < mutationType->dfe_parameters_.size() - 1) [paramString appendString:@", "]; } } return [paramString autorelease]; } } } else if (aTableView == genomicElementTypeTableView) { const std::map &genomicElementTypes = community->AllGenomicElementTypes(); int genomicElementTypeCount = (int)genomicElementTypes.size(); if (rowIndex < genomicElementTypeCount) { auto genomicElementTypeIter = genomicElementTypes.begin(); std::advance(genomicElementTypeIter, rowIndex); slim_objectid_t genomicElementTypeID = genomicElementTypeIter->first; GenomicElementType *genomicElementType = genomicElementTypeIter->second; if (aTableColumn == genomicElementTypeIDColumn) { NSString *idString = [NSString stringWithFormat:@"g%lld", (long long int)genomicElementTypeID]; if (community->all_species_.size() > 1) idString = [idString stringByAppendingFormat:@" %@", [NSString stringWithUTF8String:genomicElementType->species_.avatar_.c_str()]]; return idString; } else if (aTableColumn == genomicElementTypeColorColumn) { return [self colorForGenomicElementType:genomicElementType withID:genomicElementTypeID]; } else if (aTableColumn == genomicElementTypeMutationTypesColumn) { NSMutableString *paramString = [[NSMutableString alloc] init]; for (unsigned int mutTypeIndex = 0; mutTypeIndex < genomicElementType->mutation_fractions_.size(); ++mutTypeIndex) { MutationType *mutType = genomicElementType->mutation_type_ptrs_[mutTypeIndex]; double mutTypeFraction = genomicElementType->mutation_fractions_[mutTypeIndex]; [paramString appendFormat:@"m%lld=%.3f", (long long int)mutType->mutation_type_id_, mutTypeFraction]; if (mutTypeIndex < genomicElementType->mutation_fractions_.size() - 1) [paramString appendString:@", "]; } return [paramString autorelease]; } } } else if (aTableView == interactionTypeTableView) { const std::map &interactionTypes = community->AllInteractionTypes(); int interactionTypeCount = (int)interactionTypes.size(); if (rowIndex < interactionTypeCount) { auto interactionTypeIter = interactionTypes.begin(); std::advance(interactionTypeIter, rowIndex); slim_objectid_t interactionTypeID = interactionTypeIter->first; InteractionType *interactionType = interactionTypeIter->second; if (aTableColumn == interactionTypeIDColumn) { NSString *idString = [NSString stringWithFormat:@"i%lld", (long long int)interactionTypeID]; return idString; } else if (aTableColumn == interactionTypeMaxDistanceColumn) { return [NSString stringWithFormat:@"%.3f", interactionType->max_distance_]; } else if (aTableColumn == interactionTypeIFTypeColumn) { switch (interactionType->if_type_) { case SpatialKernelType::kFixed: return @"fixed"; case SpatialKernelType::kLinear: return @"linear"; case SpatialKernelType::kExponential: return @"exp"; case SpatialKernelType::kNormal: return @"normal"; case SpatialKernelType::kCauchy: return @"Cauchy"; case SpatialKernelType::kStudentsT: return @"Student's t"; } } else if (aTableColumn == interactionTypeIFParamsColumn) { NSMutableString *paramString = [[NSMutableString alloc] init]; // the first parameter is always the maximum interaction strength [paramString appendFormat:@"f=%.3f", interactionType->if_param1_]; // append second parameters where applicable switch (interactionType->if_type_) { case SpatialKernelType::kFixed: case SpatialKernelType::kLinear: break; case SpatialKernelType::kExponential: [paramString appendFormat:@", β=%.3f", interactionType->if_param2_]; break; case SpatialKernelType::kNormal: [paramString appendFormat:@", σ=%.3f", interactionType->if_param2_]; break; case SpatialKernelType::kCauchy: [paramString appendFormat:@", γ=%.3f", interactionType->if_param2_]; break; case SpatialKernelType::kStudentsT: [paramString appendFormat:@", ν=%.3f, σ=%.3f", interactionType->if_param2_, interactionType->if_param3_]; break; } return [paramString autorelease]; } } } else if (aTableView == scriptBlocksTableView) { std::vector &scriptBlocks = community->AllScriptBlocks(); int scriptBlockCount = (int)scriptBlocks.size(); if (rowIndex < scriptBlockCount) { SLiMEidosBlock *scriptBlock = scriptBlocks[rowIndex]; if (aTableColumn == scriptBlocksIDColumn) { slim_objectid_t block_id = scriptBlock->block_id_; NSString *idString; if (scriptBlock->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) idString = @"—"; else if (block_id == -1) idString = @"—"; else idString = [NSString stringWithFormat:@"s%lld", (long long int)block_id]; if ((community->all_species_.size() > 1) && scriptBlock->species_spec_) idString = [idString stringByAppendingFormat:@" %@", [NSString stringWithUTF8String:scriptBlock->species_spec_->avatar_.c_str()]]; else if ((community->all_species_.size() > 1) && scriptBlock->ticks_spec_) idString = [idString stringByAppendingFormat:@" %@", [NSString stringWithUTF8String:scriptBlock->ticks_spec_->avatar_.c_str()]]; return idString; } else if (aTableColumn == scriptBlocksStartColumn) { if (scriptBlock->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) return @"—"; else if (!scriptBlock->tick_range_evaluated_) return @"?"; else if (scriptBlock->tick_range_is_sequence_ == false) return @"..."; else if (scriptBlock->tick_start_ == -1) return @"MIN"; else return [NSString stringWithFormat:@"%lld", (long long int)scriptBlock->tick_start_]; } else if (aTableColumn == scriptBlocksEndColumn) { if (scriptBlock->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) return @"—"; else if (!scriptBlock->tick_range_evaluated_) return @"?"; else if (scriptBlock->tick_range_is_sequence_ == false) return @"..."; else if (scriptBlock->tick_end_ == SLIM_MAX_TICK + 1) return @"MAX"; else return [NSString stringWithFormat:@"%lld", (long long int)scriptBlock->tick_end_]; } else if (aTableColumn == scriptBlocksTypeColumn) { switch (scriptBlock->type_) { case SLiMEidosBlockType::SLiMEidosEventFirst: return @"first()"; case SLiMEidosBlockType::SLiMEidosEventEarly: return @"early()"; case SLiMEidosBlockType::SLiMEidosEventLate: return @"late()"; case SLiMEidosBlockType::SLiMEidosInitializeCallback: return @"initialize()"; case SLiMEidosBlockType::SLiMEidosMutationEffectCallback: return @"mutationEffect()"; case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback: return @"fitnessEffect()"; case SLiMEidosBlockType::SLiMEidosInteractionCallback: return @"interaction()"; case SLiMEidosBlockType::SLiMEidosMateChoiceCallback: return @"mateChoice()"; case SLiMEidosBlockType::SLiMEidosModifyChildCallback: return @"modifyChild()"; case SLiMEidosBlockType::SLiMEidosRecombinationCallback: return @"recombination()"; case SLiMEidosBlockType::SLiMEidosMutationCallback: return @"mutation()"; case SLiMEidosBlockType::SLiMEidosSurvivalCallback: return @"survival()"; case SLiMEidosBlockType::SLiMEidosReproductionCallback: return @"reproduction()"; case SLiMEidosBlockType::SLiMEidosUserDefinedFunction: { EidosASTNode *function_decl_node = scriptBlock->root_node_->children_[0]; EidosASTNode *function_name_node = function_decl_node->children_[1]; const std::string &function_name = function_name_node->token_->token_string_; NSString *functionName = [NSString stringWithUTF8String:function_name.c_str()]; return [functionName stringByAppendingString:@"()"]; } case SLiMEidosBlockType::SLiMEidosNoBlockType: return @""; // never hit } } } } } return @""; } - (NSString *)tableView:(NSTableView *)aTableView toolTipForCell:(NSCell *)aCell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex mouseLocation:(NSPoint)mouseLocation { Species *displaySpecies = [self focalDisplaySpecies]; if (displaySpecies) { if (aTableView == scriptBlocksTableView) { std::vector &scriptBlocks = community->AllScriptBlocks(); int scriptBlockCount = (int)scriptBlocks.size(); if (rowIndex < scriptBlockCount) { SLiMEidosBlock *scriptBlock = scriptBlocks[rowIndex]; const char *script_string = scriptBlock->compound_statement_node_->token_->token_string_.c_str(); NSString *ns_script_string = [NSString stringWithUTF8String:script_string]; // change whitespace to non-breaking spaces; we want to force AppKit not to wrap code // note this doesn't really prevent AppKit from wrapping our tooltip, and I'd also like to use Monaco 9; I think I need a custom popup to do that... ns_script_string = [ns_script_string stringByReplacingOccurrencesOfString:@" " withString:@"\u00A0"]; // second string is an   ns_script_string = [ns_script_string stringByReplacingOccurrencesOfString:@"\t" withString:@"\u00A0\u00A0\u00A0"]; // second string is three  s return ns_script_string; } } else if (aTableView == mutTypeTableView) { const std::map &mutationTypes = community->AllMutationTypes(); int mutationTypeCount = (int)mutationTypes.size(); if (rowIndex < mutationTypeCount) { auto mutTypeIter = mutationTypes.begin(); std::advance(mutTypeIter, rowIndex); MutationType *mutationType = mutTypeIter->second; // If we're already showing the tooltip for this muttype, short-circuit; sometimes we get called twice with no exit if (functionGraphToolTipWindow && ([functionGraphToolTipWindow mutType] == mutationType)) return (id _Nonnull)nil; // get rid of the static analyzer warning //NSLog(@"show DFE tooltip view here for mut ID %d!", mutationType->mutation_type_id_); // Make the tooltip window, configure it, and display it if (!functionGraphToolTipWindow) functionGraphToolTipWindow = [SLiMFunctionGraphToolTipWindow new]; NSPoint tipPoint = NSMakePoint(mouseLocation.x + 0, mouseLocation.y + 65); tipPoint = [[mutTypeTableView superview] convertPoint:tipPoint toView:nil]; NSRect tipRect = NSMakeRect(tipPoint.x, tipPoint.y, 0, 0); tipPoint = [[mutTypeTableView window] convertRectToScreen:tipRect].origin; [functionGraphToolTipWindow setMutType:mutationType]; // questionable for it to have a pointer to the mutation type, but I can't think of a way to crash it... [functionGraphToolTipWindow setTipPoint:tipPoint]; [functionGraphToolTipWindow orderFront:nil]; // Wire it to a tracking area so it gets taken down when the mouse moves; see mouseExited: below NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:*rect options:(NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"mut_id" : [NSNumber numberWithInteger:mutationType->mutation_type_id_]}]; [mutTypeTableView addTrackingArea:[trackingArea autorelease]]; } } else if (aTableView == interactionTypeTableView) { const std::map &interactionTypes = community->AllInteractionTypes(); int interactionTypeCount = (int)interactionTypes.size(); if (rowIndex < interactionTypeCount) { auto interactionTypeIter = interactionTypes.begin(); std::advance(interactionTypeIter, rowIndex); InteractionType *interactionType = interactionTypeIter->second; // If we're already showing the tooltip for this muttype, short-circuit; sometimes we get called twice with no exit if (functionGraphToolTipWindow && ([functionGraphToolTipWindow interactionType] == interactionType)) return (id _Nonnull)nil; // get rid of the static analyzer warning //NSLog(@"show DFE tooltip view here for interaction ID %d!", interactionType->interaction_type_id_); // Make the tooltip window, configure it, and display it if (!functionGraphToolTipWindow) functionGraphToolTipWindow = [SLiMFunctionGraphToolTipWindow new]; NSPoint tipPoint = NSMakePoint(mouseLocation.x + 0, mouseLocation.y + 65); tipPoint = [[interactionTypeTableView superview] convertPoint:tipPoint toView:nil]; NSRect tipRect = NSMakeRect(tipPoint.x, tipPoint.y, 0, 0); tipPoint = [[interactionTypeTableView window] convertRectToScreen:tipRect].origin; [functionGraphToolTipWindow setInteractionType:interactionType]; // questionable for it to have a pointer to the interaction type, but I can't think of a way to crash it... [functionGraphToolTipWindow setTipPoint:tipPoint]; [functionGraphToolTipWindow orderFront:nil]; // Wire it to a tracking area so it gets taken down when the mouse moves; see mouseExited: below NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:*rect options:(NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"int_id" : [NSNumber numberWithInteger:interactionType->interaction_type_id_]}]; [interactionTypeTableView addTrackingArea:[trackingArea autorelease]]; } } } return (id _Nonnull)nil; // get rid of the static analyzer warning } - (void)hideMiniGraphToolTipWindow { if (functionGraphToolTipWindow) { [functionGraphToolTipWindow orderOut:nil]; [functionGraphToolTipWindow setMutType:nullptr]; [functionGraphToolTipWindow setInteractionType:nullptr]; } } // Used to take down a custom tooltip window that we may have shown above, displaying a mutation type's DFE - (void)mouseExited:(NSEvent *)event { if (!functionGraphToolTipWindow) return; NSTrackingArea *trackingArea = [event trackingArea]; NSDictionary *userInfo = [trackingArea userInfo]; NSObject *mut_id_object = [userInfo objectForKey:@"mut_id"]; NSObject *int_id_object = [userInfo objectForKey:@"int_id"]; if ((!mut_id_object && !int_id_object) || (mut_id_object && int_id_object)) return; if (mut_id_object) { slim_objectid_t mut_id = (slim_objectid_t)[(NSNumber *)mut_id_object integerValue]; if (mut_id != [functionGraphToolTipWindow mutType]->mutation_type_id_) return; //NSLog(@" take down DFE tooltip view here for mut ID %d!", mut_id); [mutTypeTableView removeTrackingArea:trackingArea]; } else if (int_id_object) { slim_objectid_t int_id = (slim_objectid_t)[(NSNumber *)int_id_object integerValue]; if (int_id != [functionGraphToolTipWindow interactionType]->interaction_type_id_) return; //NSLog(@" take down DFE tooltip view here for interaction ID %d!", int_id); [interactionTypeTableView removeTrackingArea:trackingArea]; } [self hideMiniGraphToolTipWindow]; } // // NSSplitView delegate methods // #pragma mark - #pragma mark NSSplitView delegate - (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview { return YES; } - (CGFloat)splitView:(NSSplitView *)splitView constrainSplitPosition:(CGFloat)proposedPosition ofSubviewAt:(NSInteger)dividerIndex { if (splitView == overallSplitView) { if (fabs(proposedPosition - 294) < 20) proposedPosition = 294; } //NSLog(@"pos in constrainSplitPosition: %f", proposedPosition); return proposedPosition; } - (void)splitViewDidResizeSubviews:(NSNotification *)notification { [[self document] setTransient:NO]; // Since the user has taken an interest in the window, clear the document's transient status } // // NSDrawer delegate methods // #pragma mark - #pragma mark NSDrawer delegate - (void)drawerWillClose:(NSNotification *)notification { [self hideMiniGraphToolTipWindow]; } - (void)drawerDidOpen:(NSNotification *)notification { [buttonForDrawer setState:NSOnState]; } - (void)drawerDidClose:(NSNotification *)notification { [buttonForDrawer setState:NSOffState]; } @end ================================================ FILE: SLiMgui/SLiMgui.entitlements ================================================ ================================================ FILE: SLiMgui/SLiMguiLegacy-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDocumentTypes CFBundleTypeExtensions slim CFBundleTypeIconFile slim.icns CFBundleTypeMIMETypes text/plain CFBundleTypeName SLiM model CFBundleTypeRole Editor LSItemContentTypes edu.messerlab.slim LSTypeIsPackage 0 NSDocumentClass SLiMDocument CFBundleTypeExtensions txt CFBundleTypeMIMETypes text/plain CFBundleTypeName NSStringPboardType CFBundleTypeRole Viewer LSItemContentTypes public.plain-text LSTypeIsPackage 0 NSDocumentClass SLiMDocument CFBundleTypeExtensions pdf CFBundleTypeMIMETypes application/pdf CFBundleTypeName PDF document CFBundleTypeRole Viewer LSItemContentTypes com.adobe.pdf LSTypeIsPackage 0 NSDocumentClass SLiMPDFDocument CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 5.2 CFBundleSignature ???? CFBundleVersion 1 LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2016–2025 Benjamin C. Haller, http://messerlab.org/slim/. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass NSApplication UTExportedTypeDeclarations UTTypeConformsTo public.text UTTypeDescription SLiM model UTTypeIconFile slim.icns UTTypeIdentifier edu.messerlab.slim UTTypeReferenceURL https://messerlab.org/slim/ UTTypeTagSpecification com.apple.nspboard-type NSStringPboardType com.apple.ostype SLiM public.filename-extension slim public.mime-type text/plain ================================================ FILE: SLiMgui/SLiMguiLegacy_multi-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDocumentTypes CFBundleTypeExtensions slim CFBundleTypeIconFile slim.icns CFBundleTypeMIMETypes text/plain CFBundleTypeName SLiM model CFBundleTypeRole Editor LSItemContentTypes edu.messerlab.slim LSTypeIsPackage 0 NSDocumentClass SLiMDocument CFBundleTypeExtensions txt CFBundleTypeMIMETypes text/plain CFBundleTypeName NSStringPboardType CFBundleTypeRole Viewer LSItemContentTypes public.plain-text LSTypeIsPackage 0 NSDocumentClass SLiMDocument CFBundleTypeExtensions pdf CFBundleTypeMIMETypes application/pdf CFBundleTypeName PDF document CFBundleTypeRole Viewer LSItemContentTypes com.adobe.pdf LSTypeIsPackage 0 NSDocumentClass SLiMPDFDocument CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 5.2 CFBundleSignature ???? CFBundleVersion 1 LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2016–2025 Benjamin C. Haller, http://messerlab.org/slim/. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass NSApplication UTExportedTypeDeclarations UTTypeConformsTo public.text UTTypeDescription SLiM model UTTypeIconFile slim.icns UTTypeIdentifier edu.messerlab.slim UTTypeReferenceURL https://messerlab.org/slim/ UTTypeTagSpecification com.apple.nspboard-type NSStringPboardType com.apple.ostype SLiM public.filename-extension slim public.mime-type text/plain ================================================ FILE: SLiMgui/SLiMguiLegacy_multi.entitlements ================================================ com.apple.security.cs.disable-library-validation ================================================ FILE: SLiMgui/Tip_END.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 The end \b0\fs26 \ \ That's all the tips available for now! This panel won't come up again unless you (1) rewind to the first tip now, (2) reset all panels in SLiMgui's Preferences, OR (3) upgrade to a newer version of SLiMgui with additional tips.\ \ Speaking of more tips, we're always trying to make SLiM more approachable. If you have suggestions for new tips (things you found confusing about SLiM when you first started using it, hidden features you only discovered later, etc.), please let us know! You can email us with suggestions using the "Send Feedback on SLiM" menu item in the Help menu:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width4580 \height2780 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\fs26 \cf0 \ Notice there are also menu items there for others ways to stay in touch with us, including the slim-announce and slim-discuss mailing lists and the SLiM home page. Thanks! Enjoy SLiM!} ================================================ FILE: SLiMgui/Tips/Tip 1 optclick for help.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;\f2\fnil\fcharset0 Menlo-Regular; } {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Option-click for help \b0\fs26 \ \ In SLiMgui, you can option-click on any keyword, function, method, operator, etc., in the script view:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width3800 \height1720 }} \f0\fs26 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Note the crosshairs cursor. This will bring up the Help panel with targeted help about the item you option-clicked \'96 in this case, the \f2\fs24 initializeMutationRate() \f0\fs26 function:\ \fs18 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 5.tiff \width9240 \height3780 }}\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\fs26 \cf0 \ (For those not up on the jargon: to "option-click" means to hold down the "option" key, also knowns as the "alt" key, and then click on something.)} ================================================ FILE: SLiMgui/Tips/Tip 10 generation textfield.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Advancing to a given generation \b0\fs26 \ \ In SLiMgui, there is a text field that shows the generation number:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width3220 \height600 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This textfield has a hidden feature. You can click in it to select the generation number, and then type a new number:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width3220 \height600 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ When you press return, SLiMgui will play the simulation forward to the beginning of the generation you specified:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 2.tiff \width3220 \height600 }}} ================================================ FILE: SLiMgui/Tips/Tip 11 chromosome subrange.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Displaying a subrange of the chromosome \b0\fs26 \ \ SLiMgui displays a mysterious blue strip above the main chromosome view:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 2.tiff \width4720 \height1920 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This strip is for selecting a subrange of the chromosome for display in the view below. Just click and drag out a selection in the top strip, and the chromosome view will show you information about the selected range:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 3.tiff \width4720 \height2160 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ You can click and drag the selection endpoint flags to adjust the selection, or a simple click in the top strip will go back to displaying the full chromosome.} ================================================ FILE: SLiMgui/Tips/Tip 12 see script object text.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Seeing the code for defined script objects \b0\fs26 \ \ There's a drawer on SLiMgui's window that can be opened by clicking the drawer button:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width720 \height720 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The drawer that opens shows some useful additional information about the model running. You can see a table of the active mutation types, and another of the genomic element types that have been defined, for example. There is also a table showing the script blocks that have been defined. There's a little hidden feature in SLiMgui: if you hover the mouse over the entries in that table, the code for the script blocks will be displayed in a tooltip:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 2.tiff \width4000 \height2540 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ In most situations this isn't important, since you can see the code for your script blocks in the script area anyway. But if your model is dynamically adding and removing script blocks, perhaps with dynamically generated code for the added blocks, this feature can provide a very useful window into what script blocks currently exist in your running model.} ================================================ FILE: SLiMgui/Tips/Tip 13 speed slider.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 The play speed slider \b0\fs26 \ \ There's a little slider under the Play button in SLiMgui:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width3260 \height1500 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This slider controls the speed at which SLiMgui plays forward the simulation. Normally it is set to play at full speed, but it can sometimes be quite useful to slow it down. This allows you to see the changes in each generation, one by one, without having to click the Step button repeatedly, for example. It can also be quite useful in teaching situations, so that you can talk over a running model with time to explain what is happening. Just drag the slider to the speed you want.} ================================================ FILE: SLiMgui/Tips/Tip 14 console window.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 The Eidos console window \b0\fs26 \ \ There is a full Eidos interpreter console available in SLiMgui, for direct access to the scripting environment while a simulation is running. Just click on the Console button, at the top of the scripting area of the window:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width920 \height800 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This opens the console window:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width8140 \height6540 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ In this window, you can immediately execute any Eidos commands you wish. These commands can inspect or even alter the state of the currently running simulation. In the example above, a sorted list of the positions of all segregating mutations in the simulation has been printed. This can be a good way to experiment with code for a new script event; once you've figured out how to do what you want, you can copy the final code over into your simulation script.} ================================================ FILE: SLiMgui/Tips/Tip 15 variable browser.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;\f2\fnil\fcharset0 Menlo-Regular; } {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 The variable browser \b0\fs26 \ \ SLiMgui provides an easy way to inspect the state of the objects underlying your running simulation. Just click on the Variable Browser button at the top of the scripting pane:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width920 \height800 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This opens the variable browser:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width8760 \height5320 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Initially, all of the top-level constants and variables defined in the simulation are displayed (constants are shown in gray, variables in black). Variables that are objects have subvalues that can be revealed by clicking the disclosure triangle to the left of their name. Expanding \f2\fs24 sim \f0\fs26 , for example, we can see the state defining the \f2\fs24 SLiMSim \f0\fs26 object that represents the running simulation:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 2.tiff \width8760 \height5320 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The objects accessible through \f2\fs24 sim \f0\fs26 , like \f2\fs24 sim.chromosome \f0\fs26 , can be expanded in turn, and you can browse through the whole simulation in this manner. The browser automatically updates when the simulation changes state, giving you a live view into the internals of SLiM.} ================================================ FILE: SLiMgui/Tips/Tip 16 chromosome muttypes.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;\f2\fnil\fcharset0 Menlo-Regular; } {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \margl1440\margr1440\vieww9340\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Displaying only one mutation type \b0\fs26 \ \ Some simulations, particularly those involving neutral background mutations, can present a rather cluttered impression in the chromosome view:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width8420 \height1260 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This simulation does involve QTLs of mutation type \f2\fs24 m2 \f0\fs26 , but they're awfully hard to see among all the neutral mutations. You can bring up a context menu on the chromosome view by right-clicking or control-clicking on it:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 4.tiff \width8420 \height1620 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Selecting \f2\fs24 m2 \f0\fs26 , the mutation type for the QTLs, then provides an uncluttered view showing only mutations of that type:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 2.tiff \width8420 \height1260 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ With several mutation types, you can also select any subset of the mutation types for display; in a model with \f2\fs24 m1 \f0\fs26 , \f2\fs24 m2 \f0\fs26 , and \f2\fs24 m3 \f0\fs26 , you could display \f2\fs24 m1 \f0\fs26 and \f2\fs24 m3 \f0\fs26 but not \f2\fs24 m2 \f0\fs26 , for example.\ \ You can restore the chromosome view to showing all mutation types by selecting "Display all mutations" from the context menu.} ================================================ FILE: SLiMgui/Tips/Tip 17 population display options.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \margl1440\margr1440\vieww9340\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Alternative population visualizations \b0\fs26 \ \ The default visualization for the population shows every individual in the selected subpopulations, colored according to fitness:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 6.tiff \width7720 \height2920 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This simulation has three subpopulations, one maladapted and two relatively well-adapted. It can be useful to view the population differently, however. To do so, control-click or right-click on the population view, and choose a different visualization from the context menu:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 7.tiff \width7720 \height2920 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The "fitness line plot" style shows one line per supopulation, with the \i x \i0 axis being fitness and the \i y \i0 axis being frequency. It is therefore a set of superimposed fitness histograms, in essence:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 8.tiff \width7720 \height2920 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Here the patterns of genetic diversity and fitness can be seen in a somewhat different light. The "fitness bar plot" style shows a single histogram for fitness across all of the selected subpopulations:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 9.tiff \width7720 \height2920 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Depending upon what it is you want to know, each of these visualization styles might prove useful.} ================================================ FILE: SLiMgui/Tips/Tip 18 profiling simulations.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \margl1440\margr1440\vieww9340\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Profiling simulations \b0\fs26 \ \ For complex simulations that take a long time to run, it can often be useful to find out where SLiM is spending most of its time. Such an analysis, called a \i profile \i0 , can direct optimization efforts in your modeling, and can even help to find bugs. Beginning in version 2.4, SLiMgui now supports collecting such profile information for your models. Just click the small profile button (with the stopwatch icon) that now overlaps the play button:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic profile_button.jpg \width2860 \height1400 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The simulation will play forward as if you had clicked play, but it will profile the simulation as it runs. When the run completes (or when you click the profile button again to stop profiling), SLiMgui will open a profile report window:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic report.jpg \width6080 \height2360 }} \f0\fs26 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This report contains all sorts of information about the runtime performance of your model; see the SLiM manual for more details.} ================================================ FILE: SLiMgui/Tips/Tip 19 chromosome haplotypes.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \margl1440\margr1440\vieww9340\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Displaying haplotypes in the chromosome view \b0\fs26 \ \ The standard frequency-based display used by the chromosome view is useful for seeing the relative frequencies of mutations:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_347793.jpg \width8420 \height1260 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ However, this display style is unhelpful for seeing patterns of association among mutations, such as linkage disequilibrium and haplotypes that may have established in the population. For an alternative display style that is better for that purpose, you can bring up a context menu on the chromosome view by right-clicking or control-clicking on it:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_885386.jpg \width8420 \height1780 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Selecting Display Haplotypes provides a display in which a random subset of genomes from the selected subpopulations are displayed, one per pixel row in the view, clustered according to their genetic similarity:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_160492.jpg \width8420 \height1260 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This display shows the genetic structure of the population much more clearly (at the loss of some ease of comprehensibility). You can restore the chromosome view to its default frequency-based display by selecting "Display Frequencies" from the context menu.\ \ Note that a new haplotype plot is also now available as a type of graph is SLiMgui; it provides the same type of visualization, but in a larger window with various options regarding the genetic clustering and the appearance.} ================================================ FILE: SLiMgui/Tips/Tip 2 autogen script.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Autogenerating script \b0\fs26 \ \ SLiMgui provides various commands for automatically generating Eidos script to perform simple simulation tasks. This is particularly helpful for those who are new to SLiM or to scripting in Eidos.\ \ For example, the buttons underneath the subpopulation table let you autogenerate script related to population structure \'96 adding and removing subpopulations, changing their size or sex ratio or selfing rate, and so forth:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 2.tiff \width4140 \height660 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Autogeneration of commands related to output can be done with a pop-up menu located, logically, near the output pane:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 3.tiff \width5120 \height1500 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ And autogeneration of commands related to genetic structure, such as adding mutation types, genomic element types, and so forth, can be done with another pop-up menu, next to the chromosome view:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 4.tiff \width4240 \height2220 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ In all cases, just select the autogeneration command you want, and a sheet will appear allowing you to configure the new script that SLiMgui will generate for you. For example, here's the sheet shown to configure a new "output fixed mutations" command:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 5.tiff \width5520 \height3960 }}} ================================================ FILE: SLiMgui/Tips/Tip 20 script prettyprinting.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \margl1440\margr1440\vieww9360\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Script prettyprinting \b0\fs26 \ \ Nicely formatted code is much more readable, but fixing the indentation level on all the lines of a script can be tedious. SLiMgui provides an automated script prettyprinting feature that will fix the indentation of your script for you. Just click the Prettyprint Script button, at the top of the scripting area of the window:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_841421.jpg \width920 \height720 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This reformats your script by its fixing indentation according to standard conventions. For example, take this badly-formatted script (for a function that computes the average nucleotide heterozygosity in a subpopulation, available in the SLiM-Extras archive online):\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_120059.jpg \width8480 \height3620 }} \f0\fs26 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This mess gets cleaned up to this:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_197763.jpg \width8480 \height3620 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Much better!} ================================================ FILE: SLiMgui/Tips/Tip 21 MutationType DFE visualization.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;\f2\fnil\fcharset0 Menlo-Regular; } {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \margl1440\margr1440\vieww9360\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 MutationType DFE visualization \b0\fs26 \ \ Mutation types in SLiM define a distribution of fitness effects, of DFE, from which new mutations of that type are drawn. This can be a fixed selection coefficient, or a predefined distribution such as a gamma or exponential distribution customized by specified parameters, or even an Eidos script snippet that generates selection coefficients dynamically. It would be nice to have a way to see the DFE for a given mutation type, to make sure that it has been configured correctly. SLiMgui provides a way to do this. First open the drawer on the SLiMgui window by clicking the drawer button:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width720 \height720 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The drawer that opens shows several tables with supplemental information about the simulation that is running. The top table shows a list of the mutation types have been defined; for a hypothetical model, it might look like:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_880540.jpg \width4820 \height2100 }} \f0\fs26 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ If you hover the mouse cursor over the \f2\fs24 m2 \f0\fs26 line without moving, a small plot appears in a tooltip over the table, showing the DFE for \f2\fs24 m2 \f0\fs26 :\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_889172.jpg \width4820 \height2540 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The gamma distribution used by \f2\fs24 m3 \f0\fs26 can similarly be seen by hovering the cursor over that line:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_905211.jpg \width4820 \height2760 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Note that the range of the \i x \i0 -axis changes to fit the DFE. Mutation type \f2\fs24 m4 \f0\fs26 has a script-based DFE that calls \f2\fs24 rbinom() \f0\fs26 to draw a selection from a discrete distribution; that can be visualized in the same way:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic constrictorTemp_595368.jpg \width4820 \height3000 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ If there is an error in the script for the DFE, such that SLiMgui can't execute it, a question mark will be displayed in place of the DFE plot.} ================================================ FILE: SLiMgui/Tips/Tip 3 see prototypes.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fnil\fcharset0 Menlo-Regular;\f2\fswiss\fcharset0 Helvetica; } {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 See function/method prototypes \b0\fs26 \ \ When you're working on a model in SLiMgui, sometimes you just want a quick reminder of the parameters that a function or method requires, without opening up the help window. Just click to put the insertion point right inside the parentheses () for the call you're interested in. The "prototype" for the function or method will appear in the status bar at the bottom of the SLiMgui window. Here the insertion point has been placed inside the \f1\fs24 outputFixedMutations() \f0\fs26 call:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f2\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width7760 \height1360 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This shows that \f1\fs24 outputFixedMutations() \f0\fs26 is a method that takes two parameters: one named \f1\fs24 filePath \f0\fs26 that can be either \f1\fs24 NULL \f0\fs26 or a singleton \f1\fs24 string \f0\fs26 (with a default of \f1\fs24 NULL \f0\fs26 ), and one named \f1\fs24 append \f0\fs26 that takes a \f1\fs24 logical \f0\fs26 singleton, with a default of \f1\fs24 F \f0\fs26 . See the Eidos manual for details on the format of function/method prototypes.} ================================================ FILE: SLiMgui/Tips/Tip 4 code completion.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fnil\fcharset0 Menlo-Regular;\f2\fswiss\fcharset0 Helvetica; } {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Code completion \b0\fs26 \ \ One of the most powerful tools in the SLiMgui editor is code completion. This facility lets you insert the names of functions, methods, properties, variables, and keywords automatically, based upon a prefix that you've typed, or with no prefix at all. For example, if you want to add a call to an output method on the simulation object ( \f1\fs24 sim \f0\fs26 ), but you can't remember the exact name of the method you want, try typing \f1\fs24 sim.out \f0\fs26 and then pressing the key (probably labeled "esc" on your keyboard). You'll see something like this:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f2\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 6.tiff \width4000 \height1860 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The combo box (as it is called) shows all of the methods on \f1\fs24 sim \f0\fs26 that start with \f1\fs24 out \f0\fs26 .\ \ SLiMgui is smart enough to know variables you have defined, and to figure out their type in almost all cases. So suppose you define a variable \f1\fs24 g \f0\fs26 that contains \f1\fs24 Genome \f0\fs26 objects, and then you autocomplete off of that:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f2\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width3900 \height3160 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ As you can see, SLiMgui shows the properties and methods that are specific to the \f1\fs24 Genome \f0\fs26 class, because it knows \f1\fs24 g \f0\fs26 's type. If you decide you want the \f1\fs24 addNewDrawnMutation() \f0\fs26 method, but you don't want to type it all out, just type "add" and press again. You will see:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f2\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 5.tiff \width3900 \height2060 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ The combo box responds to the arrow keys, so just press the down-arrow key to select the second line, and then press return to accept the completion, leaving you with:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f2\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 4.tiff \width3900 \height1260 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Incidentally, command-. (hold down the command key and press the period key) is also a shortcut for code completion, if you prefer.\ } ================================================ FILE: SLiMgui/Tips/Tip 5 config chromosome view.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Chromosome view display options \b0\fs26 \ \ The chromosome view in SLiMgui is more configurable than you might realize. There are four buttons to the right of it:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width1240 \height1240 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Click them to toggle on/off display of particular aspects of the chromosome. The M button is on by default; it displays all segregating mutations, with a color that depends on the selection coefficient of the mutation type, and a height that depends on the frequency in the population. The F button turns on display of fixed mutations as well, showing them as full-height blue lines. The R button turns on display of the recombination map; this is useful if you have set up the recombination rate to vary along the chromosome. Finally, the G buttons turns on a color-coded display of the genomic element types used along the chromosome; if you have set up genetic structure, such as introns, exons, UTRs, etc., this display can be quite useful.} ================================================ FILE: SLiMgui/Tips/Tip 6 fast recipe access.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Opening SLiM cookbook "recipes" \b0\fs26 \ \ SLiM's manual has a "cookbook" section with several dozen different "recipes" for a wide variety of models. There is a quick and easy way to open these recipes in SLiMgui. Just go to the Open Recipe submenu of the File menu, and all of the recipes in your version of SLiM can be opened directly:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width9240 \height4300 }}} ================================================ FILE: SLiMgui/Tips/Tip 7 comment in out.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Commenting & uncommenting code \b0\fs26 \ \ When working on a script in SLiMgui, it is often useful to be able to "comment out" sections of code to disable them, and then remove the comment characters to re-enable them later. The editor in SLiMgui makes this easy. If you select some lines of code:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width4780 \height1500 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Press command-/ to comment out the selected lines:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width4780 \height1500 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ And then press command-/ again to uncomment them:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width4780 \height1500 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Basically, if the lines are commented out, command-/ will uncomment them; otherwise, it will turn them into a comment.} ================================================ FILE: SLiMgui/Tips/Tip 8 shift left right.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Indenting & outdenting code \b0\fs26 \ \ It's nice to write properly indented code, following standard coding conventions. However, it can be annoying doing the formatting by hand, inserting or removing tabs on each line. SLiMgui provides a facility for easily increasing or decreasing the indentation level of your code. Just select some lines:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width5340 \height1620 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Press command-] to increase the indentation level by inserting tab characters at the beginning of each line:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width5340 \height1620 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ Or press command-[ to decrease the indentation level by deleting tab characters from the beginning of each line:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic.tiff \width5340 \height1620 }}} ================================================ FILE: SLiMgui/Tips/Tip 9 help by title or content.rtfd/TXT.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Optima-Regular;\f1\fswiss\fcharset0 Helvetica;\f2\fnil\fcharset0 Menlo-Regular; } {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww19880\viewh13000\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\b\fs32 \cf0 Help search scoping \b0\fs26 \ \ The help window in SLiMgui has a feature that is easy to miss, but quite useful: you can change the scope of your searches in the help. If you click on the magnifying glass in the search field of the help window, you get a pop-up menu:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f1\fs24 \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width3600 \height2400 }}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 \f0\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \cf0 \ This allows you to configure the help window to search only in help item titles, or by the full text of the help items. If you search for "fixed" in titles only, for example, you will find only the method \f2\fs24 outputFixedMutations() \f0\fs26 , but if you search by content, you will find discussion of fixed mutations in many other sections, such as the \f2\fs24 convertToSubstitution \f0\fs26 property of \f2\fs24 MutationType \f0\fs26 , and the substitutions property of \f2\fs24 SLiMSim \f0\fs26 .} ================================================ FILE: SLiMgui/TipsWindowController.h ================================================ // // TipsWindowController.h // SLiM // // Created by Ben Haller on 12/2/16. // Copyright (c) 2016-2022 Philipp Messer. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import extern NSString *SLiMDefaultsShowTipsPanelKey; extern NSString *SLiMDefaultsTipsIndexKey; @interface TipsWindowController : NSObject { NSArray *tipsFilenames; NSInteger lastTipShown; } @property (nonatomic, retain) IBOutlet NSPanel *tipsWindow; @property (nonatomic, assign) IBOutlet NSTextView *tipsTextView; @property (nonatomic, assign) IBOutlet NSTextField *tipsNumberTextField; @property (nonatomic, assign) IBOutlet NSButton *suppressPanelCheckbox; @property (nonatomic, assign) IBOutlet NSButton *nextButton; + (void)showTipsWindowOnLaunch; // // Actions // - (IBAction)rewindButtonClicked:(id)sender; - (IBAction)nextButtonClicked:(id)sender; - (IBAction)doneButtonClicked:(id)sender; @end ================================================ FILE: SLiMgui/TipsWindowController.m ================================================ // // TipsWindowController.m // SLiM // // Created by Ben Haller on 12/2/16. // Copyright (c) 2016-2022 Philipp Messer. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #import "TipsWindowController.h" NSString *SLiMDefaultsShowTipsPanelKey = @"ShowTipsPanel"; NSString *SLiMDefaultsTipsIndexKey = @"TipsIndex"; NSString *SLiMDefaultsTipsCountKey = @"TipsCount"; NSString *SLiMTipsDirectoryName = @"Tips"; @implementation TipsWindowController + (void)initialize { [[NSUserDefaults standardUserDefaults] registerDefaults:@{ SLiMDefaultsShowTipsPanelKey : @YES, SLiMDefaultsTipsIndexKey : @(-1), // tip index 0, after advancing automatically SLiMDefaultsTipsCountKey : @(15), // DO NOT CHANGE; this was the number of tips prior to adding this default }]; } + (void)showTipsWindowOnLaunch { if ([[NSUserDefaults standardUserDefaults] boolForKey:SLiMDefaultsShowTipsPanelKey]) { static TipsWindowController *controller = nil; if (!controller) [[[TipsWindowController alloc] init] autorelease]; [controller showTipsPanel]; } } - (instancetype)init { if (self = [super init]) { // Assess the tips on tap NSArray *paths = [[NSBundle mainBundle] pathsForResourcesOfType:nil inDirectory:SLiMTipsDirectoryName]; tipsFilenames = [paths valueForKey:@"lastPathComponent"]; tipsFilenames = [tipsFilenames sortedArrayUsingSelector:@selector(localizedStandardCompare:)]; [tipsFilenames retain]; NSInteger newTipCount = [tipsFilenames count]; lastTipShown = [[NSUserDefaults standardUserDefaults] integerForKey:SLiMDefaultsTipsIndexKey]; // If we were showing the "end tip" but now we have new tips, then we need to go backwards from the end tip NSInteger oldTipCount = [[NSUserDefaults standardUserDefaults] integerForKey:SLiMDefaultsTipsCountKey]; if (newTipCount > oldTipCount) { if (lastTipShown == oldTipCount) lastTipShown--; [[NSUserDefaults standardUserDefaults] setInteger:newTipCount forKey:SLiMDefaultsTipsCountKey]; } } return self; } - (void)dealloc { [self setTipsWindow:nil]; [tipsFilenames release]; [super dealloc]; } - (void)showTipsPanel { // If we've already shown the user all of the available tips, then we don't show the panel again // until more become available (in a new version of SLiMgui, presumably) if (lastTipShown >= (NSInteger)[tipsFilenames count]) return; // Load the tips panel [[NSBundle mainBundle] loadNibNamed:@"TipsWindow" owner:self topLevelObjects:nil]; if (_tipsWindow) { [_tipsWindow setDelegate:self]; [self retain]; [self nextButtonClicked:nil]; // advance to the next available tip [_tipsWindow makeKeyAndOrderFront:nil]; } } - (void)windowWillClose:(NSNotification *)notification { bool suppressPanel = ([_suppressPanelCheckbox state] == NSOnState); if (suppressPanel) [[NSUserDefaults standardUserDefaults] setBool:NO forKey:SLiMDefaultsShowTipsPanelKey]; [_tipsWindow setDelegate:nil]; [self autorelease]; } // // Actions // - (IBAction)rewindButtonClicked:(id)sender { lastTipShown = -1; [self nextButtonClicked:nil]; } - (IBAction)nextButtonClicked:(id)sender { lastTipShown++; [[NSUserDefaults standardUserDefaults] setInteger:lastTipShown forKey:SLiMDefaultsTipsIndexKey]; //NSLog(@"Advanced to %ld", (long)lastTipShown); // Set the enable state of the Next button; but note we allow ourselves to go one over, to show the "out of tips" message bool moreTips = (lastTipShown >= (NSInteger)[tipsFilenames count]); [_nextButton setEnabled:!moreTips]; // Show the tip NSString *tipPath; if (lastTipShown < (NSInteger)[tipsFilenames count]) { // Tip index is in range, so grab one from our filename array tipPath = [[NSBundle mainBundle] pathForResource:[tipsFilenames objectAtIndex:lastTipShown] ofType:nil inDirectory:SLiMTipsDirectoryName]; [_tipsNumberTextField setStringValue:[NSString stringWithFormat:@"#%ld", (long)lastTipShown + 1]]; } else { // Tip index is not in range, so put up the "out of tips" message tipPath = [[NSBundle mainBundle] pathForResource:@"Tip_END.rtfd" ofType:nil inDirectory:nil]; [_tipsNumberTextField setStringValue:@"∞"]; } [_tipsTextView readRTFDFromFile:tipPath]; [_tipsTextView scrollPoint:NSZeroPoint]; } - (IBAction)doneButtonClicked:(id)sender { [_tipsWindow performClose:nil]; } @end ================================================ FILE: SLiMgui/main.m ================================================ // // main.m // SLiMgui // // Created by Ben Haller on 1/20/15. // Copyright (c) 2015-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // #import int main(int argc, const char * argv[]) { return NSApplicationMain(argc, argv); } ================================================ FILE: SLiMgui/plot.h ================================================ // // plot.h // SLiMgui // // Created by Ben Haller on 1/30/2024. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class Plot represents a user-created plot in SLiMgui. It is available only when running under SLiMgui. */ #ifndef __SLiM__plot__ #define __SLiM__plot__ #include #include #include #include "eidos_value.h" #include "slim_globals.h" @class SLiMWindowController; extern EidosClass *gSLiM_Plot_Class; class Plot : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; public: Plot(const Plot&) = delete; // no copying Plot& operator=(const Plot&) = delete; // no copying Plot(void); ~Plot(void); // // Eidos support // virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_abline(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addLegend(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_axis(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_image(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendLineEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendPointEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendSwatchEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_legendTitleEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_lines(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_matrix(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_mtext(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_points(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_rects(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_segments(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setBorderless(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_text(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_write(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); }; class Plot_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: Plot_Class(const Plot_Class &p_original) = delete; // no copy-construct Plot_Class& operator=(const Plot_Class&) = delete; // no copying inline Plot_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif /* __SLiM__plot__ */ ================================================ FILE: SLiMgui/plot.mm ================================================ // // plot.cpp // SLiMgui // // Created by Ben Haller on 1/30/24. // Copyright (c) 2024-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "plot.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #import "SLiMWindowController.h" #include #pragma mark - #pragma mark Plot #pragma mark - Plot::Plot(void) { } Plot::~Plot(void) { } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - const EidosClass *Plot::Class(void) const { return gSLiM_Plot_Class; } void Plot::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay(); // standard EidosObject behavior (not Dictionary behavior) } EidosValue_SP Plot::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_title: { // The user has no way to access this property in SLiMguiLegacy return gStaticEidosValue_StringEmpty; } // variables // all others, including gID_none default: return super::GetProperty(p_property_id); } } void Plot::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP Plot::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_abline: return ExecuteMethod_abline(p_method_id, p_arguments, p_interpreter); case gID_addLegend: return ExecuteMethod_addLegend(p_method_id, p_arguments, p_interpreter); case gID_axis: return ExecuteMethod_axis(p_method_id, p_arguments, p_interpreter); case gID_image: return ExecuteMethod_image(p_method_id, p_arguments, p_interpreter); case gID_legendLineEntry: return ExecuteMethod_legendLineEntry(p_method_id, p_arguments, p_interpreter); case gID_legendPointEntry: return ExecuteMethod_legendPointEntry(p_method_id, p_arguments, p_interpreter); case gID_legendSwatchEntry: return ExecuteMethod_legendSwatchEntry(p_method_id, p_arguments, p_interpreter); case gID_legendTitleEntry: return ExecuteMethod_legendTitleEntry(p_method_id, p_arguments, p_interpreter); case gID_lines: return ExecuteMethod_lines(p_method_id, p_arguments, p_interpreter); case gID_matrix: return ExecuteMethod_matrix(p_method_id, p_arguments, p_interpreter); case gID_mtext: return ExecuteMethod_mtext(p_method_id, p_arguments, p_interpreter); case gID_points: return ExecuteMethod_points(p_method_id, p_arguments, p_interpreter); case gID_rects: return ExecuteMethod_rects(p_method_id, p_arguments, p_interpreter); case gID_segments: return ExecuteMethod_segments(p_method_id, p_arguments, p_interpreter); case gID_setBorderless: return ExecuteMethod_setBorderless(p_method_id, p_arguments, p_interpreter); case gID_text: return ExecuteMethod_text(p_method_id, p_arguments, p_interpreter); case gEidosID_write: return ExecuteMethod_write(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* – (void)abline(...) // EidosValue_SP Plot::ExecuteMethod_abline(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)addLegend(...) // EidosValue_SP Plot::ExecuteMethod_addLegend(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)axis(...) // EidosValue_SP Plot::ExecuteMethod_axis(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)image(...) // EidosValue_SP Plot::ExecuteMethod_image(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)legendLineEntry(...) // EidosValue_SP Plot::ExecuteMethod_legendLineEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)legendPointEntry(...) // EidosValue_SP Plot::ExecuteMethod_legendPointEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)legendSwatchEntry(...) // EidosValue_SP Plot::ExecuteMethod_legendSwatchEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)legendTitleEntry(...) // EidosValue_SP Plot::ExecuteMethod_legendTitleEntry(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)lines(...) // EidosValue_SP Plot::ExecuteMethod_lines(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)matrix(...) // EidosValue_SP Plot::ExecuteMethod_matrix(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)mtext(...) // EidosValue_SP Plot::ExecuteMethod_mtext(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)points(...) // EidosValue_SP Plot::ExecuteMethod_points(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)rects(...) // EidosValue_SP Plot::ExecuteMethod_rects(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)segments(...) // EidosValue_SP Plot::ExecuteMethod_segments(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)text(...) // EidosValue_SP Plot::ExecuteMethod_text(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)setBorderless(...) // EidosValue_SP Plot::ExecuteMethod_setBorderless(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // ********************* – (void)write(string$ filePath) // EidosValue_SP Plot::ExecuteMethod_write(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // The user has no way to call this method in SLiMguiLegacy, since createPlot() does not return a Plot object. return gStaticEidosValueVOID; } // // Plot_Class // #pragma mark - #pragma mark Plot_Class #pragma mark - EidosClass *gSLiM_Plot_Class = nullptr; const std::vector *Plot_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_title, true, kEidosValueMaskString | kEidosValueMaskSingleton))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *Plot_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_abline, kEidosValueMaskVOID)) ->AddNumeric_ON("a", gStaticEidosValueNULL)->AddNumeric_ON("b", gStaticEidosValueNULL) ->AddNumeric_ON("h", gStaticEidosValueNULL)->AddNumeric_ON("v", gStaticEidosValueNULL) ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addLegend, kEidosValueMaskVOID)) ->AddString_OSN("position", gStaticEidosValueNULL)->AddInt_OSN("inset", gStaticEidosValueNULL) ->AddNumeric_OSN("labelSize", gStaticEidosValueNULL)->AddNumeric_OSN("lineHeight", gStaticEidosValueNULL) ->AddNumeric_OSN("graphicsWidth", gStaticEidosValueNULL)->AddNumeric_OSN("exteriorMargin", gStaticEidosValueNULL) ->AddNumeric_OSN("interiorMargin", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_axis, kEidosValueMaskVOID)) ->AddInt_S("side")->AddNumeric_ON("at", gStaticEidosValueNULL) ->AddArgWithDefault(kEidosValueMaskLogical | kEidosValueMaskString | kEidosValueMaskOptional, "labels", nullptr, gStaticEidosValue_LogicalT)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_image, kEidosValueMaskVOID)) ->AddObject_S(gStr_image, nullptr)->AddNumeric_S("x1")->AddNumeric_S("y1")->AddNumeric_S("x2")->AddNumeric_S("y2") ->AddLogical_OS("flipped", gStaticEidosValue_LogicalF)->AddFloat_OS("alpha", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_legendLineEntry, kEidosValueMaskVOID)) ->AddString_S("label")->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_OS("lwd", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_legendPointEntry, kEidosValueMaskVOID)) ->AddString_S("label")->AddInt_OS("symbol", gStaticEidosValue_Integer0) ->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddString_OS("border", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_OS("lwd", gStaticEidosValue_Float1)->AddNumeric_OS("size", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_legendSwatchEntry, kEidosValueMaskVOID)) ->AddString_S("label")->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red")))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_legendTitleEntry, kEidosValueMaskVOID)) ->AddString_S("label")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_lines, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddString_OS("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_OS("lwd", gStaticEidosValue_Float1)->AddFloat_OS("alpha", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_matrix, kEidosValueMaskVOID)) ->AddNumeric("matrix")->AddNumeric_S("x1")->AddNumeric_S("y1")->AddNumeric_S("x2")->AddNumeric_S("y2") ->AddLogical_OS("flipped", gStaticEidosValue_LogicalF)->AddNumeric_ON("valueRange", gStaticEidosValueNULL) ->AddString_OSN("colors", gStaticEidosValueNULL)->AddFloat_OS("alpha", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_mtext, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddString("labels") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("size", EidosValue_Float_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(10))) ->AddNumeric_ON("adj", gStaticEidosValueNULL)->AddFloat_O("alpha", gStaticEidosValue_Float1) ->AddNumeric_O("angle", gStaticEidosValue_Float0)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_points, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddInt_O("symbol", gStaticEidosValue_Integer0) ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddString_O("border", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddNumeric_O("size", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_rects, kEidosValueMaskVOID)) ->AddNumeric("x1")->AddNumeric("y1")->AddNumeric("x2")->AddNumeric("y2") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddString_O("border", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_segments, kEidosValueMaskVOID)) ->AddNumeric("x1")->AddNumeric("y1")->AddNumeric("x2")->AddNumeric("y2") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("red"))) ->AddNumeric_O("lwd", gStaticEidosValue_Float1)->AddFloat_O("alpha", gStaticEidosValue_Float1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setBorderless, kEidosValueMaskVOID)) ->AddNumeric_OS("marginLeft", gStaticEidosValue_Float0)->AddNumeric_OS("marginTop", gStaticEidosValue_Float0) ->AddNumeric_OS("marginRight", gStaticEidosValue_Float0)->AddNumeric_OS("marginBottom", gStaticEidosValue_Float0)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_text, kEidosValueMaskVOID)) ->AddNumeric("x")->AddNumeric("y")->AddString("labels") ->AddString_O("color", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("black"))) ->AddNumeric_O("size", EidosValue_Float_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(10))) ->AddNumeric_ON("adj", gStaticEidosValueNULL)->AddFloat_O("alpha", gStaticEidosValue_Float1) ->AddNumeric_O("angle", gStaticEidosValue_Float0)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gEidosStr_write, kEidosValueMaskVOID)) ->AddString_S(gEidosStr_filePath)); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: SLiMgui/slim_gui.h ================================================ // // slim_gui.h // SLiMgui // // Created by Ben Haller on 1/19/19. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class SLiMgui represents the SLiMgui application's Eidos interface. It is available only when running under SLiMgui. */ #ifndef __SLiM__slim_gui__ #define __SLiM__slim_gui__ #include #include #include #include "eidos_value.h" #include "eidos_symbol_table.h" #include "slim_globals.h" @class SLiMWindowController; extern EidosClass *gSLiM_SLiMgui_Class; class SLiMgui : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; public: Community &community_; // We have a reference to our community object SLiMWindowController *controller_; // We have a reference to the SLiMgui window controller for our simulation EidosSymbolTableEntry self_symbol_; // for fast setup of the symbol table SLiMgui(const SLiMgui&) = delete; // no copying SLiMgui& operator=(const SLiMgui&) = delete; // no copying SLiMgui(void) = delete; // no null construction SLiMgui(Community &p_community, SLiMWindowController *p_controller); ~SLiMgui(void); // // Eidos support // inline EidosSymbolTableEntry &SymbolTableEntry(void) { return self_symbol_; } virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_createPlot(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_logFileData(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_openDocument(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_pauseExecution(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_plotWithTitle(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); }; class SLiMgui_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: SLiMgui_Class(const SLiMgui_Class &p_original) = delete; // no copy-construct SLiMgui_Class& operator=(const SLiMgui_Class&) = delete; // no copying inline SLiMgui_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif /* __SLiM__slim_gui__ */ ================================================ FILE: SLiMgui/slim_gui.mm ================================================ // // slim_gui.cpp // SLiMgui // // Created by Ben Haller on 1/19/19. // Copyright (c) 2019-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "slim_gui.h" #include "log_file.h" #include "plot.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #import "SLiMWindowController.h" #include #pragma mark - #pragma mark SLiMgui #pragma mark - SLiMgui::SLiMgui(Community &p_community, SLiMWindowController *p_controller) : community_(p_community), controller_(p_controller), self_symbol_(gID_slimgui, EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_SLiMgui_Class))) { } SLiMgui::~SLiMgui(void) { } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - const EidosClass *SLiMgui::Class(void) const { return gSLiM_SLiMgui_Class; } void SLiMgui::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay(); // standard EidosObject behavior (not Dictionary behavior) } EidosValue_SP SLiMgui::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_pid: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(getpid())); } // variables // all others, including gID_none default: return super::GetProperty(p_property_id); } } void SLiMgui::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP SLiMgui::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_createPlot: return ExecuteMethod_createPlot(p_method_id, p_arguments, p_interpreter); case gID_logFileData: return ExecuteMethod_logFileData(p_method_id, p_arguments, p_interpreter); case gID_openDocument: return ExecuteMethod_openDocument(p_method_id, p_arguments, p_interpreter); case gID_pauseExecution: return ExecuteMethod_pauseExecution(p_method_id, p_arguments, p_interpreter); case gID_plotWithTitle: return ExecuteMethod_plotWithTitle(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* – (No$)createPlot(...) // EidosValue_SP SLiMgui::ExecuteMethod_createPlot(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) static bool beenHere = false; if (!beenHere) { // Emit a warning that this API is unsupported in SLiMguiLegacy. We warn, not error, so the user does not have to modify // their script to run it in SLiMguiLegacy when it is designed to run under QtSLiM; they just won't get a plot window. std::cerr << "WARNING (SLiMgui::ExecuteMethod_createPlot): createPlot() is not supported in SLiMguiLegacy, and does nothing." << std::endl; beenHere = true; } return gStaticEidosValueNULL; } // ********************* – (Nfs)logFileData(o$ logFile, is$ column) // EidosValue_SP SLiMgui::ExecuteMethod_logFileData(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) static bool beenHere = false; if (!beenHere) { // Emit a warning that this API is unsupported in SLiMguiLegacy. We warn, not error, so the user does not have to modify // their script to run it in SLiMguiLegacy when it is designed to run under QtSLiM; they just won't get data. std::cerr << "WARNING (SLiMgui::ExecuteMethod_logFileData): logFileData() is not supported in SLiMguiLegacy, and does nothing." << std::endl; beenHere = true; } return gStaticEidosValueNULL; } // ********************* – (void)openDocument(string$ path) // EidosValue_SP SLiMgui::ExecuteMethod_openDocument(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue_String *filePath_value = (EidosValue_String *)p_arguments[0].get(); std::string file_path = Eidos_ResolvedPath(Eidos_StripTrailingSlash(filePath_value->StringRefAtIndex_NOCAST(0, nullptr))); NSString *filePath = [NSString stringWithUTF8String:file_path.c_str()]; [controller_ eidos_openDocument:filePath]; return gStaticEidosValueVOID; } // ********************* – (void)pauseExecution(void) // EidosValue_SP SLiMgui::ExecuteMethod_pauseExecution(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) [controller_ eidos_pauseExecution]; return gStaticEidosValueVOID; } // ********************* – (No$)plotWithTitle(...) // EidosValue_SP SLiMgui::ExecuteMethod_plotWithTitle(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) static bool beenHere = false; if (!beenHere) { // Emit a warning that this API is unsupported in SLiMguiLegacy. We warn, not error, so the user does not have to modify // their script to run it in SLiMguiLegacy when it is designed to run under QtSLiM; they just won't get a plot window. std::cerr << "WARNING (SLiMgui::ExecuteMethod_plotWithTitle): plotWithTitle() is not supported in SLiMguiLegacy, and does nothing." << std::endl; beenHere = true; } return gStaticEidosValueNULL; } // // SLiMgui_Class // #pragma mark - #pragma mark SLiMgui_Class #pragma mark - EidosClass *gSLiM_SLiMgui_Class = nullptr; const std::vector *SLiMgui_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_pid, true, kEidosValueMaskInt | kEidosValueMaskSingleton))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *SLiMgui_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_createPlot, kEidosValueMaskNULL | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Plot_Class))->AddString_S("title") ->AddNumeric_ON("xrange", gStaticEidosValueNULL)->AddNumeric_ON("yrange", gStaticEidosValueNULL) ->AddString_OS("xlab", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("x"))) ->AddString_OS("ylab", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("y"))) ->AddNumeric_OSN("width", gStaticEidosValueNULL)->AddNumeric_OSN("height", gStaticEidosValueNULL) ->AddLogical_OS("horizontalGrid", gStaticEidosValue_LogicalF)->AddLogical_OS("verticalGrid", gStaticEidosValue_LogicalF) ->AddLogical_OS("fullBox", gStaticEidosValue_LogicalT) ->AddNumeric_OS("axisLabelSize", EidosValue_Int_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(15))) ->AddNumeric_OS("tickLabelSize", EidosValue_Int_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(10)))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_logFileData, kEidosValueMaskNULL | kEidosValueMaskFloat | kEidosValueMaskString)) ->AddObject_S("logFile", gSLiM_LogFile_Class)->AddIntString_S("column")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_openDocument, kEidosValueMaskVOID))->AddString_S(gEidosStr_filePath)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_pauseExecution, kEidosValueMaskVOID))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_plotWithTitle, kEidosValueMaskNULL | kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Plot_Class))->AddString_S("title")); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: TO_DO ================================================ OK, this has turned out not to be a "to do" file so much as a place to put useful little snippets and such... new cmake-based command line building: cd SLiM mkdir build cd build cmake .. make cause cmake to be re-run (after addition/removal of source files, etc.; this is necessary because we use GLOB with "file" in our cmake config): cd SLiM touch ./CMakeLists.txt look for exported symbols that are not tagged with "eidos" or "Eidos" somewhere in the symbol before the parameter list: nm -g -U -j ./EidosScribe | grep -E -v "^_+([^_\r\n]+_)*[^_]*[Ee]idos" dump demangled symbols from SLiMgui: nm -g -U -j ./SLiMgui | c++filt some valgrind commands (set SLIM_LEAK_CHECKING to 1, and change the compile C/C++ flags to -O1 -g): valgrind --leak-check=yes --track-origins=yes --expensive-definedness-checks=yes --num-callers=20 ./slim -testEidos (with Debug version) valgrind --leak-check=yes --track-origins=yes --expensive-definedness-checks=yes --num-callers=20 ./slim -testSLiM (with Debug version) valgrind --tool=cachegrind ./slim -seed 1 /Users/bhaller/Desktop/MK_SLiMGUIsim4.txt (with Release version) cg_annotate --sort=DLmr:1,DLmw:1 cachegrind.out.71431 valgrind --tool=callgrind ./slim -seed 1 /Users/bhaller/Desktop/MK_SLiMGUIsim4.txt (with Release version) callgrind_annotate --tree=both callgrind.out.72015 valgrind --tool=massif ./slim -seed 1 /Users/bhaller/Desktop/MK_SLiMGUIsim4.txt (with Release version) ms_print massif.out.73964 valgrind --tool=exp-sgcheck ./slim -testEidos (with Debug version; not on Darwin yet) valgrind --tool=exp-sgcheck ./slim -testSLiM (with Debug version; not on Darwin yet) cpplint.py for checking includes (https://github.com/google/styleguide/tree/gh-pages/cpplint): cd /Users/bhaller/Documents/Research/MesserLab/SLiM_project/SLiM python2.7 /Users/bhaller/bin/cpplint.py --filter=-,+build/include_what_you_use ./core/*.cpp ./eidos/*.cpp ./eidostool/*.cpp ./QtSLiM/*.cpp NOTE: there is a newer repo that I should shift over to using, https://github.com/cpplint/cpplint clang-tidy with CMake: see https://danielsieger.com/blog/2021/12/21/clang-tidy-cmake.html install a good version of clang first: sudo port install clang-17 if you use MacPorts find where clang-tidy is living: port contents clang-17 (for me it is at /opt/local/libexec/llvm-17/bin/clang-tidy) similarly locate clang and clang++ (for me, /opt/local/libexec/llvm-17/bin/clang and /opt/local/libexec/llvm-17/bin/clang++) fix the CMakeLists.txt file to have the correct paths for these things (in the section near the top, "if(TIDY)") make a build directory for SLiM with mkdir, and cd to it run cmake (one of the below): cmake -D TIDY=ON path-to-SLiM-repo cmake -D TIDY=ON -D CMAKE_BUILD_TYPE=Debug path-to-SLiM-repo cmake -D TIDY=ON -D CMAKE_BUILD_TYPE=Debug -D CMAKE_PREFIX_PATH=/Users/bhaller/Qt5.15.2/5.15.2/clang_64 -D BUILD_SLIMGUI=ON /Users/bhaller/Documents/Research/MesserLab/SLiM_project/SLiM make eidos / make slim / make SLiMgui CR/LF issues: file -e soft tells you what a file is (-e soft turns off "magic", which misdiagnoses SLiM files) tab2space -lf converts a file to Unix-style LF only convert all text files in the current directory to use LF: find . -name \*.txt -exec tab2space -lf {} {} \; -print tab issues: convert leading spaces to tabs in recipes: find . -name \*.txt -exec bash -c 'gunexpand -t 4 --first-only "$0" > /tmp/totabbuff && mv /tmp/totabbuff "$0"' {} \; -print finding out the generation number of a running simulation in lldb: lldb /usr/local/bin/slim process attach --pid X (from ps -ax | grep slim) breakpoint set --method _RunOneTick c (lldb breaks at _RunOneTick...) if you have symbols, just p generation_; if not: si (repeat until you reach movl $0x0, 0x40(%r15) which is perhaps around +30; this is generation_stage_ = SLiMGenerationStage::kStage0PreGeneration) (now %r15 is the this pointer...) register read r15 --format hex memory read --size 4 --format x --count 50 `0x00007f9e28413f80` (where the hex value of r15 goes in the backquotes) the generation is at the end of the fourth line, as SLiM is currently set up: 0x7f9e28413fb0: 0x28414310 0x00007f9e 0x00000001 0x000021ef -> 0x000021ef is the generation if the memory layout moves, just look for 0x00000007 which will be the generation stage at this point in the code; the generation is immediately before it reassuring the Clang analyzer that a precondition is met: #ifdef __clang_analyzer__ assert(p_argument_count >= 5); #endif count total lines of code in the project (excluding the gsl code): ( find ./core/ ./eidos/ ./eidostool/ ./EidosScribe ./SLiMgui ./QtSLiM '(' -name '*.mm' -or -name '*.h' -or -name '*.cpp' ')' -print0 | xargs -0 cat ) | wc -l ( find ./core/ ./eidos/ '(' -name '*.mm' -or -name '*.h' -or -name '*.cpp' ')' -print0 | xargs -0 cat ) | wc -l or better, use cloc (available in MacPorts); see https://stackoverflow.com/a/5121125/2752221 a useful LaTeX equation editor: https://www.codecogs.com/latex/eqneditor.php find all instances of a string (or regex) in the commit history: git --no-pager log -G"pedigree" to get a text diff of changes to the Xcode project file: git --no-pager diff --text SLiM.xcodeproj/project.pbxproj to dump class memory layouts in Xcode: add -Xclang and -fdump-record-layouts to "Other C++ Flags" in the project rebuild one file of interest with command-control-B go to the Report navigator, click on the build of the file click on the compile execution line in the build log, then click on the report viewer icon at the right edge of that line ================================================ FILE: VERSIONS ================================================ VERSION HISTORY This version history begins with the final release of version 2.0. Versions prior to 2.0 are lost in the mists of time; since the switch from 1.8 to 2.0 was so large, it was not worth tracking all of the individual changes that were made. Note that not every commit will be logged here; that is what the Github commit history is for. The purpose of this file is to record major feature additions, major bug fixes, etc., so that it is easier to track down which version number a particular major change occurred in. To that end, major changes made in the development head will be logged below; when we decide to roll a new version number and do a new release, all of those development head changes will be reassigned as belonging to the new version number. development head (in the master branch): add a rmultinom() function to draw from a multinomial distribution, matching rmultinom() in R version 5.2 (Eidos version 4.2): add cppcheck support in our cmake config, for development testing purposes speed up the SpatialMap smooth() method in 2D without periodicity, since this is important for the Chevy et al. competition algorithm add Plot method setBorderless() for making plots without border annotations -- no axes, ticks, labels fix display of images in Plot; it was antialiasing in some cases (such as PDF generation), which is not desirable add segments() call to Plot, for plotting a set of unconnected line segments add rects() call to Plot, for plotting a set of rectangles extend text() to support drawing text at an angle, with new [float angle = 0.0] parameter add mtext() call to Plot, for drawing text in the margins outside the plot area add rowSums() and colSums() functions to Eidos, for use with matrices as a faster alternative to apply() add the PCG random number generator, switch to pcg32_fast and pcg64_fast, remove all use of the old taus2 and MT19937-64 generators; note this completely breaks backward reproducibility fix various bugs involving conflicts between defined constants and other symbols, including #573 and #574; this sets new definition rules that could break some existing scripts (but is unlikely to) fix #575, QtSLiM terminates early when single-stepping with a rescheduled script block fix #579, crash in models with (a) tree-seq recording, (b) multiple chromosomes, AND (c) rejection of proposed offspring in modifyChild() add readLine() function for obtaining stdin input to running models, thanks to Chris Talbot fix #580, add isClose() and allClose() Eidos functions for comparison within a given tolerance shift to macOS 14 as the oldest supported macOS version in GitHub Actions; we no longer test on macOS 13, or with Qt 5.x on macOS (but Qt 5.x still gets tested on other platforms, for now) SIMD and SLEEF optimizations thanks to Andy Kern: add SIMD optimization for sqrt(), abs(), floor(), ceil(), trunc(), round(), sum(), product(), https://github.com/MesserLab/SLiM/pull/578 further SIMD optimizations for exp(), log(), log10(), log2() based on the SLEEF framework (https://github.com/shibatch/sleef), https://github.com/MesserLab/SLiM/pull/587 SIMD optimization for dnorm() using the previous SLEEF work, https://github.com/MesserLab/SLiM/pull/588 SIMD optimizations for spatial interaction strength calculations, https://github.com/MesserLab/SLiM/pull/590 add comprehensive tests for spatial interaction kernel calculations; includes SIMD vs scalar consistency tests, C++ level tests, and script-level tests, https://github.com/MesserLab/SLiM/pull/592 add SIMD acceleration for trig functions (sin(), cos(), tan(), asin(), acos(), atan(), atan2()), power functions (^ opperator, pow, SpatialMap.pow()), https://github.com/MesserLab/SLiM/pull/595 add SIMD acceleration to SpatialMap.smooth() for 2-6x speedup, https://github.com/MesserLab/SLiM/pull/599 fix #593, Student's T distribution had a sign error in the SpatialKernel method tdist(), thanks to Andy Kern, https://github.com/MesserLab/SLiM/issues/591 update citations listed by citation() CI workflow for calculating code coverage with Codecov, thanks to Andy Kern, https://github.com/MesserLab/SLiM/pull/601 fix #594, the pedigreeID and haplosomePedigreeID properties should be accessible (without other predigree-tracking features) in tree-seq models, even without pedigree tracking being enabled fix #584, a way to generate a random 64-bit integer is needed; added new rdunif64(integer$ n) function for this purpose shift over to Release 1 of the Total-Random fork of the PCG random number generator (better maintained), found at https://github.com/Total-Random/pcg-cpp/releases/tag/v1.0 fix #602, (float)rdirichlet(integer$ n, numeric alpha) function to Eidos for Dirichlet distribution draws fix #610, distribution draws behave inconsistently with out-of-range parameters rlnorm() accepted SD < 0 and returned a value; it should error, as rnorm() does (R returns NAN in the SD < 0 case, but erroring is more consistent with Eidos policies) rexp() returned values for rate < 0; this should also error, consistent with Eidos policies add zygosityOfMutations([No mutations = NULL], [integer$ hemizygousValue = 1], [integer$ haploidValue = 1]) for assessing zygosity (see also mutationsFromHaplosomes()) policy change: the nucleotide and nucleotideValue properties of Substitution are now read-only (I think it was a bug that they were ever read-write...?) change rects drawn by the Plot method rects() to have mitered corners, not bevelled fix #564, initializeMutationRateFromFile() needs a `sex` parameter fix #570, setMigrationRates() should make it easier to stop all migration (done in multitrait to avoid the annoying doc conflicts) specifically, this entails two changes: allow a singleton migration rate value, applied for all subpops given allow the destination subpop to be given as a source, iff all rates supplied are 0.0, to stop all migration: allSubpops.setMigrationRates(allSubpops, 0.0) fix #567, plot windows can have their aspect ratio distorted due to screen size and other constraints extend the Eidos identical() function to allow comparison of more than two parameters; e.g., (logical$)identical(* x, * y, ...) policy change: mean(integer(0)) or mean(float(0)) now returns NAN rather than NULL, to match R; I just noticed this, and NAN does seem better fix #612, Haplosome needs a mutationCount property fix #621, add a Laplace distribution function, rlaplace() fix a bug (a regression, introduced in SLiM 3.7!) in printing of large float values; ".0" is sometimes shown erroneously fix #598: in Xcode and Qt Creator, force AVX2 and FMA on for x86_64 builds (true since 2013), and NEON on for ARM64 builds (true since forever); this is pretty safe, and makes SIMD/SLEEF work even without the CMake magic for them version 5.1 (Eidos version 4.1): add recipe 16.11, life-long monogamous mating add the ability to suppress the header line of a LogFile, with a new [logical$ header = T] parameter to createLogFile() (#516), and adding [Nl$ header = NULL] for setFilePath() (#516) add [string$ sex = "*"] parameter to initializeRecombinationRateFromFile(), passed through to initializeRecombinationRate(); fixing an oversight (#524) write "chromosomes" and "this_chromosome" keys out to provenance as well as metadata (#520) fix an annoying autoindent cursor position bug that I suspect is a Qt 6 regression in QPlainTextEdit add a define of TSK_TRACE_ERRORS in Xcode for slim and SLiMguiLegacy, in DEBUG only, for easier tskit debugging; break on _tsk_trace_error() fix a regression that caused the sourceSubpop pseudo-parameter to be NULL in modifyChild() callbacks in all cases; add tests, improve documentation (#522) add chromosomeSubposition property to Haplosome, providing whether the haplosome is index 0 or 1 within its individual, within the set of haplosomes for its associated chromosome add a (numeric)sign(numeric x) function that returns the sign (-1, 0, 1) of each element calcFST() with a windowed range (start/end) would error; was missing a unit test (#548) calcSFS() with an integer (non-NULL) binCount would error in multi-chromosome models if mutations from multiple chromosomes (or NULL) were given survival() callback that moves individuals did not correctly update has_null_haplosomes_, leading to a crash or a DEBUG raise (#549) calcMeanFroh() did not focus on the specified chromosome correctly, in multi-species models, and thus produced incorrect results (#545) add infinite loop checking in Eidos; add [logical$ checkInfiniteLoops = T] option to initializeSLiMOptions() to disable checking for infinite loops in Eidos fix #539, crash reading malformed population file fix #535, allow transparency in plots: added [float alpha = 1.0] to Plot methods abline(), lines(), points(), and text() (required to be singleton for lines()) fix #547, mutation trajectories plot is confusing in the way it starts collecting data at the time it is opened fix #534, support uniparentally-transmitted chromosomes in hermaphrodite populations add new Plot legendTitleEntry() method for making a title line in a plot legend click-drag in the SLiMgui script view's line number area now drag-selects whole lines fix #550, add filter() function to Eidos, which is essentially a subset of R's filter(): (float)filter(numeric x, float filter, [lif$ outside = F]) partial fix for #530, add function (numeric$)tr(numeric x) to obtain the trace of a square matrix partial fix for #530, add function (numeric)outerProduct(numeric x, numeric y) to obtain the outer product of vectors x and y partial fix for #530, add function (numeric$)det(numeric x) to obtain the determinant of a square matrix partial fix for #530, add function (float)inverse(numeric x) to obtain the inverse of a square non-singular matrix partial fix for #530, add function (numeric)matrixPow(numeric x, integer$ power) to raise a matrix to an integer power partial fix for #530, extend cor() and cov() to support correlation/covariance between the columns of a matrix (or matrices), as in R policy change: cov(x, y), cor(x, y), var(x), and sd(x) with x and/or y of length 1 now returns NAN (used to return NULL); this is because you can't make a matrix containing NULLs, and Eidos doesn't have NA (which is what R uses in this case) add Duplicate command to the Edit command, command-D, useful for code editing; change Show Debugging Output to shift-command-D add "Show SLiMgui Color Scales" command to the Help menu in SLiMgui fix #553, an off-by-one bug in initializeRecombinationRateFromFile(); breaks backward compatibility, by fixing this bug add image() method to Plot to add a SpatialMap or Image to a plot: (void)image(object$ image, numeric$ x1, numeric$ x2, numeric$ y1, numeric$ y2, [logical$ flipped = F], [float$ alpha = 1.0]) add matrix() method to Plot to add a matrix-based image to a plot: (void)matrix(numeric matrix, numeric$ x1, numeric$ x2, numeric$ y1, numeric$ y2, [logical$ flipped = F], [Nif valueRange = NULL], [Ns$ colors = NULL], [float$ alpha = 1.0]) fix #538, check for unique SLiM ids only for alive individuals when loading a tree sequence fix #533, memory smasher in Haplosome method nucleotides(format="char"); besides causing random crashes, this also caused this method to return incorrect results fix #527, add calcLD_D() and calcLD_Rsquared() popgen functions, contributed by Vitor Sudbrack fix apparently new STL check that double comparators for std::sort() must induce a strict weak ordering, including with NAN values; self-tests started raising fix #558, add new ways of subsetting matrices/arrays with A[B], or assigning into subsets of matrices/arrays with A[B] = ... : with a conformable logical matrix/array, or with an integer matrix that selects elements of the target add asVector() method to strip off dimension attributes; this can be done with c() or x[] also, but it's nice to have the function, since R has it... fix #526, add a calcDxy() popgen utility function to SLiM that calculates Nei's Dxy genetic divergence statistic, contributed by Vitor Sudbrack fix #529, extend calcFST() to support multiple chromosomes extend subsetMutations() to support subsetting mutations belonging to more than one chromosome fix #560, add recipe 14.16 "Visualizing linkage disequilibrium" to demonstrate the new calcLD_D() and calcLD_Rsquared() functions version 5.0 (Eidos version 4.0): multi-chromosome transition: --- main changes to existing APIs: class Genome -> class Haplosome Haplosome class (from Genome class): property genomeType -> removed in favor of `chromosome.type` property isNullGenome -> isNullHaplosome property genomePedigreeID -> haplosomePedigreeID method mutationCountsInGenomes -> mutationCountsInHaplosomes method mutationFrequenciesInGenomes -> mutationFrequenciesInHaplosomes Subpopulation class: property genomes -> haplosomes property genomesNonNull -> haplosomesNonNull Individual class: property genomes -> haplosomes property genomesNonNull -> haplosomesNonNull property genome1 -> haploidGenome1 property genome2 -> haploidGenome2 Species class: property chromosome: now usable only in single-chromosome models property chromosomeType -> removed in favor of `chromosome.type` MutationType class: property haploidDominanceCoeff -> hemizygousDominanceCoeff (applies only facing a null haplosome, not in "true" haploid chromosomes!) mutation() callbacks: pseudo-parameter genome -> haplosome recombination() callbacks: pseudo-parameter genome1 -> haplosome1 pseudo-parameter genome2 -> haplosome2 parameter name changes: calcFST(), calcPairHeterozygosity(), calcHeterozygosity(), calcWattersonsTheta(), calcInbreedingLoad(), calcPi(), calcTajimasD() parameter name changes: addEmpty(), addRecombinant() --- main additions to APIs: (object$)initializeChromosome(integer$ id, [Ni$ length = NULL], [string$ type = "A"], [Ns$ symbol = NULL], [Ns$ name = NULL], [integer$ mutationRuns = 0]) initializeSex(): should now allow, and default to, NULL which should be used for all new models; "A" / "X" / "Y" should still be allowed for backward compatibility Haplosome class: property: chromosome => (object$) Chromosome class: property id => (integer$) property length => (integer$) property type => (string$) property symbol => (string$) property name <-> (string$) Species class: property chromosomes => (object) property sexChromosomes => (object) method: (object)chromosomesOfType(string$ type) method: (object)chromosomesWithIDs(integer ids) method: (object)chromosomesWithSymbols(string symbols) Individual class: method: (object)haplosomesForChromosomes([Niso chromosomes = NULL], [Ni index = NULL], [logical$ includeNulls = T]) Mutation class: property: chromosome => (object$) Subpopulation class: method: (object)haplosomesForChromosomes([Niso chromosomes = NULL], [Ni index = NULL], [logical$ includeNulls = T]) method: (object)addMultiRecombinant(object$ pattern, [Nfs$ sex = NULL], [No$ parent1 = NULL], [No$ parent2 = NULL], [logical$ randomizeStrands = F], [integer$ count = 1], [logical$ defer = F]) --- policy changes: policy change: the old "haplosome type" (A/X/Y) in the genome metadata is now a chromosome type (different) in the haplosome metadata policy change: similarly, haplosome type A/X/Y in SLiM output (e.g., outputFull()) is now a chromosome type (different) policy change: initialization order is a bit stricter; initializeSex() with "X" or "Y" must come earlier, before calls that would define an implicit "A" chromosome policy change: the 1D SFS graph and haplotype plot no longer depend upon the current chromosome range selection; that was weird, and doesn't work well in multichrom policy change: the "remove fixed mutations" stage of the WF tick cycle is now after "offspring become parents" (no user-visible difference except WF tick cycle diagram) policy change: mutationRuns= for initializeSLiMOptions() has been changed to [l$ doMutationRunExperiments=T]; pass mutationRuns= to initializeChromosome() instead policy change (slight): the haplosome1Null and haplosome2Null parameters to addEmpty() now apply only to type "A" chromosomes, causing a minor break in backward compatibility policy change: randomizeStrands must now usually be explicitly specified for both addRecombinant() and addMultiRecombinant(); the default is now NULL, and NULL errors so that T or F must be explicitly given unless it does not matter (i.e., there is no recombination) policy change: fix #487, changing TSK_SIMPLIFY_KEEP_UNARY to TSK_SIMPLIFY_KEEP_UNARY_IN_INDIVIDUALS for retainCoalescentOnly=F (a change in simplification behavior) policy change: color variables on Individual now exist only under SLiMgui; at the command line there is now no memory backing them, so they cannot be used as scratch space --- tree-sequence recording changes: shift to a tree sequence for each chromosome, sharing the node, individual, and population tables across all tree sequences; new TreeSeqInfo struct for per-chromosome state remembered_haplosomes_ is now remembered_nodes_; it retains nodes that are shared across the tree sequences, and that can represent multiple haplosomes Haplosome no longer keeps its tskit node id; the individual keeps two tskit node ids for first and second position, used across all chromosomes in the shared node table added a list of chromosomes to the top-level metadata (`chromosomes`), changed the top-level schema to describe this (optional) list added a description of the focal chromosome to the top-level metadata (`this_chromosome`), changed the top-level schema to describe this (required) key changed Genome metadata to Haplosome metadata, removed chromosome type, identical for all haplosomes of a given position in an individual the way the is_null flags for haplosomes is handled is completely reworked and rather complex, of necessity; see comments in the code relative to original multichrom design, changed node metadata from `is_null` to `is_vacant` to reflect semantic shift; now it indicates non-null (0) versus unused/null (1); changed schema bumped .trees version number to 0.9 treeSeqOutput() writes a single .trees file for single-chromosome models (the existing behavior), but now creates a "trees archive" directory with .trees files inside for multichrom models when saving a trees archive, [l$ overwriteDirectory=F] now controls whether an existing directory that looks like a trees archive will be overwritten (dangerous!) --- input/output formats and methods: the default symbol for implicit chromosomes are "A", "X", or "Y", so output formats don't change for legacy models fix built-in mutation/substitution output methods for multiple chromosomes: Species method outputMutations(): in models with >1 chromosome, this now outputs the chromosome symbol after the position Species method outputFixedMutations(): in models with >1 chromosome, this now outputs the chromosome symbol after the position fix built-in MS input/output methods for multiple chromosomes: Haplosome method outputMS(): check that all haplosomes belong to a single chromosome, output MS based on that Subpopulation method outputMSSample(): add a [Niso$ chromosome = NULL] parameter to identify the chromosome to sample with Haplosome method readFromMS(): check that all haplosomes belong to a single chromosome, input MS based on that fix built-in SLiM-format partial input/output methods for multiple chromosomes: Haplosome method output(): check that all haplosomes belong to a single chromosome; output the chromosome symbol in the header if multi-chrom Subpopulation method outputSample(): check that all haplosomes belong to a single chromosome fix built-in VCF input/output methods for multiple chromosomes: Haplosome method outputVCF(): check that all haplosomes belong to a single chromosome; pairs haplosomes as appropriate Subpopulation method outputVCFSample(): check that all haplosomes belong to a single chromosome; use chromosome symbols for the CHROM column add [l$ groupAsIndividuals = T] parameter; if F, each haplosome is emitted as a haploid call rather than forming diploids, regardless of the chromosome's ploidy Haplosome_Class::ExecuteMethod_readFromVCF(): require VCF data that is single-chromosome, as with output fix built-in SLiM-format full input/output methods for multiple chromosomes: Species method outputFull(): allow multi-chromosome output with a new sectioned format, for both text and binary Species method readFromPopulationFile(): support reading multi-chromosome output, for both text and binary policy change: readFromPopulationFile() breaks backward compatibility with old file formats, for both text and binary; too much has changed change file format codes for class Haplosome from GS/GM/GV to HS/HM/HV, matching the rename from Genome to Haplosome add support for writing/reading Individual tag values in outputFull(), readFromPopulationFile() add Individual output method +outputIndividuals(), for writing out a sample of individuals in SLiM format with optional metadata add Individual output method +outputIndividualsToVCF(), for writing out a sample of individuals in VCF format with optional metadata add recipe 8.3.6, output from multi-chromosome models add Individual method readIndividualsFromVCF(), allowing reading of multi-chromosome data from VCF into a vector of individuals fix a bug in the neutral-sim tracking logic in readFromVCF() that I don't think anybody ever actually hit (reading a non-neutral mutation for a neutral mutation type) rename the Haplosome method readFromVCF() to readHaplosomesFromVCF(), and readFromMS() to readHaplosomesFromMS(), for naming consistency; add SLiMgui autofix; revise recipe 18.12 for this rename rename Haplosome methods output(), outputMS(), and outputVCF() to outputHaplosomes(), outputHaplosomesToMS(), and outputHaplosomesToVCF(), to parallel the new Individual output methods extend outputFull() to support saving out substitutions, in a table at the end in the same format as outputFixedMutations() change outputFull() individualTags parameter to objectTags, persist tags for individual, mutation, substitution, haplosome, subpopulation, and chromosome extend readFromPopulationFile() to support restoring substitutions and tag values, but only for output in binary format; parsing text is too annoying add [l$ objectTags=F] flag to outputIndividuals(), outputHaplosomes(), outputFixedMutations(), and outputMutations(), mirroring the work done for outputFull() --- other multi-chromosome related changes: add autofixes in SLiMgui for the above API changes revise recipes for new terminology/APIs add multichromosome display to SLiMgui add a "chromosome display" window where you can view all the chromosomes simultaneously improved the accuracy of mutation run experiments, and the quality of the output generated by them, and extended them to be per-chromosome extend subsetMutations() (on Species) to allow the chromosome to also be specified for addSubpop(), haploid=T now requests specifically that type "A" chromosomes be made with their second haplosome null (for haplodiploidy, and backward compatibility) remove the SLiMguiLegacy haplotype plot; it would need to be fixed, and is no longer used by anybody (the one in SLiMgui remains, of course) remove the SLiMguiLegacy chromosome view OpenGL code; again, it would need to be fixed, and maintaining it is an unnecessary headache add templating of GenerateIndividualX() for speed, as was done before for the MungeIndividualX() and HaplosomeX() methods, for speed implement addMultiRecombinant() for complete control over reproduction in multi-chromosome models extend addRecombinant() and addMultiRecombinant() to allow NULL as a breaks vector with two strands, meaning "draw breakpoints for me", to avoid needing drawBreakpoints() in most cases this new code path also allows gene conversion including heteroduplex mismatch repair and gBGC to work with addRecombinant() and addMultiRecombinant() also change to allow a recombination rate to be set for haploid chromosomes; this is potentially useful with addRecombinant() etc. add addPatternForClone(), addPatternForCross(), addPatternForNull(), and addPatternForRecombinant() methods to Species for use with addMultiRecombinant() add recipe 15.22, "Complex multi-chromosome inheritance with addMultiRecombinant()", to illustrate addMultiRecombinant() modified recipes 15.14 and 15.21 to illustrate that NULL can now be passed to addRecombinant() for breaks to get automatic breakpoint generation allow the user to retain Chromosome objects with defineConstant() new PAR (pseudo-autosomal region) recipe for section 15.23, replacing the old recipe in section 14.5 (which will be moved to SLiM-Extras for posterity) new recipe 9.11 (Ne estimation) with a new multichrom-aware version of function estimateNe_Heterozygosity() that requires the chromosome to be supplied recombination() callbacks can now be optionally declared with a chromosome specifier (id or symbol), to apply to just one chromosome registerRecombinationCallback() now takes an optional chromosome parameter to specify that focal chromosome (breaking backward API compatibility) fix up the calc...() functions for multiple chromosomes, and to be robust to the case of zero mutations (existing at all, or within the supplied haplosomes), and to improve error-checking and documentation add a [Niso(0), not an empty Dictionary object (and it leaked) renamed recipe 15.10 to "Recording a pedigree and following a pedigree in a nonWF model" so it is clearer what it does split chapter 15 (nonWF models) into two chapters, involving a great deal of renumbering, reorganizing, and rewriting disable defer=T for all nonWF reproduction methods; it now does the same thing as defer=F (and is documented as having that possibility) add setValuesVectorized() method to EidosDictionaryUnretained, to set values 1:1 from a source vector into dictionary entries of objects; like vectorized property assignment, but for setValue() fix error position tracking for errors inside user-defined functions, and in most nested situations add "absorbing" boundary condition option for deviatePositions(), returning a vector of individuals to be killed because they moved out of bounds add new Subpopulation method deviatePositionsWithMap(), similar to deviatePositions() but adding a spatial map that defines inside/outside bounds disable OpenGL by default on Windows; the user can turn it on in the prefs; just too many people experiencing broken drawing and being confused add a color chart and a plot symbols chart to the SLiMgui Help menu's list of quick-reference charts it can show attempt to fix SLiMgui window visibility issues -- miniaturized windows not becoming visible, off-screen windows remaining offscreen, etc. add calcSFS() function to the popgen utility functions suite, add recipe 9.12 to demonstrate it add recipe 17.19, "Modeling indirect competition mediated by resource availability", demonstrating Sam's resource-explicit approach add calcMeanFroh() function to the popgen utility functions suite, add recipe 14.15 to demonstrate it extend deferred tick scheduling to allow scheduling to occur within a single tick, as long as it is for a later tick cycle stage investigated and diagnosed a break in backward compatibility: models involving the Y use the MT RNG differently (less), and thus diverge from SLiM 4.3 add rztpois() function to Eidos, to draw from a zero-truncated Poisson distribution extend initializeGenomicElement() to allow NULL,NULL for start,end, making it very easy to configure a single chromosome-length genomic element tweak recipes 8.3.3, 8.3.4, 8.3.5, 8.3.6, 8.3.7, 16.3, 16.9, and 16.10 to show off this new functionality, since it's now the nicest way to handle this situation add initializeRecombinationRateFromFile() and initializeMutationRateFromFile() built-in functions; tweak recipe 8.2.2 to utilize it bug fixes: fix #481, add a substitutionsOfType() method to Species, parallel to mutationsOfType() fix #473, that tree-seq scripts with a subtle timing error in the addition of a new mutation would not generate a good error message from SLiM fix #488, non-UTF-8 encoding causes poor error fix #476, external editing causes SLiMgui to get confused fix #479, CLI progress bar would be a nice option; this is now -p[rogress] fix #478, record resource usage information in the tree sequence provenance; now we also print user/sys CPU usage for -t[ime], since we now have the information fix #494, adding a -c / -check command-line option to check the syntax of the input script fix #496, calling addCustomColumn() or addMeanSDColumns() inside a for loop passing the loop index variable for custom= does bad things also, we now require an object value for the context parameter to belong to a class that is under retain-release, since this is a long-term reference and could cause a crash otherwise fix a bug with reloading pedigree IDs after outputFull(), using readFromPopulationFile() version 4.3 (Eidos version 3.3): fixed a bug in calcWattersonsTheta(): the value calculated would be incorrect if a window (start/end) was specified added TOC I and TOC II to the PDF index of the SLiM manual; removed the bottom-of-page links section, which made search difficult add subpopulationsWithNames() method to Community, to facilitate being able to use user-defined names for subpops fix bugs in some graphs in SLiMgui that could cause incorrect display or a crash in various circumstances send debug output to the main window as well as the debug output tab, to avoid important messages getting lost add a label next to the Jump button that shows the declaration of the script block containing the selection; clicking it runs the Jump menu sort recipes by correct numerical order, in both the Recipes menu and the Find Recipe panel, on all platforms update Robin Hood Hashing to 3.11.5 to get some minor fixes update zlib from 1.2.13 to 1.3.1 to get some minor fixes some fixes under the hood to make filesystem paths on Windows, using \ instead of /, be better understood by Eidos fix an occasional crash in sampleIndividuals() due to an off-by-one error in buffer sizing further extend tick range expressions, for scheduling events/callbacks, to allow the use of constants that are not defined until later in a run (variable-length burn-in, etc.) add recipe 17.6 II, to illustrate this new capability improve recording of subpopulations in the tree-sequence recording population table (#447) add calcPi() and calcTajimasD() functions thanks to Nick Bailey extend deviatePositions(), pointDeviated(), sampleNearbyPoint(), and sampleImprovedNearbyPoint() to allow vectorization with a different spatial kernel for each iteration fix a line-ending bug in readFile(); a CRLF file from Windows, read on macOS/Linux, would not strip the CR off the ends of the lines shift to supporting Qt6 and C++17 to build SLiMgui; Qt5 with C++11 is still supported for macOS, Qt6 is now the supported platform for macOS 11/12/13/14; Qt5 should only be used on macOS 10.15 for Windows, Qt6 is now the supported platform for Windows 10 (1809 or later) and Windows 11 for Linux, Qt6 is now the supported platform for Red Hat 8.6/8.8/9.2, openSUSE 15.5, and Ubuntu 22.04 among others these recommendations are based on Qt6's recommended platforms; earlier platforms should use Qt5 for more details see https://doc.qt.io/qt-6/linux.html, https://doc.qt.io/qt-6/windows.html, https://doc.qt.io/qt-6/macos.html building with CMake will now default to Qt6 if CMake can find it, and fall back to Qt5 otherwise; should be automatic add a "Use OpenGL for fast display" checkbox in the Preferences panel, making it possible to disable OpenGL when it doesn't work well (#462) add genomicElementForPosition() and hasGenomicElementForPosition() methods on Chromosome policy change: genomicElements property is now in sorted order (not in order of declaration) add a "Copy as HTML" command to copy script with syntax coloring and such version 4.2.2 (Eidos version 3.2.2): fix SLiMgui crash with code completion fix spurious VCF reading error (number of calls not equal to number of samples) due to incorrect usage of eof() version 4.2.1 (Eidos version 3.2.1): fix an issue in Linux release/installer builds with FORTIFY_SOURCE=3 (resulting in a "buffer overflow" error in self-test and in treeSeqOutput()) fix a crash in BorrowShuffleBuffer(), introduced debugging a different problem; occurs when (a) a nonWF model has (b) a subpop of size 0 (c) at reproduction() time (d) before ever having a non-zero size version 4.2 (Eidos version 3.2): fix for #418, a crash involving null genomes in nonWF (has_null_genomes_ was not set correctly by addCloned() or takeMigrants() when putting a null genome into a subpop that previously had none) big changes to Eidos under the hood - removal of the singleton/vector distinction and EidosValue subclasses, add constness flag, differentiate casting vs. non-casting accesses, etc. policy change: float indices are no longer legal for subsetting, indices must be integer (or a logical vector, as usual); this was inherited from R and is a bad idea for Eidos policy change: assignment into object properties must match the type of the property; no more promotion to integer/float from lower types add some internal magic to accelerate statements of the form "x = c(x,y)" by appending y onto the end of x directly optimize mapValue() / spatialMapValue() - 6x speedup for large-point-vector 2D case, with spatial bounds [0,1] in both dimensions, and no interpolation optimize strength() and interactionDistance() in the case where exerters != NULL; had a very bad algorithm, now O(N) instead of O(NM) where N is # exerters, M is # interactors for the focal receiver broaden the for loop syntax in Eidos to allow multiple 'in' clauses, such as "for (i in 1:5, j in 6:10) ..." as a side effect, for loop index variables are now constants until the loop has finished to demonstrate the new syntax, recipes 10.5, 14.7, and 19.7 have been modified to use it fix #422: readFromPopulationFile() for .trees files in SLiM 4.1 hits a MUCH higher peak memory usage in some cases, due to over-copying of mutation runs add deviatePositions() method to Subpopulation for even faster offspring positioning than with pointDeviated(); revise recipes 16.3 and 16.8 to use it add basic custom plotting capabilities to SLiMgui, through the slimgui object, with new methods createPlot(), lines(), points(), and text(); revise recipes 13.5 and 14.7 to demonstrate this capability add SLiMgui plotting of data from LogFile output, through a context menu in the debug output window; revise recipes 4.2.5 and 15.16 (text only, not code) to demonstrate this capability split out the new plotting functionality into a new class, Plot, available only in SLiMgui add support for legends in the SLiMgui plotting code with addLegend(), legendLineEntry(), legendPointEntry(), and legendSwatchEntry() add logFileData() method to the SLiMgui class to allow access to logged data, which is useful for live plotting in SLiMgui convert recipe 15.17 over to SLiMgui live plotting, to illustrate addLegend(), logFileData(), abline(), etc. add new serialize() mode, "pretty", for pretty-printed Dictionary output improvements to axis scaling, tick labels, axis labels, data range, etc., with some impact on built-in plots, but mostly for custom plots at this point add axis() method to Plot, for fine-scale control over axis tick marks and labels split the "squashed stabilizing selection" recipe out from section 13.6 to make a new section 13.7, utilizing lots of live SLiMgui plotting for advanced visualization add support for external editors (#423): changes made externally will now result in a panel offering to reload from disk, or (if a pref is set) automatic reloading as long as there is no conflict add multispecies tick cycle diagrams to the Help menu in QtSLiM fix Sachin's scoped assignment bug (#430): x=x+1 would do the wrong thing in a local scope where x is defined as a global variable; it would increment the global instead of making a new local extend SLiM script parsing to allow expressions for event/callback timing, evaluated at the end of initialize() callbacks; these may depend upon global constants, and even call functions events and callbacks may now be scheduled for a non-consecutive set of ticks, not just a range; for example, seq() or c() may be used to construct the times at which a script block runs as a part of this work, rescheduleScriptBlock() no longer makes copies of the script block when non-consecutive ticks are requested; a single script block can now be scheduled non-consecutively update recipe 17.4 to use this new facility instead of rescheduleScriptBlock(), and add recipe 4.1.10 demonstrating defining constants for parameters and using them for tick scheduling; also update manual sections 20.2 and 26.1 for this make the Dictionary constructor allow a non-singleton string vector, so the (JSON) result of readFile() can be passed directly to it revised recipes 16.6, 16.7, 16.8, 16.9, 16.10, and 16.11 to use new APIs and conventions (mapValue(), mapColor(), uppercasing defined constants) require the symbol name passed to defineConstant() / defineGlobal() to be a valid Eidos identifier fix a bug that would cause failures to write buffered compressed data from writeFile(), only in SLiMgui; these failures were not reported to the user well (only through SLiMgui's stderr) recipe 14.7 is a ground-up rewrite, to show the utility of using marker mutations to track true local ancestry, including live plotting in SLiMgui add estimatedLastTick() method to Community; useful for custom plotting where you want to know the span of the model up front, to set an axis range, for example adjusted recipe 5.3.1 to use zero-based subpopulation ids, simplifying the logic; the workshop uses this recipe and it was a common source of confusion version 4.1 (Eidos version 3.1): fix a minor bug with autofix when opening multiple .slim documents at once in SLiMgui fix recipe 17.10 to set the time unit, to avoid the timescale warning from tskit add "-fno-math-errno" in Other C++ Flags to improve performance and help with SIMD vectorization; not set for C, only C++, so this does not affect the GSL errno is presently used in C++ only with functions that are not affected by -fno-math-errno, like chdir() and strtoll() and such, so there is no sacrifice here at all split reproduction in nonWF models into a two-stage process of reproduction for all species, then merging for all species, to allow multispecies interactions to be used; no consequence for single-species models add a new build flag: if -D BUILD_NATIVE=ON is set, -march=native is passed to the compiler and a native build, usable only on the build machine itself, is produced for better optimization add a new build flag: if -D BUILD_LTO=ON is set, link-time optimization is enabled (if the compilers say they support it), for better optimization; if the build then fails, don't use it fix a major performance issue for nearestNeighbors(), nearestNeighborsOfPoint(), and subsetIndividuals() with very large subpopulation size memory usage debugging improvements: add a "vm" option to the usage() function, add a usage() method to Community switch from Quickselect to std::nth_element() for choosing medians when building the k-d tree; might break backward reproducibility in some edge cases improve performance of reading text SLiM-format population files, as produced by outputFull() move minimum system requirement up to 10.13 (the oldest SDK supported by Xcode 14.2); the command-line tools should still run on older systems clean up some old usage that is now flagged by Xcode (sprintf to snprintf, deprecated Cocoa API, old build-numbering scripts in the project) advance copyrights to 2023 add the ability to generate profiles at the command line as well as in SLiMgui, making it possible to profile SLiM when running parallel to turn this on in CMake, add "-DPROFILE=ON" to the CMake command line to turn this on in Xcode, set "-DSLIMPROFILING=1" in "Other C Flags" in each target you want to enable it in (not at the project level!) extend the Eidos Dictionary class (but not DataFrame) to allow integer keys, making it more of a general "associative array" class that can represent ragged arrays and similar structures as a side effect, "slim" type dictionary serialization now *always* quotes string keys (policy change), to distinguish, e.g., "10" (string) from 10 (integer) in serialized output extend the Eidos Dictionary class (and DataFrame) to allow objects of all classes to be added; dictionaries containing objects that are not under retain-release just can't be kept long-term improve InteractionType performance for certain queries, especially drawByStrength() with fixed interaction strength bug fix: drawByStrength() would return at most N individuals, where N is the size of the exerter subpop; but this is incorrect since draws are with replacement bug fix: neighborCount() and interactingNeighborCount() would return float(0) instead of integer(0) in certain circumstances, totalOfNeighborStrengths() would return integer(0) instead of float(0) add recipe 14.16, "modeling biallelic loci in script" (complementing recipes 14.15 I and 14.15 II, which show how to do it with built-in mutations and a mutation() callback) disallow multiple log files at the same path, since this is a common mistake and there's no clear use for it bug fix: problem with reading in JSON that contains an "object" (i.e., a dictionary) inside the top-level object update the zlib code in eidos_zlib to version 1.2.13 (13 Oct 2022), fixing some issues (see zlib ChangeLog file) change to DWARF, not DWARF with dSYM, even for Release builds; I don't use the dSYM file to reverse-symbolicate crash reports, so it just slows down my Release builds (but turning it off is unusual, so I'm noting it here) add an optimized case for sample() with replacement, when using a weights vector; orders of magnitude speedup for large tasks shift GitHub Actions for macOS to macos-11 and macos-12; macos-10.15 stopped working in early May 2023, deprecated add an optional parameter to initializeGeneConversion(), [l$ redrawLengthsOnFailure = F], to avoid failures to lay out tracts in some models; no behavior change by default add an optional parameter, [integer$ count = 1], to addCrossed(), addSelfed(), addCloned(), addRecombinant(), and addEmpty() to produce multiple offspring in one call this entails a policy change: if no children are generated (due to rejection by modifyChild(), in particular), object(0) is returned, not NULL add a method -(integer)compactIndices(void) to Dictionary, to compact result dictionaries down to their non-empty values with sequential keys add a new recipe, 17.11, on changing the tree-sequence simplification interval fix a bug with the Species methods mutationCounts() / mutationFrequences(), only for multispecies models; counts/frequencies would be zero for some species this did not occur when running under SLiMgui (different code path), but always happened at the command line add rank() function to Eidos (https://github.com/MesserLab/SLiM/issues/379); identical to R except that logical and string are not supported, and "random" is not supported; can be added later as needed bug fix: #361, more than 2 billion mutations overflows without a good error message bug fix: #365, problems loading a tree sequence; what I fixed is to catch the uncaught raise from std::stoll() in Species::DerivedStatesFromAscii() bug fix in pyslim to announce: https://github.com/tskit-dev/pyslim/issues/307 feature request: #378, add font bigger (command +) and font smaller (command -) menu items and shortcuts, so the user doesn't have to open the prefs window; note that with command = the shift key is needed this meant removal of the shortcuts for prettyprint script and reformat script, since they conflicted (well, prettyprint conflicted); probably nobody cares revised recipe 17.3 to show how to persist tag values from a simulation in the tree-sequence metadata add five logical tag properties to Individual, tagL0 - tagL4, to hold user-defined values of type logical add parent1 and parent2 parameters to addRecombinant() so that parental pedigreeIDs can be tracked properly if desired add sharedParentCount() method, a simplified version of relatedness() using only parent pedigree IDs to find full sibs (2) and haf-sibs (1) split SpatialMap out into its own Eidos class make -defineSpatialMap() return an o$ object that can be used independently make SpatialMap know the spatial bounds that it is aligned to; all uses of a given SpatialMap must use the same spatial bounds! if a subpop changes its bounds, it checks the bounds of the spatial maps it is using make SpatialMap know its name; it is now an intrinsic property of the map, used primarily for SLiMgui display, and must be unique within each subpopulation add a constructor for SpatialMap that duplicates the spatial map object, with a new name allow a given SpatialMap to be retained long-term (e.g., with defineConstant() / defineGlobal()) allow a given SpatialMap to be set on more than one subpopulation, with a new method addSpatialMap(); also removeSpatialMap() to take one out, and a spatialMaps property to get the maps currently on a subpop add utility methods directly on SpatialMap for the things subpop already does – mapValue(), mapColor(), mapImage() – and deprecate the Subpopulation APIs (except spatialMapValue(), which remains useful and undeprecated, and can now take a SpatialMap object) add a changeValues() method on SpatialMap to change the grid values of an existing spatial map add various properties on SpatialMap: gridDimensions, interpolate, name, spatialBounds, spatiality, tag add interpolate() to SpatialMap, to increase the resolution of a spatial map by interpolating new values add smooth() to SpatialMap, to smooth/blur a map; internally, add the SpatialKernel class (not user-visible) add add(), multiply(), subtract(), divide(), power(), exp(), range(), and gridValues() methods to SpatialMap add sampleNearbyPoint() and sampleImprovedNearbyPoint() to SpatialMap, for searching habitat and such add a t-distribution option for SpatialKernel, to provide a fat-tailed distribution that behaves better than Cauchy add a Subpopulation method pointDeviated() that can deviate points using a kernel and enforce boundary conditions in one call tweak WF reproduction, addCrossed(), addSelfed(), addCloned(), and addRecombinant() to give the new offspring the same spatial position as the first parent, allowing efficient positioning with pointDeviated() in the typical usage case policy change: type 'f' kernels now require a finite maxDistance, since the alternative doesn't really make sense (note that type 'l' already required a finite maxDistance) new option in the Individuals view for spatial models, to display the underlying grid points of the spatial map add changeColors(), blend(), and rescale() methods to SpatialMap; extend changeValues() so it can take a SpatialMap as its parameter add Laplace DFE for MutationType, type "p"; contributed by Nick O'Brien extend sampleIndividuals() and subsetIndividuals() to support constraining by tagL0 - tagL4 policy change: if tag is specified (non-NULL), the tag property must now have a defined value on all individuals; previously this was not checked, and individuals with an undefined tag value were simply excluded add [logical$ randomizeStrands = F] flag to addRecombinant; if T, strand1/strand2 and strand3/strand4 will be randomized for each offspring generated add InteractionType method setConstraints(), generalizing sex-specificity with a whole raft of other possible constraints on receivers and/or exerters; interally, now keep two k-d trees, one constrained, one not fixed a bug in passing: drawByStrength() and nearestInteractingNeighbors() were not applying sex-specificity to receivers, for the returnDict=T case (never released publicly) fixed a bug in passing: partial sex-specificity (receivers but not exerters, or exerters but not receivers) was not applied flexibly in multispecies models where one species is sexual and one is not implement 2D bicubic interpolation with periodic boundaries for the interpolate() method update from tskit C_1.0 to C_1.1.2, fixing a memory leak on load of a .trees file, and presumably other things also add testConstraints() method to InteractionType, to test whether a set of individuals satisfy constraints or not add clang-tidy support in CMake (for internal development), do a pass over eidos and core add/modify recipes for new spatial modeling features: new 15.3 V on pointDeviated(); new 15.14 on SpatialMap, interpolate(), smooth(), and sampleNearbyPoint(); new 16.12 on interaction constraints and other stuff modify recipes to demonstrate the use of tagL0: 10.4.2, 12.1, 12.5, 14.2 II, 16.17, 16.24 modify recipes that use addRecombinant() to use randomizeStrands=T where appropriate: 16.17, 16.25 add lowerTri(), upperTri(), and diag(); thanks to Nick O'Brien (@nobrien97) for this contribution modify recipes to use count= parameter to addCrossed() etc., where appropriate: 16.3, 16.23 add xy, xz, yz, and xyz properties to Individual that allow joint access to the x/y/z properties, for easier spatiality operations in complex spatial models (and to support some unit testing additions) fixed a variety of memory leaks and extended the unit testing for leaks considerably; should be pretty clean in its memory usage now switch chapters 15 and 16 (nonWF and spatial models) and reorganize/renumber to rationalize the manual organization; many recipe numbers changed speed up findInterval() by using binary search instead of linear search - O(log n) instead of O(n) to find the interval for one element, with n being the number of intervals (i.e., the length of vec) version 4.0.1 (Eidos version 3.0.1): fix the documentation for the timeUnit parameter of initializeTreeSeq(), which had not been updated for SLiM 4's policy change that the default is now "ticks" in all cases fix a bug that would bite tree-seq models that reloaded with readFromPopulationFile(); such models could trigger an internal error, or leak memory fix LogFile bug with an absolute filesystem path on Windows fix error incorrectly raised for treeSeqRememberIndividuals() with a zero-length individuals vector fix the error position reported for assignment into a non-existent property; this fixes a bug in SLiMgui's autofix feature, as a side effect (with, e.g., "sim.generation = 5") revise recipe 6.1.2 (reading a recombination map from a file) to use readCSV() instead of readFile() extend the subset() method of DataFrame to accept NULL for rows/cols, to take entire columns or entire rows respectively, for usability extend readCSV() to allow sep="", meaning that the separator is "whitespace", as in R add a stringRepresentation() method on Object, to provide the same string representation printed by print() improve memory usage for Individual, down to 192 bytes from 232 bytes, by compactifying the color information for SLiMgui and rearranging ivars to minimize wasted space the color property on Individual no longer guarantees that the value read equals the value set; when a color is set, it is now converted to RGB, so named colors do not round-trip add a meanParentAge property, to make calculating generation length simpler; unavailable in WF models (like age), 0 for parentless individuals add DataFrame asMatrix() method to convert a DataFrame into a matrix, if all columns are the same type/class modify recipe 16.12 II to use asMatrix() to read the mating and death files more elegantly version 4.0 (Eidos version 3.0): fix deprecated pyslim API issues in recipes 17.2, 17.4, 17.5, 17.7, 17.8, 17.10, 18.13: use tskit.load() instead of pyslim.load(), pyslim.recapitate() instead of ts.recapitate(), ts.metadata['SLiM']['generation'] instead of ts.slim_generation add a second recipe to section 16.19 (range expansion in a stepping-stone model), showing how to solve the same migration problem with a survival() callback fix a crash (rather than an error) when calling removeMutations() on a null genome in some situations split SLiMSim into two new classes: Species and Community moved to Community: properties logFiles, generationStage, verbosity; it also has properties tick and tag moved to Community: methods createLogFile(), register[X]Event(), deregisterScriptBlock(), rescheduleScriptBlock(), outputUsage(); also has its own simulationFinished() change generations to ticks across almost all APIs; the basic unit of time is now called a tick, "generation" is a species-specific concept remove the inSLiMgui property of SLiMSim (deprecated since SLiM 3.2.1) remove vestiges of the xDominanceCoeff parameter to initializeSex() and the dominanceCoeffX property of SLiMSim (removed originally in SLiM 3.7) change GO (generation of origin) to TO (tick of origin) in VCF output (should be able to read both annotations) addNewMutation() and addNewDrawnMutation() have had their originGeneration parameter removed (first made redundant in SLiM 3.5) originGeneration property of Mutation and Substitution renamed to originTick; fixationGeneration property of Substitution renamed to fixationTick generation parameter to recalculateFitness() renamed to tick generations parameter to rescheduleScriptBlock() renamed to ticks new addTick() method added to LogFile new output formats include both the tick and cycle, for outputFull(), outputMutations(), outputFixedMutations() top-level tree-sequence metadata produced by outputTreeSeq() now includes both the tick ("tick") and generation ("generation") tree-sequence metadata for mutations has changed the sense of "slim_time" to be in ticks, not generations tree-sequence metadata for individuals has changed the sense of "SLIM_INDIVIDUAL_METADATA_MIGRATED" to indicate migration in the last tick, not the last generation add Generation textfield below the Tick textfield in SLiMgui & SLiMguiLegacy add name and description properties to Species; these are the species name as declared in script and a user-controlled description string; they get persisted in tree-seq top-level metadata increment the tree-seq file version from 0.7 to 0.8 increment version to 4.0 - usually this is not done until it's time to roll the release, but since we're on a branch, it is useful for testing purposes LogFile methods addGeneration(), addPopulationSexRatio(), and addPopulationSize() now take [No$ species = NULL]; if not supplied, it will default to a singleton species if possible Species now supports simulationFinished(), but only in the single-species case; in multispecies models call community.simulationFinished() instead make "early()" required; the default block type has been a source of confusion and is worse now with the multispecies syntax add optional "species " and "ticks " syntax to the SLiM top-level syntax to modify block declarations make the Object Tables window in SLiMgui display all objects, across all species add initializeSpecies() function to set species-specific options in explicit-species models: [i$ tickModulo = 1], [i$ tickPhase = 1], and [Ns$ avatar = NULL], for now SLiMgui now displays avatars in the Object Tables window add allSpecies, allGenomicElementTypes, allInteractionTypes, allMutationTypes, allScriptBlocks, allSubpopulations properties to Community graph windows (and haplotype plots) are now associated with the focal species at their time of creation, and display the species avatar as a badge add a species "tab bar" in SLiMguiLegacy and QtSLiM to choose which species is the focal display species (population view, chromosome view, new graph windows) add id property to Species add lookup by id on Community for subpopulations, mutation types, genomic element types, interaction types, script blocks, and species with xWithIDs() methods add species property to Chromosome, GenomicElementType, InteractionType, MutationType, Subpopulation add active property and skipTick() method to Species, start using ticksModulo/ticksPhase to actually control species activation add 'all' as a reserved species name, add the 'ticks all' syntax for event declarations and make it required in explicit-species models add [No$ ticksSpec = NULL] to registerXEvent() so created event blocks can have a ticks specifier; add speciesSpec and tickSpec properties to SLiMEidosBlock documentation/manual revisions for all of the above remove now-obsolete pseudo-parameters from callbacks (breaking backward compatibility in 4.0; not really multispecies-related): fitness(), reproduction(), mateChoice(): removed genome1, genome2 (kept these in recombination() callbacks since they are not redundant, indicating the initial copy strand) modifyChild(): removed childGenome1, childGenome2, childIsFemale, parent1Genome1, parent1Genome2, parent2Genome1, parent2Genome2 revised recipes in reaction to these changes: 10.3.1 I/II, 11.3, 12.2 I/II, 12.3, 13.1, 14.5, 16.13, 16.14, 16.16, 16.17, 18.8 add WF/nonWF generation cycle images to QtSLiM, under the Help menu, for quick reference add a findInterval() function to Eidos change calcFST() from "average of ratios" to "ratio of averages", following current best practice; this breaks backward compatibility for users of this function, but hopefully in a good way add autocorrect smarts to SLiMgui for common changes needed to migrate to SLiM 4 switch InteractionType from using a semi-permanent sparse array to using a transient sparse vector, providing multispecies flexibility: add "species all initialize()" for non-species-specific initialization, move initializeInteractionType() to be non-species-specific, remove the `species` property from InteractionType make it required, in multispecies models, to declare interaction() callbacks as "species all interaction()"; they are always active, and are not associated with any one species move registerInteractionCallback() from Species to Community, since interaction() callbacks are now non-species-specific change InteractionType APIs to be more explicit about receiver vs. exerter, allow an exerter subpop to be specified, etc.; new APIs: – (float)clippedIntegral(No receivers) - note this method now uses saved positions from evaluate(), and thus requires evaluation – (float)distance(object$ receiver, [No exerters = NULL]) - receiver must now be singleton (matching interactionDistance()) – (float)distanceFromPoint(float point, object exerters) - renamed from distanceToPoint(), parameters switched places for consistency – (object)drawByStrength(object$ receiver, [integer$ count = 1], [No$ exerterSubpop = NULL]) - "receiver", added exerterSubpop – (integer)interactingNeighborCount(object receivers, [No$ exerterSubpop = NULL]) - changed individuals to receivers, added exerterSubpop, require all receivers in one subpop – (float)interactionDistance(object$ receiver, [No exerters = NULL]) - no change – (float)localPopulationDensity(object receivers, [No$ exerterSubpop = NULL]) - added exerterSubpop – (object)nearestInteractingNeighbors(object$ receiver, [integer$ count = 1], [No$ exerterSubpop = NULL]) - "receiver", added exerterSubpop – (object)nearestNeighbors(object$ receiver, [integer$ count = 1], [No$ exerterSubpop = NULL]) - renamed individual to receiver, added exerterSubpop – (object)nearestNeighborsOfPoint(float point, io$ exerterSubpop, [integer$ count = 1]) - parameters switched – (float)strength(object$ receiver, [No exerters = NULL]) - no change – (float)totalOfNeighborStrengths(object receivers, [No$ exerterSubpop = NULL]) - added exerterSubpop change InteractionType::evaluate() API: (void)evaluate(io subpops) - `subpops` now required, remove `immediate` remove the `subpop` pseudo-parameter from interaction() callbacks; receiver.subpop and exerter.subpop should now be used, and are not always the same revised recipes for these changes (for evaluate() now requiring subpops, in all cases; no other changes needed): 15.2, 15.3 I/II/III/IV, 15.4, 15.5, 15.6, 15.7, 15.8, 15.9, 15.10, 15.11, 15.12, 16.10, 16.11, 16.18 note that these changes break backward compatibility for most models, and break backward reproducibility for many (due to distances not being rounded off from double to float in some code paths now) move initializeSLiMModelType() to be non-species-specific (i.e., must be in "species all initialize()"), so it is shared and doesn't need to be repeated add parameter color to initializeSpecies(), and property color to Species; add property avatar to Species; tweak avatar to be [s$=""] not [Ns$=NULL], the empty string can be the default add "Multispecies Population Size ~ Time" graph to SLiMgui shift initialization requirements to allow "no-genetics" species to omit all of their genetic setup (mutation and recombination rates, mutation types, genomic element types, genomic elements) obsolete the term "generation" in favor of "cycle", "generation cycle" in favor of "tick cycle"; user-visible API changes: Species.generation -> .cycle, Community.generationStage -> .cycleStage change the "generation" metadata key in the tree sequence top-level scheme to "cycle" LogFile.addGeneration() -> .addCycle(), LogFile.addGenerationStage() -> .addCycleStage() change the column names produced by LogFile from "generation" and "gen_stage" to "cycle" and "cycle_stage" revise recipes to use the new properties and methods, have correct comments, etc. add recipes 19.1, 19.2, 19.3, 19.4, 19.5, 19.6, 19.7: new multispecies recipes! add neighborCount() and neighborCountOfPoint() methods to fill out the InteractionType API modify recipe 18.14 to unique down nucleotide-based mutations regardless of their mutation type (mutation() rather than mutation(m1)) add recipe 18.15, showing how to unique down to the ancestral state when that is desirable add an "all" tab to the species bar, to see all species simultaneously in the UI add a [No$ subpopMap = NULL] parameter to readFromPopulationFile() to allow remapping of subpopulation ids on load, for .trees add a "Scheduling" tab in the output viewer that shows tick/cycle/event/callback scheduling in chronological order relax the prohibition on calling cachedFitness() from late() in a WF model; it is now allowed IF recalculateFitness() has been called first fix a bug that caused malformed metadata for (a) models where a subpop was removed, when (b) those subpops had a larger SLiM id than other subpops (and were thus at the end of the population table) optimizations for several cases of sample() and sampleIndividuals() in the case of multiple draws without replacement add calcInbreedingLoad() function, courtesy of Chris Kyriazis add [logical$ randomizeCallbacks = T] to initializeSLiMOptions() to turn of randomization of the order individuals are processed with callbacks; T by default, breaks backward reproducibility unless set to F add new section 14.15 recipes: biallelic models showing how to use mutation() callbacks, unique down, and handle back-mutation add "Focus on Script" and "Focus on Console" keyboard shortcuts for keyboard warriors revise Python recipes to match current usage: 17.2, 17.4 II, 17.5 II, 17.7 II, 17.8 I, 17.8 III, 17.9 I, 17.10 II, 18.13 III add species color lines in the script editor in SLiMgui, in multispecies mode relax call timing restrictions to allow species to operate upon each other within callbacks more broadly add recipe 19.8: Within-host reproduction in a host–pathogen model remove the `active` property of Species, at least for now; I decided that the existence of this might paint me into a corner with respect to possible extensions to the skipTick() mechanism merge in tskit C API 1.0 final relax unnecessary restrictions on reading, writing, addSubpop(), addSubpopSplit(), treeSeqRememberIndividuals(), treeSeqSimplify(), etc. being done in first() events add willAutolog() method to LogFile to find out whether it will log automatically at the end of the current tick add addSuppliedColumn() and setSuppliedValue() methods to LogFile, to support columns that are supplied their value by script, rather than generating them remove the removeConstants parameter from rm(); it is no longer possible to remove defined constants, use defineGlobal() instead if you want to change a global value add killIndividuals() method on Species, for nonWF models only, to kill a given vector of individuals, immediately removing them from their subpopulations bug fix: add mutation frequency/count invalidation in more spots: addSubpop()/addSubpopSplit(), setSubpopulationSize(0) (WF), removeSubpopulation(), killIndividuals(), takeMigrants() the results from sim.mutationFrequencies() / sim.mutationCounts() could be incorrect after one of those calls, for a complete tick cycle (test: test_freqCountChange_*.slim) SLiM's internal tally never produced incorrect fixation/loss, so the bug should manifest only in script, only when those methods are used (test: test_fixloss_*.slim) it could also result in (harmless) incorrect display of frequencies in SLiMgui at the end of a tick in which a subpopulation was removed (test: test_subpopRemoved_*.slim) fitness() callback redesign (breaks in backward compatibility): change fitness() callbacks to be called mutationEffect() callbacks change relFitness to effect, inside mutationEffect() callbacks change fitness(NULL) callbacks to be called fitnessEffect() callbacks split registerFitnessCallback() into registerMutationEffectCallback() and registerFitnessEffectCallback() add display of the Git SHA-1 hash in slim and SLiMgui, to aid in debugging; works for builds under cmake and qmake, not under Xcode add a "progress indicator" in SLiMgui, in the Tick counter lineedit, that shows the progress towards the last tick the model will execute (assuming it doesn't stop early) shift recipes 16.5, 16.19, and 19.8 to use killIndividuals(), for pedagogical purposes version 3.7.1 (Eidos version 2.7.1): fix a problem with the python recipe for recipe 18.13 fix printf format specifier on Windows that was causing issues with old compilers on conda fix spurious "subpopulation p0 has already been used" errors in certain circumstances (#274), which was biting stdpopsim fix a crashing bug with treeSeqOutput() due to an incorrectly sized malloc buffer get rid of a spurious self-test error that happened occasionally due to changed to relatedness(); the test itself was wrong (#269) fix the appearance of SLiMgui profile output when in dark mode (#270) advance copyrights to 2022 fix recipes 17.8 and 17.9 to use sim_ancestry() instead of simulate() version 3.7 (Eidos version 2.7) change to allow .trees files to contain unreferenced empty subpops, to allow ancestral use of subpops that are now empty (see #168) disallow reuse of subpopulation ids if they have gone into the tree-sequence tables, to prevent collisions change so that entries for unused subpops are not modified in population tables loaded from a tree sequence (PR #172) add a [format='slim'] parameter to Dictionary.serialize(), adding support for JSON serialization with the 'json' format specifier (note the JSON serialization format changed slightly due to bug fixes) add a Dictionary(string$ jsonString) constructor for creating a Dictionary from a JSON serialization add an assert() function to easily assert that a condition is true add string manipulation functions: strcontains(), strfind(), strprefix(), strsuffix() for string substring searching add graph menu items to the Simulation menu in QtSLiM, as used to exist in the old SLiMgui; left out accidentally add SLiMSim method individualsWithPedigreeIDs() for fast lookup by pedigree ID, for keeping track of long-term interactions fix functions/methods that take Subpopulation/MutationType/SLiMEidosBlock to also accept an integer id, as most functions/methods already do: calcVA(), rescheduleScriptBlock(), individualsWithPedigreeIDs(), mutationCounts(), mutationFrequencies(), nearestNeighborsOfPoint(), evaluate() fix JSON parsing bugs (singleton string value, illegal values) add first() events at the start of the generation cycle add survival() callbacks governing mortality in nonWF models modify Eidos grammar for function declarations slightly, to allow a unary minus preceding a numeric default argument value switch from Subpopulation-level object pools for genome/individual to Population-level object pools; get rid of a lot of ugly code as a result, particularly in takeMigrants() minor policy change: removeMutations() will no longer warn when called from a late() event in nonWF, or an early() event in WF, *if* it was asked to create a substitution policy change: source() now raises an error if the file path it is given does not exist add [logical$ chdir = F] argument to source(), mirroring R's parameter of the same name, to make include-style usage of source() simpler (#190) add the ability to relocate an individual with a survival() callback, to enable sperm storage and other such models where life continues after death add recipe 16.22, sperm storage with a survival() callback revise recipe 16.12 to use a survival() callback add recipe 16.23, tracking separate sexes in script nonWF style fix InteractionType to allow > 2 billion interactions in a model (switch from uint32_t to uint64_t) add an "action button" at the bottom of the haplotype plot window, like that in graph windows add checks for malloc/calloc/realloc failures, for clusters that make allocations fail when the process is at its memory limit fix a small bug involved duplicate derived states getting recorded by addNewDrawnMutation() when multiple mutations are added at the same position updated to tskit C_0.99.14 (big metadata bug fix) updated to unversioned tskit code to get improved consistency checking stuff quickly; will update to a tagged version when one comes out add a concept of deprecated APIs in Eidos, to allow properties/methods to be documented as deprecated but not auto-completed now perform fuller consistency checks (mutation/node time mismatches) every generation in DEBUG, and also now after simplification in DEBUG change addRecombinant() to use (NULL, NULL, NULL) as a signal to create a null genome, rather than an empty genome without new mutations; this better reflects the existing semantics (breaks backward compatibility) add genomesNonNull property to Subpopulation and Individual, returning those genomes of the subpop/individual that are not null genomes, for easier management of models that contain null genomes add flags to addEmpty() allowing the user to request that either or both genomes ought to be null rather than just empty add isHaploid=F flag to addSubpop() remove the dominanceCoeffX property of SLiMSim and the xDominanceCoeff parameter to initializeSex() (breaks backward compatibility) add a new haploidDominanceCoeff property to MutationType to take its place with more flexibility and generality revise recipes as needed (16.13, 16.14 - the nonWF haploid recipes) to adjust for the above changes extend outputFull(), readFromPopulationFile(), and .trees I/O to support null genomes and haploids allow readFromPopulationFile() with a .trees file even when tree-seq recording is not enabled add an rnbinom() function to draw from a negative binomial distribution add -write(s$ filePath) method to Image, to write an Image as a PNG file add -spatialMapImage() method to Subpopulation to let you get an image for a spatial map add tempdir() function to Eidos for better cross-platform support; revise recipes that use /tmp to use this (9.2, 9.3, 9.6.2, 17.3 I, 17.3 II) add sysinfo() function to Eidos for better cross-platform support add grep() function to Eidos for regular expression matching add DataFrame class to Eidos (a subclass of Dictionary for representing data tables), with: properties: colnames, dim, ncol, nrow methods: cbind(), rbind(), subset(), subsetColumns(), subsetRows() other features: prints as a data table rather than a dictionary add "csv" and "tsv" options to the Dictionary method serialize(), for generating CSV/TSV content (note serialize() now returns string, not string$) add readCSV() function that reads a CSV (or TSV) file and returns a DataFrame modify Dictionary method getRowValues() to make it useful as a contrast to DataFrame.subsetRows(), by allowing ragged rows, matrix/array elements; add [drop=F]; these changes are backward-compatible change Eidos output of float values slightly; 1.0 now outputs as "1.0", not "1", to show that it is a float; seems better, breaks backward compatibility in a minor way replace recipe 14.4 (modeling inversions), incorporating changes from Vince Buffalo, Peter Ralph, & Andy Kern that fix several problems (see new discussion in the manual) add the ability to create a new grayscale Image from a 2D matrix of values, the inverse of the integerK / floatK properties, as Image(numeric matrix); RGB may be added later if needed keep a hash table from pedigree ID to individual table row, to speed up remembering of additional individuals add parent pedigree ids to individual metadata (.trees file version increment); add pedigree-based info to the parent column of the individuals table add summarizeIndividuals() to create vectors/matrices/arrays, and thereby spatial maps, that summarize a set of individuals add recipe 15.13 showing how to use summarizeIndividuals() to make a spatial map (for density-dependent fecundity) fix the reproductiveOutput property so that it doesn't increment (or, in fact, decrements back down!) if a child is rejected by modifyChild() add clippedIntegral() method to calculate the integral of an interaction function clipped to the spatial bounds of focal individuals add localPopulationDensity() method to divide total strengths by clipped integrals to provide weighted local density values revise recipe 16.11 to use localPopulationDensity() update to robin hood hashing version 3.11.3 to get a few nice fixes and speed improvements fix a floating-point roundoff issue that would cause the frequency of fixed mutations, from mutationFrequencies(), to sometimes differ very slightly from 1.0 add recipe 16.24, haplodiploidy modify recipe 16.15 (WF models implemented as nonWF models); there are now two recipes, the second one showing how to implement fitness-based mating success add support for Markdown in the Jump menu (_italic_, *italic*, __bold__, **bold**; only in section header items, not in script block comments (Qt doesn't allow within-item formatting) also headers: # a lot larger (H1), ## a little larger (H2), ### default size (H3), #### a little smaller (H4), ##### medium-small (H5), ###### a lot smaller (H6)) (note the space after the hashes is required!) add "unified" display mode in SLiMgui's individuals view Windows port, thanks to Russell Dinnage! see manual for install instructions, etc. fix recipe 5.4 II (jump menu annotations with the Gravel model) to work with the new Markdown support added policy change: the same subpop identifier, such as p1, may not be reused in a model; this was already true for tree-seq, now it is enforced for all models for consistency (breaks backward compatibility) add name and description properties to Subpopulation; persist them in treeseq metadata in the population table; switch the population table metadata schema from binary to JSON so we can add `name` and `description` in a way tskit and others can read upgrade the debugging output window: now with tabs for every output stream including LogFile and writeFile() fix relatedness() to handle overlapping generations, multiple parentage, etc, correctly; the version of relatedness() in SLiM 3.6 should not be used redesign the UI for the chromosome view and individuals view, to provide "action buttons" for more user-visible configurability add treeSeqMetadata() function to get the metadata from a .trees file, as a Dictionary, without reading in the tree sequence updated to tskit 0.99.15 to try out C API 1.0 prerelease stuff add [Ns$ timeUnit=NULL] parameter to initializeTreeSeq() to set the name of the tree-sequence recording time unit version 3.6 (build 2784; Eidos version 2.6): update to JSON for Modern C++ version 3.9.1 add metadata= parameter to treeSeqOutput(), to support user-generated metadata on the tree sequence make ampersands show up in the Jump menu properly in SLiMgui fix tab stops in SLiMgui add an F-distribution drawing function, rf(), following R, by request fix bug with source() that produced a warning about paste() semantics, and an incorrect sourced script string fix segfault-causing bug in Eidos argument processing for certain recursive calling patterns fix SLiMgui to not accept rich text pastes enable multiple selection in the Find Recipe panel revise recipes to avoid outputFull() when it takes a long time (Qt is slow processing big output dumps) add a [permanent=T] optional flag to treeSeqRememberIndividuals() to allow for individuals to be "retained" rather than "remembered", thanks to Yan Wong add [retainCoalescentOnly=T] flag for initializeTreeSeq() to modify simplification to keep more retained nodes than it otherwise would add a generationStage property to SLiMSim to access the current generation cycle stage improve command-line processing – add -h/-help option, accept -- prefix add a precision property to LogFile to govern the floating-point precision of its output (for other types of output, format() can often be used to get a specific output precision) change SLiMgui (QtSLiM) to open .py recipes in the browser, for a better overall user experience make SLiMgui on macOS not quit on the last window closing, following standard macOS user interface guidelines; requires Qt 5.15.2 due to Qt bugs SLiMgui: fix syntax coloring bug (keywords not colored properly) SLiMgui: fix missing color boxes in the help to show WF/nonWF/nuc properties/methods add dark mode support for SLiMgui, for both macOS (tied to the OS setting) and Linux (with a checkbox in the prefs) update to tskit 0.99.10 to gain some new simplify() functionality fix a bug causing a spurious error about mismatching dominance coefficients in readFromPopulationFile() with very large dominance coefficient values make <- an illegal token in Eidos, to guard against users who are used to R accidentally doing "a <- b;", which would otherwise be a comparison, "a < -b;", and would silently do nothing the status bar in SLiMgui now resizes vertically to show wrapped content when necessary (long function/method signatures) extend Eidos to allow all Unicode characters beyond 7-bit ASCII in identifiers, including accented characters, Chinese characters, and emoji fix argument processing bug (crash) for function/method calls with more than 256 arguments fix bugs with LogFile - writing zero-length compressed data gave an error, SLiMgui messed up the directory the log file was saved to add "debug points" for runtime debugging of models in SLiMgui; new debugging output window, menu items, buttons, etc. switch from QTextEdit to QPlainTextEdit for better performance (no user-visible change apart from SLiMgui performance on large amounts of output) add support for an "error stream" in Eidos, in addition to the standard output stream; add [error=F] argument to cat(), catn(), print(), str() to support this fix a bug with the reproductive output tracking for an individual being incorrect after the individual is moved with takeMigrants() fix a serious bug with takeMigrants() that could lead to corruption/crashes, if a variable holding Individual objects was kept across the call and used again add verbosity property to SLiMSim, to get/set the verbosity level fix memory leak of subpopulations after removeSubppulation() is called in WF models; could also cause an error, "ERROR (Population::TallyMutationReferences): (internal error) tally != total genome count" bump the .trees file version to reflect the change in sense for the individual flags (SLIM_TSK_INDIVIDUAL_RETAINED instead of SLIM_TSK_INDIVIDUAL_FIRST_GEN); no user-visible consequence fix bug causing output from nested events/callbacks/functions to be emitted in the wrong (non-chronological) order in some cases add a debugIndent() function to Eidos, providing the current indentation string for use in producing error output that matches the indentation of debug points add recipe 16.21, "Dynamic population structure in nonWF models" extend Dictionary with getRowValues() and appendKeysAndValuesFrom() methods to enable dataframe-like usage patterns with it extend Dictionary() constructor to allow it to be passed key-value pairs to set, or a Dictionary to copy extend Dictionary with an identicalContents() method to test for equality of Dictionary objects (containing the identical keys and values) version 3.5 (build 2663; Eidos version 2.5): added build instructions for Windows WSL, thanks to Bernard Kim (no code change) fix some issues with recipes 13.5 and 14.8 (involving live plotting in R) with platform-dependent paths, lack of PDF viewing support in QtSLiM, etc. fix bug with precedence for operator ^, which needs to be higher than unary - to follow standard mathematical convention (GitHub #99) add a tabulate() function to Eidos, more or less parallel to that in R speed up match() for large x vectors, using unordered_map; goes from O(N*M) to O(N+M) add a quantile() function to Eidos, parallel to R's default "type 7" quantile function add new tests for the treatment of NANs, fix bugs with treatment of NANs in: sort(), order(), setUnion(), setIntersection(), setDifference(), setSymmetricDifference(), dbeta(), dgamma(), dmvnorm(), rmvnorm(), identical(), match(), unique(), min(), max(), range(), pmin(), pmax(), operators >=, <=, ==, != the sort() behavior now sorts NANs to the end of the vector; this is equivalent to R's with na.last=T set (which is not R's default!) comparison operators now obey the IEEE rules for unorderability of NAN; if x is NAN, xy, x<=y, x>=y, x==y are all F, x!=y is T disallow adding mutations with addNewMutation() / addNewDrawnMutation() / addMutations() to individuals of age > 0 prevent tree sequence inconsistencies; see issue #102 extend codonsToAminoAcids() to be able to return integer codes for the amino acids, if passed long=0 add population and subpopulation fitness distribution plots to QtSLiM revise recipe 5.4 (Gravel model) thanks to Chase W. Nelson add 1D and 2D SFS plots to QtSLiM, along with a 2D frequency spectrum plot improve plots in QtSLiM, particularly PDF generation add colors() function to Eidos, providing several new color palettes including Turbo and Viridis integrate tskit 0.3.0 into SLiM, allowing more efficient tree sequence simplification (due to faster sorting) and new metadata schema support for interoperability rename SLiMgui to SLiMguiLegacy rename QtSLiM to SLiMgui, including cmake and qmake build targets and flags (BUILD_SLIMGUI=ON instead of BUILD_QTSLIM=ON, SLiMgui target instead of QtSLiM) revise recipes 13.5 and 14.8 to generate PNG images, not PDF, since the new SLiMgui requires that add Age Distribution graph to QtSLiM add Population Size ~ Time graph to QtSLiM make the Step button step continuously when held down, in QtSLiM make "generation play" (entering a new generation in the generation counter textfield) use the Play button, and be stoppable add tree sequence simplification to SLiMgui/QtSLiM profiles remove the first generation flag from SLiM .trees files, switch to the new simplify algorithm that preserves ancestors that are referenced extend mutation() callbacks to allow returning an existing mutation policy change: mutation positions, when generating a gamete, are now uniqued – breaks backward reproducibility for almost all models add subsetMutations() method on SLiMSim for fast lookup of specific mutations add new recipe 18.14: "modeling identity by state (IBS): uniquing new mutations down with a mutation() callback" add new recipe 10.6 II - Varying the dominance coefficient among mutations II policy change: originGeneration can no longer be used as scratch; it must be set to the current generation when a mutation is created policy change: pedigree tracking is now always enabled, and the keepPedigrees flag to initializeSLiMOptions() does nothing - REVERTED add line numbers and highlighting of the current line in SLiMgui, controllable in the preferences add "Jump to Line..." menu item under Edit > Find (command-L) in SLiMgui allow multiple copies of the same graph type to be opened in SLiMgui update tree-seq recipes for new tskit/pyslim APIs: 17.2, 17.4, 17.5, 17.7, 17.9, 18.13 implement pre-processing of function/method argument lists (optimization), and extend function/method signature syntax to allow arguments following an ellipsis (the named argument syntax must be used to assign them) extend paste() and paste0() to take multiple arguments, with new signatures (string$)paste(..., [string$ sep = " "]) and (string$)paste0(...), to encourage avoidance of the evil paste(c(...)) pattern note this change is not completely backward-compatible; if you were passing in a sep= argument to paste() without the named argument syntax, you need to add sep= update recipes 4.2.5, 13.5, 14.8, and 16.12 (I) to use the new paste()/paste0() syntax – use "sep=" explicitly, avoid paste(c(...)) add individual pedigree IDs to outputFull() for both text and binary formats if pedigreeIDs=T is specified, read/use them in readFromPopulationFile() add a "jump to event/callback" button to QtSLiM optimize sample() for the full shuffle case, and for better performance with large N; optimize sampleIndividuals() a bit for simple cases; may break backward compatibility for users of sample() add support for doxygen-style /** and /// comments that get used in the "jump to" popup as section headers darken the QtSLiM app icon while a model is running add relayout option to prettyprinting in SLiMgui, with option-click/alt-click of the prettyprint button add autosave option (off by default) to SLiMgui show colored function/method signatures in the SLiMgui status bar show elapsed CPU time, segregating mutations, and number of substitutions in the SLiMgui status bar make it possible to retain a Mutation object long term with defineConstant() / setValue(), with a revamp of object memory management under the hood (putting Mutation under retain/release) subtle behavior change: the reported frequency of a mutation that is removed with substitute=T used to be 0.0, now it is 1.0 (which seems more correct) add isFixed and isSegregating properties to Mutation to help diagnose the state of a mutation that has been retained long-term add recipe 9.9, "Keeping a reference to a sweep mutation" add recipe 9.10, "Tracking the fate of background mutations" add Image class to Eidos, based on lodepng extend Dictionary with allKeys, addKeysAndValuesFrom(), clearKeysAndValues() make superclass relationships explicit and documented; make Chromosome, SLiMEidosBlock, and SLiMgui inherit from EidosDictionaryUnretained move doc for setValue()/getValue() to the Eidos manual, remove all the duplicated doc for it in SLiM show superclasses in the doc browser, with a hyperlink change defineSpatialMap() to require a matrix (except in the 1D case), removing the gridSize parameter (breaking backward compatibility!), and reading the matrix in the right order (so it looks the same in SLiMgui as in the console) revise recipes 15.10 and 15.11 to adjust to the changes in defineSpatialMap() (and fix a very small bug with the spatial bounds) put Chromosome under retain/release (subclassing EidosDictionaryRetained) so it can be kept long-term add a "showSymbolTables=F" option for ls() that, if T, shows the whole chain of symbol tables that are defined, mostly for debugging purposes change scoping rules so that variables in the global scope are visible inside user-defined functions and callbacks, like global constants add a defineGlobal() function, parallel to defineConstant(), to assign a variable into the global scope add "Robin Hood Hashing" to use in place of std::unordered_map in time-critical spots: match(), Dictionary, Genome bulk operations, MutationRun split/join, EidosTypeTable symbols, tree-seq simplification and individual table management, MS output add a flushFile() function to Eidos to flush buffered content from writeFile() add LogFile class for logging data to a CSV/TSV file, plus method createLogFile() and property logFiles on SLiMSim policy change: errors during writeFile() are now errors, not warnings added files to support Bryce Carson's new Fedora package installer add serialize() method to Dictionary, make Dictionary -allKeys be in sorted order add mutationCountsInGenomes() and mutationFrequenciesInGenomes() methods for getting counts/frequencies within any sample of genomes add a calcFST() function to SLiM to provide easy FST calculations; revise recipe 11.1 to use the new function add a functionSource() function in Eidos to allow the user to see the Eidos source code for functions that are implemented in Eidos add a calcVA() function to SLiM to calculate additive genetic variance for quantitative traints added arm64 build for Apple Silicon add a calcPairHeterozygosity(), calcHeterozygosity(), calcWattersonsTheta() functions to SLiM, add optional windowing to calcFST() revise recipe 14.1 to use calcHeterozygosity() instead of using custom code add reproductiveOutput (Individual) and lifetimeReproductiveOutput (Subpopulation) properties to track individual reproductive output add a new recipe 4.2.5, "Automatic logging with LogFile" (the old one became 4.2.6) add a new recipe 16.19, "Range expansion in a stepping-stone model" add a new recipe 16.20, "Logistic population growth with the Beverton–Holt model" add lifetime reproductive output plot to QtSLiM; change the reproductive output logging to separate by sex, add lifetimeReproductiveOutputM and lifetimeReproductiveOutputF properties to cover that minor bugfix for recipe 14.7; use L instead of L-1 for chromosome length back out the earlier (9/4/2020) change making pedigree tracking always on, because the performance hit was too large; so the keepPedigrees flag to initializeSLiMOptions() now does something again put the reproductiveOutput/lifetimeReproductiveOutput properties under keepPedigrees=T, be more strict about requiring keepPedigrees=T to see/access any pedigree information at the user level version 3.4 (build 2438; Eidos version 2.4): fix a subtle bug in which mutation IDs used by mutations read in from a .trees file could be re-used, producing a conflict, if the mutations were not ancestral to any extant genome -- biting you if you wrote a .trees file out again at the end add qnorm() function in Eidos; thanks to Vince Buffalo for the PR add SHA-256 code, output of a script hash to .trees files with the parameters/model_hash subkey, and an option to omit provenance output of the full script in outputTreeSeq() with includeModel=F; increment .trees file_version to "0.4" fix a crash in haplotype plotting when the number of genomes is large fix display bug with haplotype display on retina displays add recipe 16.18: a spatial epidemiological S-I-R model new versions of recipes 9.5.2 and 9.5.3 to fix a bug involving fitness calculations with multiple mutational lineages for a single sweep; see https://groups.google.com/d/msg/slim-discuss/DW-QqzoZLgg/NCusXvBqBAAJ extended writeFile() and writeTempFile() to support writing .gz compressed files with compress=T, added zlib 1.2.11 to the project add dbeta(), dexp(), and dgamma() functions to Eidos to provide probability density functions for those distributions extended the compress=T option for writeFile() to support append=T as well, with internal buffering for performance add QtSLiM to project! fix a bug that failed to catch an overflow in the tree-sequence tables for a large model version 3.3.2 (build 2158; Eidos version 2.3.2): add recipe 5.2.4 (joining subpopulations) extend recipe 5.4 (the Gravel model) to show output from a vector of genomes sampled from multiple subpops fix a bug in recipe 13.4 (introduced in SLiM 3.3) that caused the environmental noise not to be included in the phenotypic values used for fitness calculations add an optional argument for the command-line option -l/-long, to provide the level of verbosity desired (default level is 2; defaults to 1 if -l/-long is not used) add recipe 16.17 (meiotic drive) add recipes for section 13.6 (a variety of fitness functions) speed up AddIndividualsToTable(), eliminating a bottleneck for large models with multiple subpopulations speed up outputMutations(), which was very slow if a large number of mutations were requested for output protect methods in InteractionType against being passed new juveniles that have not yet been added to the subpopulation optimize the sample size 1 case in sampleIndividuals() when criteria are specified (min/max age, etc.) add SLiMgui launch check for needed fonts fix a bug in AddNewDrawnMutation()/AddNewMutation() if a non-singleton vector of positions is given in unsorted order: the genome order is incorrect! tweak the default nonWF model to find a mate with "subpop.sampleIndividuals(1)" not "p1.sampleIndividuals(1)", for generality tweaks to allow distribution under conda-forge increase the precision of MS output from 7 decimal places to 10, to accommodate larger chromosomes with precise positions version 3.3.1 (build 2116; Eidos version 2.3.1): fix SLiMgui crashing bug involving (a) a type "s" mutation type with (b) a fixed color set for it protect against a non-existent or non-writeable /tmp directory, which apparently some systems have fix crash in SLiMgui due to inaccessible properties fix bug involving (a) genomic elements specified out of sorted order, AND (b) a non-uniform mutation rate map; some genomic elements can end up not generating any mutations at all (present since SLiM 2.5) add an option to clock() to select the clock type ("cpu" or "monotonic", for now); add the same option to executeLambda() fix a VCF output bug: blank lines in nucleotide-based output when a back-mutation is suppressed by simplifyNucleotides=T add a wait=T optional parameter to system(), allowing wait=F (or a & at the end of the command line) to execute a system command in the background enable access to pedigree IDs whenever they are valid (i.e., when tree-sequence recording is enabled, as well as when pedigree tracking is enabled), and add them to VCF output when available add an "individual" property to Genome that provides the individual to which a given genome belongs fix a crash with clonal nucleotide-based models using a custom mutation matrix shift temporary files used by -eidosTest and -slimTest into a randomly named subfolder, where necessary to prevent conflicts with other users change recipes that set a new seed to use 2^62 rather than 2^32, to avoid repeated sequences fix memory allocation issue with more than 2^26 individuals fix crash when trying to use an InteractionType after takeMigrants() has made its cached data invalid fix crash when a call to takeMigrants() tries to migrate the same individual twice (i.e., the migrants vector is not uniqued) fix signatures shown in SLiMgui for callbacks that have return values fix problems with the variable browser caused by inaccessible properties version 3.3 (build 2062; Eidos version 2.3): fix bug resulting in incorrect sex ratio (all males!) in sexual nonWF models when saved to a population file (either binary or text) add nucleotideBased flag to initializeSLiMOptions() add a NucleotideArray class to keep compact nucleotide sequences (2 bits per nucleotide) add initializeAncestralSequence() function add Chromosome.ancestralNucleotides() method add randomSequence() function add nucleotideBased property add nucleotideCount(), nucleotideFrequency() functions add codonsToAminoAcids(), nucleotidesToCodons() functions add initializeMutationTypeNuc() and nucleotideBased property on MutationType add mutationMatrix to initializeGenomicElementType() add mutationMatrix property and setMutationMatrix() method to GenomicElementType add Mutation support for nucleotides: nucleotide, nucleotideValue, ditto for Substitution add nucleotide argument to addNewMutation() and addNewDrawnMutation() add nucleotides() method to Genome add mmJukesCantor() and mmKimura() mutational models implement sequence-based mutation add mm16To256() function to expand a single-nucleotide mutation matrix into a trinucleotide mutation matrix disable initializeMutationRate() and related API in nucleotide-based models update the ancestral nucleotide sequence upon fixation add hotspot map support: initializeHotspotMap(), setHotspotMap(), and associated properties add output of nucleotides to outputMutations(), outputFixedMutations(), and other output methods add nucleotidesToCodons() function revamp the recombination model for the new gene conversion design geneConversionFraction property removed, geneConversionEnabled / geneConversionNonCrossoverFraction / geneConversionSimpleConversionFraction / geneConversionGCBias added remove gcStarts / gcEnds from the recombination() callback specification remove old nucleotide and biased gene conversion recipes fix recipe 6.1.3 (gene conversion) add setGeneConversion() method on Chromosome to change the gene conversion parameters dynamically add recipes for chapter 17 (17.1 - 17.10) add recording of nucleotides and ancestral sequence in tree-sequence recording incrementing .trees file version to 0.3 fix input/output of ASCII trees files fix a bug with ASCII output of non-nuc models with outputFull() that prevented completely verbatim reloading (float precision issue) add nucleotide support for readFromPopulationFile() with a text SLiM output file from outputFull() in a nucleotide-based model; nucleotides and ancestral seq restored correctly add writing and reading of binary files with outputFull() and readFromPopulationFile() in nucleotide-based models add heteroduplex mismatch repair, biased gene conversion, and add recipe 17.11 add readFromVCF() for reading from VCF files, readFromMS() for reading from MS files speed up output of MS format for large models add setAncestralNucleotides() method to Chromosome add recipe 17.12, demonstrating loading from an empirical VCF file disable link-time optimization (LTO) since it is causing build issues for some people fix a bug with the time base of WF models when a .trees is loaded in an early() event; a parent/child timestamp conflict could result add simplificationInterval parameter to initializeTreeSeq() (breaking backward compatibility, for those not using named arguments for checkCoalescence / runCrosschecks) vectorize initializeGenomicElement(), fix recipe 17.7 to use it make pedigree ID properties inaccessible when pedigree recording has not been enabled, to prevent confusion make tag/tagF properties inaccessible when they have not been previously set, eliminating this class of bugs in WF models, enforce that subpopulation total fitness must be finite, to catch overflows fix minor code completion bug involving completing off of language keywords, like trying to complete "for" to "format=" in ancestralNucleotides() revise recipe 9.3.2, remove recipe 9.4.2, revise recipe 9.4.4, revise recipes 11.1, 11.2 renumber recipes to make room for new chapter 13, move existing QTL recipes into chapter 13, add new recipe 13.2 to better introduce QTL model concepts revise recipes 13.3, 13.4, 13.5, 14.4, 14.5; add recipe 6.1.4 (multiple chromosomes), add recipe 12.5 (tracking separate sexes in script) remove recipe 14.6 (forcing a pedigree in a WF model) revise recipes 14.7, 15.3, 15.4, 15.5, 15.6, 15.7, 15.8, 15.9, 15.10, 15.11, 15.12 add recipe 9.6 (varying dominance coefficients) fix a bug that would produce incorrect results from mutationCounts() / mutationFrequencies() in early() events in nonWF models (not including the newly generated offspring) fix compile issues on Xcode 10.2, upgrade to JSON for Modern C++ version 3.6.1 (from 3.1.2) update GSL code to version 2.5 (no user-visible impact) change mmJukesCantor() to take alpha, not mu, for consistency; revise recipes and doc revise recipe 14.9, 16.2, 16.9, 16.10, 16.11, 16.12, 16.13 fix bug in mmKimura() mutation matrix (never released publicly) fix bug in outputMS() and outputMSSample() with filterMonomorphic=T (not the default) that could result in incorrect output, or in a crash fix uninitialized value bug with InteractionsData (never seen in the wild, probably inconsequential) fix symbol table double-dealloc bug (never seen in the wild, probably inconsequential) fix a possible misaligned pointer access when loading .trees files (never seen in the wild, only a problem on non-Intel architectures like ARM, might not even be a bug) fix an edge-case crasher in tree-seq models (never seen in the wild, perhaps only occurs when crosschecks are enabled) add ancestral sequence to memory usage stats add drawSelectionCoefficient() method to MutationType revise recipes 7.3, 7.4, 10.5.1 to eliminate unnecessary asInteger() calls convert some float-type parameters to numeric-type: initializeTreeSeq(simplificationRatio), randomNucleotides(basis), spatialMapColor(value), defineSpatialMap(values, valueRange), setSpatialBounds() highlight matches in the Find Recipe panel the selected display mutation types now apply to substitutions as well as mutations vectorize spatialMapValue() and revise recipe 15.11 to use it optimize sample() with weights, revise recipe 11.2 (splitting it into version I and II) correct recipe 16.15, which did not match the manual optimize operator subset [] with a singleton integer argument, since that is the common case fix an ordering problem with the remembered genomes after loading a .trees file (didn't match the order in the individuals table) merge in kastore C_1.1.0 (67c5a6af4b85ea263e5cab854d94b691949e88ae) and tskit post-0.1.5 (3d16a36f8e54ac01a86f182dc73103bd07badf3e) add mutation() callbacks and registerMutationCallback() make nucleotide/nucleotideValue properties on Mutation writeable fix some leaks revise recipes 9.6 and 13.5 to use mutation() callbacks switch Chromosome to keeping pointers to GenomicElement, fixing a potential crash (which I don't think anyone ever hit) make initializeGenomicElement() return the new element, following the pattern of the other initialize...() functions add GenomicElement pseudo-parameter element to mutation() callback API switch Population from a subclass of std::map to containing a std::map of subpopulations (no user-visible effect) add recipe 14.14 (Visualizing ancestry and admixture with mutation() callbacks) add fileExists() function to Eidos fix bug resulting in incorrect results from max() with (1) integer type, and (2) at least one singleton argument that is neither 0 nor 1 switch to "hardened runtime" in preparation for Apple's notarization procedure improve self-tests fix bug in matrix multiplication with type float add recipe 18.13 (Tree-sequence recording and nucleotide-based models) 3.2.1 (build 1897; Eidos version 2.2.1): add support for the slim command-line tool to read its script from stdin instead of from a supplied input script file fix bug that would bite models calling deregisterScriptBlock() more than once, leading to an incorrect event list in some cases add recipe 15.15 (Implementing a Wright–Fisher model with a nonWF model) fix a bug resulting in incorrect values for the pedigreeGrandparentIDs property of Individual; they were correct internally, but returned incorrectly add a drawBreakpoints() method on Chromosome to allow models to draw breakpoints using SLiM's logic add recipe 15.16 (Alternation of generations) add support for "make install" with cmake (see the README.md / README.html for instructions), thanks to Peter Ralph add support for link-time optimization (LTO) on platforms that support it, thanks to Kevin Thornton add an rbeta() function to Eidos for draws from the beta distribution add a pnorm() function to Eidos for a cumulative distribution function for the normal distribution fix .trees file loading to check that the chromosome length matches, avoiding weird problems downstream fix a SLiMgui crash when displaying a large number of genomic element types add a SLiMgui class and slimgui global instance, available only when running under SLiMgui, that provides some useful control options deprecate the inSLiMgui property of SLiMSim; use 'if (exists("slimgui")) ...' instead; revised recipes 13.11 and 13.17 for this change add SLiMgui.openDocument() and SLiMgui.pauseExecution() methods, and SLiMgui.pid property, to SLiMgui class; revised recipes 13.11 and 13.17 to use openDocument() add Subpopulation.configureDisplay() method, inserted a new recipe 5.3.4 to provide an example of using it fix a code completion hiccup with return/else/do/in improve error-reporting for command-line defines fix an uncaught raise optimizing illegal numeric constants update citations listed by citation() improve recipes (10.2, 10.3, 10.6.2, 16.3) that call setSeed(getSeed() + 1) to do setSeed(rdunif(1, 0, asInteger(2^32) - 1)); Matthew Hartfield pointed out the potential replicate correlation issue move back deployment target from OS X 10.11 to OS X 10.10 thanks to Deploymate fix a bug involving calling setValue() inside a for loop, and similar setValue() scenarios involving a changing value 3.2 (build 1859; Eidos version 2.2): fix a bug that could result in incorrect parent2, parent2Genome1, and parent2Genome2 values in modifyChild() callbacks in clonal models; should not impact correct model code fix incorrect values for isCloning / isSelfing in modifyChild() callbacks in nonWF models using addCloned() / addSelfed() fix incorrect modifyChild() callback source in nonWF models using addCloned(), addCrossed(), and addSelfed(); they were getting the list of callbacks from the target subpopulation fix crash in addEmpty() that nobody noticed because nobody uses it add a new addRecombinant() method that allows easier modeling of haploids, horizontal gene transfer, and pre-planned recombination breakpoints add recipe 15.13 (modeling clonal haploids in nonWF models with addRecombinant()) add recipe 15.14 (modeling clonal haploid bacteria with horizontal gene transfer) add rgeom() function to Eidos for draws from a geometric distribution optimization of property & method dispatch (signature lookup, specifically), using lookup tables instead of hash tables add outputUsage() method to dump memory usage of a simulation, incorporate this information into SLiMgui profile reports add a button in SLiMgui to change the current working directory, correctly track the cwd when multiple SLiMgui windows are open, across recycles, etc. add Find Recipe... panel in SLiMgui modify asString(NULL) to return "NULL", and the string-concatenation operator + to use "NULL" when NULL is concatenated; cat() and catn() still produce no output for NULL scheduling a callback/event into the past now results in an error add a parameter to outputMS() and outputMSSample() to allow only sites that are polymorphic within the sample to be output: filterMonomorphic=T fix code completion in SLiMgui after return, in, else, and do make code completion much smarter; iTr now completes to initializeTreeSeq(), wf to writeFile(), etc. fix recipe 11.1 issue with calcFST() function returning NAN if any mutations are either (a) missing from both subpops, or (b) fixed in both subpops fix issues with passing NAN as a color component to the Eidos color-translation functions make SLiMgui's "graph population visualization" migration rates in nonWF models reflect juvenile migration due to a non-parental destination subpop for addX() add a print=T flag to version() so scripts can switch on the Eidos or SLiM version number without generating unwanted output vectorized the exists() function so a vector of symbol names can be check for existence vectorize the Eidos color-manipulation functions, using matrix arguments/returns: color2rgb(), hsv2rgb(), rgb2hsv(), rgb2color() add new color palette functions: heatColors(), rainbow(), terrainColors(), and cmColors() fix big performance issue for nonWF models as they build up genetic diversity, due to the wrong mutation frequency tallying code path being used fix bug EidosSymbolTable buffer overrun in ContainsSymbol() and related methods; this will bite models that define a very large number of symbols (e.g., some nucleotide models) make Eidos and SLiM more robust to NAN being passed in to various functions (rpois() hang, in particular) fix corrupted spatial location data for some individuals (mostly, maybe only?, first-gen individuals) in .trees files add a usage() function to get the current or peak memory usage of the process 3.1 (build 1817; Eidos version 2.1): dmvnorm() function added to Eidos, parallel to dnorm() but for multivariate Gaussian distributions add JSON for Modern C++ (https://github.com/nlohmann/json) version 3.1.2 to SLiM for .trees provenance generation/parsing extend SLiM's .trees file provenance info (tskit schema stuff, script text, seed, parameters, os environment, file_version==0.2) fixed a inefficient bottleneck in recipe 13.4 (reading MS format files) greatly increase the performance and memory usage of the InteractionType spatial engine for big models (add sparse array for caching pairwise interactions) this breaks backward reproducibility in some models, because distances/strengths are now internally rounded to float instead of double add interactionDistance() method to InteractionType, returning INF as the distance for non-interacting pairs (including self-self distance) add nearestInteractingNeighbors() method to InteractionType, excluding non-interacting pairs from the returned set of neighbors add interactingNeighborCount() method to InteractionType, to get a count without getting identities changed the semantics of strength(); the first argument must now be a singleton individual, and it returns strengths exerted upon that individual (old semantics were ill-defined and broken for non-reciprocal interactions) rememberIndividuals() now actually remembers individuals in the tree sequence, as well as (what it did before) their associated nodes fix issues with genome recycling and subpop initialization, which could result in nodes in .trees files being marked as null genomes incorrectly and could cause memory usage to increase without bound bug fix: incorrect neutral dynamics for WF models with no non-neutral muttypes, no fitness() callbacks, but using fitnessScaling to modify fitness added remembering of the initial generation, to enable recapitation change .trees files to have rebased times on write, to simplify pyslim and reduce user error add a warning for spatial interaction types with no max distance add recipe 13.19, biased gene conversion add recipe 16.10, recapitation fix coalescence checking to work even when individuals are remembered 3.0 (build 1750; Eidos version 2.0): add initializeSLiMModelType() function to choose SLiM's model type (WF or nonWF, at present), and modelType property on SLiMSim to access it disable setSubpopulationSize(), setCloningRate(), setSelfingRate(), setSexRatio(), .cloningRate, .selfingRate, .sexRatio in nonWF models disable addSubpopSplit(), setMigrationRates(), .immigrantSubpopFractions, .immigrantSubpopIDs in nonWF models make definition of mateChoice() callbacks illegal in nonWF mode, including by registerMateChoiceCallback() add age property to Individual, for nonWF models add removeSubpopulation(), takeMigrants(), addCloned(), addSelfed(), addCrossed(), addEmpty() methods for use in nonWF models add support for reproduction() callbacks, including adding registerReproductionCallback() make convertToSubstitution default to F in nonWF models, since all non-neutral mutation types influence fitness and thus survival add outputFull() ages=T parameter to control output of age information in nonWF models; does nothing in WF models; also modify readFromPopulationFile() switch to using pointers for genomes and individuals implement the nonWF generation cycle add sampleIndividuals() and subsetIndividuals() methods on Subpopulation for faster fetching of mates, etc. add fitnessScaling property to Subpopulation and Individual, to alter fitness values per individual or per-subpop without fitness(NULL) callbacks optimizations: EidosSymbolTable improvements, constant caching, callback processing, argument processing, RNG optimizations, inlining, WF parent/child swap efficiency, fitnessScaling work add hooks for tree sequence recording improve accelerated property get/set to be more vectorized add support for accelerated method implementations, accelerate a few core methods speed up SLiMSim::mutationsOfType(), SLiMSim::countOfMutationsOfType(), Genome::containsMutations(), SLiMSim::mutationCounts(), SLiMSim::mutationFrequencies(), Genome::mutationsOfType() switch to dispatch of properties and methods using std::unordered_map to look up the signature miscellaneous other optimizations: "1/relFitness" fitness() callbacks, property return checks, symbol table crossover, std::unordered_map vs. std::map, dynamic_cast<> usage, unnecessary IsNull() calls add a "New (nonWF)" menu item to SLiMgui for quick creation of new nonWF models make WF-only and nonWF-only API visibly tagged (with colored boxes) in the SLiMgui help window add seqLen() function to Eidos, like seq_len() in R add nonWF recipes fix InteractionType bug with periodic boundaries and totalOfNeighborStrengths() / strength() fix bug where a gene conversion rate of exactly 1.0 would be treated as 0.0 fix bug with paste of code from a source with unconventional line endings add support for a logical argument to mean(), and increase the accuracy of mean() for very large integer vectors by trying to avoid floating-point overflow issues fix bug that allowed cachedFitness() to be called from a late() event, returning garbage values fix recipe 13.1, which called cachedFitness() during a late() event, which should have been illegal (and now is illegal) add recipe 13.3 III, mortality-based fitness using fitnessScaling fix a crash when calling setSubpopulationSize(0) (or removeSubpopulation()) twice on the same subpop fix searches in the help for strings that return more than one identical hit, such as "tag"; such searches were only displaying one of the multiple hits display the emergent selfing rate, cloning rate, and sex ratio in SLiMgui for nonWF models make the population visualization graph work better with nonWF models (using fitness values without density scaling, display emergent migration rates) make the fitness over time graph display fitness without density scaling, as in other aspects of SLiMgui's presentation of fitness fix a crash with the fitness over time graph when the simulation became invalid (due to a stop() call or other error) fix a rare crash on quit from SLiMgui fix a crash due to stack overflow with large population sizes (only when callbacks are involved in offspring generation) add new tree seq stuff: initializeTreeSeq(), treeSeqSimplify(), treeSeqRememberIndividuals(), treeSeqOutput() add getwd() / setwd() functions to Eidos add var(), cov(), cor() statistics functions to Eidos for variance, covariance, and (Pearson) correlation change Eidos to not automatically assume return values based upon the value of the last statement; that makes it confusing/hard to define a function that returns void policy change: property/method accesses on zero-length vectors now raise unless the return type of the property/method is unambiguous "void" is now an actual value type in Eidos, although it can only be used as a return type for functions/methods; this improves type-safety fix recipes: 13.2 (mateChoice() callback with no explicit return), 13.9 (sapply() with no value), 11.2 (cachedFitness() from a late() event) add rcauchy() for Cauchy distribution draws, and a "c" Cauchy interaction function option for InteractionType fix a display bug in SLiMgui on Retina displays extend spatial point-processing methods (pointInBounds(), point[Periodic/Reflected/Stopped/Uniform], setSpatialPosition()) to be vectorized add bounds-checking for MutationType DFE parameters and InteractionType IF parameters display interaction types in the tableview drawer in SLiMgui, with a hover preview of the interaction function, as with mutation types fix the incorrect actual recombination rate for requested recombination rates close to 0.5, impose a <= 0.5 requirement on recombination rates, add related recipe 13.18 improve options for display of subpopulations in the population view (control-click or right-click for menu) add genome1 and genome2 properties on Individual to make getting just the first or second genomes of a vector of individuals simpler, in e.g. haploid models add removeMutations(NULL) option that removes all of the mutations from the target genome, for e.g. haploid models; not allowed to be used with substitute=T revise recipe 13.13 (modeling haploids) to use genome2 and removeMutations(NULL), making is much faster and simpler make the default working directory be ~/Desktop when running in SLiMgui; makes no sense for it to be the folder the app is in increase maximum chromosome length from 1e9 to 1e15 (breaks reproducibility from a given seed, due to new MT64 RNG) fix rdunif() to be able to generate uniform draws in the full 64-bit range, using the new MT64 RNG; optimize rdunif() and rbinom() for the simple coin-flip case (breaks backward output compatibility) fix a crash in SLiMgui when displaying a haplotype plot (including in the chromosome view) in a model with null genomes, such as an X or Y model extend containsMarkerMutation() with a [returnMutation=F] argument so the found mutation can be obtained code completion can now supply argument names when in a function/method call add suppressWarnings() function in Eidos fix issue with dropped model output just before a simulation terminated due to an error add treeSeqCoalesced() and a checkCoalescence parameter for initializeTreeSeq(), to perform runtime checking for coalescence add getValue() / setValue() functionality to Substitution, make values carry over from Mutation objects when they fix add tree-seq recipes to SLiMgui add a "migrant" property to Individual that is true if the individual has migrated in this generation, add "migrant" argument to sampleIndividuals() & subsetIndividuals() add a timeUnit parameter to initializeTreeSeq() to allow the time unit to be set by the user; make it "generations" for WF and "ticks" for nonWF, by default add recipe 9.11, effective population size versus census population size 2.6 (build 1292; Eidos version 1.6): make addNewMutation() and addNewDrawnMutation() vectorized, for much higher performance when adding many mutations in bulk; note policy change that requested mutations are returned whether added or not add positionsOfMutationsOfType() method to Genome for speed of models that need that extend the integer() function to be able to construct vectors of 0 and 1 at specified positions (sort of the opposite of which()) revise recipe 13.9 to use new vectorized addNewMutation(), as well as the new positionsOfMutationsOfType() and integer() fix possible incorrect frequency/count info immediately after using addMutations() or removeMutations() (but not addNewMutation()/addNewDrawnMutation()) – the changes would not be reflected in freqs/counts immediately fix a display bug with added (or removed) mutations in SLiMgui add "Copy as Paragraph", remove "Paste and Match Style", in SLiMgui and EidosScribe fix to only highlight errors in SLiMgui if the script has not changed since the last recycle (otherwise character positions are unreliable) fix a bug causing the wrong help text to appear for "id" properties on some classes in the help panel in SLiMgui add haplotype snapshot plot (create from the Graph pop-up menu or the Simulation menu) add haplotype display option for the chromosome view (control-click and select from context menu) extended recipe 13.5 to show the new haplotype display options make so the chromosome view can display a subset (but more than one) of the mutation types defined make so displaying a subset of mutation types works when in haplotype display mode too periodic boundary conditions: added periodicity parameter for initializeSLiMOptions(), periodicity property on SLiMSim, pointPeriodic() method on Subpopulation note policy change: new parameter inserted in initializeSLiMOptions() add recipe 14.12, demonstrating periodic spatial boundaries add display of mutation type DFEs as tooltips in the info drawer add rdunif(), a function for generating draws from a discrete uniform distribution add tips in SLiMgui for the chromosome haplotype display mode, script prettyprinting, and DFE visualization in the mutation type table add recipe 13.15 showing how to implement microsatellites add recipe 13.16 showing how to implement transposable elements fix static analyzer issues, including minor bug fixes for _InitializePopulationFromBinaryFile() and a leak in haplotype plotting fix a bug in doCall() that would fail to get the return value from user-defined functions fix type-interpreter crashes with malformed function declarations fix a refcounting bug that would bite users running more than one simulations at the same time in SLiMgui switch to shared_ptr for call signatures, to fix a leak with user-defined functions improve recipe 13.12 (modeling nucleotides) and optimize SLiM to make it run faster add the ability to automatically select only non-neutral mutation types for display in the chromosome view fix a bug (never released) in pure neutral tracking with addNewMutation(), and optimize pure neutral tracking some more add ability to change the number of bins in the frequency spectrum plot optimize EidosValue internals by using malloced buffers instead of std::vector, avoiding zero-initialization and capacity-checking fix a bug (never released) in ConcatenateEidosValues() with logical vectors that would cause incorrect results in some post-2.5 GitHub versions fix a bug (never released) in the new integer() two-value filling code added post-2.5 optimize method dispatch in Eidos to gather results more efficiently, with benefits for script-intensive models add support for matrices and arrays in Eidos; new functions matrix(), array(), nrow(), ncol(), dim(), t(), cbind(), rbind(), matrixMult(), drop() changed the output format from str() and x.str() to be more R-like changed apply() to return a matrix or array in some cases; could break backward compatibility in rare cases policy change: assignment into a subset of a property is no longer legal in Eidos (e.g. x.foo[1:3] = rvalue), because it is conceptually flawed (not an lvalue) update the GSL code in Eidos to GSL version 2.4 and pulled in gsl_ran_multivariate_gaussian(), gsl_linalg_cholesky_decomp1(), and dependencies (no user-visible impact) add rmvnorm() for drawing from a multivariate distribution make version() return version numbers to the caller extend defineSpatialMap() to allow the map values to be specified as a matrix/array internal policy change: properties are no longer allowed to return NULL or be set to NULL, and must raise instead if they cannot provide a value policy change: Chromosome properties that used to return NULL when inapplicable now raise (mutation rate map and recombination rate map properties) change property semantics: singleton properties accessed on a matrix/array now mirror the dimensional structure of the target, like a unary operator rename apply() to sapply() to match R, add a simplify= parameter to govern the result's dimensionality add new apply() function to apply a lambda to margins of a matrix or array, as in R fix recipes to use sapply() instead of apply(), following the new function names, and to run in more reasonable time (for testing of them) rescale the color scheme for recombination and mutation rate maps in SLiMgui to handle a wider range(1e-6 to 1e-9), and make the mutation rate display with a more purple hue (compared to blue for recombination rate) add recipe 13.17 showing a two-trait QTL-based phenotypic model with pleiotropy and nutational correlation, plus live R-based plotting 2.5 (build 1204; Eidos version 1.5): add a check for completeness of the help information compared to class definitions, and add doc for a few missing items change the getValue()/setValue() implementation to be more memory-efficient when not used (but a little slower and less memory-efficient when used) Mutation now supports getValue()/setValue() for greater extensibility add script prettyprinting facility to SLiMgui enhance pmax() and pmin() to allow a singleton vector to be paired with a longer vector enhance max(), min(), and range() to allow any number of arguments of any length enhance seq() to support an optional length parameter enhance any() and all() to allow any number of arguments of any length add a ternary conditional operator, ? else, to the Eidos language add a sumExact() function for exact summation of floating point numbers improved numerical accuracy for complex recombination maps add ability to supply a mutation rate map instead of just an overall rate; removed the mutationRate property of Chromosome add display of the mutation rate map in SLiMgui with the R button add support for /* */ block comments to Eidos fix Context-defined functions so SLiMgui works with them better (showing the function prototype even after an error) fix a bug in InteractionType that would produce incorrect results for interactions if individuals had exactly identical coordinates speed up mateChoice() callbacks that select just a subset of all possible mates add a preserveOrder flag to the unique() function in Eidos to allow O(n log n) performance to be requested when order does not matter rename function(), method(), and property() to functionSignature(), methodSignature(), and propertySignature() respectively rename argument "function" for doCall() to "functionName" add support for user-defined functions in Eidos and SLiM add a source() function to read in and execute a source file revise recipe 11.1 to fix the FST calculation code and encapsulate it into a reusable function add menu item in SLiMgui to open the SLiM-Extras repository on GitHub fix a major bug preventing new mutations from being introduced during clonal reproduction (existing in 2.4, 2.4.1, and 2.4.2) add recipe 13.13, illustrating how to make a simple haploid model add recipe 13.14, showing how to use variation in the mutation rate along the chromosome to model varying functional density fix recipe 5.3.3, which had recipe 5.3.2's code in its file 2.4.2 (build 1167 on branch mutid_bug_242; Eidos version 1.4.2): fix for incorrect output due to non-unique mutation IDs 2.4.1 (build 1166; Eidos version 1.4.1): fix a crash (or possible bad simulation data) involving stale subpopulation pointers in genomes in multi-subpop models 2.4 (build 1163; Eidos version 1.4): add a system() function to call out to Unix to run commands add a tooltip showing the frames per second for the play speed slider, and tweak the play speed metrics add PDF viewing capability to SLiMgui for R plotting integration add a writeTempFile() Eidos function for creating randomly named unique temporary files adding inSLiMgui property to SLiMSim add recipe 13.11, live plotting with R using system() addition of catn() function, identical to cat() but with a newline appended to the output addition of paste0() function, identical to paste() but with no separator add -rescheduleScriptBlock() method to SLiMSim add ability to display only one mutation type in the chromosome view, through a context menu (added a tip on this) add a top/bottom splitter in the SLiMgui main window implement mutation runs inside Genome for better performance add a new option in initializeSLiMOptions() to control the number of mutation runs, if desired (usually unnecessary) optimize crossover mutation code with improved code flow and expanded case treatments optimize fitness calculations by caching fitness effects of mutations optimization: switch to MutationIndex instead of Mutation * optimization: keep mutation refcounts in a separate buffer added -l / -long command line option for long (i.e. verbose) output add font size preference to SLiMgui, for presentations etc. optimization of simulations in which all mutations have no direct fitness effects, particularly QTL-based models add sumOfMutationsOfType() method to Individual and Genome for fast totalling of additive QTLs in QTL-based models optimize script block handling for sims with many script blocks, to decrease callback overhead rewrite QTL-based recipes to use sumOfMutationsOfType() added a preventIncidentalSelfing option to initializeSLiMOptions(), to prevent incidental selfing in hermaphroditic models add profiling (performance monitoring) in SLiMgui add alternative displays for the population view, selectable with right-click / control-click upgraded to GSL version 2.3 and pulled in gsl_cdf_tdist_Q() and dependencies (no user-visible impact whatsoever) added mutation run experiments adding ttest() function for performing t-tests optimize fitness calculations using non-neutral mutation caches speed up pure neutral models by shifting from gsl_ran_discrete() to eidos_random_int() to choose mates fix an Eidos bug when doing a for loop on a seqAlong() vector of a zero length parameter, like "for (i in seqAlong(q)) ..." where q is zero-length (probably nobody cares) add recipe 9.5, Changing selection coefficients with setSelectionCoeff() add performance metrics related to mutations and mutation runs to SLiMgui's profile reports add setValue() / getValue() capability to MutationType, GenomicElementType, and InteractionType scripted (type "s") DFEs in MutationType now have access to all SLiM constants add mutationStackGroup property to MutationType and expand the mutation stacking algorithm accordingly fix for a potentially serious bug in the Eidos function setDifference() (no impact if you do not use that function) NOTE: this version changed model output in some cases because of a float/double change in fitness calculations NOTE: this version changed model output for pure neutral models because of a change in the random numbers used to choose mates 2.3 (build 1052; Eidos version 1.3): added x, y, and z properties to Individual for tracking spatial location add continuousSpace parameter to initializeSLiMOptions() to allow simulations to register as using continuous space make SLiMgui display subpopulations spatially when continuous space is enabled fix autocompletion bug with simulation symbols fix an omitted case in Eidos subsetting (which raised an exception) optimize ifelse() and operator ! in Eidos change mateChoice() policy for all-zero return to be equivalent to returning float(0) – reject the first parent optimization for mean() in Eidos add InteractionType class, initializeInteractionType(), and spatial queries fix copy so syntax coloring gets copied to the clipboard again add setSpatialPosition() method to Individual add spatialBounds, pointInBounds(), pointReflected(), pointStopped(), pointUniform(), and setSpatialBounds() to Subpopulation add chapter 14 recipes add chapter headers inside the Recipes menu add support for interaction() callbacks make the mutation-type argument for fitness() callbacks allow NULL, to allow non-mutation-based fitness callbacks rewrite recipes 13.1, 13.3, 13.10, 14.2, 14.3, 14.4, and 14.5 to use global fitness callbacks instead of marker mutations allow mateChoice() callbacks to return a singleton Individual that is the chosen mate rewrite recipe 11.2 to return a singleton Individual from its mateChoice() callback broaden spatiality of InteractionType to allow "y", "z", "xz", "yz" implement the sex-segregation feature of InteractionType implement the reciprocality feature of InteractionType, change default for reciprocality to F adding unevaluate() to allow interactions to be reused with fresh calculations within a generation fix a code completion bug when the simulation is invalid first passes at recipes 14.6, 14.7, 14.8, and 14.9 move color-related code from SLiM to Eidos, add new color-conversion functions to Eidos add support for spatial maps: defineSpatialMap(), spatialMapValue(), spatialMapColor() add recipes 14.10 and 14.11 add support for output of positional information in outputFull(), and reading of positional information in readFromPopulationFile() policy change: readFromPopulationFile() no longer has the side effect of recalculating fitness values, and warns if called at a time other than a late() event remove the Import Population... command in SLiMgui, which no longer fits into the fitness-calculation model of SLiM add tests for interactions and spatiality, tweak interfaces and requirements 2.2.1 (build 992; Eidos version 1.2.1): added recipe to demonstrate forcing a pedigree during mating (recipe 13.7) added recipe to show suppression of baseline hermaphroditic selfing (recipe 12.4) added recipe for estimating model parameters with ABC (recipe 13.8) added tagF property to Individual fix code completion key binding problem on OS X 10.12 add recipe for true local ancestry tracking (recipe 13.9) fix a bug preventing negative constant definitions at the command line generalize command-line defines to allow arbitrary expressions add order() function to obtain indices for sorting add recipe for heritability with quantitative genetics (recipe 13.10) add properties (color, colorSubstitution) to allow custom coloring of individuals, genomic element types, and mutation types in SLiMgui add recipe for custom coloring in SLiMgui (recipe 7.4) add/modify tests to improve code coverage accelerate bulk setting of some common read-write SLiM properties (up to a 3x speedup for test cases) switch SLiMgui over to a full document-based model (.slim files, save, revert, etc.) highlight the recycle button green when changes have been made to the script since the last recycle make multiple calls to initializeRecombinationRate() illegal, to prevent misunderstandings about how to make complex recombination maps speed up syntax coloring for large files fix hang during mate choice when all individuals are fitness <= 0.0 2.2 (build 955; Eidos version 1.2): added recombination() callback for individual-level recombination modifications add containsMarkerMutation() method to Genome add example recipe for recombination() callbacks, section 13.5 fix so final output from stop(), etc., gets to the output stream added clock() function to Eidos for CPU time usage monitoring improve tick labels in SLiMgui chromosome view bounce the SLiMgui icon once when a run completes, for notification of the user add MutationRun class for storing shared runs of mutations (performance enhancement) add a tips/tricks window visible at startup, and make some items for it converted the chromosome view to draw with OpenGL (performance enhancement) add setValue()/getValue() dictionary capabilities to SLiMSim, Subpopulation, and Individual revise recipe 11.1 to use setValue()/getValue() add -d[efine] command-line argument for slim, to allow Eidos constants to be defined on the command line NOTE: this version changed model output in many cases because the order of drawing mutations and breakpoints changed 2.1.1 (build 924; Eidos version 1.1.1): fix segfault with very large recombination maps (thanks Martin Petr) fix some bad interactions between having sex enabled and using mateChoice()/modifyChild() callbacks (thanks Nathan Oakes) fix a crash involving accessing the individuals of a subpop after changing the subpop size (caching bug) (thanks to Melissa Hubisz) sort MS output by position (fix to regression; thanks Alexandre Harris) add -mutationCounts method on SLiMSim, parallel to -mutationFrequencies 2.1 (build 907; Eidos version 1.1): Improve the fitness~time plot (display of subpopulation fitnesses, point/line plotting option) Fix for minor code-completion and status line bugs Add infinite loop prevention for mateChoice() and modifyChild() callbacks Add "replace" parameter to outputSample() and outputMSSample() – BREAKS BACKWARD COMPATIBILITY Add outputVCFSample() Fix a bug in Genome's -containsMutations() method that caused it to produce incorrect results if its argument was a non-singleton vector Add an Individual class to SLiM, and an "individuals" property to Subpopulation Add type Individual parameters to SLiM callbacks as needed Add a unique index and a tag to Mutation and Substitution Added mutation id to output formats and load code, so it is preserved across save/load The readFromPopulationFile() method of SLiMSim now sets the generation as a side effect Change Eidos class methods to also work as non-multicast class methods (receiving the vector of objects as an operand) Added size() class method in Eidos Make code completion smart about functions like sample() that return the same type/class they are passed Added sex property to Individual Added file output to outputMutations() and outputFixedMutations() Added deleteFile() function to Eidos Improve display of very narrow recombination regions and genomic elements Added DFE type 's' for user-defined scripts that generate selection coefficients Add script/output show/hide menu command for SLiMgui Add support for sex-specific recombination rates/maps Add runtime memory overflow checking, disabled with -x command-line flag Change addNewMutation() and addNewDrawnMutation() to be class methods – BREAKS BACKWARD COMPATIBILITY Accelerated vectorized property access for singleton properties Add "Open Recipe" menu in SLiMgui's File menu, for fast recipe access Add default arguments and named arguments to Eidos function/method dispatch Split ExecuteFunctionCall() into separate functions (no user-visible consequence) Add file output and append options to all output methods Add createDirectory() function to Eidos Add automatic pedigree tracking to the Individual class Add new initializeSLiMOptions() initialization function Add uniqueMutations property and uniqueMutationsOfType() method to Individual NOTE: this version changed model output in many cases, for reasons I haven't bothered to retrace 2.0.4 (build 833; Eidos version 1.0.4) Fix issue with interleaving of output from SLiM versus Eidos Fix for a code completion bug with if() statements 2.0.3 (build 828; Eidos version 1.0.3): Greatly improved code completion facilities Fix for build problem on Ubuntu 2.0.2 (build 824; Eidos version 1.0.2): Added beep() function to Eidos Added setMutationType() method on Mutation Added binary option for outputFull() Added return of saved generation in readFromPopulationFile() 2.0.1 (build 815; Eidos version 1.0.1): Added format() to Eidos Fixed performance issues in SLiMgui with a very large number of subpopulations 2.0 (build 811; Eidos version 1.0): Version history starts. ================================================ FILE: cmake/AboutTheseModules.cmake ================================================ # - Dummy module containing information about these modules for the HELP file # This file documents a snapshot of the cmake-modules available from # http://github.com/rpavlik/cmake-modules/ # The latest version of these modules can always be found there. # Additionally, you can find instructions on how to integrate these modules # into your own project either in the README.markdown file in this directory, # or on the GitHub page listed above (scroll to the bottom to see the README # rendered attractively). # # In short: Modules of the form "FindSomeName.cmake" are considered to be # "find modules", and are intended to be used indirectly by calling find_package, # not by calling include. Thus, you'll want to do something like: # find_package(SomeName) # They define a number of variables allowing you to use whatever software # they search for, such as include directories and libraries. A few also # define some functions for your use. # # All other modules provide functionality, either immediately upon including # them, or by defining functions that perform some task of varying utility # that you can use any time after including them. Note that if a module # has the filename, for example, cmake/BoostTestTargets.cmake, you only # need to call: # include(BoostTestTargets) # # For more information, see the documentation for individual modules, the # cmake-modules github page, and/or the upstream CMake documentation at # http://www.cmake.org/cmake/help/cmake-2-8-docs.html # # # Copyright 2009-2010, Iowa State University # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # http://www.boost.org/LICENSE_1_0.txt) # SPDX-License-Identifier: BSL-1.0 ================================================ FILE: cmake/GetGitRevisionDescription.cmake ================================================ # - Returns a version string from Git # # These functions force a re-configure on each git commit so that you can # trust the values of the variables in your build system. # # get_git_head_revision( [ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR]) # # Returns the refspec and sha hash of the current head revision # # git_describe( [ ...]) # # Returns the results of git describe on the source tree, and adjusting # the output so that it tests false if an error occurs. # # git_describe_working_tree( [ ...]) # # Returns the results of git describe on the working tree (--dirty option), # and adjusting the output so that it tests false if an error occurs. # # git_get_exact_tag( [ ...]) # # Returns the results of git describe --exact-match on the source tree, # and adjusting the output so that it tests false if there was no exact # matching tag. # # git_local_changes() # # Returns either "CLEAN" or "DIRTY" with respect to uncommitted changes. # Uses the return code of "git diff-index --quiet HEAD --". # Does not regard untracked files. # # Requires CMake 2.6 or newer (uses the 'function' command) # # Original Author: # 2009-2020 Ryan Pavlik # http://academic.cleardefinition.com # # Copyright 2009-2013, Iowa State University. # Copyright 2013-2020, Ryan Pavlik # Copyright 2013-2020, Contributors # SPDX-License-Identifier: BSL-1.0 # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # http://www.boost.org/LICENSE_1_0.txt) if(__get_git_revision_description) return() endif() set(__get_git_revision_description YES) # We must run the following at "include" time, not at function call time, # to find the path to this module rather than the path to a calling list file get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) # Function _git_find_closest_git_dir finds the next closest .git directory # that is part of any directory in the path defined by _start_dir. # The result is returned in the parent scope variable whose name is passed # as variable _git_dir_var. If no .git directory can be found, the # function returns an empty string via _git_dir_var. # # Example: Given a path C:/bla/foo/bar and assuming C:/bla/.git exists and # neither foo nor bar contain a file/directory .git. This wil return # C:/bla/.git # function(_git_find_closest_git_dir _start_dir _git_dir_var) set(cur_dir "${_start_dir}") set(git_dir "${_start_dir}/.git") while(NOT EXISTS "${git_dir}") # .git dir not found, search parent directories set(git_previous_parent "${cur_dir}") get_filename_component(cur_dir "${cur_dir}" DIRECTORY) if(cur_dir STREQUAL git_previous_parent) # We have reached the root directory, we are not in git set(${_git_dir_var} "" PARENT_SCOPE) return() endif() set(git_dir "${cur_dir}/.git") endwhile() set(${_git_dir_var} "${git_dir}" PARENT_SCOPE) endfunction() function(get_git_head_revision _refspecvar _hashvar) _git_find_closest_git_dir("${CMAKE_CURRENT_SOURCE_DIR}" GIT_DIR) if("${ARGN}" STREQUAL "ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR") set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR TRUE) else() set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR FALSE) endif() if(NOT "${GIT_DIR}" STREQUAL "") file(RELATIVE_PATH _relative_to_source_dir "${CMAKE_SOURCE_DIR}" "${GIT_DIR}") if("${_relative_to_source_dir}" MATCHES "[.][.]" AND NOT ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR) # We've gone above the CMake root dir. set(GIT_DIR "") endif() endif() if("${GIT_DIR}" STREQUAL "") set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE) set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE) return() endif() # Check if the current source dir is a git submodule or a worktree. # In both cases .git is a file instead of a directory. # if(NOT IS_DIRECTORY ${GIT_DIR}) # The following git command will return a non empty string that # points to the super project working tree if the current # source dir is inside a git submodule. # Otherwise the command will return an empty string. # execute_process( COMMAND "${GIT_EXECUTABLE}" rev-parse --show-superproject-working-tree WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT "${out}" STREQUAL "") # If out is empty, GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a submodule file(READ ${GIT_DIR} submodule) string(REGEX REPLACE "gitdir: (.*)$" "\\1" GIT_DIR_RELATIVE ${submodule}) string(STRIP ${GIT_DIR_RELATIVE} GIT_DIR_RELATIVE) get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE) set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") else() # GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a worktree file(READ ${GIT_DIR} worktree_ref) # The .git directory contains a path to the worktree information directory # inside the parent git repo of the worktree. # string(REGEX REPLACE "gitdir: (.*)$" "\\1" git_worktree_dir ${worktree_ref}) string(STRIP ${git_worktree_dir} git_worktree_dir) _git_find_closest_git_dir("${git_worktree_dir}" GIT_DIR) set(HEAD_SOURCE_FILE "${git_worktree_dir}/HEAD") endif() else() set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") endif() set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") if(NOT EXISTS "${GIT_DATA}") file(MAKE_DIRECTORY "${GIT_DATA}") endif() if(NOT EXISTS "${HEAD_SOURCE_FILE}") return() endif() set(HEAD_FILE "${GIT_DATA}/HEAD") configure_file("${HEAD_SOURCE_FILE}" "${HEAD_FILE}" COPYONLY) configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" "${GIT_DATA}/grabRef.cmake" @ONLY) include("${GIT_DATA}/grabRef.cmake") set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE) set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE) endfunction() function(git_describe _var) if(NOT GIT_FOUND) find_package(Git QUIET) endif() get_git_head_revision(refspec hash) if(NOT GIT_FOUND) set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) return() endif() if(NOT hash) set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) return() endif() # TODO sanitize #if((${ARGN}" MATCHES "&&") OR # (ARGN MATCHES "||") OR # (ARGN MATCHES "\\;")) # message("Please report the following error to the project!") # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") #endif() #message(STATUS "Arguments to execute_process: ${ARGN}") execute_process( COMMAND "${GIT_EXECUTABLE}" describe --tags --always ${hash} ${ARGN} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RESULT_VARIABLE res OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT res EQUAL 0) set(out "${out}-${res}-NOTFOUND") endif() set(${_var} "${out}" PARENT_SCOPE) endfunction() function(git_describe_working_tree _var) if(NOT GIT_FOUND) find_package(Git QUIET) endif() if(NOT GIT_FOUND) set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) return() endif() execute_process( COMMAND "${GIT_EXECUTABLE}" describe --dirty ${ARGN} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RESULT_VARIABLE res OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT res EQUAL 0) set(out "${out}-${res}-NOTFOUND") endif() set(${_var} "${out}" PARENT_SCOPE) endfunction() function(git_get_exact_tag _var) git_describe(out --exact-match ${ARGN}) set(${_var} "${out}" PARENT_SCOPE) endfunction() function(git_local_changes _var) if(NOT GIT_FOUND) find_package(Git QUIET) endif() get_git_head_revision(refspec hash) if(NOT GIT_FOUND) set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) return() endif() if(NOT hash) set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) return() endif() execute_process( COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD -- WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RESULT_VARIABLE res OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(res EQUAL 0) set(${_var} "CLEAN" PARENT_SCOPE) else() set(${_var} "DIRTY" PARENT_SCOPE) endif() endfunction() ================================================ FILE: cmake/GetGitRevisionDescription.cmake.in ================================================ # # Internal file for GetGitRevisionDescription.cmake # # Requires CMake 2.6 or newer (uses the 'function' command) # # Original Author: # 2009-2010 Ryan Pavlik # http://academic.cleardefinition.com # Iowa State University HCI Graduate Program/VRAC # # Copyright 2009-2012, Iowa State University # Copyright 2011-2015, Contributors # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # http://www.boost.org/LICENSE_1_0.txt) # SPDX-License-Identifier: BSL-1.0 set(HEAD_HASH) file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) if(HEAD_CONTENTS MATCHES "ref") # named branch string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") if(EXISTS "@GIT_DIR@/${HEAD_REF}") configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) else() configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY) file(READ "@GIT_DATA@/packed-refs" PACKED_REFS) if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") set(HEAD_HASH "${CMAKE_MATCH_1}") endif() endif() else() # detached HEAD configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) endif() if(NOT HEAD_HASH) file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) string(STRIP "${HEAD_HASH}" HEAD_HASH) endif() ================================================ FILE: cmake/GitSHA1.cpp.in ================================================ // For setting up the Git commit SHA-1 as a global // This is used when building under CMake #define GIT_SHA1 "@GIT_SHA1@" extern const char g_GIT_SHA1[]; const char g_GIT_SHA1[] = GIT_SHA1; ================================================ FILE: cmake/GitSHA1.h ================================================ // For setting up the Git commit SHA-1 as a global // See ../cmake/_README.txt extern const char g_GIT_SHA1[]; ================================================ FILE: cmake/GitSHA1_Xcode.cpp ================================================ // For setting up the Git commit SHA-1 as a global // This is used when building under Xcode extern const char g_GIT_SHA1[]; const char g_GIT_SHA1[] = "unknown (not defined under Xcode)"; ================================================ FILE: cmake/GitSHA1_qmake.cpp ================================================ // For setting up the Git commit SHA-1 as a global // This is used when building under qmake extern const char g_GIT_SHA1[]; #ifdef GIT_SHA1 const char g_GIT_SHA1[] = GIT_SHA1; #else #warning No GIT_SHA1 definition supplied by qmake; check QtSLiM.pro const char g_GIT_SHA1[] = "unknown (qmake has not defined GIT_SHA1)"; #endif ================================================ FILE: cmake/LICENSE_1_0.txt ================================================ Boost Software License - Version 1.0 - August 17th, 2003 Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: cmake/README.markdown ================================================ # Ryan's CMake Modules Collection Ryan A. Pavlik, Ph.D. ## Introduction This is a collection of CMake modules that I've produced during the course of a variety of software development. There are a number of find modules, especially for virtual reality and physical simulation packages, some utility modules of more general interest, and some patches or workarounds for (very old versions of) CMake itself. Each module is generally documented, and depending on how busy I was when I created it, the documentation can be fairly complete. By now, it also includes contributions both from open-source projects I work on, as well as friendly strangers on the Internet contributing their modules. I am very grateful for improvements/fixes/pull requests! As usual, contributions to files are assumed to be under the same license as they were offered to you (incoming == outgoing), and any new files must have proper copyright and SPDX license identifer headers. ## How to Integrate How would you like to use these? Some of the modules, particularly older ones, have a number of internal dependencies, and thus would be best placed wholesale into a `cmake` subdirectory of your project source. Otherwise, you may consider just picking the subset you prefer into such a `cmake` subdirectory. ### All Modules If you use Git, try installing [git-subtree][1] (included by default on Git for Windows and perhaps for your Linux distro, especially post-1.9.1), so you can easily use this repository for subtree merges, updating simply. For the initial checkout: ```sh cd yourprojectdir git subtree add --squash --prefix=cmake https://github.com/rpavlik/cmake-modules.git main ``` For updates: ```sh cd yourprojectdir git subtree pull --squash --prefix=cmake https://github.com/rpavlik/cmake-modules.git main ``` If you originally installed this by just copying the files, you'll sadly have to delete the directory, commit that, then do the `git subtree add`. Annoying, but I don't know a workaround. (Alternately you can use the method described below, for the "subset of modules", to update.) If you use some other version control, you can export a copy of this directory without the git metadata by calling: ```sh ./export-to-directory.sh ../yourprojectdir/cmake ``` You might also consider exporting to a temp directory and merging changes, since this will not overwrite by default. You can pass -f to overwrite existing files. ### Just a few modules Many newer modules don't have any or many internal dependencies. You can review them to look for any `include(` statements or other things that increase the files used (e.g. `configure_file(`, `file(READ`, `file(GENERATE`), and make sure you copy those too. If this is how you originally started using these modules, then running the following from within a clone of this repo will automatically update any files. **Be sure you have committed any local changes in your project first to avoid potentially losing work!** ```sh ./update-modules.sh ../yourprojectdir/cmake/ ``` Note that the script is not smart enough to know about changed dependent scripts/files, nor about scripts with matching names but not originating in this project (it just looks at file names/paths), so manually review the diff before committing in your project. ## How to Use At the minimum, all you have to do is add a line like this near the top of your root CMakeLists.txt file (but not before your `project()` call): ```cmake list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") ``` You might also want the extra automatic features/fixes included with the modules, for that, just add another line following the first one: ```cmake include(UseBackportedModules) ``` For more information on individual modules, look at the files themselves: they should all start with a comment. ## Licenses The modules that I wrote myself are all subject to this license: > Copyright 2009-2014, Iowa State University. > or Copyright 2014-2017, Sensics, Inc., > or Copyright 2018-2020, Collabora, Ltd., > or Copyright 2009-2020, Ryan A. Pavlik > > Distributed under the Boost Software License, Version 1.0. > > (See accompanying file `LICENSE_1_0.txt` or copy at > ) Modules based on those included with CMake are under the OSI-approved BSD license, which is included in each of those modules. A few other modules are modified from other sources - when in doubt, look at the `.cmake` file - each file has correct licensing information. If you'd like to contribute, that would be great! Just make sure to include the license boilerplate in your module, and send a pull request. ## Important License Note If you find this file inside of another project, rather at the top-level directory, you're in a separate project that is making use of these modules. That separate project can (and probably does) have its own license specifics. [1]: https://github.com/git/git/tree/master/contrib/subtree "Git Subtree upstream" ================================================ FILE: cmake/_README.txt ================================================ This folder contains a small subset of the files in: https://github.com/rpavlik/cmake-modules Specifically: ./LICENSE_1_0.txt ./README.markdown ./AboutTheseModules.cmake ./GetGitRevisionDescription.cmake ./GetGitRevisionDescription.cmake.in These are distributed under the BSL 1.0 license: ./LICENSE_1_0.txt. Note that the BSL 1.0 is compatible with SLiM's license, allowing incorporation of these files. See README.markdown for more info. Thanks to Ryan A. Pavlik for these very useful modules! ---------------------------------------------------------- The purpose of including these is to allow the current Git commit SHA1 to be shown in QtSLiM and slim's about info, making it easier to identify the version of SLiM being used. See: https://stackoverflow.com/a/4318642/2752221 for details on how this is done in CMake. Under qmake, we have a similar but inferior strategy at present based upon https://stackoverflow.com/a/66315563/2752221 that sets a define in QtSLiM.pro and sets a C global in GitSHA1_qmake.cpp. It updates only when qmake is run, so the hash shown in SLiMgui can be out of date; BEWARE. It is intended chiefly for tagging builds done by the various installers. Under Xcode we have no similar scheme, and GitSHA1_Xcode.cpp sets up a dummy string that indicates the hash is unknown. Thanks to Bryce Carson for setting me on the trail with a link to rpavlik's repo! Benjamin C. Haller 2 August 2022 ================================================ FILE: cmake/toolchain-mingw64.cmake ================================================ # CMake toolchain file for cross-compiling SLiM to Windows using MinGW-w64 # # NOTE: This file is only needed for cross-compiling to Windows from Linux. # It is NOT used for normal builds on Linux, macOS, or native Windows (MSYS2). # # Usage: # mkdir build_win64 && cd build_win64 # cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain-mingw64.cmake # make -j10 slim eidos # # Prerequisites: # sudo apt-get install mingw-w64 # # Added by Andrew Kern on 2025-12-14 # set(CMAKE_SYSTEM_NAME Windows) set(CMAKE_SYSTEM_PROCESSOR x86_64) # Specify the cross compilers set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) # Where to find the target environment set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) # Search for programs in the host environment set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # Search for libraries and headers in the target environment set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) ================================================ FILE: codecov.yml ================================================ coverage: status: project: default: # Allow coverage to drop by up to 1% before failing threshold: 1% patch: default: # Don't require new code to have specific coverage informational: true comment: # Post coverage comment on PRs layout: "reach,diff,flags,files" behavior: default require_changes: false ================================================ FILE: core/chromosome.cpp ================================================ // // chromosome.cpp // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "chromosome.h" #include "eidos_rng.h" #include "slim_globals.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "community.h" #include "species.h" #include "individual.h" #include "subpopulation.h" #include #include #include #include #include #include #include // This struct is used to represent a constant-mutation-rate subrange of a genomic element; it is used internally by Chromosome. // It is private to Chromosome, and is not exposed in Eidos in any way; it just assists with the drawing of new mutations. struct GESubrange { public: GenomicElement *genomic_element_ptr_; // pointer to the genomic element that contains this subrange slim_position_t start_position_; // the start position of the subrange slim_position_t end_position_; // the end position of the subrange GESubrange(GenomicElement *p_genomic_element_ptr, slim_position_t p_start_position, slim_position_t p_end_position); }; inline __attribute__((always_inline)) GESubrange::GESubrange(GenomicElement *p_genomic_element_ptr, slim_position_t p_start_position, slim_position_t p_end_position) : genomic_element_ptr_(p_genomic_element_ptr), start_position_(p_start_position), end_position_(p_end_position) { } #pragma mark - #pragma mark Chromosome #pragma mark - Chromosome::Chromosome(Species &p_species, ChromosomeType p_type, int64_t p_id, std::string p_symbol, slim_chromosome_index_t p_index, int p_preferred_mutcount) : id_(p_id), symbol_(p_symbol), name_(), index_(p_index), type_(p_type), exp_neg_overall_mutation_rate_H_(0.0), exp_neg_overall_mutation_rate_M_(0.0), exp_neg_overall_mutation_rate_F_(0.0), exp_neg_overall_recombination_rate_H_(0.0), exp_neg_overall_recombination_rate_M_(0.0), exp_neg_overall_recombination_rate_F_(0.0), #ifndef USE_GSL_POISSON probability_both_0_H_(0.0), probability_both_0_OR_mut_0_break_non0_H_(0.0), probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_H_(0.0), probability_both_0_M_(0.0), probability_both_0_OR_mut_0_break_non0_M_(0.0), probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_M_(0.0), probability_both_0_F_(0.0), probability_both_0_OR_mut_0_break_non0_F_(0.0), probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_F_(0.0), #endif haplosome_pool_(p_species.population_.species_haplosome_pool_), preferred_mutrun_count_(p_preferred_mutcount), x_experiments_enabled_(false), community_(p_species.community_), species_(p_species), last_position_(0), extent_immutable_(false), overall_mutation_rate_H_(0.0), overall_mutation_rate_M_(0.0), overall_mutation_rate_F_(0.0), overall_mutation_rate_H_userlevel_(0.0), overall_mutation_rate_M_userlevel_(0.0), overall_mutation_rate_F_userlevel_(0.0), overall_recombination_rate_H_(0.0), overall_recombination_rate_M_(0.0), overall_recombination_rate_F_(0.0), overall_recombination_rate_H_userlevel_(0.0), overall_recombination_rate_M_userlevel_(0.0), overall_recombination_rate_F_userlevel_(0.0), using_DSB_model_(false), non_crossover_fraction_(0.0), gene_conversion_avg_length_(0.0), gene_conversion_inv_half_length_(0.0), simple_conversion_fraction_(0.0), mismatch_repair_bias_(0.0), last_position_mutrun_(0) { // If the user has said "no mutation run experiments" in initializeSLiMOptions(), then a count of zero // supplied here is interpreted as a count of 1, and experiments will thus not be conducted. if (!species_.UserWantsMutrunExperiments() && (preferred_mutrun_count_ == 0)) preferred_mutrun_count_ = 1; // Set up the default color for fixed mutations in SLiMgui color_sub_ = "#3333FF"; if (!color_sub_.empty()) Eidos_GetColorComponents(color_sub_, &color_sub_red_, &color_sub_green_, &color_sub_blue_); // depending on the type of chromosome, cache some properties for quick reference switch (type_) { case ChromosomeType::kA_DiploidAutosome: // type "A" intrinsic_ploidy_ = 2; always_uses_null_haplosomes_ = false; is_sex_chromosome_ = false; defaults_to_zero_recombination_ = false; type_string_ = gStr_A; break; case ChromosomeType::kH_HaploidAutosome: // type "H" intrinsic_ploidy_ = 1; always_uses_null_haplosomes_ = false; is_sex_chromosome_ = false; defaults_to_zero_recombination_ = true; type_string_ = gStr_H; break; case ChromosomeType::kX_XSexChromosome: // type "X" intrinsic_ploidy_ = 2; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = true; defaults_to_zero_recombination_ = false; type_string_ = gStr_X; break; case ChromosomeType::kY_YSexChromosome: // type "Y" intrinsic_ploidy_ = 1; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = true; defaults_to_zero_recombination_ = true; type_string_ = gStr_Y; break; case ChromosomeType::kZ_ZSexChromosome: // type "Z" intrinsic_ploidy_ = 2; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = true; defaults_to_zero_recombination_ = false; type_string_ = gStr_Z; break; case ChromosomeType::kW_WSexChromosome: // type "W" intrinsic_ploidy_ = 1; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = true; defaults_to_zero_recombination_ = true; type_string_ = gStr_W; break; case ChromosomeType::kHF_HaploidFemaleInherited: // type "HF" intrinsic_ploidy_ = 1; always_uses_null_haplosomes_ = false; is_sex_chromosome_ = false; defaults_to_zero_recombination_ = true; type_string_ = gStr_HF; break; case ChromosomeType::kFL_HaploidFemaleLine: // type "FL" intrinsic_ploidy_ = 1; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = false; defaults_to_zero_recombination_ = true; type_string_ = gStr_FL; break; case ChromosomeType::kHM_HaploidMaleInherited: // type "HM" intrinsic_ploidy_ = 1; always_uses_null_haplosomes_ = false; is_sex_chromosome_ = false; defaults_to_zero_recombination_ = true; type_string_ = gStr_HM; break; case ChromosomeType::kML_HaploidMaleLine: // type "ML" intrinsic_ploidy_ = 1; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = false; defaults_to_zero_recombination_ = true; type_string_ = gStr_ML; break; case ChromosomeType::kHNull_HaploidAutosomeWithNull: // type "H-" intrinsic_ploidy_ = 2; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = false; defaults_to_zero_recombination_ = true; type_string_ = gStr_H_; // "H-" break; case ChromosomeType::kNullY_YSexChromosomeWithNull: // type "-Y" intrinsic_ploidy_ = 2; always_uses_null_haplosomes_ = true; is_sex_chromosome_ = true; defaults_to_zero_recombination_ = true; type_string_ = gStr__Y; // "-Y" break; } } Chromosome::~Chromosome(void) { //EIDOS_ERRSTREAM << "Chromosome::~Chromosome" << std::endl; if (lookup_mutation_H_) gsl_ran_discrete_free(lookup_mutation_H_); if (lookup_mutation_M_) gsl_ran_discrete_free(lookup_mutation_M_); if (lookup_mutation_F_) gsl_ran_discrete_free(lookup_mutation_F_); if (lookup_recombination_H_) gsl_ran_discrete_free(lookup_recombination_H_); if (lookup_recombination_M_) gsl_ran_discrete_free(lookup_recombination_M_); if (lookup_recombination_F_) gsl_ran_discrete_free(lookup_recombination_F_); // Dispose of any nucleotide sequence delete ancestral_seq_buffer_; ancestral_seq_buffer_ = nullptr; // Dispose of all genomic elements, which we own for (GenomicElement *element : genomic_elements_) delete element; // dispose of haplosomes within our junkyards for (Haplosome *haplosome : haplosomes_junkyard_nonnull) { haplosome->~Haplosome(); haplosome_pool_.DisposeChunk(const_cast(haplosome)); } haplosomes_junkyard_nonnull.clear(); for (Haplosome *haplosome : haplosomes_junkyard_null) { haplosome->~Haplosome(); haplosome_pool_.DisposeChunk(const_cast(haplosome)); } haplosomes_junkyard_null.clear(); // Dispose of all mutation run contexts #ifndef _OPENMP delete mutation_run_context_SINGLE_.allocation_pool_; #else for (size_t threadnum = 0; threadnum < mutation_run_context_PERTHREAD.size(); ++threadnum) { omp_destroy_lock(&mutation_run_context_PERTHREAD[threadnum]->allocation_pool_lock_); delete mutation_run_context_PERTHREAD[threadnum]->allocation_pool_; delete mutation_run_context_PERTHREAD[threadnum]; } mutation_run_context_PERTHREAD.clear(); #endif // Dispose of mutation run experiment data if (x_experiments_enabled_) { if (x_current_runtimes_) free(x_current_runtimes_); x_current_runtimes_ = nullptr; if (x_previous_runtimes_) free(x_previous_runtimes_); x_previous_runtimes_ = nullptr; } } void Chromosome::CreateNucleotideMutationRateMap(void) { // In Species::CacheNucleotideMatrices() we find the maximum sequence-based mutation rate requested. Absent a // hotspot map, this is the overall rate at which we need to generate mutations everywhere along the chromosome, // because any particular spot could have the nucleotide sequence that leads to that maximum rate; we don't want // to have to calculate the mutation rate map every time the sequence changes, so instead we use rejection // sampling. With a hotspot map, the mutation rate map is the product of the hotspot map and the maximum // sequence-based rate. Note that we could get more tricky here – even without a hotspot map we could vary // the mutation rate map based upon the genomic elements in the chromosome, since different genomic elements // may have different maximum sequence-based mutation rates. We do not do that right now, to keep the model // simple. // Note that in nucleotide-based models we completely hide the existence of the mutation rate map from the user; // all the user sees are the mutationMatrix parameters to initializeGenomicElementType() and the hotspot map // defined by initializeHotspotMap(). We still use the standard mutation rate map machinery under the hood, // though. So this method is, in a sense, an internal call to initializeMutationRate() that sets up the right // rate map to achieve what the user has requested through other APIs. double max_nucleotide_mut_rate = species_.MaxNucleotideMutationRate(); std::vector &hotspot_end_positions_H = hotspot_end_positions_H_; std::vector &hotspot_end_positions_M = hotspot_end_positions_M_; std::vector &hotspot_end_positions_F = hotspot_end_positions_F_; std::vector &hotspot_multipliers_H = hotspot_multipliers_H_; std::vector &hotspot_multipliers_M = hotspot_multipliers_M_; std::vector &hotspot_multipliers_F = hotspot_multipliers_F_; std::vector &mut_positions_H = mutation_end_positions_H_; std::vector &mut_positions_M = mutation_end_positions_M_; std::vector &mut_positions_F = mutation_end_positions_F_; std::vector &mut_rates_H = mutation_rates_H_; std::vector &mut_rates_M = mutation_rates_M_; std::vector &mut_rates_F = mutation_rates_F_; // clear the mutation map; there may be old cruft in there, if we're called by setHotspotMap() for example mut_positions_H.resize(0); mut_positions_M.resize(0); mut_positions_F.resize(0); mut_rates_H.resize(0); mut_rates_M.resize(0); mut_rates_F.resize(0); if ((hotspot_multipliers_M.size() > 0) && (hotspot_multipliers_F.size() > 0)) { // two sex-specific hotspot maps for (double multiplier_M : hotspot_multipliers_M) { double rate = max_nucleotide_mut_rate * multiplier_M; if (rate > 1.0) EIDOS_TERMINATION << "ERROR (Chromosome::CreateNucleotideMutationRateMap): the maximum mutation rate in nucleotide-based models is 1.0." << EidosTerminate(); mut_rates_M.emplace_back(rate); } for (double multiplier_F : hotspot_multipliers_F) { double rate = max_nucleotide_mut_rate * multiplier_F; if (rate > 1.0) EIDOS_TERMINATION << "ERROR (Chromosome::CreateNucleotideMutationRateMap): the maximum mutation rate in nucleotide-based models is 1.0." << EidosTerminate(); mut_rates_F.emplace_back(rate); } mut_positions_M = hotspot_end_positions_M; mut_positions_F = hotspot_end_positions_F; } else if (hotspot_multipliers_H.size() > 0) { // one hotspot map for (double multiplier_H : hotspot_multipliers_H) { double rate = max_nucleotide_mut_rate * multiplier_H; if (rate > 1.0) EIDOS_TERMINATION << "ERROR (Chromosome::CreateNucleotideMutationRateMap): the maximum mutation rate in nucleotide-based models is 1.0." << EidosTerminate(); mut_rates_H.emplace_back(rate); } mut_positions_H = hotspot_end_positions_H; } else { // No hotspot map specified at all; use a rate of 1.0 across the chromosome with an inferred length if (max_nucleotide_mut_rate > 1.0) EIDOS_TERMINATION << "ERROR (Chromosome::CreateNucleotideMutationRateMap): the maximum mutation rate in nucleotide-based models is 1.0." << EidosTerminate(); mut_rates_H.emplace_back(max_nucleotide_mut_rate); //mut_positions_H.emplace_back(?); // deferred; patched in Chromosome::InitializeDraws(). } community_.chromosome_changed_ = true; } // initialize the random lookup tables used by Chromosome to draw mutation and recombination events void Chromosome::InitializeDraws(void) { // If we are in the special case of having no genetics, do some simple initialization and return. // This leaves us in an unusual state, with nullptr for recombination/mutation rate maps and a // last_position_ of -1; this state is not normally achievable, and might cause special problems. if (!species_.HasGenetics()) { single_recombination_map_ = true; single_mutation_map_ = true; last_position_ = -1; extent_immutable_ = false; if (hotspot_multipliers_H_.size() == 0) hotspot_multipliers_H_.emplace_back(1.0); if (hotspot_end_positions_H_.size() == 0) hotspot_end_positions_H_.emplace_back(-1); if (mutation_rates_H_.size() == 0) mutation_rates_H_.emplace_back(1.0); if (mutation_end_positions_H_.size() == 0) mutation_end_positions_H_.emplace_back(-1); lookup_mutation_H_ = nullptr; overall_mutation_rate_M_userlevel_ = overall_mutation_rate_F_userlevel_ = overall_mutation_rate_H_userlevel_ = 0.0; overall_mutation_rate_M_ = overall_mutation_rate_F_ = overall_mutation_rate_H_ = 0.0; exp_neg_overall_mutation_rate_M_ = exp_neg_overall_mutation_rate_F_ = exp_neg_overall_mutation_rate_H_ = 1.0; if (recombination_rates_H_.size() == 0) recombination_rates_H_.emplace_back(1.0); if (recombination_end_positions_H_.size() == 0) recombination_end_positions_H_.emplace_back(-1); lookup_recombination_H_ = nullptr; any_recombination_rates_05_ = false; overall_recombination_rate_M_userlevel_ = overall_recombination_rate_F_userlevel_ = overall_recombination_rate_H_userlevel_ = 0.0; overall_recombination_rate_M_ = overall_recombination_rate_F_ = overall_recombination_rate_H_ = 0.0; exp_neg_overall_recombination_rate_M_ = exp_neg_overall_recombination_rate_F_ = exp_neg_overall_recombination_rate_H_ = 1.0; #ifndef USE_GSL_POISSON _InitializeJointProbabilities(overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_, probability_both_0_H_, probability_both_0_OR_mut_0_break_non0_H_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_H_); #endif return; } if (genomic_elements_.size() == 0) EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): empty chromosome." << EidosTerminate(); // BCH 8/28/2024: we now sort the genomic elements here; we need them to be sorted later on anyway // I avoided doing this before because it changed the behavior, but it is hard to imagine anybody caring // Since elements must be non-overlapping, we only need to sort by start position std::sort(genomic_elements_.begin(), genomic_elements_.end(), [](GenomicElement *e1, GenomicElement *e2) { return e1->start_position_ < e2->start_position_; }); // determine which case we are working with: separate recombination maps for the sexes, or one map auto rec_rates_H_size = recombination_rates_H_.size(); auto rec_rates_M_size = recombination_rates_M_.size(); auto rec_rates_F_size = recombination_rates_F_.size(); if ((rec_rates_H_size > 0) && (rec_rates_M_size == 0) && (rec_rates_F_size == 0)) single_recombination_map_ = true; else if ((rec_rates_H_size == 0) && (rec_rates_M_size > 0) && (rec_rates_F_size > 0)) single_recombination_map_ = false; else EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): (internal error) unclear whether we have separate recombination maps or not, or recombination rate not specified." << EidosTerminate(); // determine which case we are working with: separate mutation maps for the sexes, or one map auto mut_rates_H_size = mutation_rates_H_.size(); auto mut_rates_M_size = mutation_rates_M_.size(); auto mut_rates_F_size = mutation_rates_F_.size(); if ((mut_rates_H_size > 0) && (mut_rates_M_size == 0) && (mut_rates_F_size == 0)) single_mutation_map_ = true; else if ((mut_rates_H_size == 0) && (mut_rates_M_size > 0) && (mut_rates_F_size > 0)) single_mutation_map_ = false; else EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): (internal error) unclear whether we have separate mutation maps or not, or mutation rate not specified." << EidosTerminate(); // Calculate the last chromosome base position from our genomic elements, mutation and recombination maps // the end of the last genomic element may be before the end of the chromosome; the end of mutation and // recombination maps all need to agree, though, if they have been supplied. Checks that the maps do // not end before the end of the chromosome will be done in _InitializeOne...Map(). // BCH 9/20/2024: A chromosome declared explicitly with initializeChromosome() has an immutable length if (!extent_immutable_) { last_position_ = 0; for (GenomicElement *genomic_element : genomic_elements_) { if (genomic_element->end_position_ > last_position_) last_position_ = genomic_element->end_position_; } if (single_mutation_map_) { if (mutation_end_positions_H_.size()) last_position_ = std::max(last_position_, *(std::max_element(mutation_end_positions_H_.begin(), mutation_end_positions_H_.end()))); } else { if (mutation_end_positions_M_.size()) last_position_ = std::max(last_position_, *(std::max_element(mutation_end_positions_M_.begin(), mutation_end_positions_M_.end()))); if (mutation_end_positions_F_.size()) last_position_ = std::max(last_position_, *(std::max_element(mutation_end_positions_F_.begin(), mutation_end_positions_F_.end()))); } if (single_recombination_map_) { if (recombination_end_positions_H_.size()) last_position_ = std::max(last_position_, *(std::max_element(recombination_end_positions_H_.begin(), recombination_end_positions_H_.end()))); } else { if (recombination_end_positions_M_.size()) last_position_ = std::max(last_position_, *(std::max_element(recombination_end_positions_M_.begin(), recombination_end_positions_M_.end()))); if (recombination_end_positions_F_.size()) last_position_ = std::max(last_position_, *(std::max_element(recombination_end_positions_F_.begin(), recombination_end_positions_F_.end()))); } extent_immutable_ = true; } // Patch the hotspot end vector if it is empty; see setHotspotMap() and initializeHotspotMap(). // Basically, the length of the chromosome might not have been known yet when the user set the map. // This is done for the mutation rate maps in _InitializeOneMutationMap(); we do it here for the hotspot map. // Here we also fill in a rate of 1.0 if no rate was supplied if ((hotspot_multipliers_H_.size() == 0) && (hotspot_multipliers_M_.size() == 0) && (hotspot_multipliers_F_.size() == 0)) hotspot_multipliers_H_.emplace_back(1.0); if ((hotspot_end_positions_H_.size() == 0) && (hotspot_multipliers_H_.size() == 1)) hotspot_end_positions_H_.emplace_back(last_position_); if ((hotspot_end_positions_M_.size() == 0) && (hotspot_multipliers_M_.size() == 1)) hotspot_end_positions_M_.emplace_back(last_position_); if ((hotspot_end_positions_F_.size() == 0) && (hotspot_multipliers_F_.size() == 1)) hotspot_end_positions_F_.emplace_back(last_position_); // Now remake our mutation map info, which we delegate to _InitializeOneMutationMap() if (single_mutation_map_) { _InitializeOneMutationMap(lookup_mutation_H_, mutation_end_positions_H_, mutation_rates_H_, overall_mutation_rate_H_userlevel_, overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_, mutation_subranges_H_); // Copy the H rates into the M and F ivars, so that they can be used by DrawMutationAndBreakpointCounts() if needed overall_mutation_rate_M_userlevel_ = overall_mutation_rate_F_userlevel_ = overall_mutation_rate_H_userlevel_; overall_mutation_rate_M_ = overall_mutation_rate_F_ = overall_mutation_rate_H_; exp_neg_overall_mutation_rate_M_ = exp_neg_overall_mutation_rate_F_ = exp_neg_overall_mutation_rate_H_; } else { _InitializeOneMutationMap(lookup_mutation_M_, mutation_end_positions_M_, mutation_rates_M_, overall_mutation_rate_M_userlevel_, overall_mutation_rate_M_, exp_neg_overall_mutation_rate_M_, mutation_subranges_M_); _InitializeOneMutationMap(lookup_mutation_F_, mutation_end_positions_F_, mutation_rates_F_, overall_mutation_rate_F_userlevel_, overall_mutation_rate_F_, exp_neg_overall_mutation_rate_F_, mutation_subranges_F_); } // Now remake our recombination map info, which we delegate to _InitializeOneRecombinationMap() any_recombination_rates_05_ = false; if (single_recombination_map_) { _InitializeOneRecombinationMap(lookup_recombination_H_, recombination_end_positions_H_, recombination_rates_H_, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_, overall_recombination_rate_H_userlevel_); // Copy the H rates into the M and F ivars, so that they can be used by DrawMutationAndBreakpointCounts() if needed overall_recombination_rate_M_ = overall_recombination_rate_F_ = overall_recombination_rate_H_; exp_neg_overall_recombination_rate_M_ = exp_neg_overall_recombination_rate_F_ = exp_neg_overall_recombination_rate_H_; overall_recombination_rate_M_userlevel_ = overall_recombination_rate_F_userlevel_ = overall_recombination_rate_H_userlevel_; } else { _InitializeOneRecombinationMap(lookup_recombination_M_, recombination_end_positions_M_, recombination_rates_M_, overall_recombination_rate_M_, exp_neg_overall_recombination_rate_M_, overall_recombination_rate_M_userlevel_); _InitializeOneRecombinationMap(lookup_recombination_F_, recombination_end_positions_F_, recombination_rates_F_, overall_recombination_rate_F_, exp_neg_overall_recombination_rate_F_, overall_recombination_rate_F_userlevel_); } #ifndef USE_GSL_POISSON // Calculate joint mutation/recombination probabilities for the H/M/F cases if (single_mutation_map_ && single_recombination_map_) { _InitializeJointProbabilities(overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_, probability_both_0_H_, probability_both_0_OR_mut_0_break_non0_H_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_H_); } else if (single_mutation_map_ && !single_recombination_map_) { _InitializeJointProbabilities(overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_, overall_recombination_rate_M_, exp_neg_overall_recombination_rate_M_, probability_both_0_M_, probability_both_0_OR_mut_0_break_non0_M_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_M_); _InitializeJointProbabilities(overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_, overall_recombination_rate_F_, exp_neg_overall_recombination_rate_F_, probability_both_0_F_, probability_both_0_OR_mut_0_break_non0_F_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_F_); } else if (!single_mutation_map_ && single_recombination_map_) { _InitializeJointProbabilities(overall_mutation_rate_M_, exp_neg_overall_mutation_rate_M_, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_, probability_both_0_M_, probability_both_0_OR_mut_0_break_non0_M_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_M_); _InitializeJointProbabilities(overall_mutation_rate_F_, exp_neg_overall_mutation_rate_F_, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_, probability_both_0_F_, probability_both_0_OR_mut_0_break_non0_F_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_F_); } else if (!single_mutation_map_ && !single_recombination_map_) { _InitializeJointProbabilities(overall_mutation_rate_M_, exp_neg_overall_mutation_rate_M_, overall_recombination_rate_M_, exp_neg_overall_recombination_rate_M_, probability_both_0_M_, probability_both_0_OR_mut_0_break_non0_M_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_M_); _InitializeJointProbabilities(overall_mutation_rate_F_, exp_neg_overall_mutation_rate_F_, overall_recombination_rate_F_, exp_neg_overall_recombination_rate_F_, probability_both_0_F_, probability_both_0_OR_mut_0_break_non0_F_, probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_F_); } #endif } #ifndef USE_GSL_POISSON void Chromosome::_InitializeJointProbabilities(double p_overall_mutation_rate, double p_exp_neg_overall_mutation_rate, double p_overall_recombination_rate, double p_exp_neg_overall_recombination_rate, double &p_both_0, double &p_both_0_OR_mut_0_break_non0, double &p_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0) { // precalculate probabilities for Poisson draws of mutation count and breakpoint count double prob_mutation_0 = Eidos_FastRandomPoisson_PRECALCULATE(p_overall_mutation_rate); // exp(-mu); can be 0 due to underflow double prob_breakpoint_0 = Eidos_FastRandomPoisson_PRECALCULATE(p_overall_recombination_rate); // exp(-mu); can be 0 due to underflow // this is a validity check on the previous calculation of the exp_neg rates if ((p_exp_neg_overall_mutation_rate != prob_mutation_0) || (p_exp_neg_overall_recombination_rate != prob_breakpoint_0)) EIDOS_TERMINATION << "ERROR (Chromosome::_InitializeJointProbabilities): zero-probability does not match previous calculation." << EidosTerminate(); double prob_mutation_not_0 = 1.0 - prob_mutation_0; double prob_breakpoint_not_0 = 1.0 - prob_breakpoint_0; double prob_both_0 = prob_mutation_0 * prob_breakpoint_0; double prob_mutation_0_breakpoint_not_0 = prob_mutation_0 * prob_breakpoint_not_0; double prob_mutation_not_0_breakpoint_0 = prob_mutation_not_0 * prob_breakpoint_0; // std::cout << "element_mutation_rate_ == " << element_mutation_rate_ << std::endl; // std::cout << "prob_mutation_0 == " << prob_mutation_0 << std::endl; // std::cout << "prob_breakpoint_0 == " << prob_breakpoint_0 << std::endl; // std::cout << "prob_mutation_not_0 == " << prob_mutation_not_0 << std::endl; // std::cout << "prob_breakpoint_not_0 == " << prob_breakpoint_not_0 << std::endl; // std::cout << "prob_both_0 == " << prob_both_0 << std::endl; // std::cout << "prob_mutation_0_breakpoint_not_0 == " << prob_mutation_0_breakpoint_not_0 << std::endl; // std::cout << "prob_mutation_not_0_breakpoint_0 == " << prob_mutation_not_0_breakpoint_0 << std::endl; p_both_0 = prob_both_0; p_both_0_OR_mut_0_break_non0 = prob_both_0 + prob_mutation_0_breakpoint_not_0; p_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0 = prob_both_0 + (prob_mutation_0_breakpoint_not_0 + prob_mutation_not_0_breakpoint_0); } #endif void Chromosome::ChooseMutationRunLayout(void) { // We now have a final last position, so we can calculate our mutation run layout if (species_.HasGenetics()) { #ifdef _OPENMP // When running multi-threaded, we prefer the base number of mutruns to equal the number of threads // This allows us to subdivide responsibility along the haplosome equally among threads mutrun_count_base_ = gEidosMaxThreads; mutrun_count_multiplier_ = 1; // However, the shortest any mutrun can be is one base position, so with a short chromosome, the // number of mutruns gets clipped to the number of base positions (and not all threads will be used) if (mutrun_count_base_ > (last_position_ + 1)) mutrun_count_base_ = (int32_t)(last_position_ + 1); #else // When running single-threaded, the base number of mutruns is always 1 mutrun_count_base_ = 1; mutrun_count_multiplier_ = 1; #endif if (preferred_mutrun_count_ != 0) { // The user has given us a mutation run count, so use that count and divide the chromosome evenly if (preferred_mutrun_count_ < 1) EIDOS_TERMINATION << "ERROR (Chromosome::ChooseMutationRunLayout): there must be at least one mutation run per haplosome." << EidosTerminate(); // If the preferred number of mutation runs is actually larger than the number of discrete positions, // it gets clipped. No warning is emitted; this is pretty obvious, and the verbose output line suffices if (preferred_mutrun_count_ > (last_position_ + 1)) preferred_mutrun_count_ = (int32_t)(last_position_ + 1); // Similarly, we clip silently at SLIM_MUTRUN_MAXIMUM_COUNT; larger values are not presently allowed, // although the code is general and does not actually have a hard limit on the number of mutruns if (preferred_mutrun_count_ > SLIM_MUTRUN_MAXIMUM_COUNT) preferred_mutrun_count_ = SLIM_MUTRUN_MAXIMUM_COUNT; #ifdef _OPENMP // When running multithreaded, we have some additional restrictions to try to keep the number of mutation runs // aligned with the number of threads; but we also want to allow the user to use fewer mutruns/threads if (((preferred_mutrun_count_ % gEidosMaxThreads) == 0) || // if it is an exact multiple of the number of threads (preferred_mutrun_count_ < gEidosMaxThreads)) // or, less than the number of threads ; // then it is fine else EIDOS_TERMINATION << "ERROR (Chromosome::ChooseMutationRunLayout): when multithreaded, if the number of mutation runs is specified it must be a multiple of the number of threads, or it must be less than the number of threads (clipped mutationRuns count is " << preferred_mutrun_count_ << ", thread count is " << gEidosMaxThreads << ")." << EidosTerminate(); #endif if (preferred_mutrun_count_ == gEidosMaxThreads) // NOLINTNEXTLINE(*-branch-clone) : intentional branch clones { // We have preferred_mutrun_count_ mutrun sections, each containing 1 mutation run; this is really the same as the next case mutrun_count_base_ = preferred_mutrun_count_; mutrun_count_multiplier_ = 1; } else if ((preferred_mutrun_count_ % gEidosMaxThreads) == 0) { // We have gEidosMaxThreads mutrun sections, each containing (preferred_mutrun_count_ / gEidosMaxThreads) mutation runs mutrun_count_base_ = gEidosMaxThreads; mutrun_count_multiplier_ = preferred_mutrun_count_ / gEidosMaxThreads; } else { // The number of threads does not equal gEidosMaxThreads, so we have preferred_mutrun_count_ mutruns sections, each containing 1 mutrun mutrun_count_base_ = preferred_mutrun_count_; mutrun_count_multiplier_ = 1; } mutrun_count_ = mutrun_count_base_ * mutrun_count_multiplier_; mutrun_length_ = (slim_position_t)ceil((last_position_ + 1) / (double)mutrun_count_); if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << std::endl << "// Override mutation run count = " << mutrun_count_ << ", run length = " << mutrun_length_ << std::endl; } else { // The user has not supplied a count, so we will conduct experiments to find the best count; // for simplicity we will just always start with a single run, since that is often best anyway, // unless we're running multithreaded; then we start with one run per thread, generally mutrun_count_ = mutrun_count_base_ * mutrun_count_multiplier_; mutrun_length_ = (slim_position_t)ceil((last_position_ + 1) / (double)mutrun_count_); // When we are running experiments, the mutation run length needs to be a power of two so that it can be divided evenly, // potentially a fairly large number of times. We impose a maximum mutrun count of SLIM_MUTRUN_MAXIMUM_COUNT, so // actually it needs to just be an exact multiple of SLIM_MUTRUN_MAXIMUM_COUNT, not an exact power of two. mutrun_length_ = (slim_position_t)round(ceil(mutrun_length_ / (double)SLIM_MUTRUN_MAXIMUM_COUNT) * SLIM_MUTRUN_MAXIMUM_COUNT); if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << std::endl << "// Initial mutation run count = " << mutrun_count_ << ", run length = " << mutrun_length_ << std::endl; } } else { // No-genetics species use null haplosomes, and have no mutruns mutrun_count_base_ = 0; mutrun_count_multiplier_ = 1; mutrun_count_ = 0; mutrun_length_ = 0; } last_position_mutrun_ = mutrun_count_ * mutrun_length_ - 1; // Consistency check if (((mutrun_length_ < 1) && species_.HasGenetics()) || (mutrun_count_ * mutrun_length_ <= last_position_) || (last_position_mutrun_ < last_position_)) EIDOS_TERMINATION << "ERROR (Chromosome::ChooseMutationRunLayout): (internal error) math error in mutation run calculations." << EidosTerminate(); } // initialize one recombination map, used internally by InitializeDraws() to avoid code duplication void Chromosome::_InitializeOneRecombinationMap(gsl_ran_discrete_t *&p_lookup, std::vector &p_end_positions, std::vector &p_rates, double &p_overall_rate, double &p_exp_neg_overall_rate, double &p_overall_rate_userlevel) { // Patch the recombination interval end vector if it is empty; see setRecombinationRate() and initializeRecombinationRate(). // Basically, the length of the chromosome might not have been known yet when the user set the rate. if (p_end_positions.size() == 0) { // patching can only be done when a single uniform rate is specified if (p_rates.size() != 1) EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): recombination endpoints not specified." << EidosTerminate(); p_end_positions.emplace_back(last_position_); } if (p_end_positions[p_rates.size() - 1] < last_position_) EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): recombination endpoints do not cover the full chromosome." << EidosTerminate(); // BCH 11 April 2018: Fixing this code to use Peter Ralph's reparameterization that corrects for the way we use these rates // downstream. So the rates we are passed in p_rates are from the user, and they represent "ancestry breakpoint" rates: the // desired probability of a crossover from one base to the next. Downstream, when we actually generate breakpoints, we will // draw the number of breakpoints from a Poisson distribution with the overall rate of p_overall_rate that we sum up here, // then we will draw the location of each breakpoint from the p_lookup table we build here, and then we will sort and unique // those breakpoints so that we only ever have one breakpoint, at most, between any two bases. That uniquing is the key step // that drives this reparameterization. We need to choose a reparameterized lambda value for the Poisson draw such that the // probability P[Poisson(lambda) >= 1] == rate. Simply using lambda=rate is a good approximation when rate is small, but when // rate is larger than 0.01 it starts to break down, and when rate is 0.5 it is quite inaccurate; P[Poisson(0.5) >= 1] ~= 0.39. // So we reparameterize according to the formula: // // lambda = -log(1 - p) // // where p is the probability requested by the user and lambda is the expected mean used for the Poisson draw to ensure that // P[Poisson(lambda) >= 1] == p. Note that this formula blows up for p >= 1, and gets numerically unstable as it approaches // that limit; the lambda values chosen will become very large. Since values above 0.5 are unbiological anyway, we do not // allow them; all recombination rates must be <= 0.5 in SLiM, as of this change. 0.5 represents complete independence; // we do not allow "anti-linkage" at present since it seems to be unbiological. If somebody argues with us on that, we could // extend the limit up to, say, 0.9 without things blowing up badly, but 1.0 is out of the question. // // Here we also calculate the overall recombination rate in reparameterized units, which is what we actually use to draw // breakpoints, *and* the overall recombination rate in non-reparameterized units (as in SLiM 2.x and before), which is what // we report to the user if they ask for it (representing the expected number of crossovers along the chromosome). The // reparameterized overall recombination rate is the expected number of breakpoints we will generate *before* uniquing, // which has no meaning for the user. std::vector reparameterized_rates; reparameterized_rates.reserve(p_rates.size()); for (double user_rate : p_rates) { // this bounds check should have been done already externally to us, but no harm in making sure... if ((user_rate < 0.0) || (user_rate > 0.5)) EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): all recombination rates in SLiM must be in [0.0, 0.5]." << EidosTerminate(); #if 1 // This is the new recombination-rate paradigm that compensates for uniquing of breakpoints, etc. // keep track of whether any recombination rates in the model are 0.5; if so, gene conversion needs to exclude those positions if (user_rate == 0.5) any_recombination_rates_05_ = true; reparameterized_rates.emplace_back(-log(1.0 - user_rate)); /* A good model to test that the recombination rate observed is actually the crossover rate requested: initialize() { defineConstant("L", 10); // 10 breakpoint positions, so 11 bases defineConstant("N", 1000); // 1000 diploids initializeMutationRate(0); initializeMutationType("m1", 0.5, "f", 0.0); initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, L); initializeRecombinationRate(0.5); } 1 early() { sim.addSubpop("p1", N); sim.tag = 0; } recombination() { sim.tag = sim.tag + size(unique(breakpoints)); return F; } 10000 late() { print(sim.tag / (10000 * N * 2 * L)); } */ #else // This is the old recombination-rate paradigm, which should be used only for testing output compatibility in tests #warning old recombination-rate paradigm enabled! reparameterized_rates.emplace_back(user_rate); #endif } // Calculate the overall recombination rate and the lookup table for breakpoints std::vector B(reparameterized_rates.size()); std::vector B_userlevel(reparameterized_rates.size()); B_userlevel[0] = p_rates[0] * static_cast(p_end_positions[0]); B[0] = reparameterized_rates[0] * static_cast(p_end_positions[0]); // No +1 here, because the position to the left of the first base is not a valid recombination position. // So a 1-base model (position 0 to 0) has an end of 0, and thus an overall rate of 0. This means that // gsl_ran_discrete_preproc() is given an interval with rate 0, but that seems OK. BCH 4 April 2016 for (unsigned int i = 1; i < reparameterized_rates.size(); i++) { double length = static_cast(p_end_positions[i] - p_end_positions[i - 1]); B_userlevel[i] = p_rates[i] * length; B[i] = reparameterized_rates[i] * length; } p_overall_rate_userlevel = Eidos_ExactSum(B_userlevel.data(), p_rates.size()); p_overall_rate = Eidos_ExactSum(B.data(), reparameterized_rates.size()); // All the recombination machinery below uses the reparameterized rates #ifndef USE_GSL_POISSON p_exp_neg_overall_rate = Eidos_FastRandomPoisson_PRECALCULATE(p_overall_rate); // exp(-mu); can be 0 due to underflow #endif if (p_lookup) gsl_ran_discrete_free(p_lookup); p_lookup = gsl_ran_discrete_preproc(reparameterized_rates.size(), B.data()); } // initialize one mutation map, used internally by InitializeDraws() to avoid code duplication void Chromosome::_InitializeOneMutationMap(gsl_ran_discrete_t *&p_lookup, std::vector &p_end_positions, std::vector &p_rates, double &p_requested_overall_rate, double &p_overall_rate, double &p_exp_neg_overall_rate, std::vector &p_subranges) { // Patch the mutation interval end vector if it is empty; see setMutationRate() and initializeMutationRate(). // Basically, the length of the chromosome might not have been known yet when the user set the rate. if (p_end_positions.size() == 0) { // patching can only be done when a single uniform rate is specified if (p_rates.size() != 1) EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): mutation rate endpoints not specified." << EidosTerminate(); p_end_positions.emplace_back(last_position_); } if (p_end_positions[p_rates.size() - 1] < last_position_) EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): mutation rate endpoints do not cover the full chromosome." << EidosTerminate(); // Calculate the overall mutation rate and the lookup table for mutation events. This is more complicated than // for recombination maps, because the mutation rate map needs to be intersected with the genomic element map // such that all areas outside of any genomic element have a rate of 0. In the end we need (i) a vector of // constant-rate subregions that do not span genomic element boundaries, (ii) a lookup table to go from the // index of a constant-rate subregion to the index of the containing genomic element, and (iii) a lookup table // to go from the index of a constant-rate subregion to the first base position and length of the subregion. // In effect, each of these subregions is sort of the new equivalent of a genomic element; we intersect the // mutation rate map with the genomic element map to create a list of new genomic elements of constant mut rate. // The class we use to represent these constant-rate subregions is GESubrange, declared in chromosome.h. p_subranges.clear(); // We need to work with a *sorted* genomic elements vector here. BCH 8/28/2024: That is now guaranteed. // This code deals in two different currencies: *requested* mutation rates and *adjusted* mutation rates. // This stems from the fact that, beginning in SLiM 3.5, we unique mutation positions as we're generating // gametes. This uniquing means that some drawn positions may be filtered out, resulting in a realized // mutation rate that is lower than the requested rate (see section 22.2.3 in the manual for discussion). // We want to compensate for this effect, so that the realized rate is equal to the requested rate. To do // that, we calculate adjusted rates that, after uniquing, will result in the requested rate. However, we // want to hide this implementation detail from the user; the rate they see in Eidos from Chromosome should // be the same as the rate they requested originally, not the adjusted rate. So we do calculations with // both, so that we can satisfy that requirement. The adjustment is done piecewise, so that the adjusted // rate for each GESubrange is correct. If we have a nucleotide-based matation matrix that leads to some // rejection sampling, that will be based on the ratios of the original rates, but it will be done *after* // positions have been drawn and uniqued, so that should also be correct, I think. BCH 5 Sep. 2020 std::vector A; // a vector of requested subrange weights std::vector B; // a vector of adjusted subrange weights unsigned int mutrange_index = 0; slim_position_t end_of_previous_mutrange = -1; for (GenomicElement *ge_ptr : genomic_elements_) { GenomicElement &ge = *ge_ptr; for ( ; mutrange_index < p_rates.size(); mutrange_index++) { slim_position_t end_of_mutrange = p_end_positions[mutrange_index]; if (end_of_mutrange < ge.start_position_) { // This mutrange falls entirely before the current genomic element, so it is discarded by falling through } else if (end_of_previous_mutrange + 1 > ge.end_position_) { // This mutrange falls entirely after the current genomic element, so we need to move to the next genomic element break; } else { // This mutrange at least partially intersects the current genomic element, so we need to make a new GESubrange slim_position_t subrange_start = std::max(end_of_previous_mutrange + 1, ge.start_position_); slim_position_t subrange_end = std::min(end_of_mutrange, ge.end_position_); slim_position_t subrange_length = subrange_end - subrange_start + 1; double requested_rate = p_rates[mutrange_index]; if (requested_rate >= 1.0) EIDOS_TERMINATION << "ERROR (Chromosome::InitializeDraws): requested mutation rate is >= 1.0." << EidosTerminate(); double adjusted_rate = -log1p(-requested_rate); double requested_subrange_weight = requested_rate * subrange_length; double adjusted_subrange_weight = adjusted_rate * subrange_length; A.emplace_back(requested_subrange_weight); B.emplace_back(adjusted_subrange_weight); p_subranges.emplace_back(&ge, subrange_start, subrange_end); // Now we need to decide whether to advance the genomic element or not; advancing mutrange_index here is not needed if (end_of_mutrange >= ge.end_position_) break; } end_of_previous_mutrange = end_of_mutrange; } } p_requested_overall_rate = Eidos_ExactSum(A.data(), A.size()); p_overall_rate = Eidos_ExactSum(B.data(), B.size()); #ifndef USE_GSL_POISSON p_exp_neg_overall_rate = Eidos_FastRandomPoisson_PRECALCULATE(p_overall_rate); // exp(-mu); can be 0 due to underflow #endif if (p_lookup) gsl_ran_discrete_free(p_lookup); p_lookup = gsl_ran_discrete_preproc(B.size(), B.data()); } // prints an error message and exits void Chromosome::MutationMapConfigError(void) const { EIDOS_TERMINATION << "ERROR (Chromosome::MutationMapConfigError): (internal error) an error occurred in the configuration of mutation maps." << EidosTerminate(); } void Chromosome::RecombinationMapConfigError(void) const { EIDOS_TERMINATION << "ERROR (Chromosome::RecombinationMapConfigError): (internal error) an error occurred in the configuration of recombination maps." << EidosTerminate(); } int Chromosome::DrawSortedUniquedMutationPositions(int p_count, IndividualSex p_sex, std::vector> &p_positions) { // BCH 1 September 2020: This method generates a vector of positions, sorted and uniqued for the caller. This avoid various issues // with two mutations occurring at the same position in the same gamete. For example, nucleotide states in the tree-seq tables could // end up in a state that would normally be illegal, such as an A->G mutation followed by a G->G mutation, because SLiM's code wouldn't // understand that the second mutation occurred on the background of the first; it would think that both mutations were A->G, erroneously. // This also messed up uniquing of mutations with a mutation() callback in SLiM 3.5; you might want all mutations representing a given // nucleotide at a given position to share the same mutation object (IBS instead of IBD), but if two mutations occurred at the same // position in the same gamete, the first might be accepted, and then second – which should unique down to the first – would also be // accepted. The right fix seems to be to unique the drawn mutation positions before creating mutations, avoiding all these issues; // multiple mutations at the same position in the same gamete seems unbiological anyway. gsl_ran_discrete_t *lookup; const std::vector *subranges; if (single_mutation_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled lookup = lookup_mutation_H_; subranges = &mutation_subranges_H_; } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two if (p_sex == IndividualSex::kMale) { lookup = lookup_mutation_M_; subranges = &mutation_subranges_M_; } else if (p_sex == IndividualSex::kFemale) { lookup = lookup_mutation_F_; subranges = &mutation_subranges_F_; } else { MutationMapConfigError(); } } // draw all the positions, and keep track of the genomic element type for each gsl_rng *rng_gsl = EIDOS_GSL_RNG(omp_get_thread_num()); EidosRNG_64_bit &rng_64 = EIDOS_64BIT_RNG(omp_get_thread_num()); for (int i = 0; i < p_count; ++i) { int mut_subrange_index = static_cast(gsl_ran_discrete(rng_gsl, lookup)); const GESubrange &subrange = (*subranges)[mut_subrange_index]; GenomicElement *source_element = subrange.genomic_element_ptr_; // Draw the position along the chromosome for the mutation, within the genomic element slim_position_t position = subrange.start_position_ + static_cast(Eidos_rng_interval_uint64(rng_64, subrange.end_position_ - subrange.start_position_ + 1)); p_positions.emplace_back(position, source_element); } // sort and unique by position; 1 and 2 mutations are particularly common, so try to speed those up if (p_count > 1) { if (p_count == 2) { if (p_positions[0].first > p_positions[1].first) std::swap(p_positions[0], p_positions[1]); else if (p_positions[0].first == p_positions[1].first) p_positions.resize(1); } else { std::sort(p_positions.begin(), p_positions.end(), [](const std::pair &p1, const std::pair &p2) { return p1.first < p2.first; }); auto unique_iter = std::unique(p_positions.begin(), p_positions.end(), [](const std::pair &p1, const std::pair &p2) { return p1.first == p2.first; }); p_positions.resize(std::distance(p_positions.begin(), unique_iter)); } } return (int)p_positions.size(); } // draw a new mutation, based on the genomic element types present and their mutational proclivities MutationIndex Chromosome::DrawNewMutation(std::pair &p_position, slim_objectid_t p_subpop_index, slim_tick_t p_tick) const { const GenomicElement &source_element = *(p_position.second); const GenomicElementType &genomic_element_type = *(source_element.genomic_element_type_ptr_); MutationType *mutation_type_ptr = genomic_element_type.DrawMutationType(); double selection_coeff = mutation_type_ptr->DrawSelectionCoefficient(); // NOTE THAT THE STACKING POLICY IS NOT ENFORCED HERE, SINCE WE DO NOT KNOW WHAT HAPLOSOME WE WILL BE INSERTED INTO! THIS IS THE CALLER'S RESPONSIBILITY! MutationIndex new_mut_index = SLiM_NewMutationFromBlock(); // A nucleotide value of -1 is always used here; in nucleotide-based models this gets patched later, but that is sequence-dependent and background-dependent Mutation *mutation = gSLiM_Mutation_Block + new_mut_index; new (mutation) Mutation(mutation_type_ptr, index_, p_position.first, selection_coeff, p_subpop_index, p_tick, -1); // addition to the main registry and the muttype registries will happen if the new mutation clears the stacking policy return new_mut_index; } // apply mutation() to a generated mutation; we might return nullptr (proposed mutation rejected), the original proposed mutation (it was accepted), or a replacement Mutation * Mutation *Chromosome::ApplyMutationCallbacks(Mutation *p_mut, Haplosome *p_haplosome, GenomicElement *p_genomic_element, int8_t p_original_nucleotide, std::vector &p_mutation_callbacks) const { THREAD_SAFETY_IN_ANY_PARALLEL("Population::ApplyMutationCallbacks(): running Eidos callback"); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif slim_objectid_t mutation_type_id = p_mut->mutation_type_ptr_->mutation_type_id_; // note the focal child during the callback, so we can prevent illegal operations during the callback SLiMEidosBlockType old_executing_block_type = community_.executing_block_type_; community_.executing_block_type_ = SLiMEidosBlockType::SLiMEidosMutationCallback; bool mutation_accepted = true, mutation_replaced = false; for (SLiMEidosBlock *mutation_callback : p_mutation_callbacks) { if (mutation_callback->block_active_) { slim_objectid_t callback_mutation_type_id = mutation_callback->mutation_type_id_; if ((callback_mutation_type_id == -1) || (callback_mutation_type_id == mutation_type_id)) { #ifndef DEBUG_POINTS_ENABLED #error "DEBUG_POINTS_ENABLED is not defined; include eidos_globals.h" #endif #if DEBUG_POINTS_ENABLED // SLiMgui debugging point EidosDebugPointIndent indenter; { EidosInterpreterDebugPointsSet *debug_points = community_.DebugPoints(); EidosToken *decl_token = mutation_callback->root_node_->token_; if (debug_points && debug_points->set.size() && (decl_token->token_line_ != -1) && (debug_points->set.find(decl_token->token_line_) != debug_points->set.end())) { SLIM_ERRSTREAM << EidosDebugPointIndent::Indent() << "#DEBUG mutation("; if ((mutation_callback->mutation_type_id_ != -1) && (mutation_callback->subpopulation_id_ != -1)) SLIM_ERRSTREAM << "m" << mutation_callback->mutation_type_id_ << ", p" << mutation_callback->subpopulation_id_; else if (mutation_callback->mutation_type_id_ != -1) SLIM_ERRSTREAM << "m" << mutation_callback->mutation_type_id_; else if (mutation_callback->subpopulation_id_ != -1) SLIM_ERRSTREAM << "NULL, p" << mutation_callback->subpopulation_id_; SLIM_ERRSTREAM << ")"; if (mutation_callback->block_id_ != -1) SLIM_ERRSTREAM << " s" << mutation_callback->block_id_; SLIM_ERRSTREAM << " (line " << (decl_token->token_line_ + 1) << community_.DebugPointInfo() << ")" << std::endl; indenter.indent(); } } #endif // The callback is active and matches the mutation type id of the mutation, so we need to execute it // This code is similar to Population::ExecuteScript, but we set up an additional symbol table, and we use the return value EidosValue_Object local_mut(p_mut, gSLiM_Mutation_Class); EidosValue_Int local_originalNuc(p_original_nucleotide); // We need to actually execute the script; we start a block here to manage the lifetime of the symbol table { EidosSymbolTable callback_symbols(EidosSymbolTableType::kContextConstantsTable, &community_.SymbolTable()); EidosSymbolTable client_symbols(EidosSymbolTableType::kLocalVariablesTable, &callback_symbols); EidosFunctionMap &function_map = community_.FunctionMap(); EidosInterpreter interpreter(mutation_callback->compound_statement_node_, client_symbols, function_map, &community_, SLIM_OUTSTREAM, SLIM_ERRSTREAM #ifdef SLIMGUI , community_.check_infinite_loops_ #endif ); if (mutation_callback->contains_self_) callback_symbols.InitializeConstantSymbolEntry(mutation_callback->SelfSymbolTableEntry()); // define "self" // Set all of the callback's parameters; note we use InitializeConstantSymbolEntry() for speed. // We can use that method because we know the lifetime of the symbol table is shorter than that of // the value objects, and we know that the values we are setting here will not change (the objects // referred to by the values may change, but the values themselves will not change). // BCH 11/7/2025: note these symbols are now protected in SLiM_ConfigureContext() if (mutation_callback->contains_mut_) { local_mut.StackAllocated(); // prevent Eidos_intrusive_ptr from trying to delete this callback_symbols.InitializeConstantSymbolEntry(gID_mut, EidosValue_SP(&local_mut)); } if (mutation_callback->contains_parent_) callback_symbols.InitializeConstantSymbolEntry(gID_parent, p_haplosome->OwningIndividual()->CachedEidosValue()); if (mutation_callback->contains_haplosome_) callback_symbols.InitializeConstantSymbolEntry(gID_haplosome, p_haplosome->CachedEidosValue()); if (mutation_callback->contains_element_) callback_symbols.InitializeConstantSymbolEntry(gID_element, p_genomic_element->CachedEidosValue()); if (mutation_callback->contains_subpop_) callback_symbols.InitializeConstantSymbolEntry(gID_subpop, p_haplosome->OwningIndividual()->subpopulation_->SymbolTableEntry().second); if (mutation_callback->contains_originalNuc_) { local_originalNuc.StackAllocated(); // prevent Eidos_intrusive_ptr from trying to delete this callback_symbols.InitializeConstantSymbolEntry(gID_originalNuc, EidosValue_SP(&local_originalNuc)); } try { // Interpret the script; the result from the interpretation must be a singleton logical, T if the mutation is accepted, F if it is rejected, or a Mutation object EidosValue_SP result_SP = interpreter.EvaluateInternalBlock(mutation_callback->script_); EidosValue *result = result_SP.get(); EidosValueType resultType = result->Type(); int resultCount = result->Count(); if ((resultType == EidosValueType::kValueLogical) && (resultCount == 1)) { #if DEBUG // this checks the value type at runtime mutation_accepted = result->LogicalData()[0]; #else // unsafe cast for speed mutation_accepted = ((EidosValue_Logical *)result)->data()[0]; #endif } else if ((resultType == EidosValueType::kValueObject) && (((EidosValue_Object *)result)->Class() == gSLiM_Mutation_Class) && (resultCount == 1)) { #if DEBUG // this checks the value type at runtime Mutation *replacementMutation = (Mutation *)result->ObjectData()[0]; #else // unsafe cast for speed Mutation *replacementMutation = (Mutation *)((EidosValue_Object *)result)->data()[0]; #endif if (replacementMutation == p_mut) { // returning the proposed mutation is the same as accepting it mutation_accepted = true; } else { // First, check that the mutation fits the requirements: position, mutation type, nucleotide; we are already set up for the mutation type and can't change in // mid-stream (would we switch to running mutation() callbacks for the new type??), and we have already chosen the nucleotide for consistency with the background // BCH 3 September 2020: A thread on slim-discuss (https://groups.google.com/forum/#!topic/slim-discuss/ALnZrzIroKY) has convinced me that it would be useful // to be able to return a replacement mutation of a different mutation type, so that you can generate mutations of a single type up front and multifurcate that // generation process into multiple mutation types on the other side (perhaps with different dominance coefficients, for example). So I have relaxed the // mutation type requirement here. If the mutation type is changed in the replacement, we switch to running callbacks for the new mutation type, but we do not // go back to the beginning; we're running callbacks in the order they were declared, and just switching which type we're running, mid-stream. // BCH 4 September 2020: Peter has persuaded me to relax the nucleotide restriction as well. If the user changes the nucleotide, it may produce surprising // results (a mutation that doesn't change the nucleotide, like G->G, or a deviation from requested mutation-matrix probabilities), but that's the user's // responsibility to deal with; it can be a useful thing to do, Peter thinks. if (replacementMutation->position_ != p_mut->position_) EIDOS_TERMINATION << "ERROR (Chromosome::ApplyMutationCallbacks): a replacement mutation from a mutation() callback must match the position of the proposed mutation." << EidosTerminate(mutation_callback->identifier_token_); if ((replacementMutation->state_ == MutationState::kRemovedWithSubstitution) || (replacementMutation->state_ == MutationState::kFixedAndSubstituted)) EIDOS_TERMINATION << "ERROR (Chromosome::ApplyMutationCallbacks): a replacement mutation from a mutation() callback cannot be fixed/substituted." << EidosTerminate(mutation_callback->identifier_token_); if (replacementMutation->mutation_type_ptr_ != p_mut->mutation_type_ptr_) { //EIDOS_TERMINATION << "ERROR (Chromosome::ApplyMutationCallbacks): a replacement mutation from a mutation() callback must match the mutation type of the proposed mutation." << EidosTerminate(mutation_callback->identifier_token_); mutation_type_id = p_mut->mutation_type_ptr_->mutation_type_id_; } //if (replacementMutation->nucleotide_ != p_mut->nucleotide_) // EIDOS_TERMINATION << "ERROR (Chromosome::ApplyMutationCallbacks): a replacement mutation from a mutation() callback must match the nucleotide of the proposed mutation." << EidosTerminate(mutation_callback->identifier_token_); // It fits the bill, so replace the original proposed mutation with this new mutation; note we don't fix local_mut, it's about to go out of scope anyway p_mut = replacementMutation; mutation_replaced = true; mutation_accepted = true; } } else EIDOS_TERMINATION << "ERROR (Chromosome::ApplyMutationCallbacks): mutation() callbacks must provide a return value that is either a singleton logical or a singleton Mutation object." << EidosTerminate(mutation_callback->identifier_token_); } catch (...) { throw; } } if (!mutation_accepted) break; } } } // If a replacement mutation has been accepted at this point, we now check that it is not already present in the background haplosome; if it is present, the mutation is a no-op (implemented as a rejection) if (mutation_replaced && mutation_accepted) { if (p_haplosome->contains_mutation(p_mut)) mutation_accepted = false; } community_.executing_block_type_ = old_executing_block_type; #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(community_.profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosMutationCallback)]); #endif return (mutation_accepted ? p_mut : nullptr); } // draw a new mutation with reference to the genomic background upon which it is occurring, for nucleotide-based models and/or mutation() callbacks MutationIndex Chromosome::DrawNewMutationExtended(std::pair &p_position, slim_objectid_t p_subpop_index, slim_tick_t p_tick, Haplosome *parent_haplosome_1, Haplosome *parent_haplosome_2, slim_position_t *p_breakpoints, int p_breakpoints_count, std::vector *p_mutation_callbacks) const { slim_position_t position = p_position.first; GenomicElement &source_element = *(p_position.second); const GenomicElementType &genomic_element_type = *(source_element.genomic_element_type_ptr_); // Determine which parental haplosome the mutation will be atop (so we can get the genetic context for it) bool on_first_haplosome = true; if (p_breakpoints) { for (int break_index = 0; break_index < p_breakpoints_count; ++break_index) { if (p_breakpoints[break_index] > position) break; on_first_haplosome = !on_first_haplosome; } } Haplosome *background_haplosome = (on_first_haplosome ? parent_haplosome_1 : parent_haplosome_2); // Determine whether the mutation will be created at all, and if it is, what nucleotide to use int8_t original_nucleotide = -1, nucleotide = -1; if (genomic_element_type.mutation_matrix_) { EidosValue_Float *mm = genomic_element_type.mutation_matrix_.get(); int mm_count = mm->Count(); if (mm_count == 16) { // The mutation matrix only cares about the single-nucleotide context; figure it out HaplosomeWalker walker(background_haplosome); walker.MoveToPosition(position); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= position is guaranteed by MoveToPosition() if (pos == position) { int8_t mut_nuc = mut->nucleotide_; if (mut_nuc != -1) original_nucleotide = mut_nuc; walker.NextMutation(); } else { break; } } // No mutation is present at the position, so the background comes from the ancestral sequence if (original_nucleotide == -1) original_nucleotide = (int8_t)ancestral_seq_buffer_->NucleotideAtIndex(position); // OK, now we know the background nucleotide; determine the mutation rates to derived nucleotides double *nuc_thresholds = genomic_element_type.mm_thresholds + (size_t)original_nucleotide * 4; EidosRNG_64_bit &rng_64 = EIDOS_64BIT_RNG(omp_get_thread_num()); double draw = Eidos_rng_uniform_doubleCO(rng_64); if (draw < nuc_thresholds[0]) nucleotide = 0; else if (draw < nuc_thresholds[1]) nucleotide = 1; else if (draw < nuc_thresholds[2]) nucleotide = 2; else if (draw < nuc_thresholds[3]) nucleotide = 3; else { // The mutation is an excess mutation, the result of an overall mutation rate on this genetic background // that is less than the maximum overall mutation rate for any genetic background; it should be discarded, // as if this mutation event never occurred at all. We signal this by returning -1. return -1; } } else if (mm_count == 256) { // The mutation matrix cares about the trinucleotide context; figure it out int8_t background_nuc1 = -1, background_nuc3 = -1; HaplosomeWalker walker(background_haplosome); walker.MoveToPosition(position - 1); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= position - 1 is guaranteed by MoveToPosition() if (pos == position - 1) { int8_t mut_nuc = mut->nucleotide_; if (mut_nuc != -1) background_nuc1 = mut_nuc; walker.NextMutation(); } else if (pos == position) { int8_t mut_nuc = mut->nucleotide_; if (mut_nuc != -1) original_nucleotide = mut_nuc; walker.NextMutation(); } else if (pos == position + 1) { int8_t mut_nuc = mut->nucleotide_; if (mut_nuc != -1) background_nuc3 = mut_nuc; walker.NextMutation(); } else { break; } } // No mutation is present at the position, so the background comes from the ancestral sequence // If a base in the trinucleotide is off the end of the chromosome, we assume it is an A; that is arbitrary, // but should avoid skewed rates associated with the dynamics of C/G in certain sequences (like CpG) if (background_nuc1 == -1) background_nuc1 = ((position == 0) ? 0 : (int8_t)ancestral_seq_buffer_->NucleotideAtIndex(position - 1)); if (original_nucleotide == -1) original_nucleotide = (int8_t)ancestral_seq_buffer_->NucleotideAtIndex(position); if (background_nuc3 == -1) background_nuc3 = ((position == last_position_) ? 0 : (int8_t)ancestral_seq_buffer_->NucleotideAtIndex(position + 1)); // OK, now we know the background nucleotide; determine the mutation rates to derived nucleotides int trinuc = ((int)background_nuc1) * 16 + ((int)original_nucleotide) * 4 + (int)background_nuc3; double *nuc_thresholds = genomic_element_type.mm_thresholds + (size_t)trinuc * 4; EidosRNG_64_bit &rng_64 = EIDOS_64BIT_RNG(omp_get_thread_num()); double draw = Eidos_rng_uniform_doubleCO(rng_64); if (draw < nuc_thresholds[0]) nucleotide = 0; else if (draw < nuc_thresholds[1]) nucleotide = 1; else if (draw < nuc_thresholds[2]) nucleotide = 2; else if (draw < nuc_thresholds[3]) nucleotide = 3; else { // The mutation is an excess mutation, the result of an overall mutation rate on this genetic background // that is less than the maximum overall mutation rate for any genetic background; it should be discarded, // as if this mutation event never occurred at all. We signal this by returning -1. return -1; } } else EIDOS_TERMINATION << "ERROR (Chromosome::DrawNewMutationExtended): (internal error) unexpected mutation matrix size." << EidosTerminate(); } // Draw mutation type and selection coefficient, and create the new mutation MutationType *mutation_type_ptr = genomic_element_type.DrawMutationType(); double selection_coeff = mutation_type_ptr->DrawSelectionCoefficient(); // NOTE THAT THE STACKING POLICY IS NOT ENFORCED HERE! THIS IS THE CALLER'S RESPONSIBILITY! MutationIndex new_mut_index = SLiM_NewMutationFromBlock(); Mutation *mutation = gSLiM_Mutation_Block + new_mut_index; new (mutation) Mutation(mutation_type_ptr, index_, position, selection_coeff, p_subpop_index, p_tick, nucleotide); // Call mutation() callbacks if there are any if (p_mutation_callbacks) { Mutation *post_callback_mut = ApplyMutationCallbacks(gSLiM_Mutation_Block + new_mut_index, background_haplosome, &source_element, original_nucleotide, *p_mutation_callbacks); // If the callback didn't return the proposed mutation, it will not be used; dispose of it if (post_callback_mut != mutation) { //std::cout << "proposed mutation not used, disposing..." << std::endl; mutation->Release(); } // If it returned nullptr, the mutation event was rejected if (post_callback_mut == nullptr) { //std::cout << "mutation rejected..." << std::endl; return -1; } // Otherwise, we will request the addition of whatever mutation it returned (which might be the proposed mutation). // Note that if an existing mutation was returned, ApplyMutationCallbacks() guarantees that it is not already present in the background haplosome. MutationIndex post_callback_mut_index = post_callback_mut->BlockIndex(); if (new_mut_index != post_callback_mut_index) { //std::cout << "replacing mutation!" << std::endl; new_mut_index = post_callback_mut_index; } } // addition to the main registry and the muttype registries will happen if the new mutation clears the stacking policy return new_mut_index; } // draw a set of uniqued breakpoints according to the "crossover breakpoint" model and run them through recombination() callbacks, returning the final usable set void Chromosome::_DrawCrossoverBreakpoints(IndividualSex p_parent_sex, const int p_num_breakpoints, std::vector &p_crossovers) const { // BEWARE! Chromosome::DrawDSBBreakpoints() below must be altered in parallel with this method! #if DEBUG if (using_DSB_model_) EIDOS_TERMINATION << "ERROR (Chromosome::DrawCrossoverBreakpoints): (internal error) this method should not be called when the DSB recombination model is being used." << EidosTerminate(); #endif gsl_ran_discrete_t *lookup; const std::vector *end_positions; if (single_recombination_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled lookup = lookup_recombination_H_; end_positions = &recombination_end_positions_H_; } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two if (p_parent_sex == IndividualSex::kMale) { lookup = lookup_recombination_M_; end_positions = &recombination_end_positions_M_; } else if (p_parent_sex == IndividualSex::kFemale) { lookup = lookup_recombination_F_; end_positions = &recombination_end_positions_F_; } else { RecombinationMapConfigError(); } } // draw recombination breakpoints gsl_rng *rng_gsl = EIDOS_GSL_RNG(omp_get_thread_num()); EidosRNG_64_bit &rng_64 = EIDOS_64BIT_RNG(omp_get_thread_num()); for (int i = 0; i < p_num_breakpoints; i++) { slim_position_t breakpoint = 0; int recombination_interval = static_cast(gsl_ran_discrete(rng_gsl, lookup)); // choose a breakpoint anywhere in the chosen recombination interval with equal probability // BCH 4 April 2016: Added +1 to positions in the first interval. We do not want to generate a recombination breakpoint // to the left of the 0th base, and the code in InitializeDraws() above explicitly omits that position from its calculation // of the overall recombination rate. Using recombination_end_positions_[recombination_interval] here for the first // interval means that we use one less breakpoint position than usual; conceptually, the previous breakpoint ended at -1, // so it ought to be recombination_end_positions_[recombination_interval]+1, but we do not add one there, in order to // use one fewer positions. We then shift all the positions to the right one, with the +1 that is added here, thereby // making the position that was omitted be the position to the left of the 0th base. // // I also added +1 in the formula for regions after the 0th. In general, we want a recombination interval to own all the // positions to the left of its enclosed bases, up to and including the position to the left of the final base given as the // end position of the interval. The next interval's first owned recombination position is therefore to the left of the // base that is one position to the right of the end of the preceding interval. So we have to add one to the position // given by recombination_end_positions_[recombination_interval - 1], at minimum. Since Eidos_rng_interval_uint64() returns // a zero-based random number, that means we need a +1 here as well. // // The key fact here is that a recombination breakpoint position of 1 means "break to the left of the base at position 1" – // the breakpoint falls between bases, to the left of the base at the specified number. This is a consequence of the logic // in the crossover-mutation code, which copies mutations as long as their position is *less than* the position of the next // breakpoint. When their position is *equal*, the breakpoint gets serviced by switching strands. That logic causes the // breakpoints to fall to the left of their designated base. // // Note that Eidos_rng_interval_uint64() crashes (well, aborts fatally) if passed 0 for n. We need to guarantee that that doesn't // happen, and we don't want to waste time checking for that condition here. For a 1-base model, we are guaranteed that // the overall recombination rate will be zero, by the logic in InitializeDraws(), and so we should not be called in the // first place. For longer chromosomes that start with a 1-base recombination interval, the rate calculated by // InitializeDraws() for the first interval should be 0, so gsl_ran_discrete() should never return the first interval to // us here. For all other recombination intervals, the math of pos[x]-pos[x-1] should always result in a value >0, // since we guarantee that recombination end positions are in strictly ascending order. So we should never crash. :-> if (recombination_interval == 0) breakpoint = static_cast(Eidos_rng_interval_uint64(rng_64, (*end_positions)[recombination_interval]) + 1); else breakpoint = (*end_positions)[recombination_interval - 1] + 1 + static_cast(Eidos_rng_interval_uint64(rng_64, (*end_positions)[recombination_interval] - (*end_positions)[recombination_interval - 1])); p_crossovers.emplace_back(breakpoint); } // sort and unique if (p_num_breakpoints > 2) { std::sort(p_crossovers.begin(), p_crossovers.end()); p_crossovers.erase(std::unique(p_crossovers.begin(), p_crossovers.end()), p_crossovers.end()); } else if (p_num_breakpoints == 2) { // do our own dumb inline sort/unique if we have just two elements, to avoid the calls above // I didn't actually test this to confirm that it's faster, but models that generate many // breakpoints will generally hit the case above anyway, and models that generate few will // suffer only the additional (num_breakpoints == 2) test before falling through... slim_position_t bp1 = p_crossovers[0]; slim_position_t bp2 = p_crossovers[1]; if (bp1 > bp2) std::swap(p_crossovers[0], p_crossovers[1]); else if (bp1 == bp2) p_crossovers.resize(1); } } // draw a set of uniqued breakpoints according to the "double-stranded break" model and run them through recombination() callbacks, returning the final usable set // the information returned here also includes a list of heteroduplex regions where mismatches between the two parental strands will need to be resolved void Chromosome::_DrawDSBBreakpoints(IndividualSex p_parent_sex, const int p_num_breakpoints, std::vector &p_crossovers, std::vector &p_heteroduplex) const { THREAD_SAFETY_IN_ANY_PARALLEL("Chromosome::DrawDSBBreakpoints(): usage of statics, probably many other issues"); // BEWARE! Chromosome::DrawCrossoverBreakpoints() above must be altered in parallel with this method! #if DEBUG if (!using_DSB_model_) EIDOS_TERMINATION << "ERROR (Chromosome::DrawDSBBreakpoints): (internal error) this method should not be called when the crossover breakpoints recombination model is being used." << EidosTerminate(); #endif gsl_ran_discrete_t *lookup; const std::vector *end_positions; const std::vector *rates; if (single_recombination_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled lookup = lookup_recombination_H_; end_positions = &recombination_end_positions_H_; rates = &recombination_rates_H_; } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two if (p_parent_sex == IndividualSex::kMale) { lookup = lookup_recombination_M_; end_positions = &recombination_end_positions_M_; rates = &recombination_rates_M_; } else if (p_parent_sex == IndividualSex::kFemale) { lookup = lookup_recombination_F_; end_positions = &recombination_end_positions_F_; rates = &recombination_rates_F_; } else { RecombinationMapConfigError(); } } // Draw extents and crossover/noncrossover and simple/complex decisions for p_num_breakpoints DSBs; we may not end up using all // of them, if the uniquing step reduces the set of DSBs, but we don't want to redraw these things if we have to loop back due // to a collision, because such redrawing would be liable to produce bias towards shorter extents. (Redrawing the crossover/ // noncrossover and simple/complex decisions would probably be harmless, but it is simpler to just make all decisions up front.) int try_count = 0; gsl_rng *rng_gsl = EIDOS_GSL_RNG(omp_get_thread_num()); EidosRNG_64_bit &rng_64 = EIDOS_64BIT_RNG(omp_get_thread_num()); static std::vector> dsb_infos; // using a static prevents reallocation // If the redrawLengthsOnFailure parameter to initializeGeneConversion() is T, we jump back here on layout failure // Note that we also redraw noncrossover and simple, on this code path; that shouldn't matter since they are independent of layout generateDSBsRedrawingLengths: dsb_infos.resize(0); if (gene_conversion_avg_length_ < 2.0) { for (int i = 0; i < p_num_breakpoints; i++) { // If the gene conversion tract mean length is < 2.0, gsl_ran_geometric() will blow up, and we should treat the tract length as zero bool noncrossover = (Eidos_rng_uniform_doubleCO(rng_64) <= non_crossover_fraction_); // tuple position 2 bool simple = (Eidos_rng_uniform_doubleCO(rng_64) <= simple_conversion_fraction_); // tuple position 3 dsb_infos.emplace_back(0, 0, noncrossover, simple); } } else { for (int i = 0; i < p_num_breakpoints; i++) { slim_position_t extent1 = gsl_ran_geometric(rng_gsl, gene_conversion_inv_half_length_); // tuple position 0 slim_position_t extent2 = gsl_ran_geometric(rng_gsl, gene_conversion_inv_half_length_); // tuple position 1 bool noncrossover = (Eidos_rng_uniform_doubleCO(rng_64) <= non_crossover_fraction_); // tuple position 2 bool simple = (Eidos_rng_uniform_doubleCO(rng_64) <= simple_conversion_fraction_); // tuple position 3 dsb_infos.emplace_back(extent1, extent2, noncrossover, simple); } } // If the redrawLengthsOnFailure parameter to initializeGeneConversion() is F, we jump back here on layout failure generateDSBsWithoutRedrawingLengths: if (++try_count > 100) { // Note this block handles failure for both redraw_lengths_on_failure_ cases EIDOS_TERMINATION << "ERROR (Chromosome::DrawDSBBreakpoints): non-overlapping recombination regions could not be achieved in 100 tries; terminating. The recombination rate and/or mean gene conversion tract length may be too high."; if (!redraw_lengths_on_failure_) EIDOS_TERMINATION << " You might wish to pass redrawLengthsOnFailure=T to initializeGeneConversion() to make gene conversion tract layout more robust."; EIDOS_TERMINATION << EidosTerminate(); } // First draw DSB points; dsb_points contains positions and a flag for whether the breakpoint is at a rate=0.5 position static std::vector> dsb_points; // using a static prevents reallocation dsb_points.resize(0); for (int i = 0; i < p_num_breakpoints; i++) { slim_position_t breakpoint = 0; int recombination_interval = static_cast(gsl_ran_discrete(rng_gsl, lookup)); if (recombination_interval == 0) breakpoint = static_cast(Eidos_rng_interval_uint64(rng_64, (*end_positions)[recombination_interval]) + 1); else breakpoint = (*end_positions)[recombination_interval - 1] + 1 + static_cast(Eidos_rng_interval_uint64(rng_64, (*end_positions)[recombination_interval] - (*end_positions)[recombination_interval - 1])); if ((*rates)[recombination_interval] == 0.5) dsb_points.emplace_back(breakpoint, true); else dsb_points.emplace_back(breakpoint, false); } // Sort and unique the resulting DSB vector if (p_num_breakpoints > 1) { std::sort(dsb_points.begin(), dsb_points.end()); // sorts by the first element of the pair dsb_points.erase(std::unique(dsb_points.begin(), dsb_points.end()), dsb_points.end()); } // Assemble lists of crossover breakpoints and heteroduplex regions, starting from a clean slate int final_num_breakpoints = (int)dsb_points.size(); slim_position_t last_position_used = -1; p_crossovers.resize(0); p_heteroduplex.resize(0); for (int i = 0; i < final_num_breakpoints; i++) { std::pair &dsb_pair = dsb_points[i]; std::tuple &dsb_info = dsb_infos[i]; slim_position_t dsb_point = dsb_pair.first; if (dsb_pair.second) { // This DSB is at a rate=0.5 point, so we do not generate a gene conversion tract; it just translates directly to a crossover breakpoint // Note that we do NOT check non_crossover_fraction_ here; it does not apply to rate=0.5 positions, since they cannot undergo gene conversion if (dsb_point <= last_position_used) { if (redraw_lengths_on_failure_) goto generateDSBsRedrawingLengths; else goto generateDSBsWithoutRedrawingLengths; } p_crossovers.emplace_back(dsb_point); last_position_used = dsb_point; } else { // This DSB is not at a rate=0.5 point, so we generate a gene conversion tract around it slim_position_t tract_start = dsb_point - std::get<0>(dsb_info); slim_position_t tract_end = SLiMClampToPositionType(dsb_point + std::get<1>(dsb_info)); // We do not want to allow GC tracts to extend all the way to the chromosome beginning or end // This is partly because biologically it seems weird, and partly because a breakpoint at position 0 breaks tree-seq recording if ((tract_start <= 0) || (tract_end > last_position_) || (tract_start <= last_position_used)) { if (redraw_lengths_on_failure_) goto generateDSBsRedrawingLengths; else goto generateDSBsWithoutRedrawingLengths; } if (tract_start == tract_end) { // gene conversion tract of zero length, so no tract after all, but we do use non_crossover here if (!std::get<2>(dsb_info)) p_crossovers.emplace_back(tract_start); last_position_used = tract_start; } else { // gene conversion tract of non-zero length, so generate the tract p_crossovers.emplace_back(tract_start); if (std::get<2>(dsb_info)) p_crossovers.emplace_back(tract_end); last_position_used = tract_end; // decide if it is a simple or a complex tract if (!std::get<3>(dsb_info)) { // complex gene conversion tract; we need to save it in the list of heteroduplex regions p_heteroduplex.emplace_back(tract_start); p_heteroduplex.emplace_back(tract_end - 1); // heteroduplex positions are base positions, so the last position is to the left of the GC tract end } } } } } // This high-level function is the funnel for drawing breakpoints. It delegates down to DrawDSBBreakpoints() // or DrawCrossoverBreakpoints(), handles recombination() callbacks, and returns a sorted, uniqued vector. // You can supply it with a number of breakpoints to draw, or pass -1 to have it draw the number for you. // If the caller can handle complex gene conversion tracts, they should pass a vector for those to be placed // in. If not, pass nullptr, and this method will raise if complex gene conversion tracts are in use. For // addRecombinant() and addMultiRecombinant(), this method allows the parent to be different from the // haplosomes that are supplied; the parent individual is used to look up the haplosomes if they are passed // as nullptr. The haplosomes are used only if recombination() callbacks are in effect. The parent // is also used to look up the sex, for sex-specific recombination rates. void Chromosome::DrawBreakpoints(Individual *p_parent, Haplosome *p_haplosome1, Haplosome *p_haplosome2, int p_num_breakpoints, std::vector &p_crossovers, std::vector *p_heteroduplex, const char *p_caller_name) { if (!species_.HasGenetics()) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): in " << p_caller_name << ", recombination breakpoints cannot be drawn for a species with no genetics." << EidosTerminate(); if (!p_heteroduplex && using_DSB_model_ && (simple_conversion_fraction_ != 1.0)) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): in " << p_caller_name << ", complex gene conversion tracts cannot be active since there is no provision for handling heteroduplex regions." << EidosTerminate(); // look up parent information; note that if parent is nullptr, we do not run recombination() callbacks! // the parent is a required pseudo-parameter for the recombination() callback IndividualSex parent_sex = IndividualSex::kUnspecified; std::vector recombination_callbacks; Subpopulation *parent_subpop = nullptr; if (p_parent) { parent_sex = p_parent->sex_; parent_subpop = p_parent->subpopulation_; recombination_callbacks = species_.CallbackBlocksMatching(community_.Tick(), SLiMEidosBlockType::SLiMEidosRecombinationCallback, -1, -1, parent_subpop->subpopulation_id_, id_); // SPECIES CONSISTENCY CHECK if (&p_parent->subpopulation_->species_ != &species_) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): in " << p_caller_name << ", the parent, if supplied, must belong to the same species as the target chromosome." << EidosTerminate(); } else // !p_parent { // In a sexual model with sex-specific recombination maps, we need to know the parent we're // generating breakpoints for; in other situations it is optional, but recombination() // breakpoints will not be called if parent is NULL. if (!single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): in " << p_caller_name << ", a parent must be supplied since sex-specific recombination maps are in use (to determine which map to use, from the sex of the parent)." << EidosTerminate(); } // look up haplosome information, used only for the recombination() callback if ((!p_haplosome1 && p_haplosome2) || (!p_haplosome2 && p_haplosome1)) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): (internal error) in " << p_caller_name << ", haplosomes must either be supplied or not supplied." << EidosTerminate(); if (p_haplosome1) { // SPECIES CONSISTENCY CHECK if ((&p_haplosome1->OwningIndividual()->subpopulation_->species_ != &species_) || (&p_haplosome2->OwningIndividual()->subpopulation_->species_ != &species_)) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): in " << p_caller_name << ", parental haplosomes must belong to the same species as the target chromosome." << EidosTerminate(); if ((p_haplosome1->chromosome_index_ != index_) || (p_haplosome2->chromosome_index_ != index_)) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): in " << p_caller_name << ", parental haplosomes must belong to the target chromosome." << EidosTerminate(); // Note that if haplosomes were passed in but p_parent is nullptr, we do NOT attempt to infer a parent // subpopulation in order to get recombination() callbacks from it. In the general case that would // not work, since the two haplosomes might belong to different subpopulations; and if we can't do it // in general, we shouldn't try to do it at all. If you want recombination() callbacks, pass p_parent. } else if (p_parent) { // Get the indices of the haplosomes associated with this chromosome. Note that the first/last indices // might be the same, if this is a haploid chromosome. That is OK here. The user is allowed to set a // recombination rate on a haploid chromosome and generate breakpoints for it; what they do with that // information is up to them. (They might use them in an addRecombinant() or addMultiRecombinant() call, // for example.) In that case, of a haploid chromosome, the same single parent haplosome will be passed // twice to recombination() callbacks; that seems better than not defining one of the pseudo-parameters. int first_haplosome_index = species_.FirstHaplosomeIndices()[index_]; int last_haplosome_index = species_.LastHaplosomeIndices()[index_]; // Note that for calling recombination() callbacks below, we treat the parent's first haplosome as the // initial copy strand. If a distinction needs to be made, pass the haplosomes in to this method. p_haplosome1 = p_parent->haplosomes_[first_haplosome_index]; p_haplosome2 = p_parent->haplosomes_[last_haplosome_index]; } // Draw the number of breakpoints, if it was not supplied if (p_num_breakpoints == -1) p_num_breakpoints = DrawBreakpointCount(parent_sex); if ((p_num_breakpoints < 0) || (p_num_breakpoints > 1000000L)) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): in " << p_caller_name << ", the number of recombination breakpoints must be in [0, 1000000]." << EidosTerminate(); #if DEBUG if (p_crossovers.size()) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): (internal error) in " << p_caller_name << ", p_crossovers was not supplied empty." << EidosTerminate(); #endif // draw the breakpoints based on the recombination rate map, and sort and unique the result if (p_num_breakpoints) { if (using_DSB_model_) { if (p_heteroduplex) { // p_heteroduplex is not nullptr, so the caller intends to use it for something _DrawDSBBreakpoints(parent_sex, p_num_breakpoints, p_crossovers, *p_heteroduplex); } else { // p_heteroduplex is nullptr, so we need to pass in our own vector; it is not actually used // in this case anyway, since simple_conversion_fraction_ must be 1.0 (as checked above) std::vector heteroduplex; _DrawDSBBreakpoints(parent_sex, p_num_breakpoints, p_crossovers, heteroduplex); } } else { _DrawCrossoverBreakpoints(parent_sex, p_num_breakpoints, p_crossovers); } // p_crossovers is sorted and uniqued at this point if (recombination_callbacks.size()) { // a non-zero number of breakpoints, with recombination callbacks bool breaks_changed = species_.population_.ApplyRecombinationCallbacks(p_parent, p_haplosome1, p_haplosome2, p_crossovers, recombination_callbacks); // we only sort/unique if the breakpoints have changed, since they were sorted/uniqued before if (breaks_changed && (p_crossovers.size() > 1)) { std::sort(p_crossovers.begin(), p_crossovers.end()); p_crossovers.erase(unique(p_crossovers.begin(), p_crossovers.end()), p_crossovers.end()); } } } else if (recombination_callbacks.size()) { // zero breakpoints from the SLiM core, but we have recombination() callbacks species_.population_.ApplyRecombinationCallbacks(p_parent, p_haplosome1, p_haplosome2, p_crossovers, recombination_callbacks); if (p_crossovers.size() > 1) { std::sort(p_crossovers.begin(), p_crossovers.end()); p_crossovers.erase(unique(p_crossovers.begin(), p_crossovers.end()), p_crossovers.end()); } } else { // no breakpoints, no gene conversion, no recombination() callbacks } // values in p_crossovers and p_heteroduplex are returned to the caller // p_crossovers is guaranteed to be sorted and uniqued, which we check here // we also check that no position is less than zero or beyond the chromosome end #if DEBUG slim_position_t previous_value = -1; slim_position_t last_chrom_position = last_position_; for (slim_position_t value : p_crossovers) { if (value <= previous_value) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): (internal error) breakpoints vector is not sorted/uniqued." << EidosTerminate(); if (value > last_chrom_position) EIDOS_TERMINATION << "ERROR (Chromosome::DrawBreakpoints): (internal error) breakpoints vector goes beyond the chromosome end." << EidosTerminate(); previous_value = value; } #endif } size_t Chromosome::MemoryUsageForMutationMaps(void) { size_t usage = 0; usage = (mutation_rates_H_.size() + mutation_rates_M_.size() + mutation_rates_F_.size()) * sizeof(double); usage += (mutation_end_positions_H_.size() + mutation_end_positions_M_.size() + mutation_end_positions_F_.size()) * sizeof(slim_position_t); usage += (mutation_subranges_H_.size() + mutation_subranges_M_.size() + mutation_subranges_F_.size()) * sizeof(GESubrange); usage += (hotspot_multipliers_H_.size() + hotspot_multipliers_M_.size() + hotspot_multipliers_F_.size()) * sizeof(double); usage += (hotspot_end_positions_H_.size() + hotspot_end_positions_M_.size() + hotspot_end_positions_F_.size()) * sizeof(slim_position_t); if (lookup_mutation_H_) usage += lookup_mutation_H_->K * (sizeof(size_t) + sizeof(double)); if (lookup_mutation_M_) usage += lookup_mutation_M_->K * (sizeof(size_t) + sizeof(double)); if (lookup_mutation_F_) usage += lookup_mutation_F_->K * (sizeof(size_t) + sizeof(double)); return usage; } size_t Chromosome::MemoryUsageForRecombinationMaps(void) { size_t usage = 0; usage = (recombination_rates_H_.size() + recombination_rates_M_.size() + recombination_rates_F_.size()) * sizeof(double); usage += (recombination_end_positions_H_.size() + recombination_end_positions_M_.size() + recombination_end_positions_F_.size()) * sizeof(slim_position_t); if (lookup_recombination_H_) usage += lookup_recombination_H_->K * (sizeof(size_t) + sizeof(double)); if (lookup_recombination_M_) usage += lookup_recombination_M_->K * (sizeof(size_t) + sizeof(double)); if (lookup_recombination_F_) usage += lookup_recombination_F_->K * (sizeof(size_t) + sizeof(double)); return usage; } size_t Chromosome::MemoryUsageForAncestralSequence(void) { size_t usage = 0; if (ancestral_seq_buffer_) { std::size_t length = ancestral_seq_buffer_->size(); usage += ((length + 31) / 32) * sizeof(uint64_t); } return usage; } void Chromosome::SetUpMutationRunContexts(void) { // Make an EidosObjectPool to allocate mutation runs from; this is for memory locality, so make it nice and big #ifndef _OPENMP mutation_run_context_SINGLE_.allocation_pool_ = new EidosObjectPool("EidosObjectPool(MutationRun)", sizeof(MutationRun), 65536); #else //std::cout << "***** Initializing " << gEidosMaxThreads << " independent MutationRunContexts" << std::endl; // Make per-thread MutationRunContexts; the number of threads that we set up for here is NOT gEidosMaxThreads, // but rather, the "base" number of mutation runs per haplosome chosen by Chromosome. The chromosome is divided // into that many chunks along its length (or a multiple thereof), and there is one thread per "base" chunk. mutation_run_context_COUNT_ = mutrun_count_base_; mutation_run_context_PERTHREAD.resize(mutation_run_context_COUNT_); if (mutation_run_context_COUNT_ > 0) { // Check that each RNG was initialized by a different thread, as intended below; // this is not required, but it improves memory locality throughout the run bool threadObserved[mutation_run_context_COUNT_]; #pragma omp parallel default(none) shared(mutation_run_context_PERTHREAD, threadObserved) num_threads(mutation_run_context_COUNT_) { // Each thread allocates and initializes its own MutationRunContext, for "first touch" optimization int threadnum = omp_get_thread_num(); mutation_run_context_PERTHREAD[threadnum] = new MutationRunContext(); mutation_run_context_PERTHREAD[threadnum]->allocation_pool_ = new EidosObjectPool("EidosObjectPool(MutationRun)", sizeof(MutationRun), 65536); omp_init_lock(&mutation_run_context_PERTHREAD[threadnum]->allocation_pool_lock_); threadObserved[threadnum] = true; } // end omp parallel for (int threadnum = 0; threadnum < mutation_run_context_COUNT_; ++threadnum) if (!threadObserved[threadnum]) std::cerr << "WARNING: parallel MutationRunContexts were not correctly initialized on their corresponding threads; this may cause slower simulation." << std::endl; } #endif // end _OPENMP } // These get called if a null haplosome is requested but the null junkyard is empty, or if a non-null haplosome is requested // but the non-null junkyard is empty; so we know that the primary junkyard for the request cannot service the request. // If the other junkyard has a haplosome, we want to repurpose it; this prevents one junkyard from filling up with an // ever-growing number of haplosomes while requests to the other junkyard create new haplosomes (which can happen because // haplosomes can be transmogrified between null and non-null after creation). We create a new haplosome only if both // junkyards are empty. Haplosome *Chromosome::_NewHaplosome_NULL(Individual *p_individual) { // this does not set chromosome_subposition_; use NewHaplosome_NULL() if (haplosomes_junkyard_nonnull.size()) { Haplosome *back = haplosomes_junkyard_nonnull.back(); haplosomes_junkyard_nonnull.pop_back(); // got a non-null haplosome, need to repurpose it to be a null haplosome back->ReinitializeHaplosomeToNull(p_individual); return back; } return new (haplosome_pool_.AllocateChunk()) Haplosome(Haplosome::NullHaplosome{}, p_individual, this); } Haplosome *Chromosome::_NewHaplosome_NONNULL(Individual *p_individual) { // this does not set chromosome_subposition_; use NewHaplosome_NONNULL() if (haplosomes_junkyard_null.size()) { Haplosome *back = haplosomes_junkyard_null.back(); haplosomes_junkyard_null.pop_back(); // got a null haplosome, need to repurpose it to be a non-null haplosome cleared to nullptr back->ReinitializeHaplosomeToNonNull(p_individual, this); return back; } return new (haplosome_pool_.AllocateChunk()) Haplosome(Haplosome::NonNullHaplosome{}, p_individual, this); } // // Mutation run experiments // #pragma mark - #pragma mark Mutation run experiments #pragma mark - void Chromosome::InitiateMutationRunExperiments(void) { if (preferred_mutrun_count_ != 0) { // If the user supplied a count, go with that and don't run experiments x_experiments_enabled_ = false; if (SLiM_verbosity_level >= 2) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// Mutation run experiments disabled since a mutation run count was supplied" << std::endl; } return; } if (mutrun_length_ <= SLIM_MUTRUN_MAXIMUM_COUNT) { // If the chromosome length is too short, go with that and don't run experiments; // we want to guarantee that with SLIM_MUTRUN_MAXIMUM_COUNT runs each mutrun is at // least one mutation in length, so the code doesn't break down x_experiments_enabled_ = false; if (SLiM_verbosity_level >= 2) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// Mutation run experiments disabled since the chromosome is very short" << std::endl; } return; } x_experiments_enabled_ = true; species_.DoingMutrunExperimentsForChromosome(); x_experiment_count_ = 0; x_current_mutcount_ = mutrun_count_; x_current_runtimes_ = (double *)malloc(SLIM_MUTRUN_EXPERIMENT_LENGTH * sizeof(double)); x_current_buflen_ = 0; x_previous_mutcount_ = 0; // marks that no previous experiment has been done x_previous_runtimes_ = (double *)malloc(SLIM_MUTRUN_EXPERIMENT_LENGTH * sizeof(double)); x_previous_buflen_ = 0; if (!x_current_runtimes_ || !x_previous_runtimes_) EIDOS_TERMINATION << "ERROR (Chromosome::InitiateMutationRunExperiments): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); x_continuing_trend_ = false; x_stasis_limit_ = 5; // once we reach stasis, we will conduct 5 stasis experiments before exploring again x_stasis_alpha_ = 0.01; // initially, we use an alpha of 0.01 to break out of stasis due to a change in mean x_stasis_counter_ = 0; x_prev1_stasis_mutcount_ = 0; // we have never reached stasis before, so we have no memory of it x_prev2_stasis_mutcount_ = 0; // we have never reached stasis before, so we have no memory of it if (SLiM_verbosity_level >= 2) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// Mutation run experiments started" << std::endl; } } void Chromosome::ZeroMutationRunExperimentClock(void) { if (x_experiments_enabled_) { if (x_within_measurement_period_) std::cerr << "WARNING: ZeroMutationRunExperimentClock() called when the measurement period is already begun!" << std::endl; if (x_total_gen_clocks_ != 0) { // Clocks should only get logged in the interval within which they are used; if there are leftover counts // at this point, somebody is logging counts that are not getting used in the total. Warn once. if (!community_.warned_experiment_run_clocks_) { THREAD_SAFETY_IN_ANY_PARALLEL("Chromosome::PrepareForCycle(): usage of statics"); std::cerr << "WARNING: mutation run experiment clocks were logged outside of the measurement interval!" << std::endl; community_.warned_experiment_run_clocks_ = true; } x_total_gen_clocks_ = 0; } x_within_measurement_period_ = true; #if MUTRUN_EXPERIMENT_TIMING_OUTPUT std::cout << "tick " << community_.Tick() << ", chromosome " << id_ << ": starting timing" << std::endl; #endif } } void Chromosome::FinishMutationRunExperimentTiming(void) { if (x_experiments_enabled_) { if (!x_within_measurement_period_) std::cerr << "WARNING: FinishMutationRunExperimentTiming() called when the measurement period has not begun!" << std::endl; #if MUTRUN_EXPERIMENT_TIMING_OUTPUT std::cout << "tick " << community_.Tick() << ", chromosome " << id_ << ": ending timing with total count == " << x_total_gen_clocks_ << " (" << Eidos_ElapsedProfileTime(x_total_gen_clocks_) << " seconds)" << std::endl; #endif // We only run mutrun experiments in ticks when our species in active; when inactive, any // clocks accumulated will simply be discarded unused, since they are not representative if (species_.Active()) MaintainMutationRunExperiments(Eidos_ElapsedProfileTime(x_total_gen_clocks_)); x_total_gen_clocks_ = 0; x_within_measurement_period_ = false; } } void Chromosome::TransitionToNewExperimentAgainstCurrentExperiment(int32_t p_new_mutrun_count) { // Save off the old experiment x_previous_mutcount_ = x_current_mutcount_; std::swap(x_current_runtimes_, x_previous_runtimes_); x_previous_buflen_ = x_current_buflen_; // Set up the next experiment x_current_mutcount_ = p_new_mutrun_count; x_current_buflen_ = 0; } void Chromosome::TransitionToNewExperimentAgainstPreviousExperiment(int32_t p_new_mutrun_count) { // Set up the next experiment x_current_mutcount_ = p_new_mutrun_count; x_current_buflen_ = 0; } void Chromosome::EnterStasisForMutationRunExperiments(void) { if ((x_current_mutcount_ == x_prev1_stasis_mutcount_) || (x_current_mutcount_ == x_prev2_stasis_mutcount_)) { // One of our recent trips to stasis was at the same count, so we broke stasis incorrectly; get stricter. // The purpose for keeping two previous counts is to detect when we are ping-ponging between two values // that produce virtually identical performance; we want to detect that and just settle on one of them. x_stasis_alpha_ *= 0.5; x_stasis_limit_ *= 2; #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// Remembered previous stasis at " << x_current_mutcount_ << ", strengthening stasis criteria" << std::endl; #endif } else { // Our previous trips to stasis were at a different number of mutation runs, so reset our stasis parameters x_stasis_limit_ = 5; x_stasis_alpha_ = 0.01; #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// No memory of previous stasis at " << x_current_mutcount_ << ", resetting stasis criteria" << std::endl; #endif } x_stasis_counter_ = 1; x_continuing_trend_ = false; // Preserve a memory of the last two *different* mutcounts we entered stasis on. Only forget the old value // in x_prev2_stasis_mutcount_ if x_prev1_stasis_mutcount_ is about to get a new and different value. // This makes the anti-ping-pong mechanism described above effective even if we ping-pong irregularly. if (x_prev1_stasis_mutcount_ != x_current_mutcount_) x_prev2_stasis_mutcount_ = x_prev1_stasis_mutcount_; x_prev1_stasis_mutcount_ = x_current_mutcount_; #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ****** ENTERING STASIS AT " << x_current_mutcount_ << " : x_stasis_limit_ = " << x_stasis_limit_ << ", x_stasis_alpha_ = " << x_stasis_alpha_ << std::endl; #endif } void Chromosome::MaintainMutationRunExperiments(double p_last_gen_runtime) { // Log the last cycle time into our buffer if (x_current_buflen_ >= SLIM_MUTRUN_EXPERIMENT_LENGTH) EIDOS_TERMINATION << "ERROR (Chromosome::MaintainMutationRunExperiments): Buffer overrun, failure to reset after completion of an experiment." << EidosTerminate(); x_current_runtimes_[x_current_buflen_] = p_last_gen_runtime; // Remember the history of the mutation run count x_mutcount_history_.emplace_back(x_current_mutcount_); // If the current experiment is not over, continue running it ++x_current_buflen_; double current_mean = 0.0, previous_mean = 0.0, p = 0.0; if ((x_current_buflen_ == 10) && (x_current_mutcount_ != x_previous_mutcount_) && (x_previous_mutcount_ != 0)) { // We want to be able to cut an experiment short if it is clearly a disaster. So if we're not in stasis, and // we've run for 10 cycles, and the experiment mean is already different from the baseline at alpha 0.01, // and the experiment mean is worse than the baseline mean (if it is better, we want to continue collecting), // let's short-circuit the rest of the experiment and bail – like early termination of a medical trial. p = Eidos_TTest_TwoSampleWelch(x_current_runtimes_, x_current_buflen_, x_previous_runtimes_, x_previous_buflen_, ¤t_mean, &previous_mean); x_experiment_count_++; if ((p < 0.01) && (current_mean > previous_mean)) { #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// " << cycle_ << " : Early t-test yielded HIGHLY SIGNIFICANT p of " << p << " with negative results; terminating early." << std::endl; } #endif goto early_ttest_passed; } #if MUTRUN_EXPERIMENT_OUTPUT else if (SLiM_verbosity_level >= 2) { if (p >= 0.01) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// " << cycle_ << " : Early t-test yielded not highly significant p of " << p << "; continuing." << std::endl; } else if (current_mean > previous_mean) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// " << cycle_ << " : Early t-test yielded highly significant p of " << p << " with positive results; continuing data collection." << std::endl; } } #endif } if (x_current_buflen_ < SLIM_MUTRUN_EXPERIMENT_LENGTH) return; if (x_previous_mutcount_ == 0) { // FINISHED OUR FIRST EXPERIMENT; move on to the next experiment, which is always double the number of mutruns #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// ** " << cycle_ << " : First mutation run experiment completed with mutrun count " << x_current_mutcount_ << "; will now try " << (x_current_mutcount_ * 2) << std::endl; } #endif TransitionToNewExperimentAgainstCurrentExperiment(x_current_mutcount_ * 2); } else { // If we've just finished the second stasis experiment, run another stasis experiment before trying to draw any // conclusions. We often enter stasis with one cycle's worth of data that was actually collected quite a // while ago, because we did exploration in both directions first. This can lead to breaking out of stasis // immediately after entering, because we're comparing apples and oranges. So we avoid doing that here. if ((x_stasis_counter_ <= 1) && (x_current_mutcount_ == x_previous_mutcount_)) { TransitionToNewExperimentAgainstCurrentExperiment(x_current_mutcount_); ++x_stasis_counter_; #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// " << cycle_ << " : Mutation run experiment completed (second stasis cycle, no tests conducted)" << std::endl; } #endif return; } // Otherwise, get a result from a t-test and decide what to do p = Eidos_TTest_TwoSampleWelch(x_current_runtimes_, x_current_buflen_, x_previous_runtimes_, x_previous_buflen_, ¤t_mean, &previous_mean); x_experiment_count_++; early_ttest_passed: #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// " << cycle_ << " : Mutation run experiment completed:" << std::endl; SLIM_OUTSTREAM << "// mean == " << current_mean << " for " << x_current_mutcount_ << " mutruns (" << x_current_buflen_ << " data points)" << std::endl; SLIM_OUTSTREAM << "// mean == " << previous_mean << " for " << x_previous_mutcount_ << " mutruns (" << x_previous_buflen_ << " data points)" << std::endl; } #endif if (x_current_mutcount_ == x_previous_mutcount_) // are we in stasis? { // // FINISHED A STASIS EXPERIMENT; unless we have changed at alpha = 0.01 we stay put // bool means_different_stasis = (p < x_stasis_alpha_); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// p == " << p << " : " << (means_different_stasis ? "SIGNIFICANT DIFFERENCE" : "no significant difference") << " at stasis alpha " << x_stasis_alpha_ << std::endl; #endif if (means_different_stasis) { // OK, it looks like something has changed about our scenario, so we should come out of stasis and re-test. // We don't have any information about the new state of affairs, so we have no directional preference. // Let's try a larger number of mutation runs first, since haplosomes tend to fill up, unless we're at the max. if (x_current_mutcount_ * 2 > SLIM_MUTRUN_MAXIMUM_COUNT) TransitionToNewExperimentAgainstCurrentExperiment(x_current_mutcount_ / 2); else TransitionToNewExperimentAgainstCurrentExperiment(x_current_mutcount_ * 2); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ** " << cycle_ << " : Stasis mean changed, EXITING STASIS and trying new mutcount of " << x_current_mutcount_ << std::endl; #endif } else { // We seem to be in a constant scenario. Increment our stasis counter and see if we have reached our stasis limit if (++x_stasis_counter_ >= x_stasis_limit_) { // We reached the stasis limit, so we will try an experiment even though we don't seem to have changed; // as before, we try more mutation runs first, since increasing genetic complexity is typical if (x_current_mutcount_ * 2 > SLIM_MUTRUN_MAXIMUM_COUNT) TransitionToNewExperimentAgainstCurrentExperiment(x_current_mutcount_ / 2); else TransitionToNewExperimentAgainstCurrentExperiment(x_current_mutcount_ * 2); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ** " << cycle_ << " : Stasis limit reached, EXITING STASIS and trying new mutcount of " << x_current_mutcount_ << std::endl; #endif } else { // We have not yet reached the stasis limit, so run another stasis experiment. // In this case we don't do a transition; we want to continue comparing against the original experiment // data so that if stasis slowly drift away from us, we eventually detect that as a change in stasis. x_current_buflen_ = 0; #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// " << cycle_ << " : Stasis limit not reached (" << x_stasis_counter_ << " of " << x_stasis_limit_ << "), running another stasis experiment at " << x_current_mutcount_ << std::endl; #endif } } } else { // // FINISHED A NON-STASIS EXPERIMENT; trying a move toward more/fewer mutruns // double alpha = 0.05; bool means_different_05 = (p < alpha); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// p == " << p << " : " << (means_different_05 ? "SIGNIFICANT DIFFERENCE" : "no significant difference") << " at alpha " << alpha << std::endl; #endif int32_t trend_next = (x_current_mutcount_ < x_previous_mutcount_) ? (x_current_mutcount_ / 2) : (x_current_mutcount_ * 2); int32_t trend_limit = (x_current_mutcount_ < x_previous_mutcount_) ? mutrun_count_base_ : SLIM_MUTRUN_MAXIMUM_COUNT; // for single-threaded, mutrun_count_base_ == 1 if ((current_mean < previous_mean) || (!means_different_05 && (x_current_mutcount_ < x_previous_mutcount_))) { // We enter this case under two different conditions. The first is that the new mean is better // than the old mean; whether that is significant or not, we want to continue in the same direction // with a new experiment, which is what we do here. The other case is if the new mean is worse // than the old mean, but the difference is non-significant *and* we're trending toward fewer // mutation runs. We treat that the same way: continue with a new experiment in the same direction. // But if the new mean is worse that the old mean and we're trending toward more mutation runs, // we do NOT follow this case, because an inconclusive but negative increasing trend pushes up our // peak memory usage and can be quite inefficient, and usually we just jump back down anyway. // BCH 8/14/2023: The if() below is intended to diagnose if trend_next will go beyond trend_limit, // and is thus not a legal move. Just testing (x_current_mutcount_ == trend_limit) used to suffice, // because the base count was always a power of 2. Now that is no longer true, and so we can, e.g., // be at 768 and thinking about doubling to 1536. We test for going beyond SLIM_MUTRUN_MAXIMUM_COUNT // explicitly now, to address that case. Going too low is still effectively prevented, since we // will always reach the base count exactly before going below it. if ((x_current_mutcount_ == trend_limit) || (trend_next > SLIM_MUTRUN_MAXIMUM_COUNT)) { if (current_mean < previous_mean) { // Can't go beyond the trend limit (1 or SLIM_MUTRUN_MAXIMUM_COUNT), so we're done; ****** ENTER STASIS // We keep the current experiment as the first stasis experiment. TransitionToNewExperimentAgainstCurrentExperiment(x_current_mutcount_); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ****** " << cycle_ << " : Experiment " << (means_different_05 ? "successful" : "inconclusive but positive") << " at " << x_previous_mutcount_ << ", nowhere left to go; entering stasis at " << x_current_mutcount_ << "." << std::endl; #endif EnterStasisForMutationRunExperiments(); } else { // The means are not significantly different, and the current experiment is worse than the // previous one, and we can't go beyond the trend limit, so we're done; ****** ENTER STASIS // We keep the previous experiment as the first stasis experiment. TransitionToNewExperimentAgainstPreviousExperiment(x_previous_mutcount_); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ****** " << cycle_ << " : Experiment " << (means_different_05 ? "failed" : "inconclusive but negative") << " at " << x_previous_mutcount_ << ", nowhere left to go; entering stasis at " << x_current_mutcount_ << "." << std::endl; #endif EnterStasisForMutationRunExperiments(); } } else { if (current_mean < previous_mean) { // Even if the difference is not significant, we appear to be moving in a beneficial direction, // so we will run the next experiment against the current experiment's results #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ** " << cycle_ << " : Experiment " << (means_different_05 ? "successful" : "inconclusive but positive") << " at " << x_current_mutcount_ << " (against " << x_previous_mutcount_ << "), continuing trend with " << trend_next << " (against " << x_current_mutcount_ << ")" << std::endl; #endif TransitionToNewExperimentAgainstCurrentExperiment(trend_next); x_continuing_trend_ = true; } else { // The difference is not significant, but we might be moving in a bad direction, and a series // of such moves, each non-significant against the previous experiment, can lead us way down // the garden path. To make sure that doesn't happen, we run successive inconclusive experiments // against whichever preceding experiment had the lowest mean. #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ** " << cycle_ << " : Experiment inconclusive but negative at " << x_current_mutcount_ << " (against " << x_previous_mutcount_ << "), checking " << trend_next << " (against " << x_previous_mutcount_ << ")" << std::endl; #endif TransitionToNewExperimentAgainstPreviousExperiment(trend_next); } } } else { // The old mean was better, and either the difference is significant or the trend is toward more mutation // runs, so we want to give up on this trend and go back if (x_continuing_trend_) { // We already tried a step on the opposite side of the old position, so the old position appears ideal; ****** ENTER STASIS. // We throw away the current, failed experiment and keep the last experiment at the previous position as the first stasis experiment. TransitionToNewExperimentAgainstPreviousExperiment(x_previous_mutcount_); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ****** " << cycle_ << " : Experiment failed, already tried opposite side, so " << x_current_mutcount_ << " appears optimal; entering stasis at " << x_current_mutcount_ << "." << std::endl; #endif EnterStasisForMutationRunExperiments(); } else { // We have not tried a step on the opposite side of the old position; let's return to our old position, // which we know is better than the position we just ran an experiment at, and then advance onward to // run an experiment at the next position in that reversed trend direction. int32_t new_mutcount = ((x_current_mutcount_ > x_previous_mutcount_) ? (x_previous_mutcount_ / 2) : (x_previous_mutcount_ * 2)); if ((x_previous_mutcount_ == mutrun_count_base_) || (x_previous_mutcount_ == SLIM_MUTRUN_MAXIMUM_COUNT) || (new_mutcount < mutrun_count_base_) || (new_mutcount > SLIM_MUTRUN_MAXIMUM_COUNT)) { // can't jump over the previous mutcount, so we enter stasis at it TransitionToNewExperimentAgainstPreviousExperiment(x_previous_mutcount_); #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ****** " << cycle_ << " : Experiment failed, opposite side blocked so " << x_current_mutcount_ << " appears optimal; entering stasis at " << x_current_mutcount_ << "." << std::endl; #endif EnterStasisForMutationRunExperiments(); } else { #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ** " << cycle_ << " : Experiment failed at " << x_current_mutcount_ << ", opposite side untried, reversing trend back to " << new_mutcount << " (against " << x_previous_mutcount_ << ")" << std::endl; #endif TransitionToNewExperimentAgainstPreviousExperiment(new_mutcount); x_continuing_trend_ = true; } } } } } // Promulgate the new mutation run count if (x_current_mutcount_ != mutrun_count_) { // Fix all haplosomes. We could do this by brute force, by making completely new mutation runs for every // existing haplosome and then calling Population::UniqueMutationRuns(), but that would be inefficient, // and would also cause a huge memory usage spike. Instead, we want to preserve existing redundancy. while (x_current_mutcount_ > mutrun_count_) { #if MUTRUN_EXPERIMENT_OUTPUT std::clock_t start_clock = std::clock(); #endif if (x_current_mutcount_ > SLIM_MUTRUN_MAXIMUM_COUNT) EIDOS_TERMINATION << "ERROR (Chromosome::MaintainMutationRunExperiments): (internal error) splitting mutation runs to beyond SLIM_MUTRUN_MAXIMUM_COUNT (x_current_mutcount_ == " << x_current_mutcount_ << ")." << EidosTerminate(); // We are splitting existing runs in two, so make a map from old mutrun index to new pair of // mutrun indices; every time we encounter the same old index we will substitute the same pair. species_.population_.SplitMutationRunsForChromosome(mutrun_count_ * 2, this); // Fix the chromosome values mutrun_count_multiplier_ *= 2; mutrun_count_ *= 2; mutrun_length_ /= 2; #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ++ Splitting to achieve new mutation run count of " << mutrun_count_ << " took " << ((std::clock() - start_clock) / (double)CLOCKS_PER_SEC) << " seconds" << std::endl; #endif #if DEBUG community_.AllSpecies_CheckIntegrity(); #endif } while (x_current_mutcount_ < mutrun_count_) { #if MUTRUN_EXPERIMENT_OUTPUT std::clock_t start_clock = std::clock(); #endif if (mutrun_count_multiplier_ % 2 != 0) EIDOS_TERMINATION << "ERROR (Chromosome::MaintainMutationRunExperiments): (internal error) joining mutation runs to beyond mutrun_count_base_ (mutrun_count_base_ == " << mutrun_count_base_ << ", x_current_mutcount_ == " << x_current_mutcount_ << ")." << EidosTerminate(); // We are joining existing runs together, so make a map from old mutrun index pairs to a new // index; every time we encounter the same pair of indices we will substitute the same index. species_.population_.JoinMutationRunsForChromosome(mutrun_count_ / 2, this); // Fix the chromosome values mutrun_count_multiplier_ /= 2; mutrun_count_ /= 2; mutrun_length_ *= 2; #if MUTRUN_EXPERIMENT_OUTPUT if (SLiM_verbosity_level >= 2) SLIM_OUTSTREAM << "// ++ Joining to achieve new mutation run count of " << mutrun_count_ << " took " << ((std::clock() - start_clock) / (double)CLOCKS_PER_SEC) << " seconds" << std::endl; #endif } if (mutrun_count_ != x_current_mutcount_) EIDOS_TERMINATION << "ERROR (Chromosome::MaintainMutationRunExperiments): Failed to transition to new mutation run count" << x_current_mutcount_ << "." << EidosTerminate(); #if DEBUG community_.AllSpecies_CheckIntegrity(); #endif } } void Chromosome::PrintMutationRunExperimentSummary(void) { #if MUTRUN_EXPERIMENT_OUTPUT // Print a full mutation run count history if MUTRUN_EXPERIMENT_OUTPUT is enabled if (x_experiments_enabled_) { SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "// Chromosome " << id_ << " mutrun count history:" << std::endl; SLIM_OUTSTREAM << "// mutrun_history <- c("; bool first_count = true; for (int32_t count : x_mutcount_history_) { if (first_count) first_count = false; else SLIM_OUTSTREAM << ", "; SLIM_OUTSTREAM << count; } SLIM_OUTSTREAM << ")" << std::endl << std::endl; } #endif // If verbose output is enabled and we've been running mutation run experiments, // figure out the modal mutation run count and print that, for the user's benefit. if (x_experiments_enabled_) { int modal_index, modal_tally; int power_tallies[20]; // we only go up to 1024 mutruns right now, but this gives us some headroom for (int i = 0; i < 20; ++i) // NOLINT(*-loop-convert) : parallel to the loop below power_tallies[i] = 0; for (int32_t count : x_mutcount_history_) { int32_t power = (int32_t)round(log2(count)); power_tallies[power]++; } modal_index = -1; modal_tally = -1; for (int i = 0; i < 20; ++i) if (power_tallies[i] > modal_tally) { modal_tally = power_tallies[i]; modal_index = i; } int modal_count = (int)round(pow(2.0, modal_index)); double modal_fraction = power_tallies[modal_index] / (double)(x_mutcount_history_.size()); SLIM_OUTSTREAM << "// Chromosome " << id_ << ": " << modal_count << " (" << (modal_fraction * 100) << "% of cycles, " << x_experiment_count_ << " experiments)" << std::endl; } } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - // These are private helper methods that error if we are still in initialize() callbacks // Most properties and all methods on Chromosome should be protected by these calls to prevent bugs void Chromosome::CheckPartialInitializationForProperty(EidosGlobalStringID p_property_id) { if (community_.Tick() == 0) EIDOS_TERMINATION << "ERROR (Chromosome::CheckPartialInitializationForProperty): Chromosome property " << EidosStringRegistry::StringForGlobalStringID(p_property_id) << " cannot be accessed until initialize() callbacks are complete; the chromosome object is not yet fully initialized." << EidosTerminate(); } void Chromosome::CheckPartialInitializationForMethod(EidosGlobalStringID p_method_id) { if (community_.Tick() == 0) EIDOS_TERMINATION << "ERROR (Chromosome::CheckPartialInitializationForProperty): Chromosome method " << EidosStringRegistry::StringForGlobalStringID(p_method_id) << "() cannot be called until initialize() callbacks are complete; the chromosome object is not yet fully initialized." << EidosTerminate(); } const EidosClass *Chromosome::Class(void) const { return gSLiM_Chromosome_Class; } void Chromosome::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay() << "<" << symbol_ << ">"; } EidosValue_SP Chromosome::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_genomicElements: { CheckPartialInitializationForProperty(p_property_id); EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_GenomicElement_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (GenomicElement *genomic_element : genomic_elements_) vec->push_object_element_NORR(genomic_element); return result_SP; } case gID_id: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(id_)); } case gID_isSexChromosome: { return is_sex_chromosome_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF; } case gID_intrinsicPloidy: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(intrinsic_ploidy_)); } case gID_lastPosition: { if (!extent_immutable_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property lastPosition is not yet defined, since the length of the target chromosome has not yet been determined; you could provide a specified length to initializeChromosome(), or avoid requesting the chromosome's lastPosition until the chromosome's initialization has been finalized (after the execution of initialize() callbacks)." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(last_position_)); } case gEidosID_length: { if (!extent_immutable_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property lastPosition is not yet defined, since the length of the target chromosome has not yet been determined; you could provide a specified length to initializeChromosome(), or avoid requesting the chromosome's length until the chromosome's initialization has been finalized (after the execution of initialize() callbacks)." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(last_position_ + 1)); } case gID_species: { CheckPartialInitializationForProperty(p_property_id); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(&species_, gSLiM_Species_Class)); } case gID_symbol: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(symbol_)); } case gEidosID_type: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(type_string_)); } case gID_hotspotEndPositions: { CheckPartialInitializationForProperty(p_property_id); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotEndPositions is only defined in nucleotide-based models." << EidosTerminate(); if (!single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotEndPositions is not defined since sex-specific hotspot maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(hotspot_end_positions_H_)); } case gID_hotspotEndPositionsM: { CheckPartialInitializationForProperty(p_property_id); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotEndPositionsM is only defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotEndPositionsM is not defined since sex-specific hotspot maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(hotspot_end_positions_M_)); } case gID_hotspotEndPositionsF: { CheckPartialInitializationForProperty(p_property_id); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotEndPositionsF is only defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotEndPositionsF is not defined since sex-specific hotspot maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(hotspot_end_positions_F_)); } case gID_hotspotMultipliers: { CheckPartialInitializationForProperty(p_property_id); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotMultipliers is only defined in nucleotide-based models." << EidosTerminate(); if (!single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotMultipliers is not defined since sex-specific hotspot maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(hotspot_multipliers_H_)); } case gID_hotspotMultipliersM: { CheckPartialInitializationForProperty(p_property_id); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotMultipliersM is only defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotMultipliersM is not defined since sex-specific hotspot maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(hotspot_multipliers_M_)); } case gID_hotspotMultipliersF: { CheckPartialInitializationForProperty(p_property_id); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotMultipliersF is only defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property hotspotMultipliersF is not defined since sex-specific hotspot maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(hotspot_multipliers_F_)); } case gID_mutationEndPositions: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationEndPositions is not defined in nucleotide-based models." << EidosTerminate(); if (!single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationEndPositions is not defined since sex-specific mutation rate maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(mutation_end_positions_H_)); } case gID_mutationEndPositionsM: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationEndPositionsM is not defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationEndPositionsM is not defined since sex-specific mutation rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(mutation_end_positions_M_)); } case gID_mutationEndPositionsF: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationEndPositionsF is not defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationEndPositionsF is not defined since sex-specific mutation rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(mutation_end_positions_F_)); } case gID_mutationRates: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationRates is not defined in nucleotide-based models." << EidosTerminate(); if (!single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationRates is not defined since sex-specific mutation rate maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(mutation_rates_H_)); } case gID_mutationRatesM: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationRatesM is not defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationRatesM is not defined since sex-specific mutation rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(mutation_rates_M_)); } case gID_mutationRatesF: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationRatesF is not defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property mutationRatesF is not defined since sex-specific mutation rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(mutation_rates_F_)); } case gID_overallMutationRate: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallMutationRate is not defined in nucleotide-based models." << EidosTerminate(); if (!single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallMutationRate is not defined since sex-specific mutation rate maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(overall_mutation_rate_H_userlevel_)); } case gID_overallMutationRateM: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallMutationRateM is not defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallMutationRateM is not defined since sex-specific mutation rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(overall_mutation_rate_M_userlevel_)); } case gID_overallMutationRateF: { CheckPartialInitializationForProperty(p_property_id); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallMutationRateF is not defined in nucleotide-based models." << EidosTerminate(); if (single_mutation_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallMutationRateF is not defined since sex-specific mutation rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(overall_mutation_rate_F_userlevel_)); } case gID_overallRecombinationRate: { CheckPartialInitializationForProperty(p_property_id); if (!single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallRecombinationRate is not defined since sex-specific recombination rate maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(overall_recombination_rate_H_userlevel_)); } case gID_overallRecombinationRateM: { CheckPartialInitializationForProperty(p_property_id); if (single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallRecombinationRateM is not defined since sex-specific recombination rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(overall_recombination_rate_M_userlevel_)); } case gID_overallRecombinationRateF: { CheckPartialInitializationForProperty(p_property_id); if (single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property overallRecombinationRateF is not defined since sex-specific recombination rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(overall_recombination_rate_F_userlevel_)); } case gID_recombinationEndPositions: { CheckPartialInitializationForProperty(p_property_id); if (!single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property recombinationEndPositions is not defined since sex-specific recombination rate maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(recombination_end_positions_H_)); } case gID_recombinationEndPositionsM: { CheckPartialInitializationForProperty(p_property_id); if (single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property recombinationEndPositionsM is not defined since sex-specific recombination rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(recombination_end_positions_M_)); } case gID_recombinationEndPositionsF: { CheckPartialInitializationForProperty(p_property_id); if (single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property recombinationEndPositionsF is not defined since sex-specific recombination rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(recombination_end_positions_F_)); } case gID_recombinationRates: { CheckPartialInitializationForProperty(p_property_id); if (!single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property recombinationRates is not defined since sex-specific recombination rate maps have been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(recombination_rates_H_)); } case gID_recombinationRatesM: { CheckPartialInitializationForProperty(p_property_id); if (single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property recombinationRatesM is not defined since sex-specific recombination rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(recombination_rates_M_)); } case gID_recombinationRatesF: { CheckPartialInitializationForProperty(p_property_id); if (single_recombination_map_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property recombinationRatesF is not defined since sex-specific recombination rate maps have not been defined." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(recombination_rates_F_)); } // variables case gID_colorSubstitution: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(color_sub_)); } case gID_geneConversionEnabled: { CheckPartialInitializationForProperty(p_property_id); return using_DSB_model_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF; } case gID_geneConversionGCBias: { CheckPartialInitializationForProperty(p_property_id); if (!using_DSB_model_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property geneConversionGCBias is not defined since the DSB recombination model is not being used." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(mismatch_repair_bias_)); } case gID_geneConversionNonCrossoverFraction: { CheckPartialInitializationForProperty(p_property_id); if (!using_DSB_model_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property geneConversionNonCrossoverFraction is not defined since the DSB recombination model is not being used." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(non_crossover_fraction_)); } case gID_geneConversionMeanLength: { CheckPartialInitializationForProperty(p_property_id); if (!using_DSB_model_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property geneConversionMeanLength is not defined since the DSB recombination model is not being used." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(gene_conversion_avg_length_)); } case gID_geneConversionSimpleConversionFraction: { CheckPartialInitializationForProperty(p_property_id); if (!using_DSB_model_) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property geneConversionSimpleConversionFraction is not defined since the DSB recombination model is not being used." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(simple_conversion_fraction_)); } case gID_name: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(name_)); } case gID_tag: { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Chromosome::GetProperty): property tag accessed on chromosome before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } // all others, including gID_none default: return super::GetProperty(p_property_id); } } void Chromosome::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { case gID_colorSubstitution: { color_sub_ = p_value.StringAtIndex_NOCAST(0, nullptr); if (!color_sub_.empty()) Eidos_GetColorComponents(color_sub_, &color_sub_red_, &color_sub_green_, &color_sub_blue_); return; } case gID_name: { name_ = p_value.StringAtIndex_NOCAST(0, nullptr); return; } case gID_tag: { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; return; } // all others, including gID_none default: return super::SetProperty(p_property_id, p_value); } } EidosValue_SP Chromosome::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_ancestralNucleotides: return ExecuteMethod_ancestralNucleotides(p_method_id, p_arguments, p_interpreter); case gID_genomicElementForPosition: return ExecuteMethod_genomicElementForPosition(p_method_id, p_arguments, p_interpreter); case gID_hasGenomicElementForPosition: return ExecuteMethod_hasGenomicElementForPosition(p_method_id, p_arguments, p_interpreter); case gID_setAncestralNucleotides: return ExecuteMethod_setAncestralNucleotides(p_method_id, p_arguments, p_interpreter); case gID_setGeneConversion: return ExecuteMethod_setGeneConversion(p_method_id, p_arguments, p_interpreter); case gID_setHotspotMap: return ExecuteMethod_setHotspotMap(p_method_id, p_arguments, p_interpreter); case gID_setMutationRate: return ExecuteMethod_setMutationRate(p_method_id, p_arguments, p_interpreter); case gID_setRecombinationRate: return ExecuteMethod_setRecombinationRate(p_method_id, p_arguments, p_interpreter); case gID_drawBreakpoints: return ExecuteMethod_drawBreakpoints(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* – (is)ancestralNucleotides([Ni$ start = NULL], [Ni$ end = NULL], [s$ format = "string"]) // EidosValue_SP Chromosome::ExecuteMethod_ancestralNucleotides(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); // The ancestral sequence is actually kept by Species, so go get it if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_ancestralNucleotides): ancestralNucleotides() may only be called in nucleotide-based models." << EidosTerminate(); NucleotideArray *sequence = AncestralSequence(); EidosValue *start_value = p_arguments[0].get(); EidosValue *end_value = p_arguments[1].get(); int64_t start = (start_value->Type() == EidosValueType::kValueNULL) ? 0 : start_value->IntAtIndex_NOCAST(0, nullptr); int64_t end = (end_value->Type() == EidosValueType::kValueNULL) ? last_position_ : end_value->IntAtIndex_NOCAST(0, nullptr); if ((start < 0) || (end < 0) || (start > last_position_) || (end > last_position_) || (start > end)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_ancestralNucleotides): start and end must be within the chromosome's extent, and start must be <= end." << EidosTerminate(); if (((std::size_t)start >= sequence->size()) || ((std::size_t)end >= sequence->size())) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_ancestralNucleotides): (internal error) start and end must be within the ancestral sequence's length." << EidosTerminate(); int64_t length = end - start + 1; if (length > INT_MAX) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_ancestralNucleotides): the returned vector would exceed the maximum vector length in Eidos." << EidosTerminate(); EidosValue_String *format_value = (EidosValue_String *)p_arguments[2].get(); const std::string &format = format_value->StringRefAtIndex_NOCAST(0, nullptr); if (format == "codon") return sequence->NucleotidesAsCodonVector(start, end, /* p_force_vector */ false); if (format == "string") return sequence->NucleotidesAsStringSingleton(start, end); if (format == "integer") return sequence->NucleotidesAsIntegerVector(start, end); if (format == "char") return sequence->NucleotidesAsStringVector(start, end); EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_ancestralNucleotides): parameter format must be either 'string', 'char', 'integer', or 'codon'." << EidosTerminate(); } // ********************* – (integer)drawBreakpoints([No$ parent = NULL], [Ni$ n = NULL]) // EidosValue_SP Chromosome::ExecuteMethod_drawBreakpoints(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); EidosValue *parent_value = p_arguments[0].get(); EidosValue *n_value = p_arguments[1].get(); Individual *parent = nullptr; if (parent_value->Type() != EidosValueType::kValueNULL) parent = (Individual *)parent_value->ObjectElementAtIndex_NOCAST(0, nullptr); int num_breakpoints = -1; // means "draw them for us" if (n_value->Type() != EidosValueType::kValueNULL) { int64_t n = n_value->IntAtIndex_NOCAST(0, nullptr); if ((n < 0) || (n > 1000000L)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_drawBreakpoints): drawBreakpoints() requires 0 <= n <= 1000000." << EidosTerminate(); num_breakpoints = (int)n; } std::vector all_breakpoints; // Note that for calling recombination() callbacks below, we always treat the parent's first haplosome as // the initial copy strand. This is documented; it is perhaps a weakness of the API here, but if we // randomly chose an initial copy strand it would not be used downstream, so. // FIXME an idea: a new parameter, [l$ randomizeStrands = F], could be added that, if true, would // put a breakpoint at 0 half of the time, regardless of recombination rate, so the initial copy // strand is randomized for anyone using the generated breakpoints. This solves the problem, a // little bit clunkily. The main client of this method is users of addRecombinant(), though, and // it now has its own randomizeStrands flag, so maybe this change is unnecessary? DrawBreakpoints(parent, /* p_haplosome1 */ nullptr, /* p_haplosome2 */ nullptr, num_breakpoints, all_breakpoints, /* p_heteroduplex */ nullptr, "drawBreakpoints()"); if (all_breakpoints.size() == 0) return gStaticEidosValue_Integer_ZeroVec; else return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(all_breakpoints)); } GenomicElement *Chromosome::ElementForPosition(slim_position_t pos) { auto element_iter = std::lower_bound(genomic_elements_.begin(), genomic_elements_.end(), pos, [](const GenomicElement *e, slim_position_t position) { return e->end_position_ < position; }); if (element_iter == genomic_elements_.end()) return nullptr; GenomicElement *element = *element_iter; if (element->start_position_ > pos) return nullptr; return element; } // ********************* (object)genomicElementForPosition(integer positions) // EidosValue_SP Chromosome::ExecuteMethod_genomicElementForPosition(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); EidosValue *positions_value = p_arguments[0].get(); int positions_count = positions_value->Count(); EidosValue_Object_SP obj_result_SP = EidosValue_Object_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_GenomicElement_Class)); EidosValue_Object *obj_result = obj_result_SP->reserve(positions_count); const int64_t *positions_data = positions_value->IntData(); for (int pos_index = 0; pos_index < positions_count; ++pos_index) { int64_t pos = positions_data[pos_index]; GenomicElement *element = ElementForPosition(pos); if (element) obj_result->push_object_element_no_check_NORR(element); } return obj_result_SP; } // ********************* (logical)hasGenomicElementForPosition(integer positions) // EidosValue_SP Chromosome::ExecuteMethod_hasGenomicElementForPosition(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); EidosValue *positions_value = p_arguments[0].get(); int positions_count = positions_value->Count(); EidosValue_Logical_SP logical_result_SP = EidosValue_Logical_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Logical()); EidosValue_Logical *logical_result = logical_result_SP->reserve(positions_count); const int64_t *positions_data = positions_value->IntData(); for (int pos_index = 0; pos_index < positions_count; ++pos_index) { int64_t pos = positions_data[pos_index]; logical_result->push_logical_no_check(ElementForPosition(pos) ? true : false); } return logical_result_SP; } // ********************* (integer$)setAncestralNucleotides(is sequence) // EidosValue_SP Chromosome::ExecuteMethod_setAncestralNucleotides(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); EidosValue *sequence_value = p_arguments[0].get(); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setAncestralNucleotides): initializeAncestralNucleotides() may be only be called in nucleotide-based models." << EidosTerminate(); EidosValueType sequence_value_type = sequence_value->Type(); int sequence_value_count = sequence_value->Count(); if (sequence_value_count == 0) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setAncestralNucleotides): initializeAncestralNucleotides() requires a sequence of length >= 1." << EidosTerminate(); if (ancestral_seq_buffer_) { delete ancestral_seq_buffer_; ancestral_seq_buffer_ = nullptr; } if (sequence_value_type == EidosValueType::kValueInt) { // A vector of integers has been provided, where ACGT == 0123 const int64_t *int_data = sequence_value->IntData(); ancestral_seq_buffer_ = new NucleotideArray(sequence_value_count, int_data); } else if (sequence_value_type == EidosValueType::kValueString) { if (sequence_value_count != 1) { // A vector of characters has been provided, which must all be "A" / "C" / "G" / "T" const std::string *string_data = sequence_value->StringData(); ancestral_seq_buffer_ = new NucleotideArray(sequence_value_count, string_data); } else // sequence_value_count == 1 { const std::string &sequence_string = sequence_value->StringData()[0]; bool contains_only_nuc = true; // OK, we do a weird thing here. We want to try to construct a NucleotideArray // from sequence_string, which throws with EIDOS_TERMINATION if it fails, but // we want to actually catch that exception even if we're running at the // command line, where EIDOS_TERMINATION normally calls exit(). So we actually // play around with the error-handling state to make it do what we want it to do. // This is very naughty and should be redesigned, but right now I'm not seeing // the right redesign strategy, so... hacking it for now. Parallel code is at // Species::ExecuteContextFunction_initializeAncestralNucleotides() bool save_gEidosTerminateThrows = gEidosTerminateThrows; gEidosTerminateThrows = true; try { ancestral_seq_buffer_ = new NucleotideArray(sequence_string.length(), sequence_string.c_str()); } catch (...) { contains_only_nuc = false; // clean up the error state since we don't want this throw to be reported gEidosTermination.clear(); gEidosTermination.str(""); } gEidosTerminateThrows = save_gEidosTerminateThrows; if (!contains_only_nuc) { // A singleton string has been provided that contains characters other than ACGT; we will interpret it as a filesystem path for a FASTA file std::string file_path = Eidos_ResolvedPath(sequence_string); std::ifstream file_stream(file_path.c_str()); if (!file_stream.is_open()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setAncestralNucleotides): the file at path " << sequence_string << " could not be opened or does not exist." << EidosTerminate(); bool started_sequence = false; std::string line, fasta_sequence; while (getline(file_stream, line)) { // skippable lines are blank or start with a '>' or ';' // we skip over them if they're at the start of the file; once we start a sequence, they terminate the sequence bool skippable = ((line.length() == 0) || (line[0] == '>') || (line[0] == ';')); if (!started_sequence && skippable) continue; if (skippable) break; // otherwise, append the nucleotides from this line, removing a \r if one is present at the end of the line if (line.back() == '\r') line.pop_back(); fasta_sequence.append(line); started_sequence = true; } if (file_stream.bad()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setAncestralNucleotides): a filesystem error occurred while reading the file at path " << sequence_string << "." << EidosTerminate(); if (fasta_sequence.length() == 0) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setAncestralNucleotides): no FASTA sequence found in " << sequence_string << "." << EidosTerminate(); ancestral_seq_buffer_ = new NucleotideArray(fasta_sequence.length(), fasta_sequence.c_str()); } } } else { EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setAncestralNucleotides): (internal error) unrecognized sequence type." << EidosTerminate(); } // check that the length of the new sequence matches the chromosome length if (ancestral_seq_buffer_->size() != (std::size_t)(last_position_ + 1)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setAncestralNucleotides): The chromosome length (" << last_position_ + 1 << " base" << (last_position_ + 1 != 1 ? "s" : "") << ") does not match the ancestral sequence length (" << ancestral_seq_buffer_->size() << " base" << (ancestral_seq_buffer_->size() != 1 ? "s" : "") << ")." << EidosTerminate(); // debugging //std::cout << "ancestral sequence set: " << *ancestral_seq_buffer_ << std::endl; return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(ancestral_seq_buffer_->size())); } // ********************* – (void)setGeneConversion(numeric$ nonCrossoverFraction, numeric$ meanLength, numeric$ simpleConversionFraction, [numeric$ bias = 0]) // EidosValue_SP Chromosome::ExecuteMethod_setGeneConversion(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); if (!species_.HasGenetics()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setGeneConversion): setGeneConversion() may not be called for a species with no genetics." << EidosTerminate(); EidosValue *nonCrossoverFraction_value = p_arguments[0].get(); EidosValue *meanLength_value = p_arguments[1].get(); EidosValue *simpleConversionFraction_value = p_arguments[2].get(); EidosValue *bias_value = p_arguments[3].get(); double non_crossover_fraction = nonCrossoverFraction_value->NumericAtIndex_NOCAST(0, nullptr); double gene_conversion_avg_length = meanLength_value->NumericAtIndex_NOCAST(0, nullptr); double simple_conversion_fraction = simpleConversionFraction_value->NumericAtIndex_NOCAST(0, nullptr); double bias = bias_value->NumericAtIndex_NOCAST(0, nullptr); if ((non_crossover_fraction < 0.0) || (non_crossover_fraction > 1.0) || std::isnan(non_crossover_fraction)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setGeneConversion): setGeneConversion() nonCrossoverFraction must be between 0.0 and 1.0 inclusive (" << EidosStringForFloat(non_crossover_fraction) << " supplied)." << EidosTerminate(); if ((gene_conversion_avg_length < 0.0) || std::isnan(gene_conversion_avg_length)) // intentionally no upper bound EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setGeneConversion): setGeneConversion() meanLength must be >= 0.0 (" << EidosStringForFloat(gene_conversion_avg_length) << " supplied)." << EidosTerminate(); if ((simple_conversion_fraction < 0.0) || (simple_conversion_fraction > 1.0) || std::isnan(simple_conversion_fraction)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setGeneConversion): setGeneConversion() simpleConversionFraction must be between 0.0 and 1.0 inclusive (" << EidosStringForFloat(simple_conversion_fraction) << " supplied)." << EidosTerminate(); if ((bias < -1.0) || (bias > 1.0) || std::isnan(bias)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setGeneConversion): setGeneConversion() bias must be between -1.0 and 1.0 inclusive (" << EidosStringForFloat(bias) << " supplied)." << EidosTerminate(); if ((bias != 0.0) && !species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setGeneConversion): setGeneConversion() bias must be 0.0 in non-nucleotide-based models." << EidosTerminate(); using_DSB_model_ = true; non_crossover_fraction_ = non_crossover_fraction; gene_conversion_avg_length_ = gene_conversion_avg_length; gene_conversion_inv_half_length_ = 1.0 / (gene_conversion_avg_length / 2.0); simple_conversion_fraction_ = simple_conversion_fraction; mismatch_repair_bias_ = bias; return gStaticEidosValueVOID; } // ********************* – (void)setHotspotMap(numeric multipliers, [Ni ends = NULL], [string$ sex = "*"]) // EidosValue_SP Chromosome::ExecuteMethod_setHotspotMap(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); if (!species_.HasGenetics()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() may not be called for a species with no genetics." << EidosTerminate(); if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() may only be called in nucleotide-based models (use setMutationRate() to vary the mutation rate along the chromosome)." << EidosTerminate(); EidosValue *multipliers_value = p_arguments[0].get(); EidosValue *ends_value = p_arguments[1].get(); EidosValue *sex_value = p_arguments[2].get(); int multipliers_count = multipliers_value->Count(); // Figure out what sex we are being given a map for IndividualSex requested_sex = IndividualSex::kUnspecified; std::string sex_string = sex_value->StringAtIndex_NOCAST(0, nullptr); if (sex_string.compare("M") == 0) requested_sex = IndividualSex::kMale; else if (sex_string.compare("F") == 0) requested_sex = IndividualSex::kFemale; else if (sex_string.compare("*") == 0) requested_sex = IndividualSex::kUnspecified; else EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() requested sex '" << sex_string << "' unsupported." << EidosTerminate(); // Make sure specifying a map for that sex is legal, given our current state if (((requested_sex == IndividualSex::kUnspecified) && !single_mutation_map_) || ((requested_sex != IndividualSex::kUnspecified) && single_mutation_map_)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() cannot change the chromosome between using a single map versus separate maps for the sexes; the original configuration must be preserved." << EidosTerminate(); // Set up to replace the requested map std::vector &positions = ((requested_sex == IndividualSex::kUnspecified) ? hotspot_end_positions_H_ : ((requested_sex == IndividualSex::kMale) ? hotspot_end_positions_M_ : hotspot_end_positions_F_)); std::vector &multipliers = ((requested_sex == IndividualSex::kUnspecified) ? hotspot_multipliers_H_ : ((requested_sex == IndividualSex::kMale) ? hotspot_multipliers_M_ : hotspot_multipliers_F_)); if (ends_value->Type() == EidosValueType::kValueNULL) { // ends is missing/NULL if (multipliers_count != 1) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() requires multipliers to be a singleton if ends is not supplied." << EidosTerminate(); double multiplier = multipliers_value->NumericAtIndex_NOCAST(0, nullptr); // check values if ((multiplier < 0.0) || !std::isfinite(multiplier)) // intentionally no upper bound EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() multiplier " << EidosStringForFloat(multiplier) << " out of range; multipliers must be >= 0." << EidosTerminate(); // then adopt them multipliers.resize(0); positions.resize(0); multipliers.emplace_back(multiplier); positions.emplace_back(last_position_); } else { // ends is supplied int end_count = ends_value->Count(); if ((end_count != multipliers_count) || (end_count == 0)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() requires ends and multipliers to be of equal and nonzero size." << EidosTerminate(); // check values for (int value_index = 0; value_index < end_count; ++value_index) { double multiplier = multipliers_value->NumericAtIndex_NOCAST(value_index, nullptr); slim_position_t mutation_end_position = SLiMCastToPositionTypeOrRaise(ends_value->IntAtIndex_NOCAST(value_index, nullptr)); if (value_index > 0) if (mutation_end_position <= ends_value->IntAtIndex_NOCAST(value_index - 1, nullptr)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() requires ends to be in strictly ascending order." << EidosTerminate(); if ((multiplier < 0.0) || !std::isfinite(multiplier)) // intentionally no upper bound EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() multiplier " << multiplier << " out of range; multipliers must be >= 0." << EidosTerminate(); } // The stake here is that the last position in the chromosome is not allowed to change after the chromosome is // constructed. When we call InitializeDraws() below, we recalculate the last position – and we must come up // with the same answer that we got before, otherwise our last_position_ cache is invalid. int64_t new_last_position = ends_value->IntAtIndex_NOCAST(end_count - 1, nullptr); if (new_last_position != last_position_) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setHotspotMap): setHotspotMap() end " << new_last_position << " noncompliant; the last interval must end at the last position of the chromosome (" << last_position_ << ")." << EidosTerminate(); // then adopt them multipliers.resize(0); positions.resize(0); for (int interval_index = 0; interval_index < end_count; ++interval_index) { double multiplier = multipliers_value->NumericAtIndex_NOCAST(interval_index, nullptr); slim_position_t mutation_end_position = SLiMCastToPositionTypeOrRaise(ends_value->IntAtIndex_NOCAST(interval_index, nullptr)); multipliers.emplace_back(multiplier); positions.emplace_back(mutation_end_position); } } CreateNucleotideMutationRateMap(); InitializeDraws(); return gStaticEidosValueVOID; } // ********************* – (void)setMutationRate(numeric rates, [Ni ends = NULL], [string$ sex = "*"]) // EidosValue_SP Chromosome::ExecuteMethod_setMutationRate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); if (!species_.HasGenetics()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() may not be called for a species with no genetics." << EidosTerminate(); if (species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() may not be called in nucleotide-based models (use setHotspotMap() to vary the mutation rate along the chromosome)." << EidosTerminate(); EidosValue *rates_value = p_arguments[0].get(); EidosValue *ends_value = p_arguments[1].get(); EidosValue *sex_value = p_arguments[2].get(); int rate_count = rates_value->Count(); // Figure out what sex we are being given a map for IndividualSex requested_sex = IndividualSex::kUnspecified; std::string sex_string = sex_value->StringAtIndex_NOCAST(0, nullptr); if (sex_string.compare("M") == 0) requested_sex = IndividualSex::kMale; else if (sex_string.compare("F") == 0) requested_sex = IndividualSex::kFemale; else if (sex_string.compare("*") == 0) requested_sex = IndividualSex::kUnspecified; else EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() requested sex '" << sex_string << "' unsupported." << EidosTerminate(); // Make sure specifying a map for that sex is legal, given our current state if (((requested_sex == IndividualSex::kUnspecified) && !single_mutation_map_) || ((requested_sex != IndividualSex::kUnspecified) && single_mutation_map_)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() cannot change the chromosome between using a single map versus separate maps for the sexes; the original configuration must be preserved." << EidosTerminate(); // Set up to replace the requested map std::vector &positions = ((requested_sex == IndividualSex::kUnspecified) ? mutation_end_positions_H_ : ((requested_sex == IndividualSex::kMale) ? mutation_end_positions_M_ : mutation_end_positions_F_)); std::vector &rates = ((requested_sex == IndividualSex::kUnspecified) ? mutation_rates_H_ : ((requested_sex == IndividualSex::kMale) ? mutation_rates_M_ : mutation_rates_F_)); if (ends_value->Type() == EidosValueType::kValueNULL) { // ends is missing/NULL if (rate_count != 1) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() requires rates to be a singleton if ends is not supplied." << EidosTerminate(); double mutation_rate = rates_value->NumericAtIndex_NOCAST(0, nullptr); // check values if ((mutation_rate < 0.0) || !std::isfinite(mutation_rate)) // intentionally no upper bound EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() rate " << EidosStringForFloat(mutation_rate) << " out of range; rates must be >= 0." << EidosTerminate(); // then adopt them rates.resize(0); positions.resize(0); rates.emplace_back(mutation_rate); //positions.emplace_back(?); // deferred; patched in Chromosome::InitializeDraws(). } else { // ends is supplied int end_count = ends_value->Count(); if ((end_count != rate_count) || (end_count == 0)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() requires ends and rates to be of equal and nonzero size." << EidosTerminate(); // check values for (int value_index = 0; value_index < end_count; ++value_index) { double mutation_rate = rates_value->NumericAtIndex_NOCAST(value_index, nullptr); slim_position_t mutation_end_position = SLiMCastToPositionTypeOrRaise(ends_value->IntAtIndex_NOCAST(value_index, nullptr)); if (value_index > 0) if (mutation_end_position <= ends_value->IntAtIndex_NOCAST(value_index - 1, nullptr)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() requires ends to be in strictly ascending order." << EidosTerminate(); if ((mutation_rate < 0.0) || !std::isfinite(mutation_rate)) // intentionally no upper bound EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() rate " << EidosStringForFloat(mutation_rate) << " out of range; rates must be >= 0." << EidosTerminate(); } // The stake here is that the last position in the chromosome is not allowed to change after the chromosome is // constructed. When we call InitializeDraws() below, we recalculate the last position – and we must come up // with the same answer that we got before, otherwise our last_position_ cache is invalid. int64_t new_last_position = ends_value->IntAtIndex_NOCAST(end_count - 1, nullptr); if (new_last_position != last_position_) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setMutationRate): setMutationRate() end " << new_last_position << " noncompliant; the last interval must end at the last position of the chromosome (" << last_position_ << ")." << EidosTerminate(); // then adopt them rates.resize(0); positions.resize(0); for (int interval_index = 0; interval_index < end_count; ++interval_index) { double mutation_rate = rates_value->NumericAtIndex_NOCAST(interval_index, nullptr); slim_position_t mutation_end_position = SLiMCastToPositionTypeOrRaise(ends_value->IntAtIndex_NOCAST(interval_index, nullptr)); rates.emplace_back(mutation_rate); positions.emplace_back(mutation_end_position); } } InitializeDraws(); return gStaticEidosValueVOID; } // ********************* – (void)setRecombinationRate(numeric rates, [Ni ends = NULL], [string$ sex = "*"]) // EidosValue_SP Chromosome::ExecuteMethod_setRecombinationRate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) CheckPartialInitializationForMethod(p_method_id); if (!species_.HasGenetics()) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() may not be called for a species with no genetics." << EidosTerminate(); EidosValue *rates_value = p_arguments[0].get(); EidosValue *ends_value = p_arguments[1].get(); EidosValue *sex_value = p_arguments[2].get(); int rate_count = rates_value->Count(); // Figure out what sex we are being given a map for IndividualSex requested_sex = IndividualSex::kUnspecified; std::string sex_string = sex_value->StringAtIndex_NOCAST(0, nullptr); if (sex_string.compare("M") == 0) requested_sex = IndividualSex::kMale; else if (sex_string.compare("F") == 0) requested_sex = IndividualSex::kFemale; else if (sex_string.compare("*") == 0) requested_sex = IndividualSex::kUnspecified; else EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() requested sex '" << sex_string << "' unsupported." << EidosTerminate(); // Make sure specifying a map for that sex is legal, given our current state if (((requested_sex == IndividualSex::kUnspecified) && !single_recombination_map_) || ((requested_sex != IndividualSex::kUnspecified) && single_recombination_map_)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() cannot change the chromosome between using a single map versus separate maps for the sexes; the original configuration must be preserved." << EidosTerminate(); // Set up to replace the requested map std::vector &positions = ((requested_sex == IndividualSex::kUnspecified) ? recombination_end_positions_H_ : ((requested_sex == IndividualSex::kMale) ? recombination_end_positions_M_ : recombination_end_positions_F_)); std::vector &rates = ((requested_sex == IndividualSex::kUnspecified) ? recombination_rates_H_ : ((requested_sex == IndividualSex::kMale) ? recombination_rates_M_ : recombination_rates_F_)); if (ends_value->Type() == EidosValueType::kValueNULL) { // ends is missing/NULL if (rate_count != 1) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() requires rates to be a singleton if ends is not supplied." << EidosTerminate(); double recombination_rate = rates_value->NumericAtIndex_NOCAST(0, nullptr); // check values if ((recombination_rate < 0.0) || (recombination_rate > 0.5) || std::isnan(recombination_rate)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() rate " << recombination_rate << " out of range; rates must be in [0.0, 0.5]." << EidosTerminate(); // then adopt them rates.resize(0); positions.resize(0); rates.emplace_back(recombination_rate); //positions.emplace_back(?); // deferred; patched in Chromosome::InitializeDraws(). } else { // ends is supplied int end_count = ends_value->Count(); if ((end_count != rate_count) || (end_count == 0)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() requires ends and rates to be of equal and nonzero size." << EidosTerminate(); // check values for (int value_index = 0; value_index < end_count; ++value_index) { double recombination_rate = rates_value->NumericAtIndex_NOCAST(value_index, nullptr); slim_position_t recombination_end_position = SLiMCastToPositionTypeOrRaise(ends_value->IntAtIndex_NOCAST(value_index, nullptr)); if (value_index > 0) if (recombination_end_position <= ends_value->IntAtIndex_NOCAST(value_index - 1, nullptr)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() requires ends to be in strictly ascending order." << EidosTerminate(); if ((recombination_rate < 0.0) || (recombination_rate > 0.5) || std::isnan(recombination_rate)) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() rate " << recombination_rate << " out of range; rates must be in [0.0, 0.5]." << EidosTerminate(); } // The stake here is that the last position in the chromosome is not allowed to change after the chromosome is // constructed. When we call InitializeDraws() below, we recalculate the last position – and we must come up // with the same answer that we got before. int64_t new_last_position = ends_value->IntAtIndex_NOCAST(end_count - 1, nullptr); if (new_last_position != last_position_) EIDOS_TERMINATION << "ERROR (Chromosome::ExecuteMethod_setRecombinationRate): setRecombinationRate() rate " << new_last_position << " noncompliant; the last interval must end at the last position of the chromosome (" << last_position_ << ")." << EidosTerminate(); // then adopt them rates.resize(0); positions.resize(0); for (int interval_index = 0; interval_index < end_count; ++interval_index) { double recombination_rate = rates_value->NumericAtIndex_NOCAST(interval_index, nullptr); slim_position_t recombination_end_position = SLiMCastToPositionTypeOrRaise(ends_value->IntAtIndex_NOCAST(interval_index, nullptr)); rates.emplace_back(recombination_rate); positions.emplace_back(recombination_end_position); } } InitializeDraws(); return gStaticEidosValueVOID; } // // Chromosome_Class // #pragma mark - #pragma mark Chromosome_Class #pragma mark - EidosClass *gSLiM_Chromosome_Class = nullptr; const std::vector *Chromosome_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("Chromosome_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_genomicElements, true, kEidosValueMaskObject, gSLiM_GenomicElement_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_id, true, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_isSexChromosome, true, kEidosValueMaskLogical | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_intrinsicPloidy, true, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_lastPosition, true, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_length, true, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_hotspotEndPositions, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_hotspotEndPositionsM, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_hotspotEndPositionsF, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_hotspotMultipliers, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_hotspotMultipliersM, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_hotspotMultipliersF, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationEndPositions, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationEndPositionsM, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationEndPositionsF, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationRates, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationRatesM, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationRatesF, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_name, false, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_overallMutationRate, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_overallMutationRateM, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_overallMutationRateF, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_overallRecombinationRate, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_overallRecombinationRateM, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_overallRecombinationRateF, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_recombinationEndPositions, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_recombinationEndPositionsM, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_recombinationEndPositionsF, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_recombinationRates, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_recombinationRatesM, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_recombinationRatesF, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_species, true, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Species_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_symbol, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_geneConversionEnabled, true, kEidosValueMaskLogical | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_geneConversionGCBias, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_geneConversionNonCrossoverFraction, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_geneConversionMeanLength, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_geneConversionSimpleConversionFraction, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_type, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_colorSubstitution, false, kEidosValueMaskString | kEidosValueMaskSingleton))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *Chromosome_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("Chromosome_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_ancestralNucleotides, kEidosValueMaskInt | kEidosValueMaskString))->AddInt_OSN(gEidosStr_start, gStaticEidosValueNULL)->AddInt_OSN(gEidosStr_end, gStaticEidosValueNULL)->AddString_OS("format", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("string")))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_drawBreakpoints, kEidosValueMaskInt))->AddObject_OSN("parent", gSLiM_Individual_Class, gStaticEidosValueNULL)->AddInt_OSN("n", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_genomicElementForPosition, kEidosValueMaskObject, gSLiM_GenomicElement_Class))->AddInt("positions")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_hasGenomicElementForPosition, kEidosValueMaskLogical))->AddInt("positions")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setAncestralNucleotides, kEidosValueMaskInt | kEidosValueMaskSingleton))->AddIntString("sequence")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setGeneConversion, kEidosValueMaskVOID))->AddNumeric_S("nonCrossoverFraction")->AddNumeric_S("meanLength")->AddNumeric_S("simpleConversionFraction")->AddNumeric_OS("bias", gStaticEidosValue_Integer0)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setHotspotMap, kEidosValueMaskVOID))->AddNumeric("multipliers")->AddInt_ON("ends", gStaticEidosValueNULL)->AddString_OS("sex", gStaticEidosValue_StringAsterisk)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setMutationRate, kEidosValueMaskVOID))->AddNumeric("rates")->AddInt_ON("ends", gStaticEidosValueNULL)->AddString_OS("sex", gStaticEidosValue_StringAsterisk)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setRecombinationRate, kEidosValueMaskVOID))->AddNumeric("rates")->AddInt_ON("ends", gStaticEidosValueNULL)->AddString_OS("sex", gStaticEidosValue_StringAsterisk)); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: core/chromosome.h ================================================ // // chromosome.h // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class Chromosome represents an entire chromosome. Only the portions of the chromosome that are relevant to the simulation are explicitly modeled, so in practice, a chromosome is a vector of genomic elements defined by the input file. A chromosome also has a length, an overall mutation rate, an overall recombination rate, and parameters related to gene conversion. */ #ifndef __SLiM__chromosome__ #define __SLiM__chromosome__ #include #include #include "mutation.h" #include "mutation_type.h" #include "genomic_element.h" #include "genomic_element_type.h" #include "eidos_rng.h" #include "eidos_value.h" struct GESubrange; class Haplosome; class Species; class Individual; extern EidosClass *gSLiM_Chromosome_Class; class Chromosome : public EidosDictionaryRetained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryRetained super; #ifdef SLIMGUI public: #else private: #endif int64_t id_; std::string symbol_; std::string name_; slim_chromosome_index_t index_; ChromosomeType type_; // cached properties of the chromosome that depend upon its type int intrinsic_ploidy_; // 1 or 2; the number of haplosomes kept for the chromosome bool always_uses_null_haplosomes_; // true for types that *always* involve null haplosomes bool is_sex_chromosome_; // true for types X, Y, Z, W, and -Y bool defaults_to_zero_recombination_; // false only for types A, X, and Z, which are truly diploid std::string type_string_; // a string like "A", "X", etc. for the type // This vector contains all the genomic elements for this chromosome. It is in sorted order once initialization is complete. std::vector genomic_elements_; // OWNED POINTERS: genomic elements belong to the chromosome // We now allow different recombination maps for males and females, optionally. Unfortunately, this means we have a bit of an // explosion of state involved with recombination. We now have _H, _M, and _F versions of many ivars. The _M and _F versions // are used if sex is enabled and different recombination maps have been specified for males versus females. The _H version // is used in all other cases – when sex is not enabled (i.e., hermaphrodites), and when separate maps have not been specified. // This flag indicates which option has been chosen; after initialize() time this cannot be changed. bool single_recombination_map_ = true; // The same is now true for mutation rate maps as well; we now have _H, _M, and _F versions of those, just as with recombination // maps. This flag indicates which option has been chosen; after initialize() time this cannot be changed. bool single_mutation_map_ = true; gsl_ran_discrete_t *lookup_mutation_H_ = nullptr; // OWNED POINTER: lookup table for drawing mutations gsl_ran_discrete_t *lookup_mutation_M_ = nullptr; gsl_ran_discrete_t *lookup_mutation_F_ = nullptr; gsl_ran_discrete_t *lookup_recombination_H_ = nullptr; // OWNED POINTER: lookup table for drawing recombination breakpoints gsl_ran_discrete_t *lookup_recombination_M_ = nullptr; gsl_ran_discrete_t *lookup_recombination_F_ = nullptr; // caches to speed up Poisson draws in CrossoverMutation() double exp_neg_overall_mutation_rate_H_; double exp_neg_overall_mutation_rate_M_; double exp_neg_overall_mutation_rate_F_; double exp_neg_overall_recombination_rate_H_; double exp_neg_overall_recombination_rate_M_; double exp_neg_overall_recombination_rate_F_; #ifndef USE_GSL_POISSON // joint probabilities, used to accelerate drawing a mutation count and breakpoint count jointly double probability_both_0_H_; double probability_both_0_OR_mut_0_break_non0_H_; double probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_H_; double probability_both_0_M_; double probability_both_0_OR_mut_0_break_non0_M_; double probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_M_; double probability_both_0_F_; double probability_both_0_OR_mut_0_break_non0_F_; double probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_F_; #endif // GESubrange vectors used to facilitate new mutation generation – draw a subrange, then draw a position inside the subrange std::vector mutation_subranges_H_; std::vector mutation_subranges_M_; std::vector mutation_subranges_F_; // This object pool for haplosomes is owned by Species; we have a reference to it just for efficiency EidosObjectPool &haplosome_pool_; // NOT OWNED: a pool out of which haplosomes are allocated, for within-species locality // These haplosome junkyards, on the other hand, belong to us; each Chromosome keeps its own junkyards in which objects // are guaranteed to have the correct chromosome index already set up, and the correct chromosome mutrun configuration std::vector haplosomes_junkyard_nonnull; // OWNED: non-null haplosomes go here when we're done with them, for reuse std::vector haplosomes_junkyard_null; // OWNED: null haplosomes go here when we're done with them, for reuse Haplosome *_NewHaplosome_NULL(Individual *p_individual); // internal use only; does not set chromosome_subposition_ Haplosome *_NewHaplosome_NONNULL(Individual *p_individual); // internal use only; does not set chromosome_subposition_ // Chromosome now keeps two MutationRunPools, one for freed MutationRun objects and one for in-use MutationRun objects, // as well as an object pool out of which completely new MutationRuns are allocated, all bundled in a MutationRunContext. // When running multithreaded, each of these becomes a vector of per-thread objects, so we can alloc/free runs in parallel code. // This stuff is not set up until after initialize() callbacks; nobody should be using MutationRuns before then. #ifndef _OPENMP MutationRunContext mutation_run_context_SINGLE_; #else int mutation_run_context_COUNT_ = 0; // the number of PERTHREAD contexts std::vector mutation_run_context_PERTHREAD; #endif // Mutation run optimization. The ivars here are used only internally by Species; the canonical reference regarding the // number and length of mutation runs is kept by Chromosome (for the simulation) and by Haplosome (for each haplosome object). // If Species decides to change the number of mutation runs, it will update those canonical repositories accordingly. // A prefix of x_ is used on all mutation run experiment ivars, to avoid confusion. int preferred_mutrun_count_ = 0; // preferred mutation run length (from the user); 0 represents no preference #define SLIM_MUTRUN_EXPERIMENT_LENGTH 50 // kind of based on how large a sample size is needed to detect important differences fairly reliably by t-test #define SLIM_MUTRUN_MAXIMUM_COUNT 1024 // the most mutation runs we will ever use; hard to imagine that any model will want more than this bool x_experiments_enabled_; // if false, no experiments are run and no cycle runtimes are recorded int32_t x_experiment_count_; // a counter of the number of experiments we've run (the number of t-tests we've conducted) int32_t x_current_mutcount_; // the number of mutation runs we're currently using double *x_current_runtimes_; // cycle runtimes recorded at this mutcount (SLIM_MUTRUN_EXPERIMENT_MAXLENGTH length) int x_current_buflen_; // the number of runtimes in the current_mutcount_runtimes_ buffer int32_t x_previous_mutcount_; // the number of mutation runs we previously used double *x_previous_runtimes_; // cycle runtimes recorded at that mutcount (SLIM_MUTRUN_EXPERIMENT_MAXLENGTH length) int x_previous_buflen_; // the number of runtimes in the previous_mutcount_runtimes_ buffer bool x_continuing_trend_; // if true, the current experiment continues a trend, such that the opposite trend can be excluded int64_t x_stasis_limit_; // how many stasis experiments we're running between change experiments; gets longer over time double x_stasis_alpha_; // the alpha threshold at which we decide that stasis has been broken; gets smaller over time int64_t x_stasis_counter_; // how many stasis experiments we have run so far int32_t x_prev1_stasis_mutcount_; // the number of mutation runs we settled on when we reached stasis last time int32_t x_prev2_stasis_mutcount_; // the number of mutation runs we settled on when we reached stasis the time before last std::vector x_mutcount_history_; // a record of the mutation run count used in each cycle eidos_profile_t x_current_clock_ = 0; // the clock for timing current being done bool x_clock_running_ = false; bool x_within_measurement_period_ = false; eidos_profile_t x_total_gen_clocks_ = 0; // a counter of clocks accumulated for the current cycle's runtime (across measured code blocks) // look at StartMutationRunExperimentClock() usage to see which blocks are measured // Mutation run experiments void TransitionToNewExperimentAgainstCurrentExperiment(int32_t p_new_mutrun_count); void TransitionToNewExperimentAgainstPreviousExperiment(int32_t p_new_mutrun_count); void EnterStasisForMutationRunExperiments(void); void MaintainMutationRunExperiments(double p_last_gen_runtime); // Erroring during partial initialization void CheckPartialInitializationForProperty(EidosGlobalStringID p_property_id); void CheckPartialInitializationForMethod(EidosGlobalStringID p_method_id); public: Community &community_; Species &species_; // the total haplosome count depends on the chromosome; it will be different for an autosome versus a sex chromosome, for example slim_refcount_t total_haplosome_count_ = 0; // the number of non-null haplosomes in the population; a fixed mutation has this count #ifdef SLIMGUI slim_refcount_t gui_total_haplosome_count_ = 0; // the number of non-null haplosomes in the selected subpopulations in SLiMgui #endif slim_refcount_t tallied_haplosome_count_ = 0; // the total non-null haplosomes counted for this chromosome in the last tally // mutation and recombination machinery std::vector mutation_end_positions_H_; // end positions of each defined mutation region (BEFORE intersection with GEs) std::vector mutation_end_positions_M_; std::vector mutation_end_positions_F_; std::vector mutation_rates_H_; // mutation rates, in events per base pair (BEFORE intersection with GEs) std::vector mutation_rates_M_; std::vector mutation_rates_F_; std::vector recombination_end_positions_H_; // end positions of each defined recombination region std::vector recombination_end_positions_M_; std::vector recombination_end_positions_F_; std::vector recombination_rates_H_; // recombination rates, in probability of crossover per base pair (user-specified) std::vector recombination_rates_M_; std::vector recombination_rates_F_; bool any_recombination_rates_05_ = false; // set to T if any recombination rate is 0.5; those are excluded from gene conversion slim_position_t last_position_; // last valid position bool extent_immutable_; // can the chromosome endpoint still be changed? double overall_mutation_rate_H_; // overall mutation rate (AFTER intersection with GEs) double overall_mutation_rate_M_; // overall mutation rate double overall_mutation_rate_F_; // overall mutation rate double overall_mutation_rate_H_userlevel_; // requested (un-adjusted) overall mutation rate (AFTER intersection with GEs) double overall_mutation_rate_M_userlevel_; // requested (un-adjusted) overall mutation rate double overall_mutation_rate_F_userlevel_; // requested (un-adjusted) overall mutation rate double overall_recombination_rate_H_; // overall recombination rate (reparameterized; see _InitializeOneRecombinationMap) double overall_recombination_rate_M_; // overall recombination rate (reparameterized; see _InitializeOneRecombinationMap) double overall_recombination_rate_F_; // overall recombination rate (reparameterized; see _InitializeOneRecombinationMap) double overall_recombination_rate_H_userlevel_; // overall recombination rate (unreparameterized; see _InitializeOneRecombinationMap) double overall_recombination_rate_M_userlevel_; // overall recombination rate (unreparameterized; see _InitializeOneRecombinationMap) double overall_recombination_rate_F_userlevel_; // overall recombination rate (unreparameterized; see _InitializeOneRecombinationMap) bool using_DSB_model_; // if true, we are using the DSB recombination model, involving the variables below double non_crossover_fraction_; // fraction of DSBs that do not result in crossover double gene_conversion_avg_length_; // average gene conversion stretch length double gene_conversion_inv_half_length_; // 1.0 / (gene_conversion_avg_length_ / 2.0), used for geometric draws double simple_conversion_fraction_; // fraction of gene conversion tracts that are "simple" (no heteroduplex mismatche repair) double mismatch_repair_bias_; // GC bias in heteroduplex mismatch repair in complex gene conversion tracts bool redraw_lengths_on_failure_; // if T, we redraw lengths, not only positions, if tract layout fails int32_t mutrun_count_base_; // minimum number of mutruns used (number of threads, typically); can be multiplied by a factor int32_t mutrun_count_multiplier_; // the current factor by which mutrun_count_base_ is multiplied; a power of two in [1, 1024] int32_t mutrun_count_; // the number of mutation runs being used for all haplosomes: base x multiplier slim_position_t mutrun_length_; // the length, in base pairs, of each mutation run; the last run might not use its full length slim_position_t last_position_mutrun_; // (mutrun_count_ * mutrun_length_ - 1), for complete coverage in crossover-mutation std::string color_sub_; // color to use for substitutions by default (in SLiMgui) float color_sub_red_, color_sub_green_, color_sub_blue_; // cached color components from color_sub_; should always be in sync // nucleotide-based models NucleotideArray *ancestral_seq_buffer_ = nullptr; std::vector hotspot_end_positions_H_; // end positions of each defined hotspot region (BEFORE intersection with GEs) std::vector hotspot_end_positions_M_; std::vector hotspot_end_positions_F_; std::vector hotspot_multipliers_H_; // hotspot multipliers (BEFORE intersection with GEs) std::vector hotspot_multipliers_M_; std::vector hotspot_multipliers_F_; // private scratch space for the use of Population::RemoveAllFixedMutations() std::vector fixed_mutation_accumulator_; int8_t scratch_; // temporary scratch space for use by algorithms; regard as volatile outside your own code block // a user-defined tag value slim_usertag_t tag_value_ = SLIM_TAG_UNSET_VALUE; // PROFILING : Chromosome keeps track of some additional profile information that is per-chromosome #if (SLIMPROFILING == 1) #if SLIM_USE_NONNEUTRAL_CACHES std::vector profile_mutcount_history_; // a record of the mutation run count used in each cycle int64_t profile_mutation_total_usage_ = 0; // how many (non-unique) mutations were used by mutation runs, summed across cycles int64_t profile_nonneutral_mutation_total_ = 0; // of profile_mutation_total_usage_, how many were deemed to be nonneutral int64_t profile_mutrun_total_usage_ = 0; // how many (non-unique) mutruns were used by haplosomes, summed across cycles int64_t profile_unique_mutrun_total_ = 0; // of profile_mutrun_total_usage_, how many unique mutruns existed, summed across cycles int64_t profile_mutrun_nonneutral_recache_total_ = 0; // of profile_unique_mutrun_total_, how many mutruns regenerated their nonneutral cache #endif // SLIM_USE_NONNEUTRAL_CACHES #endif // (SLIMPROFILING == 1) Chromosome(const Chromosome&) = delete; // no copying Chromosome& operator=(const Chromosome&) = delete; // no copying Chromosome(void) = delete; // no null constructor explicit Chromosome(Species &p_species, ChromosomeType p_type, int64_t p_id, std::string p_symbol, slim_chromosome_index_t p_index, int p_preferred_mutcount); ~Chromosome(void); inline __attribute__((always_inline)) int64_t ID(void) const { return id_; } inline __attribute__((always_inline)) const std::string &Symbol(void) const { return symbol_; } inline __attribute__((always_inline)) slim_chromosome_index_t Index(void) const { return index_; } inline __attribute__((always_inline)) ChromosomeType Type(void) const { return type_; } inline __attribute__((always_inline)) const std::string &Name(void) const { return name_; } inline __attribute__((always_inline)) void SetName(const std::string &p_name) { name_ = p_name; } inline __attribute__((always_inline)) int IntrinsicPloidy(void) const { return intrinsic_ploidy_; } inline __attribute__((always_inline)) bool AlwaysUsesNullHaplosomes(void) const { return always_uses_null_haplosomes_; } inline __attribute__((always_inline)) bool IsSexChromosome(void) const { return is_sex_chromosome_; } inline __attribute__((always_inline)) bool DefaultsToZeroRecombination(void) const { return defaults_to_zero_recombination_; } inline __attribute__((always_inline)) const std::string &TypeString(void) const { return type_string_; } inline __attribute__((always_inline)) std::vector &GenomicElements(void) { return genomic_elements_; } inline __attribute__((always_inline)) const std::vector &GenomicElements(void) const { return genomic_elements_; } inline __attribute__((always_inline)) NucleotideArray *AncestralSequence(void) const { return ancestral_seq_buffer_; } // initialize the random lookup tables used by Chromosome to draw mutation and recombination events void CreateNucleotideMutationRateMap(void); void InitializeDraws(void); void _InitializeOneRecombinationMap(gsl_ran_discrete_t *&p_lookup, std::vector &p_end_positions, std::vector &p_rates, double &p_overall_rate, double &p_exp_neg_overall_rate, double &p_overall_rate_userlevel); void _InitializeOneMutationMap(gsl_ran_discrete_t *&p_lookup, std::vector &p_end_positions, std::vector &p_rates, double &p_requested_overall_rate, double &p_overall_rate, double &p_exp_neg_overall_rate, std::vector &p_subranges); void ChooseMutationRunLayout(void); inline bool UsingSingleRecombinationMap(void) const { return single_recombination_map_; } inline bool UsingSingleMutationMap(void) const { return single_mutation_map_; } inline size_t GenomicElementCount(void) const { return genomic_elements_.size(); } // draw the number of mutations that occur, based on the overall mutation rate int DrawMutationCount(IndividualSex p_sex) const; // draw a vector of mutation positions (and the corresponding GenomicElementType objects), which is sorted and uniqued for the caller int DrawSortedUniquedMutationPositions(int p_count, IndividualSex p_sex, std::vector> &p_positions); // draw a new mutation, based on the genomic element types present and their mutational proclivities MutationIndex DrawNewMutation(std::pair &p_position, slim_objectid_t p_subpop_index, slim_tick_t p_tick) const; // draw a new mutation with reference to the genomic background upon which it is occurring, for nucleotide-based models and/or mutation() callbacks Mutation *ApplyMutationCallbacks(Mutation *p_mut, Haplosome *p_haplosome, GenomicElement *p_genomic_element, int8_t p_original_nucleotide, std::vector &p_mutation_callbacks) const; MutationIndex DrawNewMutationExtended(std::pair &p_position, slim_objectid_t p_subpop_index, slim_tick_t p_tick, Haplosome *parent_haplosome_1, Haplosome *parent_haplosome_2, slim_position_t *p_breakpoints, int p_breakpoints_count, std::vector *p_mutation_callbacks) const; // draw the number of breakpoints that occur, based on the overall recombination rate int DrawBreakpointCount(IndividualSex p_sex) const; // choose a set of recombination breakpoints, based on recomb. intervals, overall recomb. rate, and gene // conversion parameters; DrawBreakpoints() is the high-level funnel that most callers should use, whereas // the low-level functions do not handle recombination() callbacks and other niceties void _DrawCrossoverBreakpoints(IndividualSex p_parent_sex, const int p_num_breakpoints, std::vector &p_crossovers) const; void _DrawDSBBreakpoints(IndividualSex p_parent_sex, const int p_num_breakpoints, std::vector &p_crossovers, std::vector &p_heteroduplex) const; void DrawBreakpoints(Individual *p_parent, Haplosome *p_haplosome1, Haplosome *p_haplosome2, int p_num_breakpoints, std::vector &p_crossovers, std::vector *p_heteroduplex, const char *p_caller_name); #ifndef USE_GSL_POISSON // draw both the mutation count and breakpoint count, using a single Poisson draw for speed void DrawMutationAndBreakpointCounts(IndividualSex p_sex, int *p_mut_count, int *p_break_count) const; // initialize the joint probabilities used by DrawMutationAndBreakpointCounts() void _InitializeJointProbabilities(double p_overall_mutation_rate, double p_exp_neg_overall_mutation_rate, double p_overall_recombination_rate, double p_exp_neg_overall_recombination_rate, double &p_both_0, double &p_both_0_OR_mut_0_break_non0, double &p_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0); #endif // looking up genomic elements quickly, by binary search GenomicElement *ElementForPosition(slim_position_t pos); // internal methods for throwing errors from inline functions when assumptions about the configuration of maps are violated void MutationMapConfigError(void) const __attribute__((__noreturn__)) __attribute__((cold)) __attribute__((analyzer_noreturn)); void RecombinationMapConfigError(void) const __attribute__((__noreturn__)) __attribute__((cold)) __attribute__((analyzer_noreturn)); // Memory usage tallying, for outputUsage() size_t MemoryUsageForMutationMaps(void); size_t MemoryUsageForRecombinationMaps(void); size_t MemoryUsageForAncestralSequence(void); // Make a null haplosome, which is associated with an individual, but has no associated chromosome, or // make a non-null haplosome, which is associated with an individual and has an associated chromosome Haplosome *NewHaplosome_NULL(Individual *p_individual, uint8_t p_chromosome_subposition); Haplosome *NewHaplosome_NONNULL(Individual *p_individual, uint8_t p_chromosome_subposition); void FreeHaplosome(Haplosome *p_haplosome); const std::vector &HaplosomesJunkyardNonnull(void) { return haplosomes_junkyard_nonnull; } const std::vector &HaplosomesJunkyardNull(void) { return haplosomes_junkyard_null; } // Mutation run contexts: each chromosome keeps per-thread "contexts" out of which mutation runs get allocated, and // into which they get freed. This eliminates between-thread locking when working with mutation runs. void SetUpMutationRunContexts(void); #ifndef _OPENMP inline int ChromosomeMutationRunContextCount(void) { return 1; } inline __attribute__((always_inline)) MutationRunContext &ChromosomeMutationRunContextForThread(__attribute__((unused)) int p_thread_num) { #if DEBUG if (p_thread_num != 0) EIDOS_TERMINATION << "ERROR (Chromosome::ChromosomeMutationRunContextForThread): (internal error) p_thread_num out of range." << EidosTerminate(); #endif return mutation_run_context_SINGLE_; } inline __attribute__((always_inline)) MutationRunContext &ChromosomeMutationRunContextForMutationRunIndex(__attribute__((unused)) slim_mutrun_index_t p_mutrun_index) { #if DEBUG if ((p_mutrun_index < 0) || (p_mutrun_index >= mutrun_count_)) EIDOS_TERMINATION << "ERROR (Chromosome::ChromosomeMutationRunContextForMutationRunIndex): (internal error) p_mutrun_index out of range." << EidosTerminate(); #endif return mutation_run_context_SINGLE_; } #else inline int ChromosomeMutationRunContextCount(void) { return mutation_run_context_COUNT_; } inline __attribute__((always_inline)) MutationRunContext &ChromosomeMutationRunContextForThread(int p_thread_num) { #if DEBUG if ((p_thread_num < 0) || (p_thread_num >= mutation_run_context_COUNT_)) EIDOS_TERMINATION << "ERROR (Chromosome::ChromosomeMutationRunContextForThread): (internal error) p_thread_num out of range." << EidosTerminate(); #endif return *(mutation_run_context_PERTHREAD[p_thread_num]); } inline __attribute__((always_inline)) MutationRunContext &ChromosomeMutationRunContextForMutationRunIndex(__attribute__((unused)) slim_mutrun_index_t p_mutrun_index) { #if DEBUG if ((p_mutrun_index < 0) || (p_mutrun_index >= mutrun_count_)) EIDOS_TERMINATION << "ERROR (Chromosome::ChromosomeMutationRunContextForMutationRunIndex): (internal error) p_mutrun_index out of range." << EidosTerminate(); #endif // The range of the haplosome that each thread is responsible for does not change across // splits/joins; one mutrun becomes two, or two become one, owned by the same thread int thread_num = (int)(p_mutrun_index / mutrun_count_multiplier_); return *(mutation_run_context_PERTHREAD[thread_num]); } #endif // Mutation run experiments void InitiateMutationRunExperiments(void); void ZeroMutationRunExperimentClock(void); void FinishMutationRunExperimentTiming(void); void PrintMutationRunExperimentSummary(void); inline __attribute__((always_inline)) bool MutationRunExperimentsEnabled(void) { return x_experiments_enabled_; } // Mutation run experiment timing. We use these methods to accumulate clocks taken in critical sections of the code. // Note that this design does NOT include time taken in first()/early()/late() events; since script blocks can do very // different work from one cycle to the next, this seems best, although it does mean that the impact of the number // of mutation runs on the execution time of Eidos events is not measured. inline __attribute__((always_inline)) void StartMutationRunExperimentClock(void) { if (x_experiments_enabled_) { #if DEBUG if (x_clock_running_) std::cerr << "WARNING: mutation run experiment clock was started when already running!"; if (!x_within_measurement_period_) std::cerr << "WARNING: mutation run experiment clock started outside the measurement period!"; #endif x_clock_running_ = true; x_current_clock_ = Eidos_BenchmarkTime(); } } inline __attribute__((always_inline)) void StopMutationRunExperimentClock(__attribute__((unused)) const char *p_caller_name) { if (x_experiments_enabled_) { eidos_profile_t end_clock = Eidos_BenchmarkTime(); #if DEBUG if (!x_clock_running_) std::cerr << "WARNING: mutation run experiment clock was stopped when not running!"; if (!x_within_measurement_period_) std::cerr << "WARNING: mutation run experiment clock stopped outside the measurement period!"; #endif #if MUTRUN_EXPERIMENT_TIMING_OUTPUT // this log generates an unreasonable amount of output and is not usually desirable //std::cout << " tick " << community_.Tick() << ", chromosome " << id_ << ": mutrun experiment clock for " << p_caller_name << " count == " << (end_clock - x_current_clock_) << std::endl; #endif x_clock_running_ = false; x_total_gen_clocks_ += (end_clock - x_current_clock_); x_current_clock_ = 0; } } // // Eidos support // virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_ancestralNucleotides(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_genomicElementForPosition(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_hasGenomicElementForPosition(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setAncestralNucleotides(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setGeneConversion(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setHotspotMap(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setMutationRate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setRecombinationRate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_drawBreakpoints(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); }; // draw the number of mutations that occur, based on the overall mutation rate inline __attribute__((always_inline)) int Chromosome::DrawMutationCount(IndividualSex p_sex) const { #ifdef USE_GSL_POISSON gsl_rng *rng_gsl = EIDOS_GSL_RNG(omp_get_thread_num()); if (single_mutation_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled return gsl_ran_poisson(rng, overall_mutation_rate_H_); } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two if (p_sex == IndividualSex::kMale) { return gsl_ran_poisson(rng, overall_mutation_rate_M_); } else if (p_sex == IndividualSex::kFemale) { return gsl_ran_poisson(rng, overall_mutation_rate_F_); } else { MutationMapConfigError(); } } #else Eidos_RNG_State *rng_state = EIDOS_STATE_RNG(omp_get_thread_num()); if (single_mutation_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled return Eidos_FastRandomPoisson(rng_state, overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_); } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two if (p_sex == IndividualSex::kMale) { return Eidos_FastRandomPoisson(rng_state, overall_mutation_rate_M_, exp_neg_overall_mutation_rate_M_); } else if (p_sex == IndividualSex::kFemale) { return Eidos_FastRandomPoisson(rng_state, overall_mutation_rate_F_, exp_neg_overall_mutation_rate_F_); } else { MutationMapConfigError(); } } #endif } // draw the number of breakpoints that occur, based on the overall recombination rate inline __attribute__((always_inline)) int Chromosome::DrawBreakpointCount(IndividualSex p_sex) const { #ifdef USE_GSL_POISSON gsl_rng *rng_gsl = EIDOS_GSL_RNG(omp_get_thread_num()); if (single_recombination_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled return gsl_ran_poisson(rng, overall_recombination_rate_H_); } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two if (p_sex == IndividualSex::kMale) { return gsl_ran_poisson(rng, overall_recombination_rate_M_); } else if (p_sex == IndividualSex::kFemale) { return gsl_ran_poisson(rng, overall_recombination_rate_F_); } else { RecombinationMapConfigError(); } } #else Eidos_RNG_State *rng_state = EIDOS_STATE_RNG(omp_get_thread_num()); if (single_recombination_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled return Eidos_FastRandomPoisson(rng_state, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_); } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two if (p_sex == IndividualSex::kMale) { return Eidos_FastRandomPoisson(rng_state, overall_recombination_rate_M_, exp_neg_overall_recombination_rate_M_); } else if (p_sex == IndividualSex::kFemale) { return Eidos_FastRandomPoisson(rng_state, overall_recombination_rate_F_, exp_neg_overall_recombination_rate_F_); } else { RecombinationMapConfigError(); } } #endif } #ifndef USE_GSL_POISSON // determine both the mutation count and the breakpoint count with (usually) a single RNG draw // this method relies on Eidos_FastRandomPoisson_NONZERO() and cannot be called when USE_GSL_POISSON is defined inline __attribute__((always_inline)) void Chromosome::DrawMutationAndBreakpointCounts(IndividualSex p_sex, int *p_mut_count, int *p_break_count) const { Eidos_RNG_State *rng_state = EIDOS_STATE_RNG(omp_get_thread_num()); EidosRNG_64_bit &rng_64 = rng_state->pcg64_rng_; double u = Eidos_rng_uniform_doubleCO(rng_64); if (single_recombination_map_ && single_mutation_map_) { // With a single map, we don't care what sex we are passed; same map for all, and sex may be enabled or disabled. // We use the _H_ variants of all ivars in this case, which are all that is set up. if (u <= probability_both_0_H_) { *p_mut_count = 0; *p_break_count = 0; } else if (u <= probability_both_0_OR_mut_0_break_non0_H_) { *p_mut_count = 0; *p_break_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_); } else if (u <= probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_H_) { *p_mut_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_); *p_break_count = 0; } else { *p_mut_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_mutation_rate_H_, exp_neg_overall_mutation_rate_H_); *p_break_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_recombination_rate_H_, exp_neg_overall_recombination_rate_H_); } } else { // With sex-specific maps, we treat males and females separately, and the individual we're given better be one of the two. // We use the _M_ and _F_ variants in this case; either the mutation map or the recombination map might be non-sex-specific, // so the _H_ variants were originally set up, but InitializeDraws() copies them into the _M_ and _F_ variants for us // so that we don't have to worry about finding the correct variant for each subcase of single/double maps. if (p_sex == IndividualSex::kMale) { if (u <= probability_both_0_M_) { *p_mut_count = 0; *p_break_count = 0; } else if (u <= probability_both_0_OR_mut_0_break_non0_M_) { *p_mut_count = 0; *p_break_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_recombination_rate_M_, exp_neg_overall_recombination_rate_M_); } else if (u <= probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_M_) { *p_mut_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_mutation_rate_M_, exp_neg_overall_mutation_rate_M_); *p_break_count = 0; } else { *p_mut_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_mutation_rate_M_, exp_neg_overall_mutation_rate_M_); *p_break_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_recombination_rate_M_, exp_neg_overall_recombination_rate_M_); } } else if (p_sex == IndividualSex::kFemale) { if (u <= probability_both_0_F_) { *p_mut_count = 0; *p_break_count = 0; } else if (u <= probability_both_0_OR_mut_0_break_non0_F_) { *p_mut_count = 0; *p_break_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_recombination_rate_F_, exp_neg_overall_recombination_rate_F_); } else if (u <= probability_both_0_OR_mut_0_break_non0_OR_mut_non0_break_0_F_) { *p_mut_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_mutation_rate_F_, exp_neg_overall_mutation_rate_F_); *p_break_count = 0; } else { *p_mut_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_mutation_rate_F_, exp_neg_overall_mutation_rate_F_); *p_break_count = Eidos_FastRandomPoisson_NONZERO(rng_state, overall_recombination_rate_F_, exp_neg_overall_recombination_rate_F_); } } else { RecombinationMapConfigError(); } } } #endif // defer this include until now, to resolve mutual dependencies #include "haplosome.h" inline __attribute__((always_inline)) Haplosome *Chromosome::NewHaplosome_NULL(Individual *p_individual, uint8_t p_chromosome_subposition) { if (haplosomes_junkyard_null.size()) { Haplosome *back = haplosomes_junkyard_null.back(); haplosomes_junkyard_null.pop_back(); //back->chromosome_index_ = index_; // guaranteed already set back->chromosome_subposition_ = p_chromosome_subposition; back->individual_ = p_individual; return back; } Haplosome *hap = _NewHaplosome_NULL(p_individual); hap->chromosome_subposition_ = p_chromosome_subposition; return hap; } inline __attribute__((always_inline)) Haplosome *Chromosome::NewHaplosome_NONNULL(Individual *p_individual, uint8_t p_chromosome_subposition) { if (haplosomes_junkyard_nonnull.size()) { Haplosome *back = haplosomes_junkyard_nonnull.back(); haplosomes_junkyard_nonnull.pop_back(); // Conceptually, we want to call ReinitializeHaplosomeNoClear() to set the haplosome up with the // current type, mutrun setup, etc. But we know that the haplosome is nonnull, and that we // want it to be nonnull, so we can do less work than ReinitializeHaplosomeNoClear(), inline. if (back->mutrun_count_ != mutrun_count_) { // the number of mutruns has changed; need to reallocate if (back->mutruns_ != back->run_buffer_) free(back->mutruns_); back->mutrun_count_ = mutrun_count_; back->mutrun_length_ = mutrun_length_; if (mutrun_count_ <= SLIM_HAPLOSOME_MUTRUN_BUFSIZE) { back->mutruns_ = back->run_buffer_; #if SLIM_CLEAR_HAPLOSOMES EIDOS_BZERO(back->run_buffer_, SLIM_HAPLOSOME_MUTRUN_BUFSIZE * sizeof(const MutationRun *)); #endif } else { #if SLIM_CLEAR_HAPLOSOMES back->mutruns_ = (const MutationRun **)calloc(mutrun_count_, sizeof(const MutationRun *)); #else back->mutruns_ = (const MutationRun **)malloc(mutrun_count_ * sizeof(const MutationRun *)); #endif } } else { #if SLIM_CLEAR_HAPLOSOMES // the number of mutruns is unchanged, but we need to zero out the reused buffer here EIDOS_BZERO(back->mutruns_, mutrun_count_ * sizeof(const MutationRun *)); #endif } //back->chromosome_index_ = index_; // guaranteed already set back->chromosome_subposition_ = p_chromosome_subposition; back->individual_ = p_individual; return back; } Haplosome *hap = _NewHaplosome_NONNULL(p_individual); hap->chromosome_subposition_ = p_chromosome_subposition; return hap; } // Frees a haplosome object (puts it in one of the junkyards); we do not clear the mutrun buffer, so it must be cleared when reused! inline __attribute__((always_inline)) void Chromosome::FreeHaplosome(Haplosome *p_haplosome) { #if DEBUG p_haplosome->individual_ = nullptr; // crash if anybody tries to use this pointer after the free #endif #if SLIM_CLEAR_HAPLOSOMES p_haplosome->clear_to_nullptr(); #endif // somebody needs to reset the tag value of reused haplosomes; it might as well be us // this used to be done by Individual::Individual(), which got passed the individual's haplosomes // FIXME: this should only get cleared, in bulk, based on a flag, like individual tag values; big waste of time; // FIXME: see s_any_haplosome_tag_set_, we are already doing this in SwapChildAndParentHaplosomes() for WF p_haplosome->tag_value_ = SLIM_TAG_UNSET_VALUE; if (p_haplosome->IsNull()) haplosomes_junkyard_null.emplace_back(p_haplosome); else haplosomes_junkyard_nonnull.emplace_back(p_haplosome); } class Chromosome_Class : public EidosDictionaryRetained_Class { private: typedef EidosDictionaryRetained_Class super; public: Chromosome_Class(const Chromosome_Class &p_original) = delete; // no copy-construct Chromosome_Class& operator=(const Chromosome_Class&) = delete; // no copying inline Chromosome_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif /* defined(__SLiM__chromosome__) */ ================================================ FILE: core/community.cpp ================================================ // // community.cpp // SLiM // // Created by Ben Haller on 2/28/2022. // Copyright (c) 2022-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "community.h" #include "species.h" #include "slim_functions.h" #include "eidos_test.h" #include "eidos_interpreter.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "eidos_ast_node.h" #include "individual.h" #include "polymorphism.h" #include "subpopulation.h" #include "interaction_type.h" #include "log_file.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "eidos_globals.h" #if EIDOS_ROBIN_HOOD_HASHING #include "robin_hood.h" #endif //TREE SEQUENCE #include #include #include "json.hpp" #include //TREE SEQUENCE //INCLUDE MSPRIME.H FOR THE CROSSCHECK CODE; NEEDS TO BE MOVED TO TSKIT // the tskit header is not designed to be included from C++, so we have to protect it... #ifdef __cplusplus extern "C" { #endif #include "kastore.h" #include "../treerec/tskit/trees.h" #include "../treerec/tskit/tables.h" #include "../treerec/tskit/genotypes.h" #include "../treerec/tskit/text_input.h" #ifdef __cplusplus } #endif #pragma mark - #pragma mark Community #pragma mark - Community::Community(void) : self_symbol_(gID_community, EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_Community_Class))) { // self_symbol_ is always a constant, but can't be marked as such on construction self_symbol_.second->MarkAsConstant(); // BCH 3/16/2022: We used to allocate the Species object here, as the first thing we did. In SLiM 4 there can // be multiple species and they can have names other than "sim", so we delay species creation until parse time. // set up the symbol tables we will use for global variables and constants; note that the global variables table // lives *above* the context constants table, which is fine since they cannot define the same symbol anyway // this satisfies Eidos, which expects the child of the intrinsic constants table to be the global variables table simulation_globals_ = new EidosSymbolTable(EidosSymbolTableType::kGlobalVariablesTable, gEidosConstantsSymbolTable); simulation_constants_ = new EidosSymbolTable(EidosSymbolTableType::kContextConstantsTable, simulation_globals_); // set up the function map with the base Eidos functions plus zero-gen functions, since we're in an initial state simulation_functions_ = *EidosInterpreter::BuiltInFunctionMap(); AddZeroTickFunctionsToMap(simulation_functions_); AddSLiMFunctionsToMap(simulation_functions_); // reading from the input file is deferred to InitializeFromFile() to make raise-handling simpler - finish construction // BCH 3/21/2025: Note that tick_ == -1 at this point, now, so we can differentiate construction from initialize() // It gets set to 0 in Community::FinishInitialization(), when we finish with the construction phase. } Community::~Community(void) { //EIDOS_ERRSTREAM << "Community::~Community" << std::endl; // our log file registry retains all log files for (LogFile *log_file : log_file_registry_) log_file->Release(); log_file_registry_.clear(); all_mutation_types_.clear(); all_genomic_element_types_.clear(); for (auto interaction_type : interaction_types_) delete interaction_type.second; interaction_types_.clear(); delete simulation_globals_; simulation_globals_ = nullptr; delete simulation_constants_; simulation_constants_ = nullptr; simulation_functions_.clear(); for (auto script_block : script_blocks_) delete script_block; script_blocks_.clear(); // All the script blocks that refer to the script are now gone delete script_; script_ = nullptr; // delete the Species last, after everything that might refer to Species state is gone for (Species *species : all_species_) delete species; } void Community::InitializeRNGFromSeed(unsigned long int *p_override_seed_ptr) { // track the random number seed given, if there is one unsigned long int rng_seed = (p_override_seed_ptr ? *p_override_seed_ptr : Eidos_GenerateRNGSeed()); Eidos_SetRNGSeed(rng_seed); if (SLiM_verbosity_level >= 1) SLIM_OUTSTREAM << "// Initial random seed:\n" << rng_seed << "\n" << std::endl; // remember the original seed for .trees provenance original_seed_ = rng_seed; } void Community::InitializeFromFile(std::istream &p_infile) { p_infile.clear(); p_infile.seekg(0, std::fstream::beg); // Reset error position indicators used by SLiMgui ClearErrorPosition(); // Read in the file; going through stringstream is fast... std::stringstream buffer; buffer << p_infile.rdbuf(); // Tokenize and parse // BCH 5/1/2019: Note that this script_ variable may leak if tokenization/parsing raises below, because this method // is called while the Community constructor is still in progress, so the destructor is not called to clean up. But // we can't actually clean up this variable, because it is used by SLiMAssertScriptRaise() to diagnose where the raise // occurred in the user's script; we'd have to redesign that code to fix this leak. So be it. It's not a large leak. script_ = new SLiMEidosScript(buffer.str()); // Set up top-level error-reporting info gEidosErrorContext.currentScript = script_; script_->Tokenize(); script_->ParseSLiMFileToAST(); const EidosASTNode *root_node = script_->AST(); // BCH 3/16/2022: The logic here used to be quite simple: loop over the parsed AST and make script blocks. In SLiM 4 // the top-level file structure is more complicated, because of species and ticks specifiers that can modify the // declared blocks. Rather than making those part of the EidosASTNodes for the blocks themselves (which already have // a very complex internal structure), I have chosen to make them separate top-level nodes that modify the meaning // of the SLiMEidosBlock node that follows them. That makes doing the parse, validating the file structure, creating // the species objects, and then creating the script blocks fairly complex. That complexity is handled here. Since // the logic here is rather complex, I have put in redundant checks to try to make sure nothing falls between the // cracks. Errors that start with "(internal error)" should never be hit, as usual; those error cases ought to be // caught by earlier checks, unless I have missed a possible code path. // Assess the top-level structure and enforce semantics that can be enforced before knowing species names/declarations // Species are declared with initialize() callbacks of the form "species initialize()" bool pending_species_spec = false, pending_ticks_spec = false; std::string pending_spec_species_name; std::vector explicit_species_decl_names; // names from "species initialize()" declarations int implied_species_decl_count = 0; // number of "initialize()" seen without "species " for (EidosASTNode *script_block_node : root_node->children_) { if (script_block_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) { // If we already have a pending specifier then we now have two specifiers in a row if (pending_species_spec) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): a species specifier must be followed by a callback declaration." << EidosTerminate(script_block_node->token_); if (pending_ticks_spec) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): a ticks specifier must be followed by an event declaration." << EidosTerminate(script_block_node->token_); if (script_block_node->children_.size() == 1) { pending_spec_species_name = script_block_node->children_[0]->token_->token_string_; if (script_block_node->token_->token_string_.compare(gStr_species) == 0) { //std::cout << "species specifier seen: " << pending_spec_species_name << std::endl; pending_species_spec = true; continue; } else if (script_block_node->token_->token_string_.compare(gStr_ticks) == 0) { //std::cout << "ticks specifier seen: " << pending_spec_species_name << std::endl; pending_ticks_spec = true; continue; } } EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): unexpected top-level token " << script_block_node->token_->token_string_ << "." << EidosTerminate(script_block_node->token_); } else { SLiMEidosBlockType type = SLiMEidosBlock::BlockTypeForRootNode(script_block_node); if (type == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) { if (pending_species_spec || pending_ticks_spec) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): user-defined functions may not be preceded by a species or ticks specifier." << EidosTerminate(script_block_node->token_); } else if ((type == SLiMEidosBlockType::SLiMEidosEventFirst) || (type == SLiMEidosBlockType::SLiMEidosEventEarly) || (type == SLiMEidosBlockType::SLiMEidosEventLate)) { if (pending_species_spec) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): event declarations may not be preceded by a species specifier; use a ticks specifier to designate an event as running only in the ticks when a particular species is active." << EidosTerminate(script_block_node->token_); } else if (type != SLiMEidosBlockType::SLiMEidosNoBlockType) // callbacks { if (pending_ticks_spec) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): callback declarations may not be preceded by a ticks specifier; use a species specifier to designate a callback as being associated with a particular species." << EidosTerminate(script_block_node->token_); if (type == SLiMEidosBlockType::SLiMEidosInitializeCallback) { if (pending_species_spec) { // We have an explicit species declaration, "species initialize()", so this is a multispecies model if (implied_species_decl_count > 0) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): an initialize() callback without a species specifier has previously been seen, so this is a single-species script, and therefore species specifiers are illegal." << EidosTerminate(script_block_node->token_); // You can have multiple initialize() callbacks for a given species, but we want to tally the name just once; could use std::set instead but we want ordering // Note that `species all` is logged as a species name here; we will handle it separately below if (std::find(explicit_species_decl_names.begin(), explicit_species_decl_names.end(), pending_spec_species_name) == explicit_species_decl_names.end()) explicit_species_decl_names.push_back(pending_spec_species_name); } else { // We have an implicit species declaration, "initialize()", so this is a single-species model if (explicit_species_decl_names.size() > 0) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): an initialize() callback with a species specifier has previously been seen, so this is a multi-species script, and therefore species specifiers are required." << EidosTerminate(script_block_node->token_); implied_species_decl_count++; } } else if (type == SLiMEidosBlockType::SLiMEidosInteractionCallback) { if (pending_species_spec && (pending_spec_species_name != "all")) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): interaction() callbacks must be declared with 'species all' in multispecies models; they are never species-specific." << EidosTerminate(script_block_node->children_[0]->token_); } else // all other callback types { if (pending_species_spec && (pending_spec_species_name == "all")) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): " << type << " callbacks may not be declared with 'species all'; they are always species-specific." << EidosTerminate(script_block_node->children_[0]->token_); } } //std::cout << "script block seen of type " << type << std::endl; pending_species_spec = false; pending_ticks_spec = false; } } // Create species objects for each declared species, or "sim" if there was only an implied species declaration if ((implied_species_decl_count > 0) && (explicit_species_decl_names.size() > 0)) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): (internal error) all initialize() callbacks must either (1) be preceded by a species specifier, for multi-species models, or (2) not be preceded by a species specifier, for single-species models." << EidosTerminate(nullptr); if ((implied_species_decl_count == 0) && (explicit_species_decl_names.size() == 0)) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): no initialize() callback found; at least one initialize() callback is required in all SLiM scripts." << EidosTerminate(nullptr); if ((explicit_species_decl_names.size() == 1) && (explicit_species_decl_names[0] == "all")) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): no species-specific initialize() callback found; at least one species-specific initialize() callback is required in all SLiM scripts." << EidosTerminate(nullptr); if (implied_species_decl_count > 0) { // This is the single-species case; create a species named "sim" all_species_.push_back(new Species(*this, 0, gStr_sim)); is_explicit_species_ = false; } else { // This is the multi-species case; create a species for each explicit declaration except `species all` int species_id = 0; for (std::string &species_name : explicit_species_decl_names) if (species_name != "all") all_species_.push_back(new Species(*this, species_id++, species_name)); is_explicit_species_ = true; } // Extract SLiMEidosBlocks from the parse tree Species *last_species_spec = nullptr, *last_ticks_spec = nullptr; bool last_spec_is_ticks_all = false; // "ticks all" is a special syntax; there is no species named "all" so it must be tracked with a separate flag bool last_spec_is_species_all = false; // "species all" is a special syntax; there is no species named "all" so it must be tracked with a separate flag for (EidosASTNode *script_block_node : root_node->children_) { if ((script_block_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (script_block_node->children_.size() == 1)) { // A "species " or "ticks " specification is present; remember what it specified EidosASTNode *child = script_block_node->children_[0]; const std::string &species_name = child->token_->token_string_; bool species_is_all = (species_name.compare("all") == 0); Species *species = (species_is_all ? nullptr : SpeciesWithName(species_name)); if (!species && !species_is_all) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): undeclared species name " << species_name << "; species must be explicitly declared with a species specifier on an initialize() block." << EidosTerminate(child->token_); if (script_block_node->token_->token_string_.compare(gStr_species) == 0) { if (!is_explicit_species_) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): no species have been explicitly declared, so species specifiers should not be used." << EidosTerminate(script_block_node->token_); last_species_spec = species; last_spec_is_species_all = species_is_all; } else if (script_block_node->token_->token_string_.compare(gStr_ticks) == 0) { if (!is_explicit_species_) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): no species have been explicitly declared, so ticks specifiers should not be used." << EidosTerminate(script_block_node->token_); last_ticks_spec = species; last_spec_is_ticks_all = species_is_all; } } else { SLiMEidosBlock *new_script_block = new SLiMEidosBlock(script_block_node); if (new_script_block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) { // User-defined functions may not have a species or ticks specifier preceding them; this was already checked above if (last_species_spec || last_ticks_spec || last_spec_is_ticks_all || last_spec_is_species_all) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): (internal error) user-defined functions may not be preceded by a species or ticks specifier." << EidosTerminate(new_script_block->root_node_->token_); } else if ((new_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventFirst) || (new_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventEarly) || (new_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventLate)) { // Events may have a ticks specifier, but not a species specifier, preceding them; this was already checked above if (last_species_spec || last_spec_is_species_all) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): (internal error) event declarations may not be preceded by a species specifier." << EidosTerminate(new_script_block->root_node_->token_); if (is_explicit_species_) { Species *block_ticks = last_ticks_spec; if (!block_ticks && !last_spec_is_ticks_all) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): when species names have been explicitly declared (such as in multispecies models), every event must be preceded by a ticks specifier of the form 'ticks '; if you want an event to run in every tick, specify 'ticks all'." << EidosTerminate(new_script_block->root_node_->token_); new_script_block->ticks_spec_ = block_ticks; // nullptr for "ticks all" } else { new_script_block->ticks_spec_ = nullptr; } } else { // Callbacks of all types may not be preceded by a ticks specifier; this was already checked above if (last_ticks_spec || last_spec_is_ticks_all) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): (internal error) callback declarations may not be preceded by a ticks specifier." << EidosTerminate(new_script_block->root_node_->token_); // Callbacks of all types may not be preceded by a species specifier in single-species models; this was already checked above if (!is_explicit_species_ && (last_species_spec || last_spec_is_species_all)) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): (internal error) callback declarations may not be preceded by a species specifier in single-species models." << EidosTerminate(new_script_block->root_node_->token_); // Callbacks of all types must be preceded by a species specifier in multispecies models if (is_explicit_species_ && !(last_species_spec || last_spec_is_species_all)) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): when species names have been explicitly declared (as in multispecies models), every callback must be preceded by a species specifier of the form 'species '; for non-species-specific initialize() and interaction() callbacks, specify 'species all'." << EidosTerminate(new_script_block->root_node_->token_); Species *block_species = (is_explicit_species_ ? last_species_spec : all_species_[0]); // nullptr for `species all` if (new_script_block->type_ == SLiMEidosBlockType::SLiMEidosInitializeCallback) { // In multispecies models, initialize() callbacks may be `species all` or `species name`; no action needed } else if (new_script_block->type_ == SLiMEidosBlockType::SLiMEidosInteractionCallback) { // interaction() callbacks must be "species all"; this was already checked above if (is_explicit_species_ && block_species) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): (internal error) interaction() callbacks in multispecies models must be declared with 'species all'; they are never species-specific." << EidosTerminate(new_script_block->root_node_->token_); // In single-species models, the above default needs correction if (!is_explicit_species_) block_species = nullptr; } else { // Other callback types may not be `species all`; this was already checked above if (last_spec_is_species_all) EIDOS_TERMINATION << "ERROR (Community::InitializeFromFile): (internal error) " << new_script_block->type_ << " callbacks may not be declared with 'species all'; they are always species-specific." << EidosTerminate(new_script_block->root_node_->token_); } new_script_block->species_spec_ = block_species; } AddScriptBlock(new_script_block, nullptr, new_script_block->root_node_->children_[0]->token_); last_species_spec = nullptr; last_ticks_spec = nullptr; last_spec_is_ticks_all = false; last_spec_is_species_all = false; } } // Zero out error-reporting info so raises elsewhere don't get attributed to this script ClearErrorContext(); } void Community::FinishInitialization(void) { // BCH 7/19/2024: At the very end of initialization of the Community, after script blocks // have been parsed, command-line constants have been defined, etc., we evaluate tick // ranges for script blocks for the first time. Anything that doesn't involve // defined constants, or involves constants defined on the command line, can already // be evaluated at this time, which makes the appearance of things in SLiMgui cleaner. if (!all_tick_ranges_evaluated_) { // Set up top-level error-reporting info gEidosErrorContext.currentScript = script_; // Do the tick range evaluation work EvaluateScriptBlockTickRanges(); // Zero out error-reporting info so raises elsewhere don't get attributed to this script ClearErrorContext(); } // We have been in the "construction" phase, with tick_ == -1 as set in the header. // Now we're done with construction, and set the tick counter to 0 for "initialization". tick_ = 0; } void Community::ValidateScriptBlockCaches(void) { #if DEBUG_BLOCK_REG_DEREG std::cout << "Tick " << tick_ << ": ValidateScriptBlockCaches() called..." << std::endl; #endif if (!script_block_types_cached_) { cached_first_events_.clear(); cached_early_events_.clear(); cached_late_events_.clear(); cached_initialize_callbacks_.clear(); cached_mutationEffect_callbacks_.clear(); cached_fitnessEffect_callbacks_onetick_.clear(); cached_fitnessEffect_callbacks_multitick_.clear(); cached_interaction_callbacks_.clear(); cached_matechoice_callbacks_.clear(); cached_modifychild_callbacks_.clear(); cached_recombination_callbacks_.clear(); cached_mutation_callbacks_.clear(); cached_survival_callbacks_.clear(); cached_reproduction_callbacks_.clear(); cached_userdef_functions_.clear(); std::vector &script_blocks = AllScriptBlocks(); #if DEBUG_BLOCK_REG_DEREG std::cout << " ValidateScriptBlockCaches() recaching, AllScriptBlocks() is:" << std::endl; for (SLiMEidosBlock *script_block : script_blocks) { std::cout << " "; script_block->Print(std::cout); std::cout << std::endl; } #endif for (auto script_block : script_blocks) { // only blocks that have been scheduled are eligible; others have to wait until the next call to EvaluateScriptBlockTickRanges() if (!script_block->tick_range_evaluated_) continue; switch (script_block->type_) { case SLiMEidosBlockType::SLiMEidosEventFirst: cached_first_events_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosEventEarly: cached_early_events_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosEventLate: cached_late_events_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosInitializeCallback: cached_initialize_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosMutationEffectCallback: cached_mutationEffect_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback: { // Note fitnessEffect() callbacks are not order-dependent, so we don't have to preserve their order // of declaration the way we do with other types of callbacks. This allows us to be very efficient // in how we look them up, which is good since sometimes we have a very large number of them. // We put those that are registered for just a single tick in a separate vector, which we sort // by tick, allowing us to look them up quickly if (script_block->tick_range_is_sequence_ && (script_block->tick_start_ == script_block->tick_end_)) { cached_fitnessEffect_callbacks_onetick_.emplace(script_block->tick_start_, script_block); } else { cached_fitnessEffect_callbacks_multitick_.emplace_back(script_block); } break; } case SLiMEidosBlockType::SLiMEidosInteractionCallback: cached_interaction_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosMateChoiceCallback: cached_matechoice_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosModifyChildCallback: cached_modifychild_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosRecombinationCallback: cached_recombination_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosMutationCallback: cached_mutation_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosSurvivalCallback: cached_survival_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosReproductionCallback: cached_reproduction_callbacks_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosUserDefinedFunction: cached_userdef_functions_.emplace_back(script_block); break; case SLiMEidosBlockType::SLiMEidosNoBlockType: break; // never hit } } script_block_types_cached_ = true; #if DEBUG_BLOCK_REG_DEREG std::cout << " ValidateScriptBlockCaches() recached, late() events cached are:" << std::endl; for (SLiMEidosBlock *script_block : cached_late_events_) { std::cout << " "; script_block->Print(std::cout); std::cout << std::endl; } #endif } } std::vector Community::ScriptBlocksMatching(slim_tick_t p_tick, SLiMEidosBlockType p_event_type, slim_objectid_t p_mutation_type_id, slim_objectid_t p_interaction_type_id, slim_objectid_t p_subpopulation_id, int64_t p_chromosome_id, Species *p_species) { if (!script_block_types_cached_) ValidateScriptBlockCaches(); std::vector *block_list = nullptr; switch (p_event_type) { case SLiMEidosBlockType::SLiMEidosEventFirst: block_list = &cached_first_events_; break; case SLiMEidosBlockType::SLiMEidosEventEarly: block_list = &cached_early_events_; break; case SLiMEidosBlockType::SLiMEidosEventLate: block_list = &cached_late_events_; break; case SLiMEidosBlockType::SLiMEidosInitializeCallback: block_list = &cached_initialize_callbacks_; break; case SLiMEidosBlockType::SLiMEidosMutationEffectCallback: block_list = &cached_mutationEffect_callbacks_; break; case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback: block_list = &cached_fitnessEffect_callbacks_multitick_; break; case SLiMEidosBlockType::SLiMEidosInteractionCallback: block_list = &cached_interaction_callbacks_; break; case SLiMEidosBlockType::SLiMEidosMateChoiceCallback: block_list = &cached_matechoice_callbacks_; break; case SLiMEidosBlockType::SLiMEidosModifyChildCallback: block_list = &cached_modifychild_callbacks_; break; case SLiMEidosBlockType::SLiMEidosRecombinationCallback: block_list = &cached_recombination_callbacks_; break; case SLiMEidosBlockType::SLiMEidosMutationCallback: block_list = &cached_mutation_callbacks_; break; case SLiMEidosBlockType::SLiMEidosSurvivalCallback: block_list = &cached_survival_callbacks_; break; case SLiMEidosBlockType::SLiMEidosReproductionCallback: block_list = &cached_reproduction_callbacks_; break; case SLiMEidosBlockType::SLiMEidosUserDefinedFunction: block_list = &cached_userdef_functions_; break; case SLiMEidosBlockType::SLiMEidosNoBlockType: break; // never hit } std::vector matches; for (SLiMEidosBlock *script_block : *block_list) { #if DEBUG_TICK_RANGES // all the blocks here should have an evaluated tick range if (!script_block->tick_range_evaluated_) { std::cout << "### unscheduled block seen in ScriptBlocksMatching()" << std::endl; continue; } #endif // check that the tick is in range if (script_block->tick_range_is_sequence_) { if ((p_tick < script_block->tick_start_) || (p_tick > script_block->tick_end_)) continue; } else { if (script_block->tick_set_.find(p_tick) == script_block->tick_set_.end()) continue; } // check that the script type matches (event, callback, etc.) - now guaranteed by the caching mechanism //if (script_block->type_ != p_event_type) // continue; // check that the mutation type id matches, if requested if (p_mutation_type_id != -1) { slim_objectid_t mutation_type_id = script_block->mutation_type_id_; if ((mutation_type_id != -1) && (p_mutation_type_id != mutation_type_id)) continue; } // check that the interaction type id matches, if requested if (p_interaction_type_id != -1) { slim_objectid_t interaction_type_id = script_block->interaction_type_id_; if ((interaction_type_id != -1) && (p_interaction_type_id != interaction_type_id)) continue; } // check that the subpopulation id matches, if requested if (p_subpopulation_id != -1) { slim_objectid_t subpopulation_id = script_block->subpopulation_id_; if ((subpopulation_id != -1) && (p_subpopulation_id != subpopulation_id)) continue; } // check that the chromosome id matches, if requested if (p_chromosome_id != -1) { int64_t chromosome_id = script_block->chromosome_id_; if ((chromosome_id != -1) && (p_chromosome_id != chromosome_id)) continue; } // check that the species matches; this check is always on, nullptr means check that the species is nullptr if (p_species != script_block->species_spec_) continue; // OK, everything matches, so we want to return this script block matches.emplace_back(script_block); } // add in any single-tick fitnessEffect() callbacks if (p_event_type == SLiMEidosBlockType::SLiMEidosFitnessEffectCallback) { auto find_range = cached_fitnessEffect_callbacks_onetick_.equal_range(p_tick); auto find_start = find_range.first; auto find_end = find_range.second; for (auto block_iter = find_start; block_iter != find_end; ++block_iter) { SLiMEidosBlock *script_block = block_iter->second; // check that the subpopulation id matches, if requested if (p_subpopulation_id != -1) { slim_objectid_t subpopulation_id = script_block->subpopulation_id_; if ((subpopulation_id != -1) && (p_subpopulation_id != subpopulation_id)) continue; } // check that the species matches; this check is always on, nullptr means check that the species is nullptr if (p_species != script_block->species_spec_) continue; // OK, everything matches, so we want to return this script block matches.emplace_back(script_block); } } return matches; } std::vector &Community::AllScriptBlocks() { return script_blocks_; } std::vector Community::AllScriptBlocksForSpecies(Species *p_species) { std::vector species_blocks; for (SLiMEidosBlock *block : script_blocks_) if (block->species_spec_ == p_species) species_blocks.push_back(block); return species_blocks; } void Community::OptimizeScriptBlock(SLiMEidosBlock *p_script_block) { // The goal here is to look for specific structures in callbacks that we are able to optimize by short-circuiting // the callback interpretation entirely and replacing it with equivalent C++ code. This is extremely messy, so // we're not going to do this for very many cases, but sometimes it is worth it. if (!p_script_block->has_cached_optimization_) { if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosFitnessEffectCallback) { const EidosASTNode *base_node = p_script_block->compound_statement_node_; if ((base_node->token_->token_type_ == EidosTokenType::kTokenLBrace) && (base_node->children_.size() == 1)) { bool opt_dnorm1_candidate = true; const EidosASTNode *expr_node = base_node->children_[0]; // we must have an intervening "return", which we jump down through if ((expr_node->token_->token_type_ == EidosTokenType::kTokenReturn) && (expr_node->children_.size() == 1)) { expr_node = expr_node->children_[0]; // parse an optional constant at the beginning, like 1.0 + ... double added_constant = NAN; if ((expr_node->token_->token_type_ == EidosTokenType::kTokenPlus) && (expr_node->children_.size() == 2)) { const EidosASTNode *constant_node = expr_node->children_[0]; const EidosASTNode *rhs_node = expr_node->children_[1]; if (constant_node->HasCachedNumericValue()) { added_constant = constant_node->CachedNumericValue(); expr_node = rhs_node; } else opt_dnorm1_candidate = false; } else { added_constant = 0.0; } // parse an optional divisor at the end, ... / div double denominator = NAN; if ((expr_node->token_->token_type_ == EidosTokenType::kTokenDiv) && (expr_node->children_.size() == 2)) { const EidosASTNode *numerator_node = expr_node->children_[0]; const EidosASTNode *denominator_node = expr_node->children_[1]; if (denominator_node->HasCachedNumericValue()) { denominator = denominator_node->CachedNumericValue(); expr_node = numerator_node; } else opt_dnorm1_candidate = false; } else { denominator = 1.0; } // parse the dnorm() function call if (opt_dnorm1_candidate && (expr_node->token_->token_type_ == EidosTokenType::kTokenLParen) && (expr_node->children_.size() >= 2)) { const EidosASTNode *call_node = expr_node->children_[0]; if ((call_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (call_node->token_->token_string_ == "dnorm")) { int child_count = (int)expr_node->children_.size(); const EidosASTNode *x_node = expr_node->children_[1]; const EidosASTNode *mean_node = (child_count >= 3) ? expr_node->children_[2] : nullptr; const EidosASTNode *sd_node = (child_count >= 4) ? expr_node->children_[3] : nullptr; double mean_value = 0.0, sd_value = 1.0; // resolve named arguments if ((x_node->token_->token_type_ == EidosTokenType::kTokenAssign) && (x_node->children_.size() == 2)) { const EidosASTNode *name_node = x_node->children_[0]; const EidosASTNode *value_node = x_node->children_[1]; if ((name_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (name_node->token_->token_string_ == "x")) x_node = value_node; else opt_dnorm1_candidate = false; } if (mean_node && (mean_node->token_->token_type_ == EidosTokenType::kTokenAssign) && (mean_node->children_.size() == 2)) { const EidosASTNode *name_node = mean_node->children_[0]; const EidosASTNode *value_node = mean_node->children_[1]; if ((name_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (name_node->token_->token_string_ == "mean")) mean_node = value_node; else opt_dnorm1_candidate = false; } if (sd_node && (sd_node->token_->token_type_ == EidosTokenType::kTokenAssign) && (sd_node->children_.size() == 2)) { const EidosASTNode *name_node = sd_node->children_[0]; const EidosASTNode *value_node = sd_node->children_[1]; if ((name_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (name_node->token_->token_string_ == "sd")) sd_node = value_node; else opt_dnorm1_candidate = false; } // the mean and sd parameters of dnorm can be omitted in the below calls, but if they are given, get their values if (mean_node) { if (mean_node->HasCachedNumericValue()) mean_value = mean_node->CachedNumericValue(); else opt_dnorm1_candidate = false; } if (sd_node) { if (sd_node->HasCachedNumericValue()) sd_value = sd_node->CachedNumericValue(); else opt_dnorm1_candidate = false; } // parse the x argument to dnorm, which can take several different forms if (opt_dnorm1_candidate) { if ((x_node->token_->token_type_ == EidosTokenType::kTokenMinus) && (x_node->children_.size() == 2) && (mean_value == 0.0)) { const EidosASTNode *lhs_node = x_node->children_[0]; const EidosASTNode *rhs_node = x_node->children_[1]; const EidosASTNode *dot_node = nullptr, *constant_node = nullptr; if (lhs_node->token_->token_type_ == EidosTokenType::kTokenDot) { dot_node = lhs_node; constant_node = rhs_node; } else if (rhs_node->token_->token_type_ == EidosTokenType::kTokenDot) { dot_node = rhs_node; constant_node = lhs_node; } if (dot_node && constant_node && (dot_node->children_.size() == 2) && constant_node->HasCachedNumericValue()) { const EidosASTNode *var_node = dot_node->children_[0]; const EidosASTNode *prop_node = dot_node->children_[1]; mean_value = constant_node->CachedNumericValue(); if ((var_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (var_node->token_->token_string_ == "individual") && (prop_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (prop_node->token_->token_string_ == "tagF")) { // callback of the form { return D + dnorm(individual.tagF - A, 0.0, B) / C; } // callback of the form { return D + dnorm(individual.tagF - A, 0.0, B); } // callback of the form { return D + dnorm(A - individual.tagF, 0.0, B) / C; } // callback of the form { return D + dnorm(A - individual.tagF, 0.0, B); } p_script_block->has_cached_optimization_ = true; p_script_block->has_cached_opt_dnorm1_ = true; p_script_block->cached_opt_A_ = mean_value; p_script_block->cached_opt_B_ = sd_value; p_script_block->cached_opt_C_ = denominator; p_script_block->cached_opt_D_ = added_constant; } } } else if ((x_node->token_->token_type_ == EidosTokenType::kTokenDot) && (x_node->children_.size() == 2)) { const EidosASTNode *var_node = x_node->children_[0]; const EidosASTNode *prop_node = x_node->children_[1]; if ((var_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (var_node->token_->token_string_ == "individual") && (prop_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (prop_node->token_->token_string_ == "tagF")) { // callback of the form { return D + dnorm(individual.tagF, A, B) / C; } // callback of the form { return D + dnorm(individual.tagF, A, B); } p_script_block->has_cached_optimization_ = true; p_script_block->has_cached_opt_dnorm1_ = true; p_script_block->cached_opt_A_ = mean_value; p_script_block->cached_opt_B_ = sd_value; p_script_block->cached_opt_C_ = denominator; p_script_block->cached_opt_D_ = added_constant; } } } } } } } // if (p_script_block->has_cached_optimization_) // std::cout << "optimized:" << std::endl << " " << base_node->token_->token_string_ << std::endl; // else // std::cout << "NOT OPTIMIZED:" << std::endl << " " << base_node->token_->token_string_ << std::endl; } else if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosMutationEffectCallback) { const EidosASTNode *base_node = p_script_block->compound_statement_node_; if ((base_node->token_->token_type_ == EidosTokenType::kTokenLBrace) && (base_node->children_.size() == 1)) { const EidosASTNode *expr_node = base_node->children_[0]; // we must have an intervening "return", which we jump down through if ((expr_node->token_->token_type_ == EidosTokenType::kTokenReturn) && (expr_node->children_.size() == 1)) { expr_node = expr_node->children_[0]; if ((expr_node->token_->token_type_ == EidosTokenType::kTokenDiv) && (expr_node->children_.size() == 2)) { const EidosASTNode *numerator_node = expr_node->children_[0]; const EidosASTNode *denominator_node = expr_node->children_[1]; if (numerator_node->HasCachedNumericValue()) { double numerator = numerator_node->CachedNumericValue(); if ((denominator_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (denominator_node->token_->token_string_ == "effect")) { // callback of the form { return A/effect; } p_script_block->has_cached_optimization_ = true; p_script_block->has_cached_opt_reciprocal = true; p_script_block->cached_opt_A_ = numerator; } } } } } // if (p_script_block->has_cached_optimization_) // std::cout << "optimized:" << std::endl << " " << base_node->token_->token_string_ << std::endl; // else // std::cout << "NOT OPTIMIZED:" << std::endl << " " << base_node->token_->token_string_ << std::endl; } } } void Community::AddScriptBlock(SLiMEidosBlock *p_script_block, EidosInterpreter *p_interpreter, const EidosToken *p_error_token) { script_blocks_.emplace_back(p_script_block); p_script_block->TokenizeAndParse(); // can raise // Check for the presence/absence of a species specifier, as required by the block type if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosNoBlockType) { EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) attempted add of a script block of type SLiMEidosNoBlockType." << EidosTerminate(p_error_token); } else if ((p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventFirst) || (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventEarly) || (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventLate) || (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction)) { if (p_script_block->species_spec_) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for an event or user-defined function has a species set." << EidosTerminate(p_error_token); } else if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosInitializeCallback) { // with explicit species, initialize() callbacks may be species-specific or not, both are allowed; without explicit species, they must be species-specific (to `sim`) if (!is_explicit_species_ && !p_script_block->species_spec_) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for an initialize() callback in a single-species model has no species set." << EidosTerminate(p_error_token); } else if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosInteractionCallback) { // interaction() callbacks are always non-species-specific if (p_script_block->species_spec_) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for an interaction() callback has a species set." << EidosTerminate(p_error_token); } else if (!p_script_block->species_spec_) { EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a callback has no species set." << EidosTerminate(p_error_token); } // SPECIES CONSISTENCY CHECK if (p_script_block->species_spec_) { bool species_has_initialized = (p_script_block->species_spec_->Cycle() >= 1); if (p_script_block->mutation_type_id_ >= 0) { // if the mutation type exists now, we check that it belongs to the specified species MutationType *muttype = MutationTypeWithID(p_script_block->mutation_type_id_); if (species_has_initialized && !muttype) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): script block is specific to a mutation type id (" << p_script_block->mutation_type_id_ << ") that does not exist." << EidosTerminate(p_error_token); if (muttype && (&muttype->species_ != p_script_block->species_spec_)) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): script block is specific to a mutation type id (" << p_script_block->mutation_type_id_ << ") that belongs to a different species." << EidosTerminate(p_error_token); } if (p_script_block->subpopulation_id_ >= 0) { // if the subpopulation exists now, we check that it belongs to the specified species Subpopulation *subpop = SubpopulationWithID(p_script_block->subpopulation_id_); // cannot error out if the subpopulation does not exist, since subpopulations are dynamic if (subpop && (&subpop->species_ != p_script_block->species_spec_)) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): script block is specific to a subpopulation id (" << p_script_block->subpopulation_id_ << ") that belongs to a different species." << EidosTerminate(p_error_token); } if (p_script_block->interaction_type_id_ >= 0) { // interaction() callbacks may not have a specified species EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block with interaction_type_id_ set has a specified species." << EidosTerminate(p_error_token); } if (p_script_block->sex_specificity_ != IndividualSex::kUnspecified) { // if the species has been initialized, we check that it is sexual if necessary if (p_script_block->type_ != SLiMEidosBlockType::SLiMEidosReproductionCallback) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a non-reproduction() callback has sex_specificity_ set." << EidosTerminate(p_error_token); if (species_has_initialized && !p_script_block->species_spec_->SexEnabled()) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a reproduction() callback has sex_specificity_ set, but the specified species is not sexual." << EidosTerminate(p_error_token); } } else if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosInteractionCallback) { // interaction() callbacks are weird; they are callbacks that are non-species-specific, so they must be checked separately if (p_script_block->mutation_type_id_ != -1) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for an interaction() callback has mutation_type_id_ set." << EidosTerminate(p_error_token); if (p_script_block->sex_specificity_ != IndividualSex::kUnspecified) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for an interaction() callback has sex_specificity_ set." << EidosTerminate(p_error_token); } else { // At this point we have an event, a user-defined function, or a non-species-specific initialize() callback, and no other specifier should be set if (p_script_block->mutation_type_id_ != -1) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a non-callback or initialize() callback has mutation_type_id_ set." << EidosTerminate(p_error_token); if (p_script_block->subpopulation_id_ != -1) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a non-callback or initialize() callback has subpopulation_id_ set." << EidosTerminate(p_error_token); if (p_script_block->chromosome_id_ != -1) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a non-callback or initialize() callback has chromosome_id_ set." << EidosTerminate(p_error_token); if (p_script_block->interaction_type_id_ != -1) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a non-callback or initialize() callback has interaction_type_id_ set." << EidosTerminate(p_error_token); if (p_script_block->sex_specificity_ != IndividualSex::kUnspecified) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): (internal error) script block for a non-callback or initialize() callback has sex_specificity_ set." << EidosTerminate(p_error_token); } // The script block passed tokenization and parsing, so it is reasonably well-formed. Now we check for cases we optimize. OptimizeScriptBlock(p_script_block); // Define the symbol for the script block, if any if (p_script_block->block_id_ != -1) { EidosSymbolTableEntry &symbol_entry = p_script_block->ScriptBlockSymbolTableEntry(); EidosGlobalStringID symbol_id = symbol_entry.first; if ((simulation_constants_->ContainsSymbol(symbol_id)) || (p_interpreter && p_interpreter->SymbolTable().ContainsSymbol(symbol_id))) EIDOS_TERMINATION << "ERROR (Community::AddScriptBlock): script block symbol " << EidosStringRegistry::StringForGlobalStringID(symbol_entry.first) << " was already defined prior to its definition here." << EidosTerminate(p_error_token); simulation_constants_->InitializeConstantSymbolEntry(symbol_entry); } // Notify the various interested parties that the script blocks have changed last_script_block_tick_cached_ = false; script_block_types_cached_ = false; scripts_changed_ = true; #if DEBUG_BLOCK_REG_DEREG std::cout << "Tick " << tick_ << ": AddScriptBlock() just added a block, script_blocks_ is:" << std::endl; for (SLiMEidosBlock *script_block : script_blocks_) { std::cout << " "; script_block->Print(std::cout); std::cout << std::endl; } #endif #ifdef SLIMGUI if (p_interpreter) // not when initializing the community from script { gSLiMScheduling << "\t\tnew script block registered: "; p_script_block->PrintDeclaration(gSLiMScheduling, this); gSLiMScheduling << std::endl; } #endif } void Community::DeregisterScheduledScriptBlocks(void) { // If we have blocks scheduled for deregistration, we sweep through and deregister them at the end of each stage of each tick. // This happens at a time when script blocks are not executing, so that we're guaranteed not to leave hanging pointers that could // cause a crash; it also guarantees that script blocks are applied consistently across each cycle stage. A single block // might be scheduled for deregistration more than once, but should only occur in script_blocks_ once, so we have to be careful // with our deallocations here; we deallocate a block only when we find it in script_blocks_. #if DEBUG_BLOCK_REG_DEREG if (scheduled_deregistrations_.size()) { std::cout << "Tick " << tick_ << ": DeregisterScheduledScriptBlocks() planning to remove:" << std::endl; for (SLiMEidosBlock *script_block : scheduled_deregistrations_) { std::cout << " "; script_block->Print(std::cout); std::cout << std::endl; } } #endif for (SLiMEidosBlock *block_to_dereg : scheduled_deregistrations_) { auto script_block_position = std::find(script_blocks_.begin(), script_blocks_.end(), block_to_dereg); if (script_block_position != script_blocks_.end()) { #if DEBUG_BLOCK_REG_DEREG std::cout << "Tick " << tick_ << ": DeregisterScheduledScriptBlocks() removing block:" << std::endl; std::cout << " "; block_to_dereg->Print(std::cout); std::cout << std::endl; #endif // Remove the symbol for it first if (block_to_dereg->block_id_ != -1) simulation_constants_->RemoveConstantForSymbol(block_to_dereg->ScriptBlockSymbolTableEntry().first); // Then remove it from our script block list and deallocate it script_blocks_.erase(script_block_position); last_script_block_tick_cached_ = false; script_block_types_cached_ = false; scripts_changed_ = true; delete block_to_dereg; } else { EIDOS_TERMINATION << "ERROR (Community::DeregisterScheduledScriptBlocks): (internal error) couldn't find block for deregistration." << EidosTerminate(); } } #if DEBUG_BLOCK_REG_DEREG if (scheduled_deregistrations_.size()) { std::cout << "Tick " << tick_ << ": DeregisterScheduledScriptBlocks() after removal:" << std::endl; for (SLiMEidosBlock *script_block : script_blocks_) { std::cout << " "; script_block->Print(std::cout); std::cout << std::endl; } } #endif scheduled_deregistrations_.resize(0); } void Community::DeregisterScheduledInteractionBlocks(void) { // Identical to DeregisterScheduledScriptBlocks() above, but for the interaction() dereg list; see deregisterScriptBlock() #if DEBUG_BLOCK_REG_DEREG if (scheduled_interaction_deregs_.size()) { std::cout << "Tick " << tick_ << ": DeregisterScheduledInteractionBlocks() planning to remove:" << std::endl; for (SLiMEidosBlock *script_block : scheduled_interaction_deregs_) { std::cout << " "; script_block->Print(std::cout); std::cout << std::endl; } } #endif for (SLiMEidosBlock *block_to_dereg : scheduled_interaction_deregs_) { auto script_block_position = std::find(script_blocks_.begin(), script_blocks_.end(), block_to_dereg); if (script_block_position != script_blocks_.end()) { #if DEBUG_BLOCK_REG_DEREG std::cout << "Tick " << tick_ << ": DeregisterScheduledInteractionBlocks() removing block:" << std::endl; std::cout << " "; block_to_dereg->Print(std::cout); std::cout << std::endl; #endif // Remove the symbol for it first if (block_to_dereg->block_id_ != -1) simulation_constants_->RemoveConstantForSymbol(block_to_dereg->ScriptBlockSymbolTableEntry().first); // Then remove it from our script block list and deallocate it script_blocks_.erase(script_block_position); last_script_block_tick_cached_ = false; script_block_types_cached_ = false; scripts_changed_ = true; delete block_to_dereg; } else { EIDOS_TERMINATION << "ERROR (Community::DeregisterScheduledInteractionBlocks): (internal error) couldn't find block for deregistration." << EidosTerminate(); } } #if DEBUG_BLOCK_REG_DEREG if (scheduled_interaction_deregs_.size()) { std::cout << "Tick " << tick_ << ": DeregisterScheduledInteractionBlocks() after removal:" << std::endl; for (SLiMEidosBlock *script_block : script_blocks_) { std::cout << " "; script_block->Print(std::cout); std::cout << std::endl; } } #endif scheduled_interaction_deregs_.resize(0); } void Community::ExecuteFunctionDefinitionBlock(SLiMEidosBlock *p_script_block) { EidosSymbolTable callback_symbols(EidosSymbolTableType::kContextConstantsTable, &SymbolTable()); EidosSymbolTable client_symbols(EidosSymbolTableType::kLocalVariablesTable, &callback_symbols); EidosInterpreter interpreter(p_script_block->root_node_->children_[0], client_symbols, simulation_functions_, this, SLIM_OUTSTREAM, SLIM_ERRSTREAM #ifdef SLIMGUI , check_infinite_loops_ #endif ); try { // Interpret the script; the result from the interpretation is not used for anything EidosValue_SP result = interpreter.EvaluateInternalBlock(p_script_block->script_); } catch (...) { throw; } } bool Community::SubpopulationIDInUse(slim_objectid_t p_subpop_id) { // This method returns whether a subpop ID is conceptually "in use": whether it is being used, has ever // been used, or is reserved for use in some way by, by any SLiM species or by any tree sequence. // First check our own data structures; we now do not allow reuse of subpop ids, even disjoint in time for (Species *species : all_species_) if (species->used_subpop_ids_.find(p_subpop_id) != species->used_subpop_ids_.end()) return true; // Then have each species check for a conflict with its tree-sequence population table for (Species *species : all_species_) if (species->_SubpopulationIDInUse(p_subpop_id)) return true; return false; } bool Community::SubpopulationNameInUse(const std::string &p_subpop_name) { // This method returns whether a subpop name is conceptually "in use": whether it is being used, has ever // been used, or is reserved for use in some way by, by any SLiM species or by any tree sequence. // First check our own data structures; we now do not allow reuse of subpop names, even disjoint in time for (Species *species : all_species_) if (species->used_subpop_names_.count(p_subpop_name)) return true; // The tree-sequence population table does not keep names for populations, so no conflicts can occur return false; } Subpopulation *Community::SubpopulationWithID(slim_objectid_t p_subpop_id) { for (Species *species : all_species_) { Subpopulation *found_subpop = species->SubpopulationWithID(p_subpop_id); if (found_subpop) return found_subpop; } return nullptr; } Subpopulation *Community::SubpopulationWithName(const std::string &p_subpop_name) { for (Species *species : all_species_) { Subpopulation *found_subpop = species->SubpopulationWithName(p_subpop_name); if (found_subpop) return found_subpop; } return nullptr; } MutationType *Community::MutationTypeWithID(slim_objectid_t p_muttype_id) { for (Species *species : all_species_) { MutationType *found_muttype = species->MutationTypeWithID(p_muttype_id); if (found_muttype) return found_muttype; } return nullptr; } GenomicElementType *Community::GenomicElementTypeWithID(slim_objectid_t p_getype_id) { for (Species *species : all_species_) { GenomicElementType *found_getype = species->GenomicElementTypeWithID(p_getype_id); if (found_getype) return found_getype; } return nullptr; } SLiMEidosBlock *Community::ScriptBlockWithID(slim_objectid_t p_script_block_id) { for (SLiMEidosBlock *block : script_blocks_) if (block->block_id_ == p_script_block_id) return block; return nullptr; } Species *Community::SpeciesWithID(slim_objectid_t p_species_id) { // Species IDs are just indices into all_species_ if ((p_species_id < 0) || (p_species_id >= (slim_objectid_t)all_species_.size())) return nullptr; return all_species_[p_species_id]; } Species *Community::SpeciesWithName(const std::string &species_name) { for (Species *species : all_species_) { if (species->name_ == species_name) return species; } return nullptr; } void Community::InvalidateInteractionsForSpecies(Species *p_invalid_species) { for (auto iter : interaction_types_) iter.second->InvalidateForSpecies(p_invalid_species); } void Community::InvalidateInteractionsForSubpopulation(Subpopulation *p_invalid_subpop) { for (auto iter : interaction_types_) iter.second->InvalidateForSubpopulation(p_invalid_subpop); } Species *Community::SpeciesForIndividualsVector(const Individual * const *individuals, int value_count) { if (value_count == 0) return nullptr; Species *consensus_species = &individuals[0]->subpopulation_->species_; if (consensus_species->community_.all_species_.size() == 1) // with only one species, all objects must be in this species return consensus_species; for (int value_index = 1; value_index < value_count; ++value_index) { Species *species = &individuals[value_index]->subpopulation_->species_; if (species != consensus_species) return nullptr; } return consensus_species; } Species *Community::SpeciesForIndividuals(EidosValue *value) { if (value->Type() != EidosValueType::kValueObject) EIDOS_TERMINATION << "ERROR (Community::SpeciesForIndividuals): (internal error) value is not of type object." << EidosTerminate(); EidosValue_Object *object_value = (EidosValue_Object *)value; int value_count = object_value->Count(); if (value_count == 0) // allow an empty vector that is not of class Individual, to allow object() to pass our checks return nullptr; if (object_value->Class() != gSLiM_Individual_Class) EIDOS_TERMINATION << "ERROR (Community::SpeciesForIndividuals): (internal error) value is not of class Individual." << EidosTerminate(); if (value_count == 1) return &((Individual *)object_value->ObjectElementAtIndex_NOCAST(0, nullptr))->subpopulation_->species_; EidosValue_Object *object_vector_value = (EidosValue_Object *)object_value; const Individual * const *individuals = (Individual **)object_vector_value->data(); return Community::SpeciesForIndividualsVector(individuals, value_count); } Species *Community::SpeciesForHaplosomesVector(const Haplosome * const *haplosomes, int value_count) { if (value_count == 0) return nullptr; Species *consensus_species = &haplosomes[0]->OwningIndividual()->subpopulation_->species_; if (consensus_species->community_.all_species_.size() == 1) // with only one species, all objects must be in this species return consensus_species; for (int value_index = 1; value_index < value_count; ++value_index) { const Species *species = &haplosomes[value_index]->OwningIndividual()->subpopulation_->species_; if (species != consensus_species) return nullptr; } return consensus_species; } Species *Community::SpeciesForHaplosomes(EidosValue *value) { if (value->Type() != EidosValueType::kValueObject) EIDOS_TERMINATION << "ERROR (Community::SpeciesForHaplosomes): (internal error) value is not of type object." << EidosTerminate(); EidosValue_Object *object_value = (EidosValue_Object *)value; int value_count = object_value->Count(); if (value_count == 0) // allow an empty vector that is not of class Individual, to allow object() to pass our checks return nullptr; if (object_value->Class() != gSLiM_Haplosome_Class) EIDOS_TERMINATION << "ERROR (Community::SpeciesForHaplosomes): (internal error) value is not of class Haplosome." << EidosTerminate(); if (value_count == 1) return &((Haplosome *)object_value->ObjectElementAtIndex_NOCAST(0, nullptr))->OwningIndividual()->subpopulation_->species_; EidosValue_Object *object_vector_value = (EidosValue_Object *)object_value; const Haplosome * const *haplosomes = (Haplosome **)object_vector_value->data(); return Community::SpeciesForHaplosomesVector(haplosomes, value_count); } Species *Community::SpeciesForMutationsVector(const Mutation * const *mutations, int value_count) { if (value_count == 0) return nullptr; Species *consensus_species = &mutations[0]->mutation_type_ptr_->species_; if (consensus_species->community_.all_species_.size() == 1) // with only one species, all objects must be in this species return consensus_species; for (int value_index = 1; value_index < value_count; ++value_index) { Species *species = &mutations[value_index]->mutation_type_ptr_->species_; if (species != consensus_species) return nullptr; } return consensus_species; } Species *Community::SpeciesForMutations(EidosValue *value) { if (value->Type() != EidosValueType::kValueObject) EIDOS_TERMINATION << "ERROR (Community::SpeciesForMutations): (internal error) value is not of type object." << EidosTerminate(); EidosValue_Object *object_value = (EidosValue_Object *)value; int value_count = object_value->Count(); if (value_count == 0) // allow an empty vector that is not of class Individual, to allow object() to pass our checks return nullptr; if (object_value->Class() != gSLiM_Mutation_Class) EIDOS_TERMINATION << "ERROR (Community::SpeciesForMutations): (internal error) value is not of class Mutation." << EidosTerminate(); if (value_count == 1) return &((Mutation *)object_value->ObjectElementAtIndex_NOCAST(0, nullptr))->mutation_type_ptr_->species_; EidosValue_Object *object_vector_value = (EidosValue_Object *)object_value; const Mutation * const *mutations = (Mutation **)object_vector_value->data(); return Community::SpeciesForMutationsVector(mutations, value_count); } EidosValue_SP Community::_EvaluateTickRangeNode(const EidosASTNode *p_node, std::string &p_error_string) { // We have a tick range expression that we need to execute, and it should return an integer value to us. // This is basically a lambda call, so the code here is parallel to the executeLambda() code in many ways. // Additional semantic restrictions are imposed by Community::EvaluateScriptBlockTickRanges(). // We consider the tick range to be the union of the ranges of all of the tokens it comprises. // We don't need to do any error-handling in try/catch since we are not creating a new error-handling // scope (the way executing a lambda or a script block does). // Unusually, we want to allow only global constants to be used in tick range expressions; // anything else (context constants like community and sim, global variables, etc.) will // potentially change value, and thus imply that the tick range is dynamically based on // current simulation state, which is not true. There is still a hole here, in that a // global constant might refer to a Dictionary, and the tick range could reference a // value in that Dictionary that changes dynamically; but that is contrived, and the user // will soon discover that it doesn't work, and I'm not going to worry about it. Better // would be to have a real concept in Eidos of constant expressions, and require one here. // BCH 7/19/2024: This method can now return nullptr, beware! It does so if evaluating the // tick range node raises, specifically with an "undefined identifier" warning. It is then // assumed that that is a global constant that will be defined later, and so we return // nullptr to indicate "not ready yet, try again later". That particular error is not // reported to the user, since it is not, in fact, an error in this context. Other raises // will not be caught here and will continue upwards and be reported. EidosSymbolTable *client_symbols = nullptr; { client_symbols = &SymbolTable(); while (client_symbols && (client_symbols->TableType() != EidosSymbolTableType::kEidosDefinedConstantsTable) && (client_symbols->TableType() != EidosSymbolTableType::kEidosIntrinsicConstantsTable)) client_symbols = client_symbols->ChainSymbolTable(); if (!client_symbols) EIDOS_TERMINATION << "ERROR (Community::_EvaluateTickRangeNode): (internal error) couldn't find the defined constants or intrinsic constants symbol tables." << EidosTerminate(nullptr); //std::cout << std::endl << "Community::_EvaluateTickRangeNode() client_symbols:" << std::endl; //client_symbols->PrintSymbolTableChain(std::cout); } EidosFunctionMap &function_map = FunctionMap(); EidosInterpreter interpreter(p_node, *client_symbols, function_map, this, SLIM_OUTSTREAM, SLIM_ERRSTREAM #ifdef SLIMGUI , check_infinite_loops_ #endif ); EidosValue_SP result_SP; // BCH 7/19/2024: Here we enable the use of a special exception, SLiMUndefinedIdentifierException, // when an identifier is undefined inside the tick range expression. This allows us to catch that // specific exception type and handle it. This is gross but effective! I think it works in every // case but x[Y] where x is defined but y is undefined; that will still raise. Nobody will notice // that they can't defer that specific expression type. :-> SLiMUndefinedIdentifierException is // raised by _GetValue_SpecialRaise(), a special EidosSymbolTable method that is used to fetch // symbol table values in EidosInterpreter::Evaluate_Identifier() only when a special flag is set // enabling this mode of operation. We set that flag with SetUseCustomUndefinedIdentifierRaise(). // This rather convoluted design is necessary because Eidos normally does not necessarily throw for // errors at all; when running on the command line, it simply logs the error and exits. So we // needed a special flag to change that behavior to throwing a custom exception in all cases, to // make the interpreter tolerant at runtime of this specific case. // BCH 3/21/2025: Broadening this mechanism to also encompass a raise due to an undefined function // name, only when tick == -1 (during construction). We get called by FinishInitialization(), at // which point user-defined functions have not yet been parsed, and we want to fail silently and // try again later if a tick range expression depends on a user-defined function. The evaluation // should succeed after initialize(). See https://github.com/MesserLab/SLiM/issues/495. Note that // we do not protect against an undefined function name in doCall(), only in direct function calls. // I'm not sure there's a really solid reason for that choice, it's just what I decided to do. interpreter.SetUseCustomUndefinedIdentifierRaise(true); if (tick_ == -1) interpreter.SetUseCustomUndefinedFunctionRaise(true); try { result_SP = interpreter.FastEvaluateNode(p_node); } catch (SLiMUndefinedIdentifierException &e) { // for undefined identifiers, cache the name of the undefined constant and return nullptr p_error_string = e.what(); return EidosValue_SP(); } catch (SLiMUndefinedFunctionException &e) { // for undefined functions, we don't need to remember the name; just return nullptr return EidosValue_SP(); } // no need to set the "custom undefined..." flags back, it's a local interpreter anyway p_error_string = ""; // no execution error, so clear out any cached error string present EidosValueType result_type = result_SP->Type(); if (result_type != EidosValueType::kValueInt) EIDOS_TERMINATION << "ERROR (Community::_EvaluateTickRangeNode): tick range expressions must evaluate to an integer value." << EidosTerminate(p_node->ErrorPositionForNodeAndChildren()); int result_count = result_SP->Count(); const int64_t *int_data = result_SP->IntData(); for (int index = 0; index < result_count; ++index) { int64_t result_element = int_data[index]; if ((result_element < 1) || (result_element > SLIM_MAX_TICK)) EIDOS_TERMINATION << "ERROR (Community::_EvaluateTickRangeNode): the tick expression " << p_node->token_->token_string_ << " contains an element that is out of range (" << result_element << ")." << EidosTerminate(p_node->ErrorPositionForNodeAndChildren()); } return result_SP; } SLiMCycleStage Community::CycleStageForScriptBlockType(SLiMEidosBlockType p_block_type) { // Figure out what cycle stage the rescheduled block executes in; this is annoying, but necessary for the new scheduling check call SLiMCycleStage stage = SLiMCycleStage::kStagePostCycle; // unused below, just here to silence a warning // NOLINTBEGIN(*-branch-clone) : multiple internal tick stages map to the same user-level stage if (model_type_ == SLiMModelType::kModelTypeWF) { switch (p_block_type) { case SLiMEidosBlockType::SLiMEidosEventFirst: stage = SLiMCycleStage::kWFStage0ExecuteFirstScripts; break; case SLiMEidosBlockType::SLiMEidosEventEarly: stage = SLiMCycleStage::kWFStage1ExecuteEarlyScripts; break; case SLiMEidosBlockType::SLiMEidosEventLate: stage = SLiMCycleStage::kWFStage5ExecuteLateScripts; break; case SLiMEidosBlockType::SLiMEidosInitializeCallback: stage = SLiMCycleStage::kStagePreCycle; break; case SLiMEidosBlockType::SLiMEidosMutationEffectCallback: stage = SLiMCycleStage::kWFStage6CalculateFitness; break; case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback: stage = SLiMCycleStage::kWFStage6CalculateFitness; break; case SLiMEidosBlockType::SLiMEidosInteractionCallback: stage = SLiMCycleStage::kWFStage7AdvanceTickCounter; break; case SLiMEidosBlockType::SLiMEidosMateChoiceCallback: stage = SLiMCycleStage::kWFStage2GenerateOffspring; break; case SLiMEidosBlockType::SLiMEidosModifyChildCallback: stage = SLiMCycleStage::kWFStage2GenerateOffspring; break; case SLiMEidosBlockType::SLiMEidosRecombinationCallback: stage = SLiMCycleStage::kWFStage2GenerateOffspring; break; case SLiMEidosBlockType::SLiMEidosMutationCallback: stage = SLiMCycleStage::kWFStage2GenerateOffspring; break; // script block types that are not allowed in WF models, or have no cycle stage case SLiMEidosBlockType::SLiMEidosSurvivalCallback: case SLiMEidosBlockType::SLiMEidosReproductionCallback: case SLiMEidosBlockType::SLiMEidosNoBlockType: case SLiMEidosBlockType::SLiMEidosUserDefinedFunction: EIDOS_TERMINATION << "ERROR (Community::CycleStageForScriptBlockType): (internal error) CycleStageForScriptBlockType() cannot be called on this type of script block." << EidosTerminate(); } } else { switch (p_block_type) { case SLiMEidosBlockType::SLiMEidosEventFirst: stage = SLiMCycleStage::kNonWFStage0ExecuteFirstScripts; break; case SLiMEidosBlockType::SLiMEidosEventEarly: stage = SLiMCycleStage::kNonWFStage2ExecuteEarlyScripts; break; case SLiMEidosBlockType::SLiMEidosEventLate: stage = SLiMCycleStage::kNonWFStage6ExecuteLateScripts; break; case SLiMEidosBlockType::SLiMEidosInitializeCallback: stage = SLiMCycleStage::kStagePreCycle; break; case SLiMEidosBlockType::SLiMEidosMutationEffectCallback: stage = SLiMCycleStage::kNonWFStage3CalculateFitness; break; case SLiMEidosBlockType::SLiMEidosFitnessEffectCallback: stage = SLiMCycleStage::kNonWFStage3CalculateFitness; break; case SLiMEidosBlockType::SLiMEidosInteractionCallback: stage = SLiMCycleStage::kNonWFStage7AdvanceTickCounter; break; case SLiMEidosBlockType::SLiMEidosModifyChildCallback: stage = SLiMCycleStage::kNonWFStage1GenerateOffspring; break; case SLiMEidosBlockType::SLiMEidosRecombinationCallback: stage = SLiMCycleStage::kNonWFStage1GenerateOffspring; break; case SLiMEidosBlockType::SLiMEidosMutationCallback: stage = SLiMCycleStage::kNonWFStage1GenerateOffspring; break; case SLiMEidosBlockType::SLiMEidosSurvivalCallback: stage = SLiMCycleStage::kNonWFStage4SurvivalSelection; break; case SLiMEidosBlockType::SLiMEidosReproductionCallback: stage = SLiMCycleStage::kNonWFStage1GenerateOffspring; break; // script block types that are not allowed in nonWF models, or have no cycle stage case SLiMEidosBlockType::SLiMEidosMateChoiceCallback: case SLiMEidosBlockType::SLiMEidosNoBlockType: case SLiMEidosBlockType::SLiMEidosUserDefinedFunction: EIDOS_TERMINATION << "ERROR (Community::CycleStageForScriptBlockType): (internal error) CycleStageForScriptBlockType() cannot be called on this type of script block." << EidosTerminate(); } } // NOLINTEND(*-branch-clone) return stage; } bool Community::IsPastOrPresent(slim_tick_t p_block_tick, SLiMEidosBlockType p_block_type) { // This checks whether the given tick, for the given script block type, would be past or present (true) // versus future (false). If the tick is less than the current tick, then it is clearly past; if it is // greater. it is clearly future. The tricky part is if it is equal; then we have to look at the tick // cycle stage and determine whether, within the current tick, it is past/present or future. See also // Community::CheckScheduling() for a very similar piece of code, different only in how it handles the // problem. // Note that these timing calculations are really just an approximation in some cases. For example, // a fitnessEffect() callback normally runs during fitness calculation, and that is what this code and // Community::CycleStageForScriptBlockType() above assume; but fitness calculation can occur at other // times too. If the user defines a new constant in a first() event that activates a fitnessEffect() // callback in the current tick, and then immediately recalculates fitness, the callback might not be // used in the calculation -- although in fact I think it would be, given the design of the code. The // point being that there is not an exact 1-to-1 correspondence between script block types and cycle // stages, in reality, and so this timing check is just an approximation. if (p_block_tick < tick_) return true; if (p_block_tick == tick_) { SLiMCycleStage block_cycle_stage = CycleStageForScriptBlockType(p_block_type); if (block_cycle_stage <= cycle_stage_) return true; } return false; } void Community::EvaluateScriptBlockTickRanges() { if (all_tick_ranges_evaluated_) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): (internal error) EvaluateScriptBlockTickRanges() called unexpectedly." << EidosTerminate(nullptr); std::vector &script_blocks = AllScriptBlocks(); // Evaluate tick range expressions to determine the start and end tick for each block // Assume all succeed in evaluating, until one fails all_tick_ranges_evaluated_ = true; bool any_scheduling_change = false; // set to true if anything actually gets scheduled for (auto script_block : script_blocks) { if ((script_block->type_ != SLiMEidosBlockType::SLiMEidosInitializeCallback) && !script_block->tick_range_evaluated_) { const EidosASTNode *start_tick_node = script_block->start_tick_node_; const EidosASTNode *colon_node = script_block->colon_node_; const EidosASTNode *end_tick_node = script_block->end_tick_node_; #if DEBUG_TICK_RANGES std::cout << "script block " << *script_block << " (" << script_block << "):"; std::cout << "\n start tick expression:"; if (start_tick_node) start_tick_node->PrintTreeWithIndent(std::cout, 2); else std::cout << "\n -- none --"; if (colon_node) std::cout << "\n colon node present"; std::cout << "\n end tick expression:"; if (end_tick_node) end_tick_node->PrintTreeWithIndent(std::cout, 2); else std::cout << "\n -- none --"; std::cout << std::endl; #endif if (start_tick_node && colon_node && end_tick_node) { // e.g., 5:10 -- both expressions must evaluate to singletons, AND both must be "primary"/"postfix". That means they must be a simple // number or identifier, a dot-expression (property access), a bracket-expression (subset), a paren-expression (function or method call), // or an expression that was grouped by parentheses. The last one is tricky, because the parser doesn't generate a node representing // the grouping parentheses; instead, for this work, I added a flag, was_parenthesized_, that is set on a node if it was originally // inside grouping parentheses. This flag gets the information we need out of the parser. This requirement exists in the first place // so that X:Y range expressions don't violate the normal Eidos precedence rules for operator :. Basically, X and Y both have to be // higher precedence than operator :, so that X:Y and (X):(Y) mean the same thing. That is what our restrictions guarantee. // See Parse_SLiMEidosBlock() for more comments on this rather complicated problem, which is rooted in wanting to allow the X: and :Y // range syntaxes, as well as wanting to avoid user errors with the (surprising) operator : precedence rules. EidosTokenType start_type = start_tick_node->token_->token_type_; EidosTokenType end_type = end_tick_node->token_->token_type_; if ((start_type != EidosTokenType::kTokenNumber) && (start_type != EidosTokenType::kTokenIdentifier) && (start_type != EidosTokenType::kTokenDot) && (start_type != EidosTokenType::kTokenLBracket) && (start_type != EidosTokenType::kTokenLParen) && !start_tick_node->was_parenthesized_) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the start and end tick expressions must both be simple expressions (numbers, identifiers, expressions inside parentheses, and similar) when a tick range, X:Y, is used; this avoids precedence issues with the sequence operator, ':'. To avoid this error, use parentheses to group the start and end tick expressions to make the precedence explicit." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); if ((end_type != EidosTokenType::kTokenNumber) && (end_type != EidosTokenType::kTokenIdentifier) && (end_type != EidosTokenType::kTokenDot) && (end_type != EidosTokenType::kTokenLBracket) && (end_type != EidosTokenType::kTokenLParen) && !end_tick_node->was_parenthesized_) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the start and end tick expressions must both be simple expressions (numbers, identifiers, expressions inside parentheses, and similar) when a tick range, X:Y, is used; this avoids precedence issues with the sequence operator, ':'. To avoid this error, use parentheses to group the left and right sides to make the precedence explicit." << EidosTerminate(end_tick_node->ErrorPositionForNodeAndChildren()); EidosValue_SP start_expr_value = _EvaluateTickRangeNode(start_tick_node, script_block->unevaluated_error_string_); if (!start_expr_value) { all_tick_ranges_evaluated_ = false; continue; } EidosValue_SP end_expr_value = _EvaluateTickRangeNode(end_tick_node, script_block->unevaluated_error_string_); if (!end_expr_value) { all_tick_ranges_evaluated_ = false; continue; } if (start_expr_value->Count() != 1) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the start and end tick expressions must both evaluate to a singleton integer when a tick range, X:Y, is used." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); if (end_expr_value->Count() != 1) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the start and end tick expressions must both evaluate to a singleton integer when a tick range, X:Y, is used." << EidosTerminate(end_tick_node->ErrorPositionForNodeAndChildren()); script_block->tick_range_evaluated_ = true; script_block->tick_range_is_sequence_ = true; script_block->tick_start_ = (slim_tick_t)start_expr_value->IntAtIndex_NOCAST(0, nullptr); script_block->tick_end_ = (slim_tick_t)end_expr_value->IntAtIndex_NOCAST(0, nullptr); script_block->tick_set_.clear(); any_scheduling_change = true; if (script_block->tick_end_ < script_block->tick_start_) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the end tick expression " << end_tick_node->token_->token_string_ << " evaluated to be less than the start tick expression " << start_tick_node->token_->token_string_ << " (" << script_block->tick_end_ << " < " << script_block->tick_start_ << ")." << EidosTerminate(end_tick_node->ErrorPositionForNodeAndChildren()); // With a specified start tick, it is an error for that start tick // to be past/present, since a fire of the event will be missed if (IsPastOrPresent(script_block->tick_start_, script_block->type_)) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the start tick expression " << start_tick_node->token_->token_string_ << " evaluated to " << script_block->tick_start_ << ", which is past/present; the current tick is " << tick_ << ". This means that the event will not be able to execute in one or more of its scheduled ticks, which is an error." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); #if DEBUG_TICK_RANGES std::cout << " tick range " << script_block->tick_start_ << " to " << script_block->tick_end_ << std::endl; #endif } else if (start_tick_node && colon_node) { // e.g., 5: -- the start expression must evaluate to a singleton EidosValue_SP start_expr_value = _EvaluateTickRangeNode(start_tick_node, script_block->unevaluated_error_string_); if (!start_expr_value) { all_tick_ranges_evaluated_ = false; continue; } if (start_expr_value->Count() != 1) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the start tick expression must evaluate to a singleton integer when a tick range, X:, is used." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); script_block->tick_range_evaluated_ = true; script_block->tick_range_is_sequence_ = true; script_block->tick_start_ = (slim_tick_t)start_expr_value->IntAtIndex_NOCAST(0, nullptr); script_block->tick_end_ = SLIM_MAX_TICK + 1; script_block->tick_set_.clear(); any_scheduling_change = true; // With a specified start tick, it is an error for that start tick // to be past/present, since a fire of the event will be missed if (IsPastOrPresent(script_block->tick_start_, script_block->type_)) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the start tick expression " << start_tick_node->token_->token_string_ << " evaluated to " << script_block->tick_start_ << ", which is past/present; the current tick is " << tick_ << ". This means that the event will not be able to execute in one or more of its scheduled ticks, which is an error." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); #if DEBUG_TICK_RANGES std::cout << " tick range " << script_block->tick_start_ << " to end." << std::endl; #endif } else if (colon_node && end_tick_node) { // e.g., :5 -- the end expression must evaluate to a singleton EidosValue_SP end_expr_value = _EvaluateTickRangeNode(end_tick_node, script_block->unevaluated_error_string_); if (!end_expr_value) { all_tick_ranges_evaluated_ = false; continue; } if (end_expr_value->Count() != 1) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the end tick expression must evaluate to a singleton integer when a tick range, :X, is used." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); script_block->tick_range_evaluated_ = true; script_block->tick_range_is_sequence_ = true; script_block->tick_start_ = 1; script_block->tick_end_ = (slim_tick_t)end_expr_value->IntAtIndex_NOCAST(0, nullptr); script_block->tick_set_.clear(); any_scheduling_change = true; // With an implied start, it is an error for that start tick // to be past/present, since a fire of the event will be missed if (tick_ > 0) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): with an implied start tick at the start of model execution, the event has missed one or more of its scheduled ticks, which is an error." << EidosTerminate(colon_node->ErrorPositionForNodeAndChildren()); #if DEBUG_TICK_RANGES std::cout << " tick range beginning to " << script_block->tick_end_ << "." << std::endl; #endif } else if (start_tick_node && !colon_node) { // e.g., 5 -- in this case, the expression may evaluate to a non-singleton integer value, like "seq(1, 100, by=2)" EidosValue_SP expr_value = _EvaluateTickRangeNode(start_tick_node, script_block->unevaluated_error_string_); if (!expr_value) { all_tick_ranges_evaluated_ = false; continue; } const int64_t *expr_data = expr_value->IntData(); int expr_count = expr_value->Count(); if (expr_count == 0) { // set to run in no ticks; we do this with an empty set script_block->tick_range_evaluated_ = true; script_block->tick_range_is_sequence_ = false; script_block->tick_set_.clear(); any_scheduling_change = true; } else { // if it is a singleton, or a consecutive range, we detect that and handle it efficiently bool is_sequential = true; int64_t first_value = expr_data[0]; int64_t prev_value = first_value; for (int index = 1; index < expr_count; ++index) { int64_t value = expr_data[index]; if (value != prev_value + 1) { is_sequential = false; break; } prev_value = value; } if (is_sequential) { script_block->tick_range_evaluated_ = true; script_block->tick_range_is_sequence_ = true; script_block->tick_start_ = (slim_tick_t)first_value; script_block->tick_end_ = (slim_tick_t)prev_value; script_block->tick_set_.clear(); any_scheduling_change = true; // With a specified start tick, it is an error for that start tick // to be past/present, since a fire of the event will be missed if (IsPastOrPresent(script_block->tick_start_, script_block->type_)) { if (script_block->tick_start_ == script_block->tick_end_) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the tick range expression " << start_tick_node->token_->token_string_ << " evaluated to tick " << script_block->tick_start_ << ", which is past/present; the current tick is " << tick_ << ". This means that the event will not be able to execute in its scheduled tick, which is an error." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); else EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the tick range expression " << start_tick_node->token_->token_string_ << " evaluated to begin in tick " << script_block->tick_start_ << ", which is past/present; the current tick is " << tick_ << ". This means that the event will not be able to execute in one or more of its scheduled ticks, which is an error." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); } #if DEBUG_TICK_RANGES std::cout << " tick range " << script_block->tick_start_ << " to " << script_block->tick_end_ << std::endl; #endif } else { if (expr_count > 1000000) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): for efficiency reasons, a non-sequential tick range expression may not contain more than 1,000,000 values; use rescheduleScriptBlock() if you really need to do this, but be aware that it may not perform well. Often a better solution is to use a sequential range, and test an additional criterion inside the event or callback body." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); script_block->tick_range_evaluated_ = true; script_block->tick_range_is_sequence_ = false; any_scheduling_change = true; std::unordered_set &tick_set = script_block->tick_set_; tick_set.clear(); for (int index = 0; index < expr_count; ++index) { slim_tick_t tick = (slim_tick_t)expr_data[index]; if (tick_set.find(tick) == tick_set.end()) { tick_set.emplace(tick); // With a non-sequential range, it is an error for any tick // to be past/present, since a fire of the event will be missed if (IsPastOrPresent(tick, script_block->type_)) EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): the tick range expression " << start_tick_node->token_->token_string_ << " evaluated to include tick " << tick << ", which is past/present; the current tick is " << tick_ << ". This means that the event will not be able to execute in one or more of its scheduled ticks, which is an error." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); } else { EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): a non-sequential tick range expression may not contain duplicate elements (" << tick << " is duplicated). Use unique() to remove duplicates if desired." << EidosTerminate(start_tick_node->ErrorPositionForNodeAndChildren()); } } #if DEBUG_TICK_RANGES std::cout << " non-sequential tick range (" << tick_set.size() << " elements)." << std::endl; #endif } } } else if (!start_tick_node && !colon_node && !end_tick_node) { // no tick specifier -- active in every tick // since there is no dependency on an expression, this will always be evaluated immediately script_block->tick_range_evaluated_ = true; script_block->tick_range_is_sequence_ = true; script_block->tick_start_ = -1; script_block->tick_end_ = SLIM_MAX_TICK + 1; script_block->tick_set_.clear(); any_scheduling_change = true; #if DEBUG_TICK_RANGES std::cout << " tick range is every tick." << std::endl; #endif } else { EIDOS_TERMINATION << "ERROR (Community::EvaluateScriptBlockTickRanges): (internal error) unhandled tick range case." << EidosTerminate(nullptr); } } } if (any_scheduling_change) { // Notify the various interested parties that the script blocks have changed last_script_block_tick_cached_ = false; script_block_types_cached_ = false; scripts_changed_ = true; } // We are now open for business } void Community::FlagUnevaluatedScriptBlockTickRanges() { // This is called at execution end, to error if any script blocks did not execute because their // tick range could not be evaluated; this depends on script_block->unevaluated_error_string_ // being set up by _EvaluateTickRangeNode(), based on the error message string set up in // SLiMUndefinedIdentifierException by EidosSymbolTable::_GetValue_SpecialRaise(). if (all_tick_ranges_evaluated_) return; std::vector &script_blocks = AllScriptBlocks(); for (auto script_block : script_blocks) { if ((script_block->type_ != SLiMEidosBlockType::SLiMEidosInitializeCallback) && !script_block->tick_range_evaluated_) { if (!script_block->unevaluated_error_string_.length()) EIDOS_TERMINATION << "ERROR (Community::FlagUnevaluatedScriptBlockTickRanges): (internal error) An internal error occurred regarding script block scheduling. Please report this error." << EidosTerminate(script_block->root_node_->ErrorPositionForNodeAndChildren()); EIDOS_TERMINATION << "ERROR (Community::FlagUnevaluatedScriptBlockTickRanges): At simulation end, this script block had never been executed because its tick range could never be evaluated. This was due to a reference to a global constant, " << script_block->unevaluated_error_string_ << ", that was never defined. (Note that variables, including global variables, are not visible in tick range expressions and cannot be used; only global constants may be used.)\n\nIf the non-execution of this script block is intentional, you can avoid this error by calling deregisterScriptBlock() to deregister the block before the simulation ends." << EidosTerminate(script_block->root_node_->ErrorPositionForNodeAndChildren()); } } } slim_tick_t Community::FirstTick(void) { slim_tick_t first_tick = SLIM_MAX_TICK + 1; std::vector &script_blocks = AllScriptBlocks(); // Figure out our first tick; it is the earliest tick in which an Eidos event is set up to run, // since an Eidos event that adds a subpopulation is necessary to get things started for (auto script_block : script_blocks) { if ((script_block->type_ == SLiMEidosBlockType::SLiMEidosEventFirst) || (script_block->type_ == SLiMEidosBlockType::SLiMEidosEventEarly) || (script_block->type_ == SLiMEidosBlockType::SLiMEidosEventLate)) { if (script_block->tick_range_is_sequence_) { if ((script_block->tick_start_ < first_tick) && (script_block->tick_start_ >= 1)) first_tick = script_block->tick_start_; } else { for (const auto &tick : script_block->tick_set_) { if ((tick < first_tick) && (tick >= 1)) first_tick = tick; } } } } return first_tick; } slim_tick_t Community::EstimatedLastTick(void) { // return our cached value if we have one if (last_script_block_tick_cached_) return last_script_block_tick_; // otherwise, fill the cache std::vector &script_blocks = AllScriptBlocks(); slim_tick_t last_tick = 1; // The estimate is derived from the last tick in which an Eidos block is registered. // Any block type works, since the simulation could plausibly be stopped within a callback. // However, blocks that do not specify an end tick don't count. for (auto script_block : script_blocks) { if (script_block->tick_range_is_sequence_) { if ((script_block->tick_end_ > last_tick) && (script_block->tick_end_ <= SLIM_MAX_TICK)) last_tick = script_block->tick_end_; } else { for (const auto &tick : script_block->tick_set_) if ((tick > last_tick) && (tick <= SLIM_MAX_TICK)) last_tick = tick; } } last_script_block_tick_ = last_tick; last_script_block_tick_cached_ = true; return last_script_block_tick_; } void Community::SetModelType(SLiMModelType p_new_type) { if (model_type_set_) EIDOS_TERMINATION << "ERROR (Community::SetModelType): (internal error) the model has already been declared." << EidosTerminate(); model_type_set_ = true; model_type_ = p_new_type; // propagate the model type decision downward to ensure consistency for (Species *species : all_species_) { species->model_type_ = model_type_; species->population_.model_type_ = model_type_; } } void Community::SetTick(slim_tick_t p_new_tick) { tick_ = p_new_tick; // The tree sequence tick increments when generating offspring occurs, not at the ends of ticks as delineated by SLiM. // This prevents the tree sequence code from seeing two "generations" with the same value for the tick counter. if (((model_type_ == SLiMModelType::kModelTypeWF) && (CycleStage() < SLiMCycleStage::kWFStage2GenerateOffspring)) || ((model_type_ == SLiMModelType::kModelTypeNonWF) && (CycleStage() < SLiMCycleStage::kNonWFStage1GenerateOffspring))) tree_seq_tick_ = tick_ - 1; else tree_seq_tick_ = tick_; tree_seq_tick_offset_ = 0; } // This function is called by both SLiM and SLiMgui to run a tick. In SLiM, it simply calls _RunOneTick(), // with no exception handling; in that scenario exceptions should not be thrown, since EidosTerminate() will log an // error and then call exit(). In SLiMgui, EidosTerminate() will raise an exception, and it will be caught right // here and converted to an "invalid simulation" state (simulation_valid_ == false), which will be noticed by SLiMgui // and will cause error reporting to occur based upon the error-tracking variables set. bool Community::RunOneTick(void) { #ifdef SLIMGUI if (simulation_valid_) { try { #endif return _RunOneTick(); #ifdef SLIMGUI } catch (...) { simulation_valid_ = false; return false; } } // Zero out error-reporting info so raises elsewhere don't get attributed to this script ClearErrorContext(); #endif return false; } // This function is called only by the SLiM self-testing machinery. It has no exception handling; raises will // blow through to the catch block in the test harness so that they can be handled there. bool Community::_RunOneTick(void) { // ****************************************************************** // // Stage 0: Pre-cycle bookkeeping // cycle_stage_ = SLiMCycleStage::kStagePreCycle; // Define the current script around each cycle execution, for error reporting gEidosErrorContext.currentScript = script_; // Activate all species at the beginning of the tick, according their modulo/phase if (tick_ == 0) { #ifdef SLIMGUI gSLiMScheduling << "# initialize() callbacks executing:" << std::endl; #endif for (Species *species : all_species_) species->SetActive(true); } else { for (Species *species : all_species_) { slim_tick_t phase = species->TickPhase(); if (tick_ >= phase) { slim_tick_t modulo = species->TickModulo(); if ((modulo == 1) || ((tick_ - phase) % modulo == 0)) { species->SetActive(true); continue; } } species->SetActive(false); } #ifdef SLIMGUI gSLiMScheduling << "# tick " << tick_ << ": "; bool first_species = true; for (Species *species : all_species_) { if (!first_species) gSLiMScheduling << ", "; if (species->Active()) gSLiMScheduling << "species " << species->name_ << " active (cycle " << species->cycle_ << ")"; else gSLiMScheduling << "species " << species->name_ << " INACTIVE"; first_species = false; } gSLiMScheduling << std::endl; #endif } // Activate registered script blocks at the beginning of the tick, unless the block's species/ticks specifier refers to an inactive species std::vector &script_blocks = AllScriptBlocks(); for (SLiMEidosBlock *script_block : script_blocks) { if ((!script_block->species_spec_ || script_block->species_spec_->Active()) && (!script_block->ticks_spec_ || script_block->ticks_spec_->Active())) { script_block->block_active_ = -1; // block is active this tick } else { script_block->block_active_ = 0; // block is inactive this tick // Check for deactivation causing a block not to execute at all; we consider this an error since it is almost certainly not what the user wants if (script_block->tick_range_evaluated_) if ((script_block->tick_range_is_sequence_ && (script_block->tick_start_ == script_block->tick_end_) && (script_block->tick_start_ == tick_)) || (!script_block->tick_range_is_sequence_ && (script_block->tick_set_.size() == 1) && (script_block->tick_set_.find(tick_) == script_block->tick_set_.begin()))) EIDOS_TERMINATION << "ERROR (Community::_RunOneTick): A script block that is scheduled to execute only in a single tick (tick " << tick_ << ") was deactivated in that tick due to a 'species' or 'ticks' specifier in its declaration; the script block will thus not execute at all." << EidosTerminate(script_block->identifier_token_); } } // Execute either initialize() callbacks (for tick 0) or the full cycle if (tick_ == 0) { AllSpecies_RunInitializeCallbacks(); CheckLongTermBoundary(); return true; } else { for (Species *species : all_species_) if (species->Active()) species->PrepareForCycle(); // Non-zero ticks are handled by separate functions for WF and nonWF models if (model_type_ == SLiMModelType::kModelTypeWF) return _RunOneTickWF(); else return _RunOneTickNonWF(); } } void Community::AllSpecies_RunInitializeCallbacks(void) { // The zero tick is handled here by shared code, since it is the same for WF and nonWF models // execute user-defined function blocks first; no need to profile this, it's just the definitions not the executions std::vector function_blocks = ScriptBlocksMatching(-1, SLiMEidosBlockType::SLiMEidosUserDefinedFunction, -1, -1, -1, -1, nullptr); for (auto script_block : function_blocks) ExecuteFunctionDefinitionBlock(script_block); if (SLiM_verbosity_level >= 1) SLIM_OUTSTREAM << "// RunInitializeCallbacks():" << std::endl; #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif // execute non-species-specific (`species all`) initialize() callbacks first active_species_ = nullptr; RunInitializeCallbacks(); // execute initialize() callbacks for each species, in species declaration order for (Species *species : all_species_) { active_species_ = species; active_species_->RunInitializeCallbacks(); active_species_ = nullptr; } DeregisterScheduledScriptBlocks(); // compile results from initialization into our overall state for (Species *species : all_species_) { const std::map &muttypes = species->MutationTypes(); const std::map &getypes = species->GenomicElementTypes(); all_mutation_types_.insert(muttypes.begin(), muttypes.end()); all_genomic_element_types_.insert(getypes.begin(), getypes.end()); } // set up global symbols for all species, and for ourselves for (Species *species : all_species_) { EidosSymbolTableEntry &symbol_entry = species->SymbolTableEntry(); EidosGlobalStringID symbol_id = symbol_entry.first; std::string symbol_string = EidosStringRegistry::StringForGlobalStringID(symbol_id); if (simulation_constants_->ContainsSymbol(symbol_id)) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_RunInitializeCallbacks): A species with name '" << symbol_string << "' cannot be defined because that name is already in use." << EidosTerminate(); if (!Eidos_GoodSymbolForDefine(symbol_string) && (symbol_string != "sim")) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_RunInitializeCallbacks): A species with name '" << symbol_string << "' cannot be defined because the symbol '" << symbol_string << "' is reserved." << EidosTerminate(); simulation_constants_->InitializeConstantSymbolEntry(symbol_entry); } simulation_constants_->InitializeConstantSymbolEntry(SymbolTableEntry()); // we're done with the initialization tick, so remove the zero-tick functions RemoveZeroTickFunctionsFromMap(simulation_functions_); // BCH 3/6/2024: Here is where we now determine the tick ranges for script blocks; we now // do this in a deferred fashion to allow tick ranges to contain constant expressions. // It needs to be done before the call to FirstTick() below so tick ranges are valid. // Nobody should call FirstTick() or EstimatedLastTick() before this point! // BCH 7/19/2024: Note that we will try again in each tick, if some expressions can't be // evaluated right away; but that won't figure into the first tick calculated here. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); // determine the first tick and emit our start log tick_start_ = FirstTick(); // SLIM_MAX_TICK + 1 if it can't find a first block if (tick_start_ == SLIM_MAX_TICK + 1) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_RunInitializeCallbacks): No Eidos event found to start the simulation." << EidosTerminate(); if (SLiM_verbosity_level >= 1) SLIM_OUTSTREAM << "\n// Starting run at tick :\n" << tick_start_ << " " << "\n" << std::endl; // start at the beginning; note that tree_seq_tick_ will not equal tick_ until after reproduction SetTick(tick_start_); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[0]); #endif // Zero out error-reporting info so raises elsewhere don't get attributed to this script ClearErrorContext(); #if (SLIMPROFILING == 1) // PROFILING if (gEidosProfilingClientCount) CollectSLiMguiMemoryUsageProfileInfo(); #endif } void Community::RunInitializeCallbacks(void) { // zero out the initialization check counts num_interaction_types_ = 0; num_modeltype_declarations_ = 0; // execute `species all` initialize() callbacks, which should always have a tick of 0 set std::vector init_blocks = ScriptBlocksMatching(0, SLiMEidosBlockType::SLiMEidosInitializeCallback, -1, -1, -1, -1, nullptr); for (auto script_block : init_blocks) ExecuteEidosEvent(script_block); // check for complete initialization // In multispecies models, we are responsible for finalizing the model type decision at the end of our initialization // In single-species models, the Species will do this after its init instead; see Species::RunInitializeCallbacks(). if (is_explicit_species_) { // We default to WF, but here we explicitly declare our model type so everybody knows the default was not changed // This cements the choice of WF if a `species all` callback does not declare a model type explicitly if (num_modeltype_declarations_ == 0) SetModelType(SLiMModelType::kModelTypeWF); } } // execute a script event in the population; the script is assumed to be due to trigger void Community::ExecuteEidosEvent(SLiMEidosBlock *p_script_block) { if (!p_script_block->block_active_) return; #ifndef DEBUG_POINTS_ENABLED #error "DEBUG_POINTS_ENABLED is not defined; include eidos_globals.h" #endif #if DEBUG_POINTS_ENABLED // SLiMgui debugging point EidosDebugPointIndent indenter; { EidosInterpreterDebugPointsSet *debug_points = DebugPoints(); EidosToken *decl_token = p_script_block->root_node_->token_; if (debug_points && debug_points->set.size() && (decl_token->token_line_ != -1) && (debug_points->set.find(decl_token->token_line_) != debug_points->set.end())) { SLIM_ERRSTREAM << EidosDebugPointIndent::Indent() << "#DEBUG "; if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventFirst) SLIM_ERRSTREAM << "first()"; else if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventEarly) SLIM_ERRSTREAM << "early()"; else if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventLate) SLIM_ERRSTREAM << "late()"; else if (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosInitializeCallback) SLIM_ERRSTREAM << "initialize()"; else SLIM_ERRSTREAM << "???"; if (p_script_block->block_id_ != -1) SLIM_ERRSTREAM << " s" << p_script_block->block_id_; SLIM_ERRSTREAM << " (line " << (decl_token->token_line_ + 1) << DebugPointInfo() << ")" << std::endl; indenter.indent(); } } #endif #ifdef SLIMGUI if ((p_script_block->type_ == SLiMEidosBlockType::SLiMEidosInitializeCallback) || (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventFirst) || (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventEarly) || (p_script_block->type_ == SLiMEidosBlockType::SLiMEidosEventLate)) { // These four types of script blocks log out to the scheduling stream when executed in SLiMgui gSLiMScheduling << "\tevent: "; p_script_block->PrintDeclaration(gSLiMScheduling, this); gSLiMScheduling << std::endl; } #endif SLiMEidosBlockType old_executing_block_type = executing_block_type_; executing_block_type_ = p_script_block->type_; #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif EidosSymbolTable callback_symbols(EidosSymbolTableType::kContextConstantsTable, &SymbolTable()); EidosSymbolTable client_symbols(EidosSymbolTableType::kLocalVariablesTable, &callback_symbols); EidosFunctionMap &function_map = FunctionMap(); EidosInterpreter interpreter(p_script_block->compound_statement_node_, client_symbols, function_map, this, SLIM_OUTSTREAM, SLIM_ERRSTREAM #ifdef SLIMGUI , check_infinite_loops_ #endif ); if (p_script_block->contains_self_) callback_symbols.InitializeConstantSymbolEntry(p_script_block->SelfSymbolTableEntry()); // define "self" try { // Interpret the script; the result from the interpretation is not used for anything and must be void EidosValue_SP result = interpreter.EvaluateInternalBlock(p_script_block->script_); if (result->Type() != EidosValueType::kValueVOID) EIDOS_TERMINATION << "ERROR (Community::ExecuteEidosEvent): " << p_script_block->type_ << " callbacks must not return a value; use a \"return;\" statement to explicitly return void if desired." << EidosTerminate(p_script_block->identifier_token_); } catch (...) { throw; } #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_callback_totals_[(int)executing_block_type_]); #endif executing_block_type_ = old_executing_block_type; } void Community::AllSpecies_CheckIntegrity(void) { #if DEBUG // Check the integrity of all the information in the individuals and haplosomes of the parental population for (Species *species : all_species_) species->Species_CheckIntegrity(); #endif #if DEBUG // Check for species consistency across all of the objects in each species for (size_t species_index = 0; species_index < all_species_.size(); ++species_index) { Species *species = all_species_[species_index]; if (&species->community_ != this) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) species->community_ mismatch." << EidosTerminate(); if (species->model_type_ != model_type_) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) species->model_type_ mismatch." << EidosTerminate(); if (species->species_id_ != (int)species_index) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) species->species_id_ mismatch." << EidosTerminate(); const std::vector &chromosomes = species->Chromosomes(); size_t chromosomes_count = chromosomes.size(); for (size_t chromosome_index = 0; chromosome_index < chromosomes_count; chromosome_index++) { Chromosome *chromosome = chromosomes[chromosome_index]; if (&chromosome->species_ != species) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) chromosome->species_ mismatch." << EidosTerminate(); if (&chromosome->community_ != this) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) chromosome->community_ mismatch." << EidosTerminate(); } Population &population = species->population_; const std::map &muttypes = species->MutationTypes(); const std::map &getypes = species->GenomicElementTypes(); if (&population.species_ != species) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) population.species_ mismatch." << EidosTerminate(); for (auto const &subpop_iter : population.subpops_) if (&subpop_iter.second->species_ != species) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) subpopulation->species_ mismatch." << EidosTerminate(); for (auto const &muttype_iter : muttypes) if (&muttype_iter.second->species_ != species) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) muttype->species_ mismatch." << EidosTerminate(); for (auto const &getype_iter : getypes) if (&getype_iter.second->species_ != species) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) getype->species_ mismatch." << EidosTerminate(); } #endif #if DEBUG #if DEBUG_LESS_INTENSIVE // These tests are extremely intensive, so sometimes it's useful to dial them down... if ((Tick() % 10) != 5) return; #endif // Check the integrity of the mutation registry; all MutationIndex values should be in range for (Species *species : all_species_) { int registry_size; const MutationIndex *registry = species->population_.MutationRegistry(®istry_size); std::vector indices; for (int registry_index = 0; registry_index < registry_size; ++registry_index) { MutationIndex mutation_index = registry[registry_index]; if ((mutation_index < 0) || (mutation_index >= gSLiM_Mutation_Block_Capacity)) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) mutation index " << mutation_index << " out of the mutation block." << EidosTerminate(); indices.push_back(mutation_index); } size_t original_size = indices.size(); std::sort(indices.begin(), indices.end()); indices.resize(static_cast(std::distance(indices.begin(), std::unique(indices.begin(), indices.end())))); if (indices.size() != original_size) EIDOS_TERMINATION << "ERROR (Community::AllSpecies_CheckIntegrity): (internal error) duplicate mutation index in the mutation registry (size difference " << (original_size - indices.size()) << ")." << EidosTerminate(); } #endif } void Community::AllSpecies_PurgeRemovedObjects(void) { // Purge removed subpopulations and killed individuals in all subpopulations. This doesn't have // to happen at any particular frequency, really, but it frees up memory, and it also allows // frequency/count tallying to use MutationRun refcounts to run faster, so we do it after every // stage of the tick cycle in nonWF models. In WF models, individuals can't be killed so that // is a non-issue, and removal of subpops is generally infrequent, so we purge removed subpops // with PurgeRemovedSubpopulations() only in Population::SwapGenerations(). for (Species *species : all_species_) { species->EmptyGraveyard(); // needs to be done first; uses subpopulation references species->population_.PurgeRemovedSubpopulations(); } } // // _RunOneTickWF() : runs all the stages for one cycle of a WF model // bool Community::_RunOneTickWF(void) { #if (SLIMPROFILING == 1) // PROFILING #if SLIM_USE_NONNEUTRAL_CACHES if (gEidosProfilingClientCount) for (Species *species : all_species_) species->CollectMutationProfileInfo(); #endif #endif // ****************************************************************** // // Stage 0: Execute first() script events for the current cycle // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kWFStage0ExecuteFirstScripts; std::vector first_blocks = ScriptBlocksMatching(tick_, SLiMEidosBlockType::SLiMEidosEventFirst, -1, -1, -1, -1, nullptr); for (auto script_block : first_blocks) ExecuteEidosEvent(script_block); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[1]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 1: Execute early() script events for the current cycle // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kWFStage1ExecuteEarlyScripts; std::vector early_blocks = ScriptBlocksMatching(tick_, SLiMEidosBlockType::SLiMEidosEventEarly, -1, -1, -1, -1, nullptr); for (auto script_block : early_blocks) ExecuteEidosEvent(script_block); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[2]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 2: Generate offspring: evolve all subpopulations // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif for (Species *species : all_species_) species->CheckMutationStackPolicy(); cycle_stage_ = SLiMCycleStage::kWFStage2GenerateOffspring; // increment the tree-sequence tick immediately, since we are now going to make a new generation of individuals tree_seq_tick_++; tree_seq_tick_offset_ = 0; // note that tick_ is incremented later! // first all species generate offspring for (Species *species : all_species_) if (species->Active()) { executing_species_ = species; #ifdef SLIMGUI if (is_explicit_species_) gSLiMScheduling << "\toffspring generation: species " << species->name_ << std::endl; #endif species->WF_GenerateOffspring(); species->has_recalculated_fitness_ = false; executing_species_ = nullptr; } // then all species switch generations; this prevents access to the child generation of one species while another is still generating offspring for (Species *species : all_species_) if (species->Active()) species->WF_SwitchToChildGeneration(); // invalidate interactions, now that the generation they were valid for has disappeared // BCH 5 Oct. 2024: this moved upward slightly in the tick cycle; used to happen in "remove fixed mutations" for (Species *species : all_species_) if (species->Active()) InvalidateInteractionsForSpecies(species); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); // Deregister any interaction() callbacks that have been scheduled for deregistration, since it is now safe to do so // BCH 5 Oct. 2024: this moved upward slightly in the tick cycle; used to happen in "remove fixed mutations" DeregisterScheduledInteractionBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[3]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 3: Swap generations // // BCH 10/5/2024: Note this stage swapped positions with "remove fixed mutations" as part of // the multispecies work; I do not expect that change to have any user-visible fallout { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kWFStage3SwapGenerations; for (Species *species : all_species_) if (species->Active()) species->WF_SwapGenerations(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[4]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 4: Remove fixed mutations and associated tasks // // BCH 10/5/2024: Note this stage swapped positions with "swap generations" as part of // the multispecies work; I do not expect that change to have any user-visible fallout { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kWFStage4RemoveFixedMutations; for (Species *species : all_species_) if (species->Active()) species->MaintainMutationRegistry(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[5]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 5: Execute late() script events for the current cycle // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kWFStage5ExecuteLateScripts; std::vector late_blocks = ScriptBlocksMatching(tick_, SLiMEidosBlockType::SLiMEidosEventLate, -1, -1, -1, -1, nullptr); for (auto script_block : late_blocks) ExecuteEidosEvent(script_block); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[6]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 6: Calculate fitness values for the new parental generation // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kWFStage6CalculateFitness; for (Species *species : all_species_) if (species->Active()) { executing_species_ = species; #ifdef SLIMGUI if (is_explicit_species_) gSLiMScheduling << "\tfitness recalculation: species " << species->name_ << std::endl; #endif species->RecalculateFitness(); executing_species_ = nullptr; } // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); // Maintain our mutation run experiments; we want this overhead to appear within the stage 6 profile // FIXME wait, why should this overhead appear in the fitness recalculation step?? for (Species *species : all_species_) species->FinishMutationRunExperimentTimings(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[7]); #endif #ifdef SLIMGUI // Let SLiMgui survey the population for mean fitness and such, if it is our target // We do this outside of profiling and mutation run experiments, since SLiMgui overhead should not affect those for (Species *species : all_species_) species->population_.SurveyPopulation(); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); // ****************************************************************** // // Stage 7: Advance the tick counter and do end-cycle tasks // { cycle_stage_ = SLiMCycleStage::kWFStage7AdvanceTickCounter; #ifdef SLIMGUI // re-tally for SLiMgui; this tallies into separate counters, uses the selected subpops, etc. for (Species *species : all_species_) if (species->HasGenetics()) species->population_.TallyMutationReferencesAcrossPopulation_SLiMgui(); #endif for (Species *species : all_species_) if (species->Active()) species->MaintainTreeSequence(); // LogFile output for (LogFile *log_file : log_file_registry_) log_file->TickEndCallout(); // BCH 7/19/2024: At the end of each tick we again try to determine the tick ranges // for script blocks; we now do this in a deferred fashion to allow tick ranges to // contain constant expressions, including expressions involving constants that only // get defined later in the run. We do it at tick end so the result is immediately // visible in SLiMgui. It must be done before the tick counter increments, and // before the call to EstimatedLastTick() below. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); // Advance the tick and cycle counters (note that tree_seq_tick_ was incremented earlier!) tick_++; for (Species *species : all_species_) if (species->Active()) species->AdvanceCycleCounter(); #if (SLIMPROFILING == 1) // PROFILING if (gEidosProfilingClientCount) CollectSLiMguiMemoryUsageProfileInfo(); #endif // Decide whether the simulation is over. We need to call EstimatedLastTick() every time; we can't // cache it, because it can change based upon changes in script registration / deregistration. bool result; if (sim_declared_finished_) result = false; else result = (tick_ <= EstimatedLastTick()); if (!result) SimulationHasFinished(); // Use a special cycle stage for the interstitial space between ticks, when Eidos console input runs cycle_stage_ = SLiMCycleStage::kStagePostCycle; // Zero out error-reporting info so raises elsewhere don't get attributed to this script ClearErrorContext(); return result; } } // // _RunOneTickNonWF() : runs all the stages for one cycle of a nonWF model // bool Community::_RunOneTickNonWF(void) { #if (SLIMPROFILING == 1) // PROFILING #if SLIM_USE_NONNEUTRAL_CACHES if (gEidosProfilingClientCount) for (Species *species : all_species_) species->CollectMutationProfileInfo(); #endif #endif // ****************************************************************** // // Stage 0: Execute first() script events for the current cycle // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kNonWFStage0ExecuteFirstScripts; std::vector first_blocks = ScriptBlocksMatching(tick_, SLiMEidosBlockType::SLiMEidosEventFirst, -1, -1, -1, -1, nullptr); for (auto script_block : first_blocks) ExecuteEidosEvent(script_block); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[1]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_PurgeRemovedObjects(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 1: Generate offspring: call reproduction() callbacks // { // increment the tree-seq tick at the start of reproduction; note that in first() events it is one less than tick_! tree_seq_tick_++; tree_seq_tick_offset_ = 0; #if defined(SLIMGUI) // zero out offspring counts used for SLiMgui's display for (Species *species : all_species_) { if (species->species_active_) { for (std::pair &subpop_pair : species->population_.subpops_) { Subpopulation *subpop = subpop_pair.second; subpop->gui_offspring_cloned_M_ = 0; subpop->gui_offspring_cloned_F_ = 0; subpop->gui_offspring_selfed_ = 0; subpop->gui_offspring_crossed_ = 0; subpop->gui_offspring_empty_ = 0; } // zero out migration counts used for SLiMgui's display for (std::pair &subpop_pair : species->population_.subpops_) { Subpopulation *subpop = subpop_pair.second; subpop->gui_premigration_size_ = subpop->parent_subpop_size_; subpop->gui_migrants_.clear(); } } } #endif #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif for (Species *species : all_species_) species->CheckMutationStackPolicy(); cycle_stage_ = SLiMCycleStage::kNonWFStage1GenerateOffspring; // BCH 28 September 2022: Offspring generation in nonWF models is now done in two passes: first all species generate // their offspring, then all species merge their offspring. This allows multispecies interactions to remain valid // through the whole reproduction process. In effect, reproduction is now kind of two separate tick cycle stages, // but this is not emphasized since it only makes a difference to multispecies models; conceptually it is one stage. for (Species *species : all_species_) if (species->Active()) { executing_species_ = species; #ifdef SLIMGUI if (is_explicit_species_) gSLiMScheduling << "\toffspring generation: species " << species->name_ << std::endl; #endif species->nonWF_GenerateOffspring(); executing_species_ = nullptr; } for (Species *species : all_species_) if (species->Active()) { executing_species_ = species; #ifdef SLIMGUI if (is_explicit_species_) gSLiMScheduling << "\tmerge offspring: species " << species->name_ << std::endl; #endif species->nonWF_MergeOffspring(); species->has_recalculated_fitness_ = false; executing_species_ = nullptr; } // Deregister any interaction() callbacks that have been scheduled for deregistration, since it is now safe to do so DeregisterScheduledInteractionBlocks(); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[2]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_PurgeRemovedObjects(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 2: Execute early() script events for the current cycle // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kNonWFStage2ExecuteEarlyScripts; std::vector early_blocks = ScriptBlocksMatching(tick_, SLiMEidosBlockType::SLiMEidosEventEarly, -1, -1, -1, -1, nullptr); for (auto script_block : early_blocks) ExecuteEidosEvent(script_block); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[3]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_PurgeRemovedObjects(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 3: Calculate fitness values for the new population // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kNonWFStage3CalculateFitness; for (Species *species : all_species_) if (species->Active()) { executing_species_ = species; #ifdef SLIMGUI if (is_explicit_species_) gSLiMScheduling << "\tfitness recalculation: species " << species->name_ << std::endl; #endif species->RecalculateFitness(); executing_species_ = nullptr; } // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); // Invalidate interactions, now that the cycle they were valid for is disappearing for (Species *species : all_species_) if (species->Active()) InvalidateInteractionsForSpecies(species); // Deregister any interaction() callbacks that have been scheduled for deregistration, since it is now safe to do so DeregisterScheduledInteractionBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[4]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_PurgeRemovedObjects(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 4: Viability/survival selection // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kNonWFStage4SurvivalSelection; for (Species *species : all_species_) if (species->Active()) { executing_species_ = species; #ifdef SLIMGUI if (is_explicit_species_) gSLiMScheduling << "\tviability/survival: species " << species->name_ << std::endl; #endif species->nonWF_ViabilitySurvival(); executing_species_ = nullptr; } // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[5]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_PurgeRemovedObjects(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 5: Remove fixed mutations and associated tasks // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kNonWFStage5RemoveFixedMutations; for (Species *species : all_species_) if (species->Active()) species->MaintainMutationRegistry(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[6]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_PurgeRemovedObjects(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 6: Execute late() script events for the current cycle // { #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif cycle_stage_ = SLiMCycleStage::kNonWFStage6ExecuteLateScripts; std::vector late_blocks = ScriptBlocksMatching(tick_, SLiMEidosBlockType::SLiMEidosEventLate, -1, -1, -1, -1, nullptr); for (auto script_block : late_blocks) ExecuteEidosEvent(script_block); // the stage is done, so deregister script blocks as requested DeregisterScheduledScriptBlocks(); // Maintain our mutation run experiments; we want this overhead to appear within the stage 6 profile // FIXME wait, why should this overhead appear in late() events?? for (Species *species : all_species_) species->FinishMutationRunExperimentTimings(); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(profile_stage_totals_[7]); #endif } // BCH 4/8/2025: Now we check between tick cycle stages, to allow deferred scheduling within one tick. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); CheckLongTermBoundary(); AllSpecies_PurgeRemovedObjects(); AllSpecies_CheckIntegrity(); // ****************************************************************** // // Stage 7: Advance the tick counter and do end-cycle tasks // { cycle_stage_ = SLiMCycleStage::kNonWFStage7AdvanceTickCounter; #ifdef SLIMGUI // Let SLiMgui survey the population for mean fitness and such, if it is our target // We do this outside of profiling and mutation run experiments, since SLiMgui overhead should not affect those for (Species *species : all_species_) species->population_.SurveyPopulation(); #endif #ifdef SLIMGUI // re-tally for SLiMgui; this tallies into separate counters, uses the selected subpops, etc. for (Species *species : all_species_) if (species->HasGenetics()) species->population_.TallyMutationReferencesAcrossPopulation_SLiMgui(); #endif for (Species *species : all_species_) if (species->Active()) species->MaintainTreeSequence(); // LogFile output for (LogFile *log_file : log_file_registry_) log_file->TickEndCallout(); // BCH 7/19/2024: At the end of each tick we again try to determine the tick ranges // for script blocks; we now do this in a deferred fashion to allow tick ranges to // contain constant expressions, including expressions involving constants that only // get defined later in the run. We do it at tick end so the result is immediately // visible in SLiMgui. It must be done before the tick counter increments, and // before the call to EstimatedLastTick() below. if (!all_tick_ranges_evaluated_) EvaluateScriptBlockTickRanges(); // Advance the tick counter (note that tree_seq_tick_ is incremented after first() events in the next tick!) tick_++; for (Species *species : all_species_) if (species->Active()) species->AdvanceCycleCounter(); for (Species *species : all_species_) { if (species->Active()) for (std::pair &subpop_pair : species->population_.subpops_) subpop_pair.second->IncrementIndividualAges(); } #if (SLIMPROFILING == 1) // PROFILING if (gEidosProfilingClientCount) CollectSLiMguiMemoryUsageProfileInfo(); #endif // Decide whether the simulation is over. We need to call EstimatedLastTick() every time; we can't // cache it, because it can change based upon changes in script registration / deregistration. bool result; if (sim_declared_finished_) result = false; else result = (tick_ <= EstimatedLastTick()); if (!result) SimulationHasFinished(); // Use a special cycle stage for the interstitial space between ticks, when Eidos console input runs cycle_stage_ = SLiMCycleStage::kStagePostCycle; // Zero out error-reporting info so raises elsewhere don't get attributed to this script ClearErrorContext(); return result; } } void Community::SimulationHasFinished(void) { // This is an opportunity for final calculation/output when a simulation finishes for (Species *species : all_species_) species->SimulationHasFinished(); // Error on any script blocks that never got scheduled FlagUnevaluatedScriptBlockTickRanges(); } void Community::TabulateSLiMMemoryUsage_Community(SLiMMemoryUsage_Community *p_usage, EidosSymbolTable *p_current_symbols) { EIDOS_BZERO(p_usage, sizeof(SLiMMemoryUsage_Community)); // Community usage p_usage->communityObjects_count = 1; p_usage->communityObjects = p_usage->communityObjects_count * sizeof(Community); // Mutation global buffers p_usage->mutationRefcountBuffer = SLiMMemoryUsageForMutationRefcounts(); p_usage->mutationUnusedPoolSpace = SLiMMemoryUsageForFreeMutations(); // note that in SLiMgui everybody shares this // InteractionType { p_usage->interactionTypeObjects_count = interaction_types_.size(); p_usage->interactionTypeObjects = sizeof(InteractionType) * p_usage->interactionTypeObjects_count; for (auto iter : interaction_types_) { p_usage->interactionTypeKDTrees += iter.second->MemoryUsageForKDTrees(); p_usage->interactionTypePositionCaches += iter.second->MemoryUsageForPositions(); } p_usage->interactionTypeSparseVectorPool += InteractionType::MemoryUsageForSparseVectorPool(); } // Eidos usage p_usage->eidosASTNodePool = gEidosASTNodePool->MemoryUsageForAllNodes(); p_usage->eidosSymbolTablePool = MemoryUsageForSymbolTables(p_current_symbols); p_usage->eidosValuePool = gEidosValuePool->MemoryUsageForAllNodes(); for (auto const &filebuf_pair : gEidosBufferedZipAppendData) p_usage->fileBuffers += filebuf_pair.second.capacity(); // Total SumUpMemoryUsage_Community(*p_usage); } #if (SLIMPROFILING == 1) // PROFILING void Community::StartProfiling(void) { gEidosProfilingClientCount++; // prepare for profiling by measuring profile block overhead and lag Eidos_PrepareForProfiling(); // initialize counters profile_elapsed_CPU_clock = 0; profile_elapsed_wall_clock = 0; profile_start_tick = Tick(); // call this first, purely for its side effect of emptying out any pending profile counts // note that the accumulators governed by this method get zeroed out down below for (Species *focal_species : all_species_) focal_species->CollectMutationProfileInfo(); // zero out profile counts for cycle stages for (int i = 0; i < 9; ++i) profile_stage_totals_[i] = 0; // zero out profile counts for callback types (note SLiMEidosUserDefinedFunction is excluded; that is not a category we profile) profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosEventFirst)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosEventEarly)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosEventLate)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosInitializeCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosMutationEffectCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosFitnessEffectCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosInteractionCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosMateChoiceCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosModifyChildCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosRecombinationCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosMutationCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosReproductionCallback)] = 0; profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosSurvivalCallback)] = 0; // zero out profile counts for script blocks; dynamic scripts will be zeroed on construction std::vector &script_blocks = AllScriptBlocks(); for (SLiMEidosBlock *script_block : script_blocks) if (script_block->type_ != SLiMEidosBlockType::SLiMEidosUserDefinedFunction) // exclude user-defined functions; not user-visible as blocks script_block->root_node_->ZeroProfileTotals(); // zero out profile counts for all user-defined functions EidosFunctionMap &function_map = FunctionMap(); for (auto functionPairIter = function_map.begin(); functionPairIter != function_map.end(); ++functionPairIter) { const EidosFunctionSignature *signature = functionPairIter->second.get(); if (signature->body_script_ && signature->user_defined_) signature->body_script_->AST()->ZeroProfileTotals(); } #if SLIM_USE_NONNEUTRAL_CACHES // zero out mutation run metrics that are collected by CollectMutationProfileInfo() for (Species *focal_species : all_species_) { focal_species->profile_nonneutral_regime_history_.clear(); focal_species->profile_max_mutation_index_ = 0; for (Chromosome *focal_chromosome : focal_species->Chromosomes()) { focal_chromosome->profile_mutcount_history_.clear(); focal_chromosome->profile_mutation_total_usage_ = 0; focal_chromosome->profile_nonneutral_mutation_total_ = 0; focal_chromosome->profile_mutrun_total_usage_ = 0; focal_chromosome->profile_unique_mutrun_total_ = 0; focal_chromosome->profile_mutrun_nonneutral_recache_total_ = 0; } } #endif // zero out memory usage metrics EIDOS_BZERO(&profile_last_memory_usage_Community, sizeof(SLiMMemoryUsage_Community)); EIDOS_BZERO(&profile_total_memory_usage_Community, sizeof(SLiMMemoryUsage_Community)); EIDOS_BZERO(&profile_last_memory_usage_AllSpecies, sizeof(SLiMMemoryUsage_Species)); EIDOS_BZERO(&profile_total_memory_usage_AllSpecies, sizeof(SLiMMemoryUsage_Species)); total_memory_tallies_ = 0; time(&profile_start_date); profile_start_clock = std::chrono::steady_clock::now(); } void Community::StopProfiling(void) { time(&profile_end_date); profile_end_clock = std::chrono::steady_clock::now(); profile_end_tick = Tick(); gEidosProfilingClientCount--; } void Community::CollectSLiMguiMemoryUsageProfileInfo(void) { // Gather the data EIDOS_BZERO(&profile_last_memory_usage_AllSpecies, sizeof(SLiMMemoryUsage_Species)); TabulateSLiMMemoryUsage_Community(&profile_last_memory_usage_Community, nullptr); for (Species *species : all_species_) { species->TabulateSLiMMemoryUsage_Species(&species->profile_last_memory_usage_Species); // Add this tick's usage for this species into its single-species accumulator AccumulateMemoryUsageIntoTotal_Species(species->profile_last_memory_usage_Species, species->profile_total_memory_usage_Species); // Add this tick's usage for this species into this tick's overall species accumulator AccumulateMemoryUsageIntoTotal_Species(species->profile_last_memory_usage_Species, profile_last_memory_usage_AllSpecies); } // Add this tick's data into our top-level accumulators AccumulateMemoryUsageIntoTotal_Community(profile_last_memory_usage_Community, profile_total_memory_usage_Community); AccumulateMemoryUsageIntoTotal_Species(profile_last_memory_usage_AllSpecies, profile_total_memory_usage_AllSpecies); // Increment our accumulator count; we divide by this to get averages total_memory_tallies_++; } #endif #ifdef SLIMGUI void Community::FileWriteNotification(const std::string &p_file_path, std::vector &&p_lines, bool p_append) { auto buffer_iter = std::find(file_write_paths_.begin(), file_write_paths_.end(), p_file_path); if (buffer_iter == file_write_paths_.end()) { // No existing buffer for this path, so make a new one; this does not mean the file is new! file_write_paths_.emplace_back(p_file_path); file_write_buffers_.emplace_back(std::move(p_lines)); file_write_appends_.emplace_back(p_append); } else { // Use the existing buffer for this path size_t buffer_index = std::distance(file_write_paths_.begin(), buffer_iter); std::vector &buffer = file_write_buffers_[buffer_index]; if (!p_append) buffer.clear(); for (std::string &line : p_lines) buffer.emplace_back(std::move(line)); // Note the append flag; the vector here is always parallel to the main vector file_write_appends_[buffer_index] = p_append; } } #endif void Community::AllSpecies_TSXC_Enable(void) { // This is called by command-line slim if a -TSXC command-line option is supplied; the point of this is to allow // tree-sequence recording to be turned on, with mutation recording and runtime crosschecks, with a simple // command-line flag, so that my existing test suite can be crosschecked easily. The -TSXC flag is not public. for (Species *species : all_species_) species->TSXC_Enable(); if (SLiM_verbosity_level >= 1) SLIM_ERRSTREAM << "// ********** Turning on tree-sequence recording with crosschecks (-TSXC)." << std::endl << std::endl; } void Community::AllSpecies_TSF_Enable(void) { // This is called by command-line slim if a -TSF command-line option is supplied; the point of this is to allow // tree-sequence recording to be turned on, with mutation recording but without runtime crosschecks, with a simple // command-line flag, so that my existing test suite can be tested with tree-seq easily. -TSF is not public. for (Species *species : all_species_) species->TSF_Enable(); if (SLiM_verbosity_level >= 1) SLIM_ERRSTREAM << "// ********** Turning on tree-sequence recording without crosschecks (-TSF)." << std::endl << std::endl; } ================================================ FILE: core/community.h ================================================ // // community.h // SLiM // // Created by Ben Haller on 2/28/2022. // Copyright (c) 2022-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* SLiM encapsulates an ecological community - a multispecies simulation run - as a Community object, containing Species objects representing species in the simulated community. */ #ifndef __SLiM__community__ #define __SLiM__community__ #include #include #include "species.h" #include "slim_globals.h" #include "eidos_script.h" #include "eidos_value.h" #include "eidos_functions.h" #include "slim_eidos_block.h" class EidosInterpreter; class Individual; class LogFile; #ifdef SLIMGUI // Forward declaration of EidosInterpreterDebugPointsSet; see eidos_interpreter.cpp struct EidosInterpreterDebugPointsSet_struct; typedef EidosInterpreterDebugPointsSet_struct EidosInterpreterDebugPointsSet; #endif extern EidosClass *gSLiM_Community_Class; #pragma mark - #pragma mark Community #pragma mark - class Community : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; private: // the way we handle script blocks is complicated and is private even against SLiMgui SLiMEidosScript *script_; // OWNED POINTER: the whole input file script std::vector script_blocks_; // OWNED POINTERS: script blocks, both from the input file script and programmatic std::vector scheduled_deregistrations_; // NOT OWNED: blocks in script_blocks_ that are scheduled for deregistration std::vector scheduled_interaction_deregs_; // NOT OWNED: interaction() callbacks in script_blocks_ that are scheduled for deregistration // a cache of the last tick for the simulation, for speed bool all_tick_ranges_evaluated_ = false; // false until all tick ranges have been determined bool last_script_block_tick_cached_ = false; slim_tick_t last_script_block_tick_; // the last tick in which a bounded script block is scheduled to run // scripts blocks prearranged for fast lookup; these are all stored in script_blocks_ as well bool script_block_types_cached_ = false; std::vector cached_first_events_; std::vector cached_early_events_; std::vector cached_late_events_; std::vector cached_initialize_callbacks_; std::vector cached_mutationEffect_callbacks_; std::unordered_multimap cached_fitnessEffect_callbacks_onetick_; // see ValidateScriptBlockCaches() for details std::vector cached_fitnessEffect_callbacks_multitick_; std::vector cached_interaction_callbacks_; std::vector cached_matechoice_callbacks_; std::vector cached_modifychild_callbacks_; std::vector cached_recombination_callbacks_; std::vector cached_mutation_callbacks_; std::vector cached_survival_callbacks_; std::vector cached_reproduction_callbacks_; std::vector cached_userdef_functions_; #ifdef SLIMGUI EidosInterpreterDebugPointsSet *debug_points_ = nullptr; // NOT OWNED; line numbers for all lines with debugging points set #endif // these maps compile the objects from all species into a single sorted list for presentation in the UI etc. std::map all_mutation_types_; // NOT OWNED: each species owns its own mutation types std::map all_genomic_element_types_; // NOT OWNED: each species owns its own genomic element types std::map interaction_types_; // OWNED POINTERS: this map is the owner of all allocated InteractionType objects int num_interaction_types_; int num_modeltype_declarations_; #ifdef SLIMGUI public: bool simulation_valid_ = true; // set to false if a terminating condition is encountered while running in SLiMgui #else private: #endif std::vectorall_species_; // a vector of the species being simulated, in declaration order Species *active_species_ = nullptr; // the species presently executing; currently used only for initialize() callback dispatch EidosSymbolTable *simulation_globals_ = nullptr; // A symbol table of global variables, typically empty; the parent of simulation_constants_ EidosSymbolTable *simulation_constants_ = nullptr; // A symbol table of constants defined by SLiM (p1, g1, m1, s1, etc.) EidosFunctionMap simulation_functions_; // A map of all defined functions in the simulation // these ivars track top-level simulation state: the tick, cycle stage, etc. slim_tick_t tick_start_ = 0; // the first tick number for which the simulation will run slim_tick_t tick_ = -1; // the current tick reached in simulation EidosValue_SP cached_value_tick_; // a cached value for tick_; invalidates automatically when used SLiMCycleStage cycle_stage_ = SLiMCycleStage::kStagePreCycle; // the within-cycle stage currently being executed bool sim_declared_finished_ = false; // a flag set by Community.simulationFinished() to halt the sim at the end of the current tick EidosSymbolTableEntry self_symbol_; // for fast setup of the symbol table slim_usertag_t tag_value_ = SLIM_TAG_UNSET_VALUE; // a user-defined tag value // LogFile registry, for logging data out to a file std::vector log_file_registry_; // OWNED POINTERS (under retain/release) public: bool is_explicit_species_ = false; // true if we have explicit species declarations (even if only one, even if named "sim") bool model_type_set_ = false; // true if the model type has been set, either explicitly or implicitly SLiMModelType model_type_ = SLiMModelType::kModelTypeWF; // the overall model type: WF or nonWF, at present; affects many other things! // warning flags; used to issue warnings only once per run of the simulation bool warned_early_mutation_add_ = false; bool warned_early_mutation_remove_ = false; bool warned_early_output_ = false; bool warned_early_read_ = false; bool warned_no_max_distance_ = false; bool warned_readFromVCF_mutIDs_unused_ = false; bool warned_no_ancestry_read_ = false; bool warned_experiment_run_clocks_ = false; bool warned_spatial_map_color_deprecated_ = false; bool warned_spatial_map_image_deprecated_ = false; // checking for infinite loops, configurable with initializeSLiMOptions() #ifdef SLIMGUI bool check_infinite_loops_ = true; #endif // these ivars are set around callbacks so we know what type of callback we're in, to prevent illegal operations during callbacks // to make them easier to find, such checks should always be marked with a comment: // TIMING RESTRICTION SLiMEidosBlockType executing_block_type_ = SLiMEidosBlockType::SLiMEidosNoBlockType; // the innermost callback type we're executing now Species *executing_species_ = nullptr; // the species executing in the tick cycle right now Individual *focal_modification_child_; // set during a modifyChild() callback to indicate the child being modified // change flags; used only by SLiMgui, to know that something has changed and a UI update is needed; start as true to provoke an initial display bool interaction_types_changed_ = true; bool mutation_types_changed_ = true; bool genomic_element_types_changed_ = true; bool chromosome_changed_ = true; bool scripts_changed_ = true; // provenance-related stuff: remembering the seed and command-line args unsigned long int original_seed_; // the initial seed value, from the user via the -s CLI option, or auto-generated std::vector cli_params_; // CLI parameters; an empty vector when run in SLiMgui, at least for now #if (SLIMPROFILING == 1) // PROFILING : Community now keeps track of overall profiling variables time_t profile_start_date; // resolution of seconds, easy to convert to a date string time_t profile_end_date; // resolution of seconds, easy to convert to a date string std::chrono::steady_clock::time_point profile_start_clock; // sub-second resolution, good for elapsed time std::chrono::steady_clock::time_point profile_end_clock; // sub-second resolution, good for elapsed time std::clock_t profile_elapsed_CPU_clock; // elapsed CPU time inside SLiM eidos_profile_t profile_elapsed_wall_clock; // elapsed wall time inside SLiM slim_tick_t profile_start_tick; // the SLiM tick when profiling started slim_tick_t profile_end_tick; // the SLiM tick when profiling ended // PROFILING : Community keeps track of script timing counts eidos_profile_t profile_stage_totals_[9]; // profiling clocks; index 0 is initialize(), the rest follow sequentially; [8] is TS simplification eidos_profile_t profile_callback_totals_[13]; // profiling clocks; these follow SLiMEidosBlockType, except no SLiMEidosUserDefinedFunction // PROFILING : Community keeps track of its memory usage profile info (as does Species) SLiMMemoryUsage_Community profile_last_memory_usage_Community; SLiMMemoryUsage_Community profile_total_memory_usage_Community; SLiMMemoryUsage_Species profile_last_memory_usage_AllSpecies; SLiMMemoryUsage_Species profile_total_memory_usage_AllSpecies; int64_t total_memory_tallies_; // this is the total number of accumulations, for both Community and all Species under it #endif // (SLIMPROFILING == 1) Community(const Community&) = delete; // no copying Community& operator=(const Community&) = delete; // no copying explicit Community(void); // construct a Community; call InitializeFromFile() next ~Community(void); // destructor void InitializeFromFile(std::istream &p_infile); // parse an input file; call after construction void InitializeRNGFromSeed(unsigned long int *p_override_seed_ptr); // call after InitializeFromFile(), generally void FinishInitialization(void); // call last, after InitializeRNGFromSeed() void TabulateSLiMMemoryUsage_Community(SLiMMemoryUsage_Community *p_usage, EidosSymbolTable *p_current_symbols); // used by outputUsage() and SLiMgui profiling // Managing script blocks; these two methods should be used as a matched pair, bracketing each cycle stage that calls out to script void ValidateScriptBlockCaches(void); std::vector ScriptBlocksMatching(slim_tick_t p_tick, SLiMEidosBlockType p_event_type, slim_objectid_t p_mutation_type_id, slim_objectid_t p_interaction_type_id, slim_objectid_t p_subpopulation_id, int64_t p_chromosome_id, Species *p_species); std::vector &AllScriptBlocks(); std::vector AllScriptBlocksForSpecies(Species *p_species); void OptimizeScriptBlock(SLiMEidosBlock *p_script_block); void AddScriptBlock(SLiMEidosBlock *p_script_block, EidosInterpreter *p_interpreter, const EidosToken *p_error_token); void DeregisterScheduledScriptBlocks(void); void DeregisterScheduledInteractionBlocks(void); void ExecuteFunctionDefinitionBlock(SLiMEidosBlock *p_script_block); // execute a SLiMEidosBlock that defines a function void CheckScheduling(slim_tick_t p_target_tick, SLiMCycleStage p_target_stage); // Managing resources shared across the community bool SubpopulationIDInUse(slim_objectid_t p_subpop_id); // not whether a SLiM subpop with this ID currently exists, but whether the ID is "in use" bool SubpopulationNameInUse(const std::string &p_subpop_name); // not whether a SLiM subpop with this name currently exists, but whether the name is "in use" Subpopulation *SubpopulationWithID(slim_objectid_t p_subpop_id); Subpopulation *SubpopulationWithName(const std::string &p_subpop_name); MutationType *MutationTypeWithID(slim_objectid_t p_muttype_id); GenomicElementType *GenomicElementTypeWithID(slim_objectid_t p_getype_id); SLiMEidosBlock *ScriptBlockWithID(slim_objectid_t p_script_block_id); Species *SpeciesWithID(slim_objectid_t p_species_id); Species *SpeciesWithName(const std::string &species_name); inline InteractionType *InteractionTypeWithID(slim_objectid_t p_inttype_id) { auto id_iter = interaction_types_.find(p_inttype_id); return (id_iter == interaction_types_.end()) ? nullptr : id_iter->second; } inline __attribute__((always_inline)) const std::map &AllMutationTypes(void) const { return all_mutation_types_; } inline __attribute__((always_inline)) const std::map &AllGenomicElementTypes(void) { return all_genomic_element_types_; } inline __attribute__((always_inline)) const std::map &AllInteractionTypes(void) { return interaction_types_; } void InvalidateInteractionsForSpecies(Species *p_invalid_species); void InvalidateInteractionsForSubpopulation(Subpopulation *p_invalid_subpop); // Checking for species identity; these return nullptr if the objects do not all belong to the same species // Calls to these methods, and other such species checks, should be labeled SPECIES CONSISTENCY CHECK to make them easier to find static Species *SpeciesForIndividualsVector(const Individual * const *individuals, int value_count); static Species *SpeciesForIndividuals(EidosValue *value); static Species *SpeciesForHaplosomesVector(const Haplosome * const *haplosomes, int value_count); static Species *SpeciesForHaplosomes(EidosValue *value); static Species *SpeciesForMutationsVector(const Mutation * const *mutations, int value_count); static Species *SpeciesForMutations(EidosValue *value); // Running ticks bool RunOneTick(void); // run one tick and advance the tick count; returns false if finished bool _RunOneTick(void); // does the work of RunOneTick(), with no try/catch void ExecuteEidosEvent(SLiMEidosBlock *p_script_block); void AllSpecies_RunInitializeCallbacks(void); // run initialize() callbacks and check for complete initialization void RunInitializeCallbacks(void); // run `species all` initialize() callbacks void AllSpecies_CheckIntegrity(void); void AllSpecies_PurgeRemovedObjects(void); bool _RunOneTickWF(void); // called by _RunOneTick() to run a tick (WF models) bool _RunOneTickNonWF(void); // called by _RunOneTick() to run a tick (nonWF models) EidosValue_SP _EvaluateTickRangeNode(const EidosASTNode *p_node, std::string &p_error_string); // evaluate a node that represents a tick range expression SLiMCycleStage CycleStageForScriptBlockType(SLiMEidosBlockType p_block_type); // look up the cycle stage a block executes in bool IsPastOrPresent(slim_tick_t p_block_tick, SLiMEidosBlockType p_block_type); // is the given tick/cycle stage past/present (true) or future (false)? void EvaluateScriptBlockTickRanges(void); // evaluate tick range expressions to find when a block is scheduled void FlagUnevaluatedScriptBlockTickRanges(void); // error for script blocks whose tick range is unevaluated slim_tick_t FirstTick(void); // derived from the first tick in which an Eidos block is registered slim_tick_t EstimatedLastTick(void); // derived from the last tick in which an Eidos block is registered void SimulationHasFinished(void); #if (SLIMPROFILING == 1) // PROFILING void StartProfiling(void); void StopProfiling(void); void CollectSLiMguiMemoryUsageProfileInfo(void); #endif // accessors inline __attribute__((always_inline)) const std::vector &AllSpecies(void) { return all_species_; } inline __attribute__((always_inline)) EidosSymbolTable &SymbolTable(void) const { return *simulation_constants_; } inline __attribute__((always_inline)) EidosFunctionMap &FunctionMap(void) { return simulation_functions_; } inline __attribute__((always_inline)) SLiMModelType ModelType(void) const { return model_type_; } void SetModelType(SLiMModelType p_new_type); inline __attribute__((always_inline)) slim_tick_t Tick(void) const { return tick_; } void SetTick(slim_tick_t p_new_tick); inline __attribute__((always_inline)) SLiMCycleStage CycleStage(void) const { return cycle_stage_; } inline __attribute__((always_inline)) SLiMEidosScript *Script(void) { return script_; } inline __attribute__((always_inline)) std::string ScriptString(void) { return script_->String(); } // *********************************** Support for debugging points in SLiMgui #ifdef SLIMGUI // Set the debug points to be used; note that a copy is *not* made, the caller guarantees the lifetime of the passed object inline void SetDebugPoints(EidosInterpreterDebugPointsSet *p_debug_points) { debug_points_ = p_debug_points; } virtual EidosInterpreterDebugPointsSet *DebugPoints(void) override { return debug_points_; } virtual std::string DebugPointInfo(void) override { return std::string(", tick ") + std::to_string(tick_); }; #endif // *********************************** Support for file observing notifications in SLiMgui #ifdef SLIMGUI virtual void FileWriteNotification(const std::string &p_file_path, std::vector &&p_lines, bool p_append) override; std::vector file_write_paths_; // full file paths for the file writes we have seen, in order of occurrence std::vector> file_write_buffers_; // remembered file write lines, keyed by the full file path; read by SLiMgui std::vector file_write_appends_; // a parallel map just keeping a bool flag: true == append, false == overwrite #endif // TREE SEQUENCE RECORDING #pragma mark - #pragma mark treeseq recording ivars #pragma mark - // Most tree-seq stuff belongs to Species, but the tick clock is synchronized across the community, and command-line flags are managed here void AllSpecies_TSXC_Enable(void); // forces tree-seq with crosschecks on for all species; called by the undocumented -TSXC option void AllSpecies_TSF_Enable(void); // forces tree-seq without crosschecks on for all species; called by the undocumented -TSF option slim_tick_t tree_seq_tick_ = 0; // the tick for the tree sequence code, incremented after generating offspring // this is needed since addSubpop() in an early() event makes one gen, and then the offspring // arrive in the same tick according to SLiM, which confuses the tree-seq code // BCH 5/13/2021: We now increment this after first() events in nonWF models, too double tree_seq_tick_offset_ = 0; // this is a fractional offset added to tree_seq_tick_; this is needed to make successive calls // to addSubpopSplit() arrive at successively later times; see Population::AddSubpopulationSplit() std::string treeseq_time_unit_; // set in initializeTreeSeq(), written out to .trees; has no effect on the simulation, just user data // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - inline EidosSymbolTableEntry &SymbolTableEntry(void) { return self_symbol_; }; virtual EidosValue_SP ContextDefinedFunctionDispatch(const std::string &p_function_name, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosSymbolTable *SymbolsFromBaseSymbols(EidosSymbolTable *p_base_symbols); // derive a symbol table, adding our own symbols if needed static const std::vector *ZeroTickFunctionSignatures(void); // all zero-tick functions static void AddZeroTickFunctionsToMap(EidosFunctionMap &p_map); static void RemoveZeroTickFunctionsFromMap(EidosFunctionMap &p_map); static const std::vector *SLiMFunctionSignatures(void); // all non-zero-tick functions static void AddSLiMFunctionsToMap(EidosFunctionMap &p_map); EidosValue_SP ExecuteContextFunction_initializeSLiMModelType(const std::string &p_function_name, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteContextFunction_initializeInteractionType(const std::string &p_function_name, const std::vector &p_arguments, EidosInterpreter &p_interpreter); virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_createLogFile(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_estimatedLastTick(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_deregisterScriptBlock(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_genomicElementTypesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_interactionTypesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_mutationTypesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_scriptBlocksWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_speciesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_subpopulationsWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_subpopulationsWithNames(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_outputUsage(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_registerFirstEarlyLateEvent(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_registerInteractionCallback(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_rescheduleScriptBlock(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_simulationFinished(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_usage(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); }; class Community_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: Community_Class(const Community_Class &p_original) = delete; // no copy-construct Community_Class& operator=(const Community_Class&) = delete; // no copying inline Community_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif /* defined(__SLiM__community__) */ ================================================ FILE: core/community_eidos.cpp ================================================ // // community_eidos.cpp // SLiM // // Created by Ben Haller on 2/28/2022. // Copyright (c) 2022-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "community.h" #include "haplosome.h" #include "individual.h" #include "subpopulation.h" #include "polymorphism.h" #include "interaction_type.h" #include "log_file.h" #include #include #include #include #include #include #include #include #include #include #include #include static std::string PrintBytes(size_t p_bytes) { std::ostringstream sstream; sstream << p_bytes << " bytes"; if (p_bytes > 1024.0 * 1024.0 * 1024.0 * 1024.0) sstream << " (" << (p_bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0)) << " TB" << ")"; else if (p_bytes > 1024.0 * 1024.0 * 1024.0) sstream << " (" << (p_bytes / (1024.0 * 1024.0 * 1024.0)) << " GB" << ")"; else if (p_bytes > 1024.0 * 1024.0) sstream << " (" << (p_bytes / (1024.0 * 1024.0)) << " MB" << ")"; else if (p_bytes > 1024) sstream << " (" << (p_bytes / 1024.0) << " K" << ")"; return sstream.str(); } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - EidosValue_SP Community::ContextDefinedFunctionDispatch(const std::string &p_function_name, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused(p_interpreter) // we only define initialize...() functions; so we must be in an initialize() callback if (tick_ != 0) EIDOS_TERMINATION << "ERROR (Community::ContextDefinedFunctionDispatch): the function " << p_function_name << "() may only be called in an initialize() callback." << EidosTerminate(); // Non-species-specific initialization if (p_function_name.compare(gStr_initializeSLiMModelType) == 0) return this->ExecuteContextFunction_initializeSLiMModelType(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeInteractionType) == 0) return this->ExecuteContextFunction_initializeInteractionType(p_function_name, p_arguments, p_interpreter); // Species-specific initialization if (!active_species_) EIDOS_TERMINATION << "ERROR (Community::ContextDefinedFunctionDispatch): no active species in context-defined function dispatch; " << p_function_name << "() must be called from a species-specific initialize() callback." << EidosTerminate(); if (p_function_name.compare(gStr_initializeAncestralNucleotides) == 0) return active_species_->ExecuteContextFunction_initializeAncestralNucleotides(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeGenomicElement) == 0) return active_species_->ExecuteContextFunction_initializeGenomicElement(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeGenomicElementType) == 0) return active_species_->ExecuteContextFunction_initializeGenomicElementType(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeMutationType) == 0) return active_species_->ExecuteContextFunction_initializeMutationType(p_function_name, p_arguments, p_interpreter); // NOLINT(*-branch-clone) : intentional consecutive branches else if (p_function_name.compare(gStr_initializeMutationTypeNuc) == 0) return active_species_->ExecuteContextFunction_initializeMutationType(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeRecombinationRate) == 0) return active_species_->ExecuteContextFunction_initializeRecombinationRate(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeChromosome) == 0) return active_species_->ExecuteContextFunction_initializeChromosome(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeGeneConversion) == 0) return active_species_->ExecuteContextFunction_initializeGeneConversion(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeMutationRate) == 0) return active_species_->ExecuteContextFunction_initializeMutationRate(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeHotspotMap) == 0) return active_species_->ExecuteContextFunction_initializeHotspotMap(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeSex) == 0) return active_species_->ExecuteContextFunction_initializeSex(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeSLiMOptions) == 0) return active_species_->ExecuteContextFunction_initializeSLiMOptions(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeSpecies) == 0) return active_species_->ExecuteContextFunction_initializeSpecies(p_function_name, p_arguments, p_interpreter); else if (p_function_name.compare(gStr_initializeTreeSeq) == 0) return active_species_->ExecuteContextFunction_initializeTreeSeq(p_function_name, p_arguments, p_interpreter); EIDOS_TERMINATION << "ERROR (Community::ContextDefinedFunctionDispatch): the function " << p_function_name << "() is not implemented by Community." << EidosTerminate(); } const std::vector *Community::ZeroTickFunctionSignatures(void) { // Allocate our own EidosFunctionSignature objects static std::vector sim_0_signatures_; if (!sim_0_signatures_.size()) { THREAD_SAFETY_IN_ANY_PARALLEL("Community::ZeroTickFunctionSignatures(): not warmed up"); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeAncestralNucleotides, nullptr, kEidosValueMaskInt | kEidosValueMaskSingleton, "SLiM")) ->AddIntString("sequence")); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeGenomicElement, nullptr, kEidosValueMaskObject, gSLiM_GenomicElement_Class, "SLiM")) ->AddIntObject("genomicElementType", gSLiM_GenomicElementType_Class)->AddInt_ON("start", gStaticEidosValueNULL)->AddInt_ON("end", gStaticEidosValueNULL)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeGenomicElementType, nullptr, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_GenomicElementType_Class, "SLiM")) ->AddIntString_S("id")->AddIntObject("mutationTypes", gSLiM_MutationType_Class)->AddNumeric("proportions")->AddFloat_ON("mutationMatrix", gStaticEidosValueNULL)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeInteractionType, nullptr, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_InteractionType_Class, "SLiM")) ->AddIntString_S("id")->AddString_S(gStr_spatiality)->AddLogical_OS(gStr_reciprocal, gStaticEidosValue_LogicalF)->AddNumeric_OS(gStr_maxDistance, gStaticEidosValue_FloatINF)->AddString_OS(gStr_sexSegregation, gStaticEidosValue_StringDoubleAsterisk)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeMutationType, nullptr, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_MutationType_Class, "SLiM")) ->AddIntString_S("id")->AddNumeric_S("dominanceCoeff")->AddString_S("distributionType")->AddEllipsis()); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeMutationTypeNuc, nullptr, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_MutationType_Class, "SLiM")) ->AddIntString_S("id")->AddNumeric_S("dominanceCoeff")->AddString_S("distributionType")->AddEllipsis()); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeRecombinationRate, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddNumeric("rates")->AddInt_ON("ends", gStaticEidosValueNULL)->AddString_OS("sex", gStaticEidosValue_StringAsterisk)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeChromosome, nullptr, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Chromosome_Class, "SLiM"))->AddInt_S("id")->AddInt_OSN("length", gStaticEidosValueNULL)->AddString_OS("type", gStaticEidosValue_StringA)->AddString_OSN("symbol", gStaticEidosValueNULL)->AddString_OSN("name", gStaticEidosValueNULL)->AddInt_OS("mutationRuns", gStaticEidosValue_Integer0)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeGeneConversion, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddNumeric_S("nonCrossoverFraction")->AddNumeric_S("meanLength")->AddNumeric_S("simpleConversionFraction")->AddNumeric_OS("bias", gStaticEidosValue_Integer0)->AddLogical_OS("redrawLengthsOnFailure", gStaticEidosValue_LogicalF)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeMutationRate, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddNumeric("rates")->AddInt_ON("ends", gStaticEidosValueNULL)->AddString_OS("sex", gStaticEidosValue_StringAsterisk)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeHotspotMap, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddNumeric("multipliers")->AddInt_ON("ends", gStaticEidosValueNULL)->AddString_OS("sex", gStaticEidosValue_StringAsterisk)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeSex, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddString_OSN("chromosomeType", gStaticEidosValueNULL)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeSLiMOptions, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddLogical_OS("keepPedigrees", gStaticEidosValue_LogicalF)->AddString_OS("dimensionality", gStaticEidosValue_StringEmpty)->AddString_OS("periodicity", gStaticEidosValue_StringEmpty)->AddLogical_OS("doMutationRunExperiments", gStaticEidosValue_LogicalT)->AddLogical_OS("preventIncidentalSelfing", gStaticEidosValue_LogicalF)->AddLogical_OS("nucleotideBased", gStaticEidosValue_LogicalF)->AddLogical_OS("randomizeCallbacks", gStaticEidosValue_LogicalT)->AddLogical_OS("checkInfiniteLoops", gStaticEidosValue_LogicalT)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeSpecies, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddInt_OS("tickModulo", gStaticEidosValue_Integer1)->AddInt_OS("tickPhase", gStaticEidosValue_Integer1)->AddString_OS(gStr_avatar, gStaticEidosValue_StringEmpty)->AddString_OS("color", gStaticEidosValue_StringEmpty)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeTreeSeq, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddLogical_OS("recordMutations", gStaticEidosValue_LogicalT)->AddNumeric_OSN("simplificationRatio", gStaticEidosValueNULL)->AddInt_OSN("simplificationInterval", gStaticEidosValueNULL)->AddLogical_OS("checkCoalescence", gStaticEidosValue_LogicalF)->AddLogical_OS("runCrosschecks", gStaticEidosValue_LogicalF)->AddLogical_OS("retainCoalescentOnly", gStaticEidosValue_LogicalT)->AddString_OSN("timeUnit", gStaticEidosValueNULL)); sim_0_signatures_.emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature(gStr_initializeSLiMModelType, nullptr, kEidosValueMaskVOID, "SLiM")) ->AddString_S("modelType")); } return &sim_0_signatures_; } void Community::AddZeroTickFunctionsToMap(EidosFunctionMap &p_map) { const std::vector *signatures = ZeroTickFunctionSignatures(); if (signatures) { for (const EidosFunctionSignature_CSP &signature : *signatures) p_map.emplace(signature->call_name_, signature); } } void Community::RemoveZeroTickFunctionsFromMap(EidosFunctionMap &p_map) { const std::vector *signatures = ZeroTickFunctionSignatures(); if (signatures) { for (const EidosFunctionSignature_CSP &signature : *signatures) p_map.erase(signature->call_name_); } } void Community::AddSLiMFunctionsToMap(EidosFunctionMap &p_map) { const std::vector *signatures = SLiMFunctionSignatures(); if (signatures) { for (const EidosFunctionSignature_CSP &signature : *signatures) p_map.emplace(signature->call_name_, signature); } } EidosSymbolTable *Community::SymbolsFromBaseSymbols(EidosSymbolTable *p_base_symbols) { // Since we keep our own symbol table long-term, this function does not actually re-derive a new table, but just returns the cached table if (p_base_symbols != gEidosConstantsSymbolTable) EIDOS_TERMINATION << "ERROR (Community::SymbolsFromBaseSymbols): (internal error) SLiM requires that its parent symbol table be the standard Eidos symbol table." << EidosTerminate(); return simulation_constants_; } // ********************* (void)initializeSLiMModelType(string$ modelType) // EidosValue_SP Community::ExecuteContextFunction_initializeSLiMModelType(const std::string &p_function_name, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_function_name, p_interpreter) EidosValue *arg_modelType_value = p_arguments[0].get(); std::ostream &output_stream = p_interpreter.ExecutionOutputStream(); if (num_modeltype_declarations_ > 0) EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeSLiMModelType): initializeSLiMModelType() may be called only once." << EidosTerminate(); if (is_explicit_species_ && active_species_) EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeSLiMModelType): in multispecies models, initializeSLiMModelType() may only be called from a non-species-specific (`species all`) initialize() callback." << EidosTerminate(); if ((num_interaction_types_ > 0) || (active_species_ && active_species_->HasDoneAnyInitialization())) EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeSLiMModelType): initializeSLiMModelType() must be called before all other initialization functions." << EidosTerminate(); { // string$ modelType std::string model_type = arg_modelType_value->StringAtIndex_NOCAST(0, nullptr); if (model_type == "WF") SetModelType(SLiMModelType::kModelTypeWF); else if (model_type == "nonWF") SetModelType(SLiMModelType::kModelTypeNonWF); else EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeSLiMModelType): in initializeSLiMModelType(), legal values for parameter modelType are only 'WF' or 'nonWF'." << EidosTerminate(); } if (SLiM_verbosity_level >= 1) { output_stream << "initializeSLiMModelType("; // modelType output_stream << "modelType = "; if (model_type_ == SLiMModelType::kModelTypeWF) output_stream << "'WF'"; else if (model_type_ == SLiMModelType::kModelTypeNonWF) output_stream << "'nonWF'"; output_stream << ");" << std::endl; } num_modeltype_declarations_++; return gStaticEidosValueVOID; } // ********************* (object$)initializeInteractionType(is$ id, string$ spatiality, [logical$ reciprocal = F], [numeric$ maxDistance = INF], [string$ sexSegregation = "**"]) // EidosValue_SP Community::ExecuteContextFunction_initializeInteractionType(const std::string &p_function_name, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_function_name, p_arguments, p_interpreter) EidosValue *id_value = p_arguments[0].get(); EidosValue *spatiality_value = p_arguments[1].get(); EidosValue *reciprocal_value = p_arguments[2].get(); EidosValue *maxDistance_value = p_arguments[3].get(); EidosValue *sexSegregation_value = p_arguments[4].get(); std::ostream &output_stream = p_interpreter.ExecutionOutputStream(); if (is_explicit_species_ && active_species_) EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeInteractionType): in multispecies models, initializeInteractionType() may only be called from a non-species-specific (`species all`) initialize() callback." << EidosTerminate(); slim_objectid_t map_identifier = SLiM_ExtractObjectIDFromEidosValue_is(id_value, 0, 'i'); std::string spatiality_string = spatiality_value->StringAtIndex_NOCAST(0, nullptr); bool reciprocal = reciprocal_value->LogicalAtIndex_NOCAST(0, nullptr); // UNUSED double max_distance = maxDistance_value->NumericAtIndex_NOCAST(0, nullptr); std::string sex_string = sexSegregation_value->StringAtIndex_NOCAST(0, nullptr); IndividualSex receiver_sex = IndividualSex::kUnspecified, exerter_sex = IndividualSex::kUnspecified; if (InteractionTypeWithID(map_identifier)) EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeInteractionType): initializeInteractionType() interaction type m" << map_identifier << " already defined." << EidosTerminate(); if (sex_string == "**") { receiver_sex = IndividualSex::kUnspecified; exerter_sex = IndividualSex::kUnspecified; } else if (sex_string == "*M") { receiver_sex = IndividualSex::kUnspecified; exerter_sex = IndividualSex::kMale; } else if (sex_string == "*F") { receiver_sex = IndividualSex::kUnspecified; exerter_sex = IndividualSex::kFemale; } else if (sex_string == "M*") { receiver_sex = IndividualSex::kMale; exerter_sex = IndividualSex::kUnspecified; } else if (sex_string == "MM") { receiver_sex = IndividualSex::kMale; exerter_sex = IndividualSex::kMale; } else if (sex_string == "MF") { receiver_sex = IndividualSex::kMale; exerter_sex = IndividualSex::kFemale; } else if (sex_string == "F*") { receiver_sex = IndividualSex::kFemale; exerter_sex = IndividualSex::kUnspecified; } else if (sex_string == "FM") { receiver_sex = IndividualSex::kFemale; exerter_sex = IndividualSex::kMale; } else if (sex_string == "FF") { receiver_sex = IndividualSex::kFemale; exerter_sex = IndividualSex::kFemale; } else EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeInteractionType): initializeInteractionType() unsupported sexSegregation value (must be '**', '*M', '*F', 'M*', 'MM', 'MF', 'F*', 'FM', or 'FF')." << EidosTerminate(); InteractionType *new_interaction_type = new InteractionType(*this, map_identifier, spatiality_string, reciprocal, max_distance, receiver_sex, exerter_sex); interaction_types_.emplace(map_identifier, new_interaction_type); interaction_types_changed_ = true; // define a new Eidos variable to refer to the new mutation type EidosSymbolTableEntry &symbol_entry = new_interaction_type->SymbolTableEntry(); if (p_interpreter.SymbolTable().ContainsSymbol(symbol_entry.first)) EIDOS_TERMINATION << "ERROR (Community::ExecuteContextFunction_initializeInteractionType): initializeInteractionType() symbol " << EidosStringRegistry::StringForGlobalStringID(symbol_entry.first) << " was already defined prior to its definition here." << EidosTerminate(); SymbolTable().InitializeConstantSymbolEntry(symbol_entry); if (SLiM_verbosity_level >= 1) { output_stream << "initializeInteractionType(" << map_identifier << ", \"" << spatiality_string << "\""; if (reciprocal == true) output_stream << ", reciprocal=T"; if (!std::isinf(max_distance)) output_stream << ", maxDistance=" << max_distance; if (sex_string != "**") output_stream << ", sexSegregation=\"" << sex_string << "\""; output_stream << ");" << std::endl; } num_interaction_types_++; return symbol_entry.second; } const EidosClass *Community::Class(void) const { return gSLiM_Community_Class; } void Community::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay(); // standard EidosObject behavior (not Dictionary behavior) } EidosValue_SP Community::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_allGenomicElementTypes: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_GenomicElementType_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (auto getype : all_genomic_element_types_) vec->push_object_element_NORR(getype.second); return result_SP; } case gID_allInteractionTypes: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_InteractionType_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (auto inttype : interaction_types_) vec->push_object_element_NORR(inttype.second); return result_SP; } case gID_allMutationTypes: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_MutationType_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (auto muttype : all_mutation_types_) vec->push_object_element_NORR(muttype.second); return result_SP; } case gID_allScriptBlocks: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_SLiMEidosBlock_Class); EidosValue_SP result_SP = EidosValue_SP(vec); std::vector &script_blocks = AllScriptBlocks(); for (auto script_block : script_blocks) if (script_block->type_ != SLiMEidosBlockType::SLiMEidosUserDefinedFunction) // exclude function blocks; not user-visible vec->push_object_element_NORR(script_block); return result_SP; } case gID_allSpecies: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Species_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (auto species : all_species_) vec->push_object_element_NORR(species); return result_SP; } case gID_allSubpopulations: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Subpopulation_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (auto species : all_species_) for (auto pop : species->population_.subpops_) vec->push_object_element_NORR(pop.second); return result_SP; } case gID_logFiles: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_LogFile_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (LogFile *logfile : log_file_registry_) vec->push_object_element_RR(logfile); return result_SP; } case gID_modelType: { static EidosValue_SP static_model_type_string_WF; static EidosValue_SP static_model_type_string_nonWF; #pragma omp critical (GetProperty_modelType_cache) { if (!static_model_type_string_WF) { static_model_type_string_WF = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("WF")); static_model_type_string_nonWF = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("nonWF")); } } switch (model_type_) { case SLiMModelType::kModelTypeWF: return static_model_type_string_WF; case SLiMModelType::kModelTypeNonWF: return static_model_type_string_nonWF; default: return gStaticEidosValueNULL; // never hit; here to make the compiler happy } } // variables case gID_tick: { if (cached_value_tick_ && (cached_value_tick_->IntData()[0] != tick_)) cached_value_tick_.reset(); if (!cached_value_tick_) cached_value_tick_ = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tick_)); return cached_value_tick_; } case gID_cycleStage: { SLiMCycleStage cycle_stage = CycleStage(); std::string cycle_stage_str = StringForSLiMCycleStage(cycle_stage); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(cycle_stage_str)); } case gID_tag: { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Community::GetProperty): property tag accessed on simulation object before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } case gID_verbosity: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(SLiM_verbosity_level)); // all others, including gID_none default: return super::GetProperty(p_property_id); } } void Community::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { case gID_tick: { int64_t value = p_value.IntAtIndex_NOCAST(0, nullptr); slim_tick_t old_tick = tick_; slim_tick_t new_tick = SLiMCastToTickTypeOrRaise(value); SetTick(new_tick); // Setting the tick into the future is generally harmless; the simulation logic is designed to handle that anyway, since // that happens every tick. Setting the tick into the past is a bit tricker, since some things that have already // occurred need to be invalidated. In particular, historical data cached by SLiMgui needs to be fixed. Note that here we // do NOT remove Substitutions that are in the future, or otherwise try to backtrack the actual simulation state. If the user // actually restores a past state with readFromPopulationFile(), all that kind of stuff will be reset; but if all they do is // set the tick counter back, the model state is unchanged, substitutions are still fixed, etc. This means that the // simulation code needs to be robust to the possibility that some records, e.g. for Substitutions, may appear to be about // events in the future. But usually users will only set the tick back if they also call readFromPopulationFile(). if (tick_ < old_tick) { #ifdef SLIMGUI // Fix fitness histories for SLiMgui. Note that mutation_loss_times_ and mutation_fixation_times_ are not fixable, since // their entries are not separated out by tick, so we just leave them as is, containing information about // alternative futures of the model. for (Species *species : all_species_) { for (auto history_record_iter : species->population_.fitness_histories_) { FitnessHistory &history_record = history_record_iter.second; double *history = history_record.history_; if (history) { int old_last_valid_history_index = std::max(0, old_tick - 2); // if tick==2, tick 1 was the last valid entry, and it is at index 0 int new_last_valid_history_index = std::max(0, tick_ - 2); // ditto // make sure that we don't overrun the end of the buffer if (old_last_valid_history_index > history_record.history_length_ - 1) old_last_valid_history_index = history_record.history_length_ - 1; for (int entry_index = new_last_valid_history_index + 1; entry_index <= old_last_valid_history_index; ++entry_index) history[entry_index] = NAN; } } for (auto history_record_iter : species->population_.subpop_size_histories_) { SubpopSizeHistory &history_record = history_record_iter.second; slim_popsize_t *history = history_record.history_; if (history) { int old_last_valid_history_index = std::max(0, old_tick - 2); // if tick==2, tick 1 was the last valid entry, and it is at index 0 int new_last_valid_history_index = std::max(0, tick_ - 2); // ditto // make sure that we don't overrun the end of the buffer if (old_last_valid_history_index > history_record.history_length_ - 1) old_last_valid_history_index = history_record.history_length_ - 1; for (int entry_index = new_last_valid_history_index + 1; entry_index <= old_last_valid_history_index; ++entry_index) history[entry_index] = 0; } } } #endif } return; } case gID_tag: { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; return; } case gID_verbosity: { int64_t value = p_value.IntAtIndex_NOCAST(0, nullptr); SLiM_verbosity_level = value; // at the command line we bounds-check this, but here I see no need return; } // all others, including gID_none default: return super::SetProperty(p_property_id, p_value); } } EidosValue_SP Community::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_createLogFile: return ExecuteMethod_createLogFile(p_method_id, p_arguments, p_interpreter); case gID_estimatedLastTick: return ExecuteMethod_estimatedLastTick(p_method_id, p_arguments, p_interpreter); case gID_deregisterScriptBlock: return ExecuteMethod_deregisterScriptBlock(p_method_id, p_arguments, p_interpreter); case gID_genomicElementTypesWithIDs: return ExecuteMethod_genomicElementTypesWithIDs(p_method_id, p_arguments, p_interpreter); case gID_interactionTypesWithIDs: return ExecuteMethod_interactionTypesWithIDs(p_method_id, p_arguments, p_interpreter); case gID_mutationTypesWithIDs: return ExecuteMethod_mutationTypesWithIDs(p_method_id, p_arguments, p_interpreter); case gID_scriptBlocksWithIDs: return ExecuteMethod_scriptBlocksWithIDs(p_method_id, p_arguments, p_interpreter); case gID_speciesWithIDs: return ExecuteMethod_speciesWithIDs(p_method_id, p_arguments, p_interpreter); case gID_subpopulationsWithIDs: return ExecuteMethod_subpopulationsWithIDs(p_method_id, p_arguments, p_interpreter); case gID_subpopulationsWithNames: return ExecuteMethod_subpopulationsWithNames(p_method_id, p_arguments, p_interpreter); case gID_outputUsage: return ExecuteMethod_outputUsage(p_method_id, p_arguments, p_interpreter); case gID_registerFirstEvent: case gID_registerEarlyEvent: case gID_registerLateEvent: return ExecuteMethod_registerFirstEarlyLateEvent(p_method_id, p_arguments, p_interpreter); case gID_registerInteractionCallback: return ExecuteMethod_registerInteractionCallback(p_method_id, p_arguments, p_interpreter); case gID_rescheduleScriptBlock: return ExecuteMethod_rescheduleScriptBlock(p_method_id, p_arguments, p_interpreter); case gID_simulationFinished: return ExecuteMethod_simulationFinished(p_method_id, p_arguments, p_interpreter); case gEidosID_usage: return ExecuteMethod_usage(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* – (object$)createLogFile(string$ filePath, [Ns initialContents = NULL], [logical$ append = F], [logical$ compress = F], [string$ sep = ","], [Ni$ logInterval = NULL], [Ni$ flushInterval = NULL], [logical$ header = T]) EidosValue_SP Community::ExecuteMethod_createLogFile(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue_SP result_SP(nullptr); EidosValue_String *filePath_value = (EidosValue_String *)p_arguments[0].get(); EidosValue *initialContents_value = p_arguments[1].get(); EidosValue *append_value = p_arguments[2].get(); EidosValue *compress_value = p_arguments[3].get(); EidosValue_String *sep_value = (EidosValue_String *)p_arguments[4].get(); EidosValue *logInterval_value = p_arguments[5].get(); EidosValue *flushInterval_value = p_arguments[6].get(); EidosValue *header_value = p_arguments[7].get(); // process parameters const std::string &filePath = filePath_value->StringRefAtIndex_NOCAST(0, nullptr); std::vector initialContents; bool append = append_value->LogicalAtIndex_NOCAST(0, nullptr); bool do_compress = compress_value->LogicalAtIndex_NOCAST(0, nullptr); const std::string &sep = sep_value->StringRefAtIndex_NOCAST(0, nullptr); bool autologging = false, explicitFlushing = false; int64_t logInterval = 0, flushInterval = 0; bool emit_header = header_value->LogicalAtIndex_NOCAST(0, nullptr); if (initialContents_value->Type() != EidosValueType::kValueNULL) { EidosValue_String *ic_string_value = (EidosValue_String *)initialContents_value; int ic_count = initialContents_value->Count(); for (int ic_index = 0; ic_index < ic_count; ++ic_index) initialContents.emplace_back(&ic_string_value->StringRefAtIndex_NOCAST(ic_index, nullptr)); } if (logInterval_value->Type() == EidosValueType::kValueNULL) { // NULL turns off autologging autologging = false; logInterval = 0; } else { autologging = true; logInterval = logInterval_value->IntAtIndex_NOCAST(0, nullptr); } if (flushInterval_value->Type() == EidosValueType::kValueNULL) { // NULL requests our automatic flushing behavior explicitFlushing = false; flushInterval = 0; } else { explicitFlushing = true; flushInterval = flushInterval_value->IntAtIndex_NOCAST(0, nullptr); } // Create the LogFile object LogFile *logfile = new LogFile(*this); result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(logfile, gSLiM_LogFile_Class)); // Add it to our registry; it has a retain count from new that we will take over at this point log_file_registry_.emplace_back(logfile); // Configure it logfile->SetLogInterval(autologging, logInterval); logfile->SetFlushInterval(explicitFlushing, flushInterval); logfile->ConfigureFile(filePath, initialContents, append, emit_header, do_compress, sep); // Check for duplicate LogFiles using the same path; this is a common error so I'm making it illegal const std::string &resolved_path = logfile->ResolvedFilePath(); for (LogFile *existing_log_file : log_file_registry_) { if (existing_log_file != logfile) { const std::string &existing_path = existing_log_file->ResolvedFilePath(); if (resolved_path == existing_path) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_createLogFile): createLogFile() cannot create a new log file at " << resolved_path << " because an existing log file is already using that path." << EidosTerminate(); } } return result_SP; } // ********************* - (integer$)estimatedLastTick(void) // EidosValue_SP Community::ExecuteMethod_estimatedLastTick(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) slim_tick_t last_tick = EstimatedLastTick(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(last_tick)); } // ********************* - (void)deregisterScriptBlock(io scriptBlocks) // EidosValue_SP Community::ExecuteMethod_deregisterScriptBlock(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *scriptBlocks_value = p_arguments[0].get(); int block_count = scriptBlocks_value->Count(); // We just schedule the blocks for deregistration; we do not deregister them immediately, because that would leave stale pointers lying around for (int block_index = 0; block_index < block_count; ++block_index) { SLiMEidosBlock *block = SLiM_ExtractSLiMEidosBlockFromEidosValue_io(scriptBlocks_value, block_index, this, nullptr, "deregisterScriptBlock()"); // agnostic to species if (block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) { // this should never be hit, because the user should have no way to get a reference to a function block EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_deregisterScriptBlock): (internal error) deregisterScriptBlock() cannot be called on user-defined function script blocks." << EidosTerminate(); } else if (block->type_ == SLiMEidosBlockType::SLiMEidosInteractionCallback) { // interaction() callbacks have to work differently, because they can be called at any time after an // interaction has been evaluated, up until the interaction is invalidated; we can't make pointers // to interaction() callbacks go stale except at that specific point in the cycle if (std::find(scheduled_interaction_deregs_.begin(), scheduled_interaction_deregs_.end(), block) != scheduled_interaction_deregs_.end()) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_deregisterScriptBlock): deregisterScriptBlock() called twice on the same script block." << EidosTerminate(); scheduled_interaction_deregs_.emplace_back(block); #if DEBUG_BLOCK_REG_DEREG std::cout << "deregisterScriptBlock() called for block:" << std::endl; std::cout << " "; block->Print(std::cout); std::cout << std::endl; #endif } else { // all other script blocks go on the main list and get cleared out at the end of each cycle stage if (std::find(scheduled_deregistrations_.begin(), scheduled_deregistrations_.end(), block) != scheduled_deregistrations_.end()) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_deregisterScriptBlock): deregisterScriptBlock() called twice on the same script block." << EidosTerminate(); scheduled_deregistrations_.emplace_back(block); #if DEBUG_BLOCK_REG_DEREG std::cout << "deregisterScriptBlock() called for block:" << std::endl; std::cout << " "; block->Print(std::cout); std::cout << std::endl; #endif } #ifdef SLIMGUI gSLiMScheduling << "\t\tderegisterScriptBlock() called for block: "; block->PrintDeclaration(gSLiMScheduling, this); gSLiMScheduling << std::endl; #endif } return gStaticEidosValueVOID; } // ********************* – (object)genomicElementTypesWithIDs(integer ids) // EidosValue_SP Community::ExecuteMethod_genomicElementTypesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *ids_value = p_arguments[0].get(); int ids_count = ids_value->Count(); const int64_t *ids_data = ids_value->IntData(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_GenomicElementType_Class))->resize_no_initialize_RR(ids_count); for (int id_index = 0; id_index < ids_count; id_index++) { slim_objectid_t id = SLiMCastToObjectidTypeOrRaise(ids_data[id_index]); GenomicElementType *object = GenomicElementTypeWithID(id); if (!object) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_genomicElementTypesWithIDs): genomicElementTypesWithIDs() did not find a genomic element type with id " << id << "." << EidosTerminate(); vec->set_object_element_no_check_NORR(object, id_index); } return EidosValue_SP(vec); } // ********************* – (object)interactionTypesWithIDs(integer ids) // EidosValue_SP Community::ExecuteMethod_interactionTypesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *ids_value = p_arguments[0].get(); int ids_count = ids_value->Count(); const int64_t *ids_data = ids_value->IntData(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_InteractionType_Class))->resize_no_initialize_RR(ids_count); for (int id_index = 0; id_index < ids_count; id_index++) { slim_objectid_t id = SLiMCastToObjectidTypeOrRaise(ids_data[id_index]); InteractionType *object = InteractionTypeWithID(id); if (!object) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_interactionTypesWithIDs): interactionTypesWithIDs() did not find an interaction type with id " << id << "." << EidosTerminate(); vec->set_object_element_no_check_NORR(object, id_index); } return EidosValue_SP(vec); } // ********************* – (object)mutationTypesWithIDs(integer ids) // EidosValue_SP Community::ExecuteMethod_mutationTypesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *ids_value = p_arguments[0].get(); int ids_count = ids_value->Count(); const int64_t *ids_data = ids_value->IntData(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_MutationType_Class))->resize_no_initialize_RR(ids_count); for (int id_index = 0; id_index < ids_count; id_index++) { slim_objectid_t id = SLiMCastToObjectidTypeOrRaise(ids_data[id_index]); MutationType *object = MutationTypeWithID(id); if (!object) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_mutationTypesWithIDs): mutationTypesWithIDs() did not find a mutation type with id " << id << "." << EidosTerminate(); vec->set_object_element_no_check_NORR(object, id_index); } return EidosValue_SP(vec); } // ********************* – (object)scriptBlocksWithIDs(integer ids) // EidosValue_SP Community::ExecuteMethod_scriptBlocksWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *ids_value = p_arguments[0].get(); int ids_count = ids_value->Count(); const int64_t *ids_data = ids_value->IntData(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_SLiMEidosBlock_Class))->resize_no_initialize_RR(ids_count); for (int id_index = 0; id_index < ids_count; id_index++) { slim_objectid_t id = SLiMCastToObjectidTypeOrRaise(ids_data[id_index]); SLiMEidosBlock *object = ScriptBlockWithID(id); if (!object) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_scriptBlocksWithIDs): scriptBlocksWithIDs() did not find a script block with id " << id << "." << EidosTerminate(); vec->set_object_element_no_check_NORR(object, id_index); } return EidosValue_SP(vec); } // ********************* – (object)speciesWithIDs(integer ids) // EidosValue_SP Community::ExecuteMethod_speciesWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *ids_value = p_arguments[0].get(); int ids_count = ids_value->Count(); const int64_t *ids_data = ids_value->IntData(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Species_Class))->resize_no_initialize_RR(ids_count); for (int id_index = 0; id_index < ids_count; id_index++) { slim_objectid_t id = SLiMCastToObjectidTypeOrRaise(ids_data[id_index]); Species *object = SpeciesWithID(id); if (!object) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_speciesWithIDs): speciesWithIDs() did not find a species with id " << id << "." << EidosTerminate(); vec->set_object_element_no_check_NORR(object, id_index); } return EidosValue_SP(vec); } // ********************* – (object)subpopulationsWithIDs(integer ids) // EidosValue_SP Community::ExecuteMethod_subpopulationsWithIDs(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *ids_value = p_arguments[0].get(); int ids_count = ids_value->Count(); const int64_t *ids_data = ids_value->IntData(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Subpopulation_Class))->resize_no_initialize_RR(ids_count); for (int id_index = 0; id_index < ids_count; id_index++) { slim_objectid_t id = SLiMCastToObjectidTypeOrRaise(ids_data[id_index]); Subpopulation *object = SubpopulationWithID(id); if (!object) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_subpopulationsWithIDs): subpopulationsWithIDs() did not find a subpopulation with id " << id << "." << EidosTerminate(); vec->set_object_element_no_check_NORR(object, id_index); } return EidosValue_SP(vec); } // ********************* – (object)subpopulationsWithNames(string names) // EidosValue_SP Community::ExecuteMethod_subpopulationsWithNames(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *names_value = p_arguments[0].get(); int names_count = names_value->Count(); const std::string *names_data = names_value->StringData(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Subpopulation_Class))->resize_no_initialize_RR(names_count); for (int name_index = 0; name_index < names_count; name_index++) { const std::string &name = names_data[name_index]; Subpopulation *object = SubpopulationWithName(name); if (!object) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_subpopulationsWithNames): subpopulationsWithNames() did not find a subpopulation with name " << name << "." << EidosTerminate(); vec->set_object_element_no_check_NORR(object, name_index); } return EidosValue_SP(vec); } // ********************* – (void)outputUsage(void) // EidosValue_SP Community::ExecuteMethod_outputUsage(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // BEWARE: See also the -usage() method, which must be maintained in parallel with this // before writing anything, erase a progress line if we've got one up, to try to make a clean slate Eidos_EraseProgress(); // Save flags/precision and set to precision 1 std::ostream &out = p_interpreter.ExecutionOutputStream(); std::ios_base::fmtflags oldflags = out.flags(); std::streamsize oldprecision = out.precision(); out << std::fixed << std::setprecision(1); // Tally up usage across the simulation SLiMMemoryUsage_Community usage_community; SLiMMemoryUsage_Species usage_all_species; EIDOS_BZERO(&usage_all_species, sizeof(SLiMMemoryUsage_Species)); TabulateSLiMMemoryUsage_Community(&usage_community, &p_interpreter.SymbolTable()); for (Species *species : all_species_) { SLiMMemoryUsage_Species usage_one_species; species->TabulateSLiMMemoryUsage_Species(&usage_one_species); AccumulateMemoryUsageIntoTotal_Species(usage_one_species, usage_all_species); } // Print header out << "Memory usage summary:" << std::endl; // Chromosome out << " Chromosome objects(" << usage_all_species.chromosomeObjects_count << "): " << PrintBytes(usage_all_species.chromosomeObjects) << std::endl; out << " Mutation rate maps: " << PrintBytes(usage_all_species.chromosomeMutationRateMaps) << std::endl; out << " Recombination rate maps: " << PrintBytes(usage_all_species.chromosomeRecombinationRateMaps) << std::endl; out << " Ancestral nucleotides: " << PrintBytes(usage_all_species.chromosomeAncestralSequence) << std::endl; // Haplosome out << " Haplosome objects (" << usage_all_species.haplosomeObjects_count << "): " << PrintBytes(usage_all_species.haplosomeObjects) << std::endl; out << " External MutationRun* buffers: " << PrintBytes(usage_all_species.haplosomeExternalBuffers) << std::endl; out << " Unused pool space: " << PrintBytes(usage_all_species.haplosomeUnusedPoolSpace) << std::endl; out << " Unused pool buffers: " << PrintBytes(usage_all_species.haplosomeUnusedPoolBuffers) << std::endl; // GenomicElement out << " GenomicElement objects (" << usage_all_species.genomicElementObjects_count << "): " << PrintBytes(usage_all_species.genomicElementObjects) << std::endl; // GenomicElementType out << " GenomicElementType objects (" << usage_all_species.genomicElementTypeObjects_count << "): " << PrintBytes(usage_all_species.genomicElementTypeObjects) << std::endl; // Individual out << " Individual objects (" << usage_all_species.individualObjects_count << "): " << PrintBytes(usage_all_species.individualObjects) << std::endl; out << " External Haplosome* buffers: " << PrintBytes(usage_all_species.individualHaplosomeVectors) << std::endl; out << " Individuals awaiting reuse: " << PrintBytes(usage_all_species.individualJunkyardAndHaplosomes) << std::endl; out << " Unused pool space: " << PrintBytes(usage_all_species.individualUnusedPoolSpace) << std::endl; // InteractionType out << " InteractionType objects (" << usage_community.interactionTypeObjects_count << "): " << PrintBytes(usage_community.interactionTypeObjects) << std::endl; if (usage_community.interactionTypeObjects_count) { out << " k-d trees: " << PrintBytes(usage_community.interactionTypeKDTrees) << std::endl; out << " Position caches: " << PrintBytes(usage_community.interactionTypePositionCaches) << std::endl; out << " Sparse vector pool: " << PrintBytes(usage_community.interactionTypeSparseVectorPool) << std::endl; } // Mutation out << " Mutation objects (" << usage_all_species.mutationObjects_count << "): " << PrintBytes(usage_all_species.mutationObjects) << std::endl; out << " Refcount buffer: " << PrintBytes(usage_community.mutationRefcountBuffer) << std::endl; out << " Unused pool space: " << PrintBytes(usage_community.mutationUnusedPoolSpace) << std::endl; // MutationRun out << " MutationRun objects (" << usage_all_species.mutationRunObjects_count << "): " << PrintBytes(usage_all_species.mutationRunObjects) << std::endl; out << " External MutationIndex buffers: " << PrintBytes(usage_all_species.mutationRunExternalBuffers) << std::endl; out << " Nonneutral mutation caches: " << PrintBytes(usage_all_species.mutationRunNonneutralCaches) << std::endl; out << " Unused pool space: " << PrintBytes(usage_all_species.mutationRunUnusedPoolSpace) << std::endl; out << " Unused pool buffers: " << PrintBytes(usage_all_species.mutationRunUnusedPoolBuffers) << std::endl; // MutationType out << " MutationType objects (" << usage_all_species.mutationTypeObjects_count << "): " << PrintBytes(usage_all_species.mutationTypeObjects) << std::endl; // Species (including the Population object) out << " Species objects: " << PrintBytes(usage_all_species.speciesObjects) << std::endl; out << " Tree-sequence tables: " << PrintBytes(usage_all_species.speciesTreeSeqTables) << std::endl; // Subpopulation out << " Subpopulation objects (" << usage_all_species.subpopulationObjects_count << "): " << PrintBytes(usage_all_species.subpopulationObjects) << std::endl; out << " Fitness caches: " << PrintBytes(usage_all_species.subpopulationFitnessCaches) << std::endl; out << " Parent tables: " << PrintBytes(usage_all_species.subpopulationParentTables) << std::endl; out << " Spatial maps: " << PrintBytes(usage_all_species.subpopulationSpatialMaps) << std::endl; if (usage_all_species.subpopulationSpatialMapsDisplay) out << " Spatial map display (SLiMgui): " << PrintBytes(usage_all_species.subpopulationSpatialMapsDisplay) << std::endl; // Substitution out << " Substitution objects (" << usage_all_species.substitutionObjects_count << "): " << PrintBytes(usage_all_species.substitutionObjects) << std::endl; // Eidos usage out << " Eidos: " << std::endl; out << " EidosASTNode pool: " << PrintBytes(usage_community.eidosASTNodePool) << std::endl; out << " EidosSymbolTable pool: " << PrintBytes(usage_community.eidosSymbolTablePool) << std::endl; out << " EidosValue pool: " << PrintBytes(usage_community.eidosValuePool) << std::endl; out << " File buffers: " << PrintBytes(usage_community.fileBuffers) << std::endl; out << " # Total accounted for: " << PrintBytes(usage_community.totalMemoryUsage + usage_all_species.totalMemoryUsage) << std::endl; out << std::endl; // Restore flags/precision out.flags(oldflags); out.precision(oldprecision); return gStaticEidosValueVOID; } void Community::CheckScheduling(slim_tick_t p_target_tick, SLiMCycleStage p_target_stage) { // See also Community::IsPastOrPresent() for essentially the same logic, but handling // the timing problem in a different way. if (p_target_tick < tick_) EIDOS_TERMINATION << "ERROR (Community::CheckScheduling): event/callback scheduled for a past tick would not run." << EidosTerminate(); if ((p_target_tick == tick_) && (p_target_stage < cycle_stage_)) EIDOS_TERMINATION << "ERROR (Community::CheckScheduling): event/callback scheduled for the current tick, but for a past cycle stage, would not run." << EidosTerminate(); if ((p_target_tick == tick_) && (p_target_stage == cycle_stage_)) EIDOS_TERMINATION << "ERROR (Community::CheckScheduling): event/callback scheduled for the current tick, but for the currently executing cycle stage, would not run." << EidosTerminate(); } // ********************* – (object$)registerFirstEvent(Nis$ id, string$ source, [Ni$ start = NULL], [Ni$ end = NULL], [No$ ticksSpec = NULL]) // ********************* – (object$)registerEarlyEvent(Nis$ id, string$ source, [Ni$ start = NULL], [Ni$ end = NULL], [No$ ticksSpec = NULL]) // ********************* – (object$)registerLateEvent(Nis$ id, string$ source, [Ni$ start = NULL], [Ni$ end = NULL], [No$ ticksSpec = NULL]) // EidosValue_SP Community::ExecuteMethod_registerFirstEarlyLateEvent(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *id_value = p_arguments[0].get(); EidosValue *source_value = p_arguments[1].get(); EidosValue *start_value = p_arguments[2].get(); EidosValue *end_value = p_arguments[3].get(); EidosValue *ticksSpec_value = p_arguments[4].get(); slim_objectid_t script_id = -1; // used if the id is NULL, to indicate an anonymous block std::string script_string = source_value->StringAtIndex_NOCAST(0, nullptr); slim_tick_t start_tick = ((start_value->Type() != EidosValueType::kValueNULL) ? SLiMCastToTickTypeOrRaise(start_value->IntAtIndex_NOCAST(0, nullptr)) : 1); slim_tick_t end_tick = ((end_value->Type() != EidosValueType::kValueNULL) ? SLiMCastToTickTypeOrRaise(end_value->IntAtIndex_NOCAST(0, nullptr)) : SLIM_MAX_TICK + 1); if (id_value->Type() != EidosValueType::kValueNULL) script_id = SLiM_ExtractObjectIDFromEidosValue_is(id_value, 0, 's'); SLiMEidosBlockType target_type; if (p_method_id == gID_registerFirstEvent) target_type = SLiMEidosBlockType::SLiMEidosEventFirst; else if (p_method_id == gID_registerEarlyEvent) target_type = SLiMEidosBlockType::SLiMEidosEventEarly; else if (p_method_id == gID_registerLateEvent) target_type = SLiMEidosBlockType::SLiMEidosEventLate; else EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_registerFirstEarlyLateEvent): (internal error) unrecognized p_method_id." << EidosTerminate(); if (start_tick > end_tick) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_registerFirstEarlyLateEvent): register" << ((p_method_id == gID_registerFirstEvent) ? "First" : ((p_method_id == gID_registerEarlyEvent) ? "Early" : "Late")) << "Event() requires start <= end." << EidosTerminate(); SLiMCycleStage target_stage; if (target_type == SLiMEidosBlockType::SLiMEidosEventFirst) target_stage = (model_type_ == SLiMModelType::kModelTypeWF) ? SLiMCycleStage::kWFStage0ExecuteFirstScripts : SLiMCycleStage::kNonWFStage0ExecuteFirstScripts; else if (target_type == SLiMEidosBlockType::SLiMEidosEventEarly) target_stage = (model_type_ == SLiMModelType::kModelTypeWF) ? SLiMCycleStage::kWFStage1ExecuteEarlyScripts : SLiMCycleStage::kNonWFStage2ExecuteEarlyScripts; else if (target_type == SLiMEidosBlockType::SLiMEidosEventLate) target_stage = (model_type_ == SLiMModelType::kModelTypeWF) ? SLiMCycleStage::kWFStage5ExecuteLateScripts : SLiMCycleStage::kNonWFStage6ExecuteLateScripts; else EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_registerFirstEarlyLateEvent): (internal error) unrecognized target_type." << EidosTerminate(); Species *ticksSpec = ((ticksSpec_value->Type() != EidosValueType::kValueNULL) ? (Species *)ticksSpec_value->ObjectElementAtIndex_NOCAST(0, nullptr) : nullptr); if (ticksSpec && !is_explicit_species_) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_registerFirstEarlyLateEvent): ticksSpec must be NULL in models without explicit species declarations." << EidosTerminate(); CheckScheduling(start_tick, target_stage); SLiMEidosBlock *new_script_block = new SLiMEidosBlock(script_id, script_string, target_type, start_tick, end_tick, nullptr, ticksSpec); AddScriptBlock(new_script_block, &p_interpreter, nullptr); // takes ownership from us return new_script_block->SelfSymbolTableEntry().second; } // ********************* – (object$)registerInteractionCallback(Nis$ id, string$ source, io$ intType, [Nio$ subpop = NULL], [Ni$ start = NULL], [Ni$ end = NULL]) // EidosValue_SP Community::ExecuteMethod_registerInteractionCallback(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *id_value = p_arguments[0].get(); EidosValue *source_value = p_arguments[1].get(); EidosValue *intType_value = p_arguments[2].get(); EidosValue *subpop_value = p_arguments[3].get(); EidosValue *start_value = p_arguments[4].get(); EidosValue *end_value = p_arguments[5].get(); slim_objectid_t script_id = -1; // used if the id is NULL, to indicate an anonymous block std::string script_string = source_value->StringAtIndex_NOCAST(0, nullptr); slim_objectid_t int_type_id = (intType_value->Type() == EidosValueType::kValueInt) ? SLiMCastToObjectidTypeOrRaise(intType_value->IntAtIndex_NOCAST(0, nullptr)) : ((InteractionType *)intType_value->ObjectElementAtIndex_NOCAST(0, nullptr))->interaction_type_id_; slim_objectid_t subpop_id = -1; slim_tick_t start_tick = ((start_value->Type() != EidosValueType::kValueNULL) ? SLiMCastToTickTypeOrRaise(start_value->IntAtIndex_NOCAST(0, nullptr)) : 1); slim_tick_t end_tick = ((end_value->Type() != EidosValueType::kValueNULL) ? SLiMCastToTickTypeOrRaise(end_value->IntAtIndex_NOCAST(0, nullptr)) : SLIM_MAX_TICK + 1); if (id_value->Type() != EidosValueType::kValueNULL) script_id = SLiM_ExtractObjectIDFromEidosValue_is(id_value, 0, 's'); if (subpop_value->Type() != EidosValueType::kValueNULL) subpop_id = (subpop_value->Type() == EidosValueType::kValueInt) ? SLiMCastToObjectidTypeOrRaise(subpop_value->IntAtIndex_NOCAST(0, nullptr)) : ((Subpopulation *)subpop_value->ObjectElementAtIndex_NOCAST(0, nullptr))->subpopulation_id_; if (start_tick > end_tick) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_registerInteractionCallback): registerInteractionCallback() requires start <= end." << EidosTerminate(); CheckScheduling(start_tick, (model_type_ == SLiMModelType::kModelTypeWF) ? SLiMCycleStage::kWFStage7AdvanceTickCounter : SLiMCycleStage::kNonWFStage7AdvanceTickCounter); SLiMEidosBlock *new_script_block = new SLiMEidosBlock(script_id, script_string, SLiMEidosBlockType::SLiMEidosInteractionCallback, start_tick, end_tick, nullptr, nullptr); new_script_block->interaction_type_id_ = int_type_id; new_script_block->subpopulation_id_ = subpop_id; // SPECIES CONSISTENCY CHECK (done by AddScriptBlock()) AddScriptBlock(new_script_block, &p_interpreter, nullptr); // takes ownership from us return new_script_block->SelfSymbolTableEntry().second; } // ********************* – (object)rescheduleScriptBlock(io$ block, [Ni$ start = NULL], [Ni$ end = NULL], [Ni ticks = NULL]) // EidosValue_SP Community::ExecuteMethod_rescheduleScriptBlock(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *block_value = (EidosValue_Object *)p_arguments[0].get(); EidosValue *start_value = p_arguments[1].get(); EidosValue *end_value = p_arguments[2].get(); EidosValue *ticks_value = p_arguments[3].get(); SLiMEidosBlock *block = SLiM_ExtractSLiMEidosBlockFromEidosValue_io(block_value, 0, this, nullptr, "rescheduleScriptBlock()"); // agnostic to species bool start_null = (start_value->Type() == EidosValueType::kValueNULL); bool end_null = (end_value->Type() == EidosValueType::kValueNULL); bool ticks_null = (ticks_value->Type() == EidosValueType::kValueNULL); if (block->type_ == SLiMEidosBlockType::SLiMEidosUserDefinedFunction) { // this should never be hit, because the user should have no way to get a reference to a function block EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_rescheduleScriptBlock): (internal error) rescheduleScriptBlock() cannot be called on user-defined function script blocks." << EidosTerminate(); } SLiMCycleStage stage = CycleStageForScriptBlockType(block->type_); if ((!start_null || !end_null) && ticks_null) { // start/end case; this is simple slim_tick_t start = (start_null ? 1 : SLiMCastToTickTypeOrRaise(start_value->IntAtIndex_NOCAST(0, nullptr))); slim_tick_t end = (end_null ? SLIM_MAX_TICK + 1 : SLiMCastToTickTypeOrRaise(end_value->IntAtIndex_NOCAST(0, nullptr))); if (start > end) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_rescheduleScriptBlock): rescheduleScriptBlock() requires start <= end." << EidosTerminate(); CheckScheduling(start, stage); block->tick_range_evaluated_ = true; block->tick_range_is_sequence_ = true; block->tick_start_ = start; block->tick_end_ = end; block->tick_set_.clear(); last_script_block_tick_cached_ = false; script_block_types_cached_ = false; scripts_changed_ = true; #ifdef SLIMGUI gSLiMScheduling << "\t\trescheduleScriptBlock() called (with start " << start << ", end " << end << ") for block: "; block->PrintDeclaration(gSLiMScheduling, this); gSLiMScheduling << std::endl; #endif return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(block, gSLiM_SLiMEidosBlock_Class)); } else if (!ticks_null && (start_null && end_null)) { // ticks case; we no longer schedule multiple blocks, we use ticks_set_ with the focal block // first, fetch the ticks and make sure they are in bounds int tick_count = ticks_value->Count(); const int64_t *ticks_data = ticks_value->IntData(); if (tick_count == 0) { // set to run in no ticks; we do this with an empty set block->tick_range_evaluated_ = true; block->tick_range_is_sequence_ = false; block->tick_set_.clear(); } else { // if it is a singleton, or a consecutive range, we detect that and handle it efficiently // we don't try to detect consecutive ranges that have been scrambled, since we'd have to sort; // I don't want this algorithm to be O(N log N), since we might get called with a very large vector bool is_sequential = true; int64_t first_value = ticks_data[0]; int64_t prev_value = first_value; for (int index = 1; index < tick_count; ++index) { int64_t tick_64 = ticks_data[index]; if ((tick_64 < 1) || (tick_64 > SLIM_MAX_TICK)) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_rescheduleScriptBlock): tick value out of range (" << tick_64 << ")." << EidosTerminate(); if (tick_64 != prev_value + 1) { is_sequential = false; break; } prev_value = tick_64; } if (is_sequential) { block->tick_range_evaluated_ = true; block->tick_range_is_sequence_ = true; block->tick_start_ = (slim_tick_t)first_value; block->tick_end_ = (slim_tick_t)prev_value; block->tick_set_.clear(); CheckScheduling(block->tick_start_, stage); } else { std::unordered_set &set = block->tick_set_; slim_tick_t min_tick = SLIM_MAX_TICK; block->tick_range_evaluated_ = true; block->tick_range_is_sequence_ = false; set.clear(); for (int tick_index = 0; tick_index < tick_count; ++tick_index) { slim_tick_t tick = (slim_tick_t)ticks_data[tick_index]; if (set.find(tick) != set.end()) EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_rescheduleScriptBlock): rescheduleScriptBlock() requires that the tick vector contain unique values; the same tick cannot be used twice." << EidosTerminate(); if (tick < min_tick) min_tick = tick; set.emplace(tick); } CheckScheduling(min_tick, stage); // if the minimum tick is OK, they are all OK } } last_script_block_tick_cached_ = false; script_block_types_cached_ = false; scripts_changed_ = true; #ifdef SLIMGUI gSLiMScheduling << "\t\trescheduleScriptBlock() called (with a ticks schedule) for block: "; block->PrintDeclaration(gSLiMScheduling, this); gSLiMScheduling << std::endl; #endif return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(block, gSLiM_SLiMEidosBlock_Class)); } else { EIDOS_TERMINATION << "ERROR (Community::ExecuteMethod_rescheduleScriptBlock): rescheduleScriptBlock() requires that either start/end or ticks be supplied, but not both." << EidosTerminate(); } } // ********************* - (void)simulationFinished(void) // EidosValue_SP Community::ExecuteMethod_simulationFinished(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // Note that Species::ExecuteMethod_simulationFinished() calls this method to forward simulationFinished() on to us! // This means that we need to have an identical Eidos method signature, etc., so the forwarding goes smoothly. #ifdef SLIMGUI gSLiMScheduling << "\t\tsimulationFinished() called" << std::endl; #endif sim_declared_finished_ = true; return gStaticEidosValueVOID; } // ********************* – (float$)usage(void) // EidosValue_SP Community::ExecuteMethod_usage(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // BEWARE: See also the -outputUsage() method, which must be maintained in parallel with this // Tally up usage across the simulation SLiMMemoryUsage_Community usage_community; SLiMMemoryUsage_Species usage_all_species; EIDOS_BZERO(&usage_all_species, sizeof(SLiMMemoryUsage_Species)); TabulateSLiMMemoryUsage_Community(&usage_community, &p_interpreter.SymbolTable()); for (Species *species : all_species_) { SLiMMemoryUsage_Species usage_one_species; species->TabulateSLiMMemoryUsage_Species(&usage_one_species); AccumulateMemoryUsageIntoTotal_Species(usage_one_species, usage_all_species); } size_t usage = usage_community.totalMemoryUsage + usage_all_species.totalMemoryUsage; double usage_MB = usage / (1024.0 * 1024.0); EidosValue_SP result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(usage_MB)); return result_SP; } // // Community_Class // #pragma mark - #pragma mark Community_Class #pragma mark - EidosClass *gSLiM_Community_Class = nullptr; const std::vector *Community_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("Community_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_allGenomicElementTypes, true, kEidosValueMaskObject, gSLiM_GenomicElementType_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_allInteractionTypes, true, kEidosValueMaskObject, gSLiM_InteractionType_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_allMutationTypes, true, kEidosValueMaskObject, gSLiM_MutationType_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_allScriptBlocks, true, kEidosValueMaskObject, gSLiM_SLiMEidosBlock_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_allSpecies, true, kEidosValueMaskObject, gSLiM_Species_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_allSubpopulations, true, kEidosValueMaskObject, gSLiM_Subpopulation_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_logFiles, true, kEidosValueMaskObject, gSLiM_LogFile_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_modelType, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tick, false, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_cycleStage, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_verbosity, false, kEidosValueMaskInt | kEidosValueMaskSingleton))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *Community_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("Community_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_createLogFile, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_LogFile_Class))->AddString_S(gEidosStr_filePath)->AddString_ON("initialContents", gStaticEidosValueNULL)->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddLogical_OS("compress", gStaticEidosValue_LogicalF)->AddString_OS("sep", gStaticEidosValue_StringComma)->AddInt_OSN("logInterval", gStaticEidosValueNULL)->AddInt_OSN("flushInterval", gStaticEidosValueNULL)->AddLogical_OS("header", gStaticEidosValue_LogicalT)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_estimatedLastTick, kEidosValueMaskInt | kEidosValueMaskSingleton))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_deregisterScriptBlock, kEidosValueMaskVOID))->AddIntObject("scriptBlocks", gSLiM_SLiMEidosBlock_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_genomicElementTypesWithIDs, kEidosValueMaskObject, gSLiM_GenomicElementType_Class))->AddInt("ids")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_interactionTypesWithIDs, kEidosValueMaskObject, gSLiM_InteractionType_Class))->AddInt("ids")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_mutationTypesWithIDs, kEidosValueMaskObject, gSLiM_MutationType_Class))->AddInt("ids")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_outputUsage, kEidosValueMaskVOID))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_registerFirstEvent, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_SLiMEidosBlock_Class))->AddIntString_SN("id")->AddString_S(gEidosStr_source)->AddInt_OSN("start", gStaticEidosValueNULL)->AddInt_OSN("end", gStaticEidosValueNULL)->AddObject_OSN("ticksSpec", gSLiM_Species_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_registerEarlyEvent, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_SLiMEidosBlock_Class))->AddIntString_SN("id")->AddString_S(gEidosStr_source)->AddInt_OSN("start", gStaticEidosValueNULL)->AddInt_OSN("end", gStaticEidosValueNULL)->AddObject_OSN("ticksSpec", gSLiM_Species_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_registerLateEvent, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_SLiMEidosBlock_Class))->AddIntString_SN("id")->AddString_S(gEidosStr_source)->AddInt_OSN("start", gStaticEidosValueNULL)->AddInt_OSN("end", gStaticEidosValueNULL)->AddObject_OSN("ticksSpec", gSLiM_Species_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_registerInteractionCallback, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_SLiMEidosBlock_Class))->AddIntString_SN("id")->AddString_S(gEidosStr_source)->AddIntObject_S("intType", gSLiM_InteractionType_Class)->AddIntObject_OSN("subpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)->AddInt_OSN("start", gStaticEidosValueNULL)->AddInt_OSN("end", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_rescheduleScriptBlock, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_SLiMEidosBlock_Class))->AddIntObject_S("block", gSLiM_SLiMEidosBlock_Class)->AddInt_OSN("start", gStaticEidosValueNULL)->AddInt_OSN("end", gStaticEidosValueNULL)->AddInt_ON("ticks", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_scriptBlocksWithIDs, kEidosValueMaskObject, gSLiM_SLiMEidosBlock_Class))->AddInt("ids")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_simulationFinished, kEidosValueMaskVOID))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_speciesWithIDs, kEidosValueMaskObject, gSLiM_Species_Class))->AddInt("ids")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_subpopulationsWithIDs, kEidosValueMaskObject, gSLiM_Subpopulation_Class))->AddInt("ids")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_subpopulationsWithNames, kEidosValueMaskObject, gSLiM_Subpopulation_Class))->AddString("names")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gEidosStr_usage, kEidosValueMaskFloat | kEidosValueMaskSingleton))); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: core/core.pro ================================================ #------------------------------------------------- # # Project created by QtCreator 2019-07-10T21:52:57 # #------------------------------------------------- QT -= core gui TEMPLATE = lib CONFIG += staticlib # Uncomment this line for a production build, to build for both Intel and Apple Silicon. This only works with Qt6; # Qt5 for macOS is built for Intel only. Uncomment this for all components or you will get link errors. #QMAKE_APPLE_DEVICE_ARCHS = x86_64 arm64 # Uncomment the lines below to enable ASAN (Address Sanitizer), for debugging of memory issues, in every # .pro file project-wide. See https://clang.llvm.org/docs/AddressSanitizer.html for discussion of ASAN # Also set the ASAN_OPTIONS env. variable, in the Run Settings section of the Project tab in Qt Creator, to # strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 # This also enables undefined behavior sanitizing, in conjunction with ASAN, because why not. #CONFIG += sanitizer sanitize_address sanitize_undefined # BCH 4/8/2026: I force AVX2 and FMA on for x86_64 builds (true since 2013), and NEON on for ARM64 builds # (true since forever), without the compiler capability testing done in CMakeFlags.txt. This is a hack; # if it breaks things for an end user they can build with CMake instead or disable these defines. Note # that building in Qt Creator is not the primary supported build method for SLiM, and is probably mostly # used only by me; I just want this on for development. See https://github.com/MesserLab/SLiM/issues/598. # Note that these settings are set in eidos.pro, core.pro, and QtSLiM.pro. message("Target architecture is: $${QMAKE_TARGET.arch}") QMAKE_CFLAGS += -Xarch_x86_64 -mavx2 -Xarch_x86_64 -mfma QMAKE_CXXFLAGS += -Xarch_x86_64 -mavx2 -Xarch_x86_64 -mfma QMAKE_CFLAGS += -Xarch_x86_64 -DEIDOS_HAS_AVX2=1 -Xarch_x86_64 -DEIDOS_HAS_FMA=1 QMAKE_CXXFLAGS += -Xarch_x86_64 -DEIDOS_HAS_AVX2=1 -Xarch_x86_64 -DEIDOS_HAS_FMA=1 QMAKE_CFLAGS += -Xarch_arm64 -DEIDOS_HAS_NEON=1 QMAKE_CXXFLAGS += -Xarch_arm64 -DEIDOS_HAS_NEON=1 # Set up to build QtSLiM; note that these settings are set in eidos.pro, core.pro, and QtSLiM.pro DEFINES += EIDOS_GUI DEFINES += SLIMGUI=1 CONFIG -= qt CONFIG += c++11 CONFIG += c11 QMAKE_CFLAGS += -std=c11 QMAKE_CFLAGS_DEBUG += -g -Og -DDEBUG=1 -DSLIMPROFILING=0 QMAKE_CFLAGS_RELEASE += -O3 -DSLIMPROFILING=1 QMAKE_CXXFLAGS_DEBUG += -g -Og -DDEBUG=1 -DSLIMPROFILING=0 QMAKE_CXXFLAGS_RELEASE += -O3 -DSLIMPROFILING=1 # get rid of spurious errors on Ubuntu, for now linux-*: { QMAKE_CXXFLAGS += -Wno-unknown-pragmas -Wno-attributes -Wno-unused-parameter } # prevent link dependency cycles QMAKE_LFLAGS += $$QMAKE_LFLAGS_NOUNDEF # eidos library dependency win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../eidos/release/ -leidos else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../eidos/debug/ -leidos else:unix: LIBS += -L$$OUT_PWD/../eidos/ -leidos INCLUDEPATH += $$PWD/../eidos DEPENDPATH += $$PWD/../eidos win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../eidos/release/libeidos.a else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../eidos/debug/libeidos.a else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../eidos/release/eidos.lib else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../eidos/debug/eidos.lib else:unix: PRE_TARGETDEPS += $$OUT_PWD/../eidos/libeidos.a # gsl library dependency win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../gsl/release/ -lgsl else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../gsl/debug/ -lgsl else:unix: LIBS += -L$$OUT_PWD/../gsl/ -lgsl INCLUDEPATH += $$PWD/../gsl $$PWD/../gsl/blas $$PWD/../gsl/block $$PWD/../gsl/cblas $$PWD/../gsl/cdf INCLUDEPATH += $$PWD/../gsl/complex $$PWD/../gsl/err $$PWD/../gsl/interpolation $$PWD/../gsl/linalg $$PWD/../gsl/matrix INCLUDEPATH += $$PWD/../gsl/permutation $$PWD/../gsl/randist $$PWD/../gsl/rng $$PWD/../gsl/specfunc $$PWD/../gsl/sys INCLUDEPATH += $$PWD/../gsl/vector DEPENDPATH += $$PWD/../gsl win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../gsl/release/libgsl.a else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../gsl/debug/libgsl.a else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../gsl/release/gsl.lib else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../gsl/debug/gsl.lib else:unix: PRE_TARGETDEPS += $$OUT_PWD/../gsl/libgsl.a # tskit library dependency win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../treerec/tskit/release/ -ltskit else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../treerec/tskit/debug/ -ltskit else:unix: LIBS += -L$$OUT_PWD/../treerec/tskit/ -ltskit INCLUDEPATH += $$PWD/../treerec/tskit $$PWD/../treerec $$PWD/../treerec/tskit/kastore DEPENDPATH += $$PWD/../treerec/tskit win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../treerec/tskit/release/libtskit.a else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../treerec/tskit/debug/libtskit.a else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../treerec/tskit/release/tskit.lib else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../treerec/tskit/debug/tskit.lib else:unix: PRE_TARGETDEPS += $$OUT_PWD/../treerec/tskit/libtskit.a SOURCES += \ chromosome.cpp \ community.cpp \ community_eidos.cpp \ genomic_element_type.cpp \ genomic_element.cpp \ haplosome.cpp \ individual.cpp \ interaction_type.cpp \ log_file.cpp \ mutation_run.cpp \ mutation_type.cpp \ mutation.cpp \ polymorphism.cpp \ population.cpp \ slim_eidos_block.cpp \ slim_functions.cpp \ slim_globals.cpp \ slim_test.cpp \ slim_test_core.cpp \ slim_test_genetics.cpp \ slim_test_other.cpp \ sparse_vector.cpp \ spatial_kernel.cpp \ spatial_map.cpp \ species.cpp \ species_eidos.cpp \ subpopulation.cpp \ substitution.cpp HEADERS += \ chromosome.h \ community.h \ genomic_element_type.h \ genomic_element.h \ haplosome.h \ individual.h \ interaction_type.h \ log_file.h \ mutation_run.h \ mutation_type.h \ mutation.h \ polymorphism.h \ population.h \ slim_eidos_block.h \ slim_functions.h \ slim_globals.h \ slim_test.h \ sparse_vector.h \ spatial_kernel.h \ spatial_map.h \ species.h \ subpopulation.h \ substitution.h ================================================ FILE: core/genomic_element.cpp ================================================ // // genomic_element.cpp // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "genomic_element.h" #include "slim_globals.h" #include "species.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include #include #include #pragma mark - #pragma mark GenomicElement #pragma mark - GenomicElement::GenomicElement(GenomicElementType *p_genomic_element_type_ptr, slim_position_t p_start_position, slim_position_t p_end_position) : genomic_element_type_ptr_(p_genomic_element_type_ptr), start_position_(p_start_position), end_position_(p_end_position) { } // This is unused except by debugging code and in the debugger itself std::ostream &operator<<(std::ostream &p_outstream, const GenomicElement &p_genomic_element) { p_outstream << "GenomicElement{genomic_element_type_ g" << p_genomic_element.genomic_element_type_ptr_->genomic_element_type_id_ << ", start_position_ " << p_genomic_element.start_position_ << ", end_position_ " << p_genomic_element.end_position_ << "}"; return p_outstream; } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - void GenomicElement::GenerateCachedEidosValue(void) { // Note that this cache cannot be invalidated as long as a symbol table might exist that this value has been placed into self_value_ = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_GenomicElement_Class)); } const EidosClass *GenomicElement::Class(void) const { return gSLiM_GenomicElement_Class; } void GenomicElement::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay(); // standard EidosObject behavior (not Dictionary behavior) } EidosValue_SP GenomicElement::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_genomicElementType: // ACCELERATED return genomic_element_type_ptr_->SymbolTableEntry().second; case gID_startPosition: // ACCELERATED return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(start_position_)); case gID_endPosition: // ACCELERATED return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(end_position_)); // variables case gID_tag: // ACCELERATED { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (GenomicElement::GetProperty): property tag accessed on genomic element before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } // all others, including gID_none default: return super::GetProperty(p_property_id); } } EidosValue *GenomicElement::GetProperty_Accelerated_startPosition(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { GenomicElement *value = (GenomicElement *)(p_values[value_index]); int_result->set_int_no_check(value->start_position_, value_index); } return int_result; } EidosValue *GenomicElement::GetProperty_Accelerated_endPosition(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { GenomicElement *value = (GenomicElement *)(p_values[value_index]); int_result->set_int_no_check(value->end_position_, value_index); } return int_result; } EidosValue *GenomicElement::GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { GenomicElement *value = (GenomicElement *)(p_values[value_index]); slim_usertag_t tag_value = value->tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (GenomicElement::GetProperty_Accelerated_tag): property tag accessed on genomic element before being set." << EidosTerminate(); int_result->set_int_no_check(tag_value, value_index); } return int_result; } EidosValue *GenomicElement::GetProperty_Accelerated_genomicElementType(EidosObject **p_values, size_t p_values_size) { EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_GenomicElementType_Class))->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { GenomicElement *value = (GenomicElement *)(p_values[value_index]); object_result->set_object_element_no_check_NORR(value->genomic_element_type_ptr_, value_index); } return object_result; } void GenomicElement::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { switch (p_property_id) { case gID_tag: { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; return; } default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP GenomicElement::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_setGenomicElementType: return ExecuteMethod_setGenomicElementType(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* - (void)setGenomicElementType(io$ genomicElementType) // EidosValue_SP GenomicElement::ExecuteMethod_setGenomicElementType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *genomicElementType_value = p_arguments[0].get(); Species &species = genomic_element_type_ptr_->species_; GenomicElementType *getype_ptr = SLiM_ExtractGenomicElementTypeFromEidosValue_io(genomicElementType_value, 0, &species.community_, &species, "setGenomicElementType()"); // SPECIES CONSISTENCY CHECK genomic_element_type_ptr_ = getype_ptr; return gStaticEidosValueVOID; } // // GenomicElement_Class // #pragma mark - #pragma mark GenomicElement_Class #pragma mark - EidosClass *gSLiM_GenomicElement_Class = nullptr; const std::vector *GenomicElement_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("GenomicElement_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_genomicElementType, true, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_GenomicElementType_Class))->DeclareAcceleratedGet(GenomicElement::GetProperty_Accelerated_genomicElementType)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_startPosition, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(GenomicElement::GetProperty_Accelerated_startPosition)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_endPosition, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(GenomicElement::GetProperty_Accelerated_endPosition)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(GenomicElement::GetProperty_Accelerated_tag)); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *GenomicElement_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("GenomicElement_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setGenomicElementType, kEidosValueMaskVOID))->AddIntObject_S("genomicElementType", gSLiM_GenomicElementType_Class)); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: core/genomic_element.h ================================================ // // genomic_element.h // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class GenomicElement represents a portion of a chromosome with particular properties. A genomic element is defined by its type, which might represent introns versus extrons for example, and the start and end positions of the element on the chromosome. */ #ifndef __SLiM__genomic_element__ #define __SLiM__genomic_element__ #include #include "genomic_element_type.h" #include "eidos_value.h" extern EidosClass *gSLiM_GenomicElement_Class; class GenomicElement : public EidosObject { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosObject super; public: EidosValue_SP self_value_; // cached EidosValue object for speed GenomicElementType *genomic_element_type_ptr_; // pointer to the type of genomic element this is slim_position_t start_position_; // the start position of the element slim_position_t end_position_; // the end position of the element slim_usertag_t tag_value_ = SLIM_TAG_UNSET_VALUE; // a user-defined tag value GenomicElement(const GenomicElement &p_original) = delete; // no copying GenomicElement& operator= (const GenomicElement &p_original) = delete; // no copying GenomicElement(void) = delete; // no null constructor GenomicElement(GenomicElementType *p_genomic_element_type_ptr, slim_position_t p_start_position, slim_position_t p_end_position); // // Eidos support // void GenerateCachedEidosValue(void); inline __attribute__((always_inline)) EidosValue_SP CachedEidosValue(void) { if (!self_value_) GenerateCachedEidosValue(); return self_value_; }; virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_setGenomicElementType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); // Accelerated property access; see class EidosObject for comments on this mechanism static EidosValue *GetProperty_Accelerated_startPosition(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_endPosition(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_genomicElementType(EidosObject **p_values, size_t p_values_size); }; // support stream output of GenomicElement, for debugging std::ostream &operator<<(std::ostream &p_outstream, const GenomicElement &p_genomic_element); class GenomicElement_Class : public EidosClass { private: typedef EidosClass super; public: GenomicElement_Class(const GenomicElement_Class &p_original) = delete; // no copy-construct GenomicElement_Class& operator=(const GenomicElement_Class&) = delete; // no copying inline GenomicElement_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif /* defined(__SLiM__genomic_element__) */ ================================================ FILE: core/genomic_element_type.cpp ================================================ // // genomic_element_type.cpp // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "genomic_element_type.h" #include "slim_globals.h" #include "community.h" #include "species.h" #include "mutation_type.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include #include #include #pragma mark - #pragma mark GenomicElementType #pragma mark - GenomicElementType::GenomicElementType(Species &p_species, slim_objectid_t p_genomic_element_type_id, std::vector p_mutation_type_ptrs, std::vector p_mutation_fractions) : self_symbol_(EidosStringRegistry::GlobalStringIDForString(SLiMEidosScript::IDStringWithPrefix('g', p_genomic_element_type_id)), EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_GenomicElementType_Class))), species_(p_species), genomic_element_type_id_(p_genomic_element_type_id), mutation_type_ptrs_(std::move(p_mutation_type_ptrs)), mutation_fractions_(std::move(p_mutation_fractions)), mutation_matrix_(nullptr) { // self_symbol_ is always a constant, but can't be marked as such on construction self_symbol_.second->MarkAsConstant(); InitializeDraws(); } GenomicElementType::~GenomicElementType(void) { //EIDOS_ERRSTREAM << "GenomicElementType::~GenomicElementType" << std::endl; if (lookup_mutation_type_) { gsl_ran_discrete_free(lookup_mutation_type_); lookup_mutation_type_ = nullptr; } if (mm_thresholds) { free(mm_thresholds); mm_thresholds = nullptr; } } void GenomicElementType::InitializeDraws(void) { size_t mutation_type_count = mutation_type_ptrs_.size(); if (mutation_type_count != mutation_fractions_.size()) EIDOS_TERMINATION << "ERROR (GenomicElementType::InitializeDraws): mutation types and fractions have different sizes." << EidosTerminate(); if (lookup_mutation_type_) { gsl_ran_discrete_free(lookup_mutation_type_); lookup_mutation_type_ = nullptr; } // We allow an empty mutation type vector initially, because people might want to add mutation types in script. // However, if DrawMutationType() is called and our vector is still empty, that will be an error. if (mutation_type_count) { // Prepare to randomly draw mutation types std::vector A(mutation_type_count); bool nonzero_seen = false; for (unsigned int i = 0; i < mutation_type_count; i++) { double fraction = mutation_fractions_[i]; if (fraction > 0.0) nonzero_seen = true; A[i] = fraction; } // A mutation type vector with all zero proportions is treated the same as an empty vector: we allow it // on the assumption that it will be fixed later, but if it isn't, that will be an error. if (nonzero_seen) lookup_mutation_type_ = gsl_ran_discrete_preproc(mutation_type_count, A.data()); } } MutationType *GenomicElementType::DrawMutationType(void) const { if (!lookup_mutation_type_) EIDOS_TERMINATION << "ERROR (GenomicElementType::DrawMutationType): empty mutation type vector for genomic element type." << EidosTerminate(); gsl_rng *rng_gsl = EIDOS_GSL_RNG(omp_get_thread_num()); return mutation_type_ptrs_[gsl_ran_discrete(rng_gsl, lookup_mutation_type_)]; } void GenomicElementType::SetNucleotideMutationMatrix(const EidosValue_Float_SP &p_mutation_matrix) { mutation_matrix_ = p_mutation_matrix; // integrity checks if (mutation_matrix_->DimensionCount() != 2) EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): initializeGenomicElementType() requires mutationMatrix to be a 4x4 or 64x4 matrix." << EidosTerminate(); const int64_t *dims = mutation_matrix_->Dimensions(); if ((dims[0] == 4) && (dims[1] == 4)) { // This is the 4x4 matrix case, providing rates for each original nucleotide (rows) to each derived nucleotide (cols) // check for zeros in the necessary positions static int required_zeros_4x4[4] = {0, 5, 10, 15}; for (int required_zeros : required_zeros_4x4) if (mutation_matrix_->FloatAtIndex_NOCAST(required_zeros, nullptr) != 0.0) EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): the mutationMatrix must contain 0.0 for all entries that correspond to a nucleotide mutating to itself." << EidosTerminate(); // check that each row sums to <= 1.0; in fact this has to be <= 1.0 even when multiplied by the hotspot map, but this is a preliminary sanity check // check also for no negative values, and for all values being finite for (int row = 0; row < 4; ++row) { double row_1 = mutation_matrix_->FloatAtIndex_NOCAST(row, nullptr); double row_2 = mutation_matrix_->FloatAtIndex_NOCAST(row + 4, nullptr); double row_3 = mutation_matrix_->FloatAtIndex_NOCAST(row + 8, nullptr); double row_4 = mutation_matrix_->FloatAtIndex_NOCAST(row + 12, nullptr); if ((row_1 < 0.0) || (row_2 < 0.0) || (row_3 < 0.0) || (row_4 < 0.0) || !std::isfinite(row_1) || !std::isfinite(row_2) || !std::isfinite(row_3) || !std::isfinite(row_4)) EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): initializeGenomicElementType() requires all mutation matrix values to be finite and >= 0.0." << EidosTerminate(); if (row_1 + row_2 + row_3 + row_4 > 1.0) EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): initializeGenomicElementType() requires the sum of each mutation matrix row (the total probability of mutating for the given nucleotide or trinucleotide) to be <= 1.0." << EidosTerminate(); } } else if ((dims[0] == 64) && (dims[1] == 4)) { // This is the 64x4 matrix case, providing rates for each original trinucleotide (rows) to each derived nucleotide (cols) // check for zeros in the necessary positions static int required_zeros_64x4[64] = {0, 1, 2, 3, 16, 17, 18, 19, 32, 33, 34, 35, 48, 49, 50, 51, 68, 69, 70, 71, 84, 85, 86, 87, 100, 101, 102, 103, 116, 117, 118, 119, 136, 137, 138, 139, 152, 153, 154, 155, 168, 169, 170, 171, 184, 185, 186, 187, 204, 205, 206, 207, 220, 221, 222, 223, 236, 237, 238, 239, 252, 253, 254, 255}; for (int required_zeros : required_zeros_64x4) if (mutation_matrix_->FloatAtIndex_NOCAST(required_zeros, nullptr) != 0.0) EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): the mutationMatrix must contain 0.0 for all entries that correspond to a nucleotide mutating to itself." << EidosTerminate(); // check that each row sums to <= 1.0; in fact this has to be <= 1.0 even when multiplied by the hotspot map, but this is a preliminary sanity check // check also for no negative values for (int row = 0; row < 64; ++row) { double row_1 = mutation_matrix_->FloatAtIndex_NOCAST(row, nullptr); double row_2 = mutation_matrix_->FloatAtIndex_NOCAST(row + 64, nullptr); double row_3 = mutation_matrix_->FloatAtIndex_NOCAST(row + 128, nullptr); double row_4 = mutation_matrix_->FloatAtIndex_NOCAST(row + 192, nullptr); if ((row_1 < 0.0) || (row_2 < 0.0) || (row_3 < 0.0) || (row_4 < 0.0) || !std::isfinite(row_1) || !std::isfinite(row_2) || !std::isfinite(row_3) || !std::isfinite(row_4)) EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): initializeGenomicElementType() requires all mutation matrix values to be finite and >= 0.0." << EidosTerminate(); if (row_1 + row_2 + row_3 + row_4 > 1.0) EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): initializeGenomicElementType() requires the sum of each mutation matrix row (the total probability of mutating for the given nucleotide or trinucleotide) to be <= 1.0." << EidosTerminate(); } } else EIDOS_TERMINATION << "ERROR (GenomicElementType::SetNucleotideMutationMatrix): initializeGenomicElementType() requires mutationMatrix to be a 4x4 or 64x4 matrix." << EidosTerminate(); } // This is unused except by debugging code and in the debugger itself std::ostream &operator<<(std::ostream &p_outstream, const GenomicElementType &p_genomic_element_type) { p_outstream << "GenomicElementType{mutation_types_ "; if (p_genomic_element_type.mutation_type_ptrs_.size() == 0) { p_outstream << "*"; } else { p_outstream << "<"; for (unsigned int i = 0; i < p_genomic_element_type.mutation_type_ptrs_.size(); ++i) { p_outstream << p_genomic_element_type.mutation_type_ptrs_[i]->mutation_type_id_; if (i < p_genomic_element_type.mutation_type_ptrs_.size() - 1) p_outstream << " "; } p_outstream << ">"; } p_outstream << ", mutation_fractions_ "; if (p_genomic_element_type.mutation_fractions_.size() == 0) { p_outstream << "*"; } else { p_outstream << "<"; for (unsigned int i = 0; i < p_genomic_element_type.mutation_fractions_.size(); ++i) { p_outstream << p_genomic_element_type.mutation_fractions_[i]; if (i < p_genomic_element_type.mutation_fractions_.size() - 1) p_outstream << " "; } p_outstream << ">"; } p_outstream << "}"; return p_outstream; } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - const EidosClass *GenomicElementType::Class(void) const { return gSLiM_GenomicElementType_Class; } void GenomicElementType::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay() << ""; } EidosValue_SP GenomicElementType::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_id: // ACCELERATED { if (!cached_value_getype_id_) cached_value_getype_id_ = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(genomic_element_type_id_)); return cached_value_getype_id_; } case gID_mutationTypes: { EidosValue_Object *vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_MutationType_Class); EidosValue_SP result_SP = EidosValue_SP(vec); for (auto mut_type : mutation_type_ptrs_) vec->push_object_element_NORR(mut_type); return result_SP; } case gID_mutationFractions: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(mutation_fractions_)); } case gID_mutationMatrix: { if (!mutation_matrix_) EIDOS_TERMINATION << "ERROR (GenomicElementType::GetProperty): property mutationMatrix is only defined in nucleotide-based models." << EidosTerminate(); return mutation_matrix_; } case gID_species: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(&species_, gSLiM_Species_Class)); } // variables case gEidosID_color: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(color_)); case gID_tag: // ACCELERATED { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (GenomicElementType::GetProperty): property tag accessed on genomic element type before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } // all others, including gID_none default: return super::GetProperty(p_property_id); } } EidosValue *GenomicElementType::GetProperty_Accelerated_id(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { GenomicElementType *value = (GenomicElementType *)(p_values[value_index]); int_result->set_int_no_check(value->genomic_element_type_id_, value_index); } return int_result; } EidosValue *GenomicElementType::GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { GenomicElementType *value = (GenomicElementType *)(p_values[value_index]); slim_usertag_t tag_value = value->tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (GenomicElementType::GetProperty): property tag accessed on genomic element type before being set." << EidosTerminate(); int_result->set_int_no_check(tag_value, value_index); } return int_result; } void GenomicElementType::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { switch (p_property_id) { case gEidosID_color: { color_ = p_value.StringAtIndex_NOCAST(0, nullptr); if (!color_.empty()) Eidos_GetColorComponents(color_, &color_red_, &color_green_, &color_blue_); // tweak a flag to make SLiMgui update species_.community_.genomic_element_types_changed_ = true; return; } case gID_tag: { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; return; } default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP GenomicElementType::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_setMutationFractions: return ExecuteMethod_setMutationFractions(p_method_id, p_arguments, p_interpreter); case gID_setMutationMatrix: return ExecuteMethod_setMutationMatrix(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* - (void)setMutationFractions(io mutationTypes, numeric proportions) // EidosValue_SP GenomicElementType::ExecuteMethod_setMutationFractions(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *mutationTypes_value = p_arguments[0].get(); EidosValue *proportions_value = p_arguments[1].get(); int mut_type_id_count = mutationTypes_value->Count(); int proportion_count = proportions_value->Count(); if (mut_type_id_count != proportion_count) EIDOS_TERMINATION << "ERROR (GenomicElementType::ExecuteMethod_setMutationFractions): setMutationFractions() requires the sizes of mutationTypes and proportions to be equal." << EidosTerminate(); std::vector mutation_types; std::vector mutation_fractions; for (int mut_type_index = 0; mut_type_index < mut_type_id_count; ++mut_type_index) { MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutationTypes_value, mut_type_index, &species_.community_, &species_, "setMutationFractions()"); // SPECIES CONSISTENCY CHECK double proportion = proportions_value->NumericAtIndex_NOCAST(mut_type_index, nullptr); if ((proportion < 0) || !std::isfinite(proportion)) // == 0 is allowed but must be fixed before the simulation executes; see InitializeDraws() EIDOS_TERMINATION << "ERROR (GenomicElementType::ExecuteMethod_setMutationFractions): setMutationFractions() proportions must be greater than or equal to zero (" << EidosStringForFloat(proportion) << " supplied)." << EidosTerminate(); if (std::find(mutation_types.begin(), mutation_types.end(), mutation_type_ptr) != mutation_types.end()) EIDOS_TERMINATION << "ERROR (GenomicElementType::ExecuteMethod_setMutationFractions): setMutationFractions() mutation type m" << mutation_type_ptr->mutation_type_id_ << " used more than once." << EidosTerminate(); mutation_types.emplace_back(mutation_type_ptr); mutation_fractions.emplace_back(proportion); // check whether we are now using a mutation type that is non-neutral; check and set pure_neutral_ if ((mutation_type_ptr->dfe_type_ != DFEType::kFixed) || (mutation_type_ptr->dfe_parameters_[0] != 0.0)) species_.pure_neutral_ = false; } // Everything seems to be in order, so replace our mutation info with the new info mutation_type_ptrs_ = mutation_types; mutation_fractions_ = mutation_fractions; // Reinitialize our mutation type lookup based on the new info InitializeDraws(); // Notify interested parties of the change species_.community_.genomic_element_types_changed_ = true; return gStaticEidosValueVOID; } // ********************* - (void)setMutationMatrix(float mutationMatrix) // EidosValue_SP GenomicElementType::ExecuteMethod_setMutationMatrix(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (!species_.IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (GenomicElementType::ExecuteMethod_setMutationMatrix): setMutationMatrix() may only be called in nucleotide-based models." << EidosTerminate(); EidosValue *mutationMatrix_value = p_arguments[0].get(); SetNucleotideMutationMatrix(EidosValue_Float_SP((EidosValue_Float *)(mutationMatrix_value))); // the change to the mutation matrix means everything downstream has to be recached species_.MaxNucleotideMutationRateChanged(); return gStaticEidosValueVOID; } // // GenomicElementType_Class // #pragma mark - #pragma mark GenomicElementType_Class #pragma mark - EidosClass *gSLiM_GenomicElementType_Class = nullptr; const std::vector *GenomicElementType_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("GenomicElementType_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_id, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(GenomicElementType::GetProperty_Accelerated_id)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationTypes, true, kEidosValueMaskObject, gSLiM_MutationType_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationFractions, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationMatrix, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_species, true, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Species_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(GenomicElementType::GetProperty_Accelerated_tag)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_color, false, kEidosValueMaskString | kEidosValueMaskSingleton))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *GenomicElementType_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("GenomicElementType_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setMutationFractions, kEidosValueMaskVOID))->AddIntObject("mutationTypes", gSLiM_MutationType_Class)->AddNumeric("proportions")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setMutationMatrix, kEidosValueMaskVOID))->AddFloat("mutationMatrix")); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } ================================================ FILE: core/genomic_element_type.h ================================================ // // genomic_element_type.h // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class GenomicElementType represents a possible type of genomic element, defined by the types of mutations the element undergoes, and the relative fractions of each of those mutation types. Exons and introns might be represented by different genomic element types, for example, and might have different types of mutations (exons undergo adaptive mutations while introns do not, perhaps). At present, these mutational dynamics are the only defining characteristics of genomic elements. */ #ifndef __SLiM__genomic_element_type__ #define __SLiM__genomic_element_type__ #include #include #include "slim_globals.h" #include "eidos_rng.h" #include "mutation_type.h" #include "eidos_value.h" class Species; extern EidosClass *gSLiM_GenomicElementType_Class; class GenomicElementType : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; private: gsl_ran_discrete_t *lookup_mutation_type_ = nullptr; // OWNED POINTER: a lookup table for getting a mutation type for this genomic element EidosSymbolTableEntry self_symbol_; // for fast setup of the symbol table public: Species &species_; slim_objectid_t genomic_element_type_id_; // the id by which this genomic element type is indexed in the chromosome EidosValue_SP cached_value_getype_id_; // a cached value for genomic_element_type_id_; reset() if that changes std::vector mutation_type_ptrs_; // mutation types identifiers in this element std::vector mutation_fractions_; // relative fractions of each mutation type std::string color_; // color to use when displayed (in SLiMgui) float color_red_, color_green_, color_blue_; // cached color components from color_; should always be in sync slim_usertag_t tag_value_ = SLIM_TAG_UNSET_VALUE; // a user-defined tag value EidosValue_Float_SP mutation_matrix_; // in nucleotide-based models only, the 4x4 or 64x4 float mutation matrix double *mm_thresholds = nullptr; // mutation matrix threshold values for determining derived nucleotides; cached in CacheNucleotideMatrices() GenomicElementType(const GenomicElementType&) = delete; // no copying GenomicElementType& operator=(const GenomicElementType&) = delete; // no copying GenomicElementType(void) = delete; // no null construction GenomicElementType(Species &p_species, slim_objectid_t p_genomic_element_type_id, std::vector p_mutation_type_ptrs, std::vector p_mutation_fractions); ~GenomicElementType(void); void InitializeDraws(void); // reinitialize our mutation-type lookup after changing our mutation type or proportions MutationType *DrawMutationType(void) const; // draw a mutation type from the distribution for this genomic element type void SetNucleotideMutationMatrix(const EidosValue_Float_SP &p_mutation_matrix); // // Eidos support // inline EidosSymbolTableEntry &SymbolTableEntry(void) { return self_symbol_; } virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_setMutationFractions(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setMutationMatrix(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); // Accelerated property access; see class EidosObject for comments on this mechanism static EidosValue *GetProperty_Accelerated_id(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size); }; // support stream output of GenomicElementType, for debugging std::ostream &operator<<(std::ostream &p_outstream, const GenomicElementType &p_genomic_element_type); class GenomicElementType_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: GenomicElementType_Class(const GenomicElementType_Class &p_original) = delete; // no copy-construct GenomicElementType_Class& operator=(const GenomicElementType_Class&) = delete; // no copying inline GenomicElementType_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif /* defined(__SLiM__genomic_element_type__) */ ================================================ FILE: core/haplosome.cpp ================================================ // // haplosome.cpp // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "haplosome.h" #include "slim_globals.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "community.h" #include "species.h" #include "polymorphism.h" #include "subpopulation.h" #include "eidos_sorting.h" #include #include #include #include #include #include #include #pragma mark - #pragma mark Haplosome #pragma mark - // Static class variables in support of Haplosome's bulk operation optimization; see Haplosome::WillModifyRunForBulkOperation() int64_t Haplosome::s_bulk_operation_id_ = 0; slim_mutrun_index_t Haplosome::s_bulk_operation_mutrun_index_ = -1; SLiMBulkOperationHashTable Haplosome::s_bulk_operation_runs_; Haplosome::~Haplosome(void) { if (mutruns_ != run_buffer_) free(mutruns_); mutruns_ = nullptr; mutrun_count_ = 0; } Chromosome *Haplosome::AssociatedChromosome(void) const { return individual_->subpopulation_->species_.Chromosomes()[chromosome_index_]; } // prints an error message, a stacktrace, and exits; called only for DEBUG void Haplosome::NullHaplosomeAccessError(void) const { EIDOS_TERMINATION << "ERROR (Haplosome::NullHaplosomeAccessError): (internal error) a null haplosome was accessed." << EidosTerminate(); } MutationRun *Haplosome::WillModifyRun(slim_mutrun_index_t p_run_index, MutationRunContext &p_mutrun_context) { #if DEBUG if (p_run_index >= mutrun_count_) EIDOS_TERMINATION << "ERROR (Haplosome::WillModifyRun): (internal error) attempt to modify an out-of-index run." << EidosTerminate(); #endif // This method used to support in-place modification for mutruns with a use count of 1, // saving the new mutation run allocation; this is now done only in WillModifyRun_UNSHARED(). // See the header comment for more information. const MutationRun *original_run = mutruns_[p_run_index]; MutationRun *new_run = MutationRun::NewMutationRun(p_mutrun_context); // take from shared pool of used objects new_run->copy_from_run(*original_run); mutruns_[p_run_index] = new_run; // We return a non-const pointer to the caller, giving them permission to modify this new run return new_run; } MutationRun *Haplosome::WillModifyRun_UNSHARED(slim_mutrun_index_t p_run_index, MutationRunContext &p_mutrun_context) { #if DEBUG if (p_run_index >= mutrun_count_) EIDOS_TERMINATION << "ERROR (Haplosome::WillModifyRun_UNSHARED): (internal error) attempt to modify an out-of-index run." << EidosTerminate(); #endif // This method avoids the new mutation run allocation, unless the mutation run is empty. // This is based on a guarantee from the caller that the run is unshared (unless it is empty). // See the header comment for more information. const MutationRun *original_run = mutruns_[p_run_index]; if (original_run->size() == 0) { MutationRun *new_run = MutationRun::NewMutationRun(p_mutrun_context); // take from shared pool of used objects new_run->copy_from_run(*original_run); mutruns_[p_run_index] = new_run; // We return a non-const pointer to the caller, giving them permission to modify this new run return new_run; } else { // We have been guaranteed by the caller that this mutation run is unshared, so we can cast away the const MutationRun *unlocked_run = const_cast(original_run); unlocked_run->will_modify_run(); // in-place modification of runs requires notification, for cache invalidation // We return a non-const pointer to the caller, giving them permission to modify this run return unlocked_run; } } void Haplosome::BulkOperationStart(int64_t p_operation_id, slim_mutrun_index_t p_mutrun_index) { THREAD_SAFETY_IN_ACTIVE_PARALLEL("Haplosome::BulkOperationStart(): s_bulk_operation_id_"); if (s_bulk_operation_id_ != 0) { //EIDOS_TERMINATION << "ERROR (Haplosome::BulkOperationStart): (internal error) unmatched bulk operation start." << EidosTerminate(); // It would be nice to be able to throw an exception here, but in the present design, the // bulk operation info can get messed up if the bulk operation throws an exception that // blows through the call to Haplosome::BulkOperationEnd(). // Note this warning is not suppressed by gEidosSuppressWarnings; that is deliberate std::cout << "WARNING (Haplosome::BulkOperationStart): (internal error) unmatched bulk operation start." << std::endl; // For now, we assume that the end call got blown through, and we close out the old operation. Haplosome::BulkOperationEnd(s_bulk_operation_id_, s_bulk_operation_mutrun_index_); } s_bulk_operation_id_ = p_operation_id; s_bulk_operation_mutrun_index_ = p_mutrun_index; } MutationRun *Haplosome::WillModifyRunForBulkOperation(int64_t p_operation_id, slim_mutrun_index_t p_mutrun_index, MutationRunContext &p_mutrun_context) { THREAD_SAFETY_IN_ACTIVE_PARALLEL("Haplosome::WillModifyRunForBulkOperation(): s_bulk_operation_id_"); if (p_mutrun_index != s_bulk_operation_mutrun_index_) EIDOS_TERMINATION << "ERROR (Haplosome::WillModifyRunForBulkOperation): (internal error) incorrect run index during bulk operation." << EidosTerminate(); if (p_mutrun_index >= mutrun_count_) EIDOS_TERMINATION << "ERROR (Haplosome::WillModifyRunForBulkOperation): (internal error) attempt to modify an out-of-index run." << EidosTerminate(); #if 0 #warning Haplosome::WillModifyRunForBulkOperation disabled... // The trivial version of this function just calls WillModifyRun(), // requesting that the caller perform the operation return WillModifyRun(p_run_index); #else // The interesting version remembers the operation in progress, using the ID, and // tracks original/final MutationRun pointers, returning nullptr if an original is matched. const MutationRun *original_run = mutruns_[p_mutrun_index]; if (p_operation_id != s_bulk_operation_id_) EIDOS_TERMINATION << "ERROR (Haplosome::WillModifyRunForBulkOperation): (internal error) missing bulk operation start." << EidosTerminate(); auto found_run_pair = s_bulk_operation_runs_.find(original_run); if (found_run_pair == s_bulk_operation_runs_.end()) { // This MutationRun is not in the map, so we need to set up a new entry MutationRun *product_run = MutationRun::NewMutationRun(p_mutrun_context); product_run->copy_from_run(*original_run); mutruns_[p_mutrun_index] = product_run; try { s_bulk_operation_runs_.emplace(original_run, product_run); } catch (...) { EIDOS_TERMINATION << "ERROR (Haplosome::WillModifyRunForBulkOperation): (internal error) SLiM encountered a raise from an internal hash table; please report this." << EidosTerminate(nullptr); } //std::cout << "WillModifyRunForBulkOperation() created product for " << original_run << std::endl; return product_run; } else { // This MutationRun is in the map, so we can just reuse it to redo the operation mutruns_[p_mutrun_index] = found_run_pair->second; //std::cout << " WillModifyRunForBulkOperation() substituted known product for " << original_run << std::endl; return nullptr; } #endif } void Haplosome::BulkOperationEnd(int64_t p_operation_id, slim_mutrun_index_t p_mutrun_index) { THREAD_SAFETY_IN_ACTIVE_PARALLEL("Haplosome::BulkOperationEnd(): s_bulk_operation_id_"); if ((p_operation_id == s_bulk_operation_id_) && (p_mutrun_index == s_bulk_operation_mutrun_index_)) { s_bulk_operation_runs_.clear(); s_bulk_operation_id_ = 0; s_bulk_operation_mutrun_index_ = -1; } else { EIDOS_TERMINATION << "ERROR (Haplosome::BulkOperationEnd): (internal error) unmatched bulk operation end." << EidosTerminate(); } } void Haplosome::TallyHaplosomeReferences_Checkback(slim_refcount_t *p_mutrun_ref_tally, slim_refcount_t *p_mutrun_tally, int64_t p_operation_id) { #if DEBUG if (mutrun_count_ == 0) NullHaplosomeAccessError(); #endif for (int run_index = 0; run_index < mutrun_count_; ++run_index) { if (mutruns_[run_index]->operation_id_ != p_operation_id) { (*p_mutrun_ref_tally) += mutruns_[run_index]->use_count(); (*p_mutrun_tally)++; mutruns_[run_index]->operation_id_ = p_operation_id; } } } void Haplosome::MakeNull(void) { if (mutrun_count_) { if (mutruns_ != run_buffer_) free(mutruns_); mutruns_ = nullptr; mutrun_count_ = 0; mutrun_length_ = 0; } } void Haplosome::ReinitializeHaplosomeToNull(Individual *individual) { // Transmogrify a haplosome (which might be null or non-null) into a null haplosome individual_ = individual; if (mutrun_count_) { // was a non-null haplosome, needs to become null if (mutruns_ != run_buffer_) free(mutruns_); mutruns_ = nullptr; // chromosome_index_ remains untouched; we still belong to the same chromosome mutrun_count_ = 0; mutrun_length_ = 0; } } void Haplosome::ReinitializeHaplosomeToNonNull(Individual *individual, Chromosome *p_chromosome) { // Transmogrify a haplosome (which might be null or non-null) into a non-null haplosome with a specific configuration individual_ = individual; #if DEBUG // We should always be reinitializing a haplosome that already belongs to the chromosome; // the only reason the chromosome is passed in is that it knows the mutrun configuration. if (chromosome_index_ != p_chromosome->Index()) EIDOS_TERMINATION << "ERROR (Haplosome::ReinitializeHaplosomeToNonNull): (internal error) incorrect chromosome index." << EidosTerminate(); #endif if (mutrun_count_ == 0) { // was a null haplosome, needs to become not null mutrun_count_ = p_chromosome->mutrun_count_; mutrun_length_ = p_chromosome->mutrun_length_; if (mutrun_count_ <= SLIM_HAPLOSOME_MUTRUN_BUFSIZE) { mutruns_ = run_buffer_; #if SLIM_CLEAR_HAPLOSOMES EIDOS_BZERO(run_buffer_, SLIM_HAPLOSOME_MUTRUN_BUFSIZE * sizeof(const MutationRun *)); #endif } else { #if SLIM_CLEAR_HAPLOSOMES mutruns_ = (const MutationRun **)calloc(mutrun_count_, sizeof(const MutationRun *)); #else mutruns_ = (const MutationRun **)malloc(mutrun_count_ * sizeof(const MutationRun *)); #endif } } else if (mutrun_count_ != p_chromosome->mutrun_count_) { // the number of mutruns has changed; need to reallocate if (mutruns_ != run_buffer_) free(mutruns_); mutrun_count_ = p_chromosome->mutrun_count_; mutrun_length_ = p_chromosome->mutrun_length_; if (mutrun_count_ <= SLIM_HAPLOSOME_MUTRUN_BUFSIZE) { mutruns_ = run_buffer_; #if SLIM_CLEAR_HAPLOSOMES EIDOS_BZERO(run_buffer_, SLIM_HAPLOSOME_MUTRUN_BUFSIZE * sizeof(const MutationRun *)); #endif } else { #if SLIM_CLEAR_HAPLOSOMES mutruns_ = (const MutationRun **)calloc(mutrun_count_, sizeof(const MutationRun *)); #else mutruns_ = (const MutationRun **)malloc(mutrun_count_ * sizeof(const MutationRun *)); #endif } } else { #if SLIM_CLEAR_HAPLOSOMES // the number of mutruns has not changed; need to zero out EIDOS_BZERO(mutruns_, mutrun_count_ * sizeof(const MutationRun *)); #endif } } void Haplosome::record_derived_states(Species *p_species) const { // This is called by Species::RecordAllDerivedStatesFromSLiM() to record all the derived states present // in a given haplosome that was just created by readFromPopulationFile() or some similar situation. It should // make calls to record the derived state at each position in the haplosome that has any mutation. Mutation *mut_block_ptr = gSLiM_Mutation_Block; THREAD_SAFETY_IN_ACTIVE_PARALLEL("Haplosome::record_derived_states(): usage of statics"); static std::vector record_vec; for (int run_index = 0; run_index < mutrun_count_; ++run_index) { const MutationRun *mutrun = mutruns_[run_index]; int mutrun_size = mutrun->size(); slim_position_t last_pos = -1; //record_vec.resize(0); // should always be left clear by the code below for (int mut_index = 0; mut_index < mutrun_size; ++mut_index) { MutationIndex mutation_index = (*mutrun)[mut_index]; Mutation *mutation = mut_block_ptr + mutation_index; slim_position_t mutation_pos = mutation->position_; if (mutation_pos != last_pos) { // New position, so we finish the previous derived-state block... if (last_pos != -1) { p_species->RecordNewDerivedState(this, last_pos, record_vec); record_vec.resize(0); } // ...and start a new derived-state block last_pos = mutation_pos; } record_vec.emplace_back(mutation); } // record the last derived block, if any if (last_pos != -1) { p_species->RecordNewDerivedState(this, last_pos, record_vec); record_vec.resize(0); } } } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - void Haplosome::GenerateCachedEidosValue(void) { // Note that this cache cannot be invalidated as long as a symbol table might exist that this value has been placed into self_value_ = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_Haplosome_Class)); } const EidosClass *Haplosome::Class(void) const { return gSLiM_Haplosome_Class; } void Haplosome::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay() << "<"; p_ostream << AssociatedChromosome()->Type(); if (mutrun_count_ == 0) p_ostream << ":null>"; else p_ostream << ":" << mutation_count() << ">"; } EidosValue_SP Haplosome::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_chromosome: { // We reach our chromosome through our individual; note this prevents standalone haplosome objects Chromosome *chromosome = AssociatedChromosome(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(chromosome, gSLiM_Chromosome_Class)); } case gID_chromosomeSubposition: // ACCELERATED { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(chromosome_subposition_)); } case gID_haplosomePedigreeID: // ACCELERATED { Species &species = individual_->subpopulation_->species_; if (!species.PedigreesEnabledByUser() && !species.RecordingTreeSequence()) EIDOS_TERMINATION << "ERROR (Haplosome::GetProperty): property haplosomePedigreeID is not available because neither pedigree recording nor tree-sequence recording has been enabled." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(haplosome_id_)); } case gID_individual: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(individual_, gSLiM_Individual_Class)); } case gID_isNullHaplosome: // ACCELERATED return ((mutrun_count_ == 0) ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); case gID_mutationCount: { if (IsDeferred()) EIDOS_TERMINATION << "ERROR (Haplosome::GetProperty): the mutations of deferred haplosomes cannot be accessed." << EidosTerminate(); int mut_count = mutation_count(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(mut_count)); } case gID_mutations: { if (IsDeferred()) EIDOS_TERMINATION << "ERROR (Haplosome::GetProperty): the mutations of deferred haplosomes cannot be accessed." << EidosTerminate(); Mutation *mut_block_ptr = gSLiM_Mutation_Block; int mut_count = mutation_count(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class))->resize_no_initialize_RR(mut_count); EidosValue_SP result_SP = EidosValue_SP(vec); int set_index = 0; for (int run_index = 0; run_index < mutrun_count_; ++run_index) { const MutationRun *mutrun = mutruns_[run_index]; const MutationIndex *mut_start_ptr = mutrun->begin_pointer_const(); const MutationIndex *mut_end_ptr = mutrun->end_pointer_const(); for (const MutationIndex *mut_ptr = mut_start_ptr; mut_ptr < mut_end_ptr; ++mut_ptr) vec->set_object_element_no_check_no_previous_RR(mut_block_ptr + *mut_ptr, set_index++); } return result_SP; } // variables case gID_tag: // ACCELERATED { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Haplosome::GetProperty): property tag accessed on haplosome before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } // all others, including gID_none default: return super::GetProperty(p_property_id); } } EidosValue *Haplosome::GetProperty_Accelerated_haplosomePedigreeID(EidosObject **p_values, size_t p_values_size) { Species *consensus_species = Community::SpeciesForHaplosomesVector((Haplosome **)p_values, (int)p_values_size); EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); if (p_values_size == 0) return int_result; if (consensus_species) { // check that pedigrees are enabled, once Species &species = ((Haplosome *)(p_values[0]))->individual_->subpopulation_->species_; if (!species.PedigreesEnabledByUser() && !species.RecordingTreeSequence()) EIDOS_TERMINATION << "ERROR (Haplosome::GetProperty): property haplosomePedigreeID is not available because neither pedigree recording nor tree-sequence recording has been enabled." << EidosTerminate(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Haplosome *value = (Haplosome *)(p_values[value_index]); int_result->set_int_no_check(value->haplosome_id_, value_index); } } else { // we have a mix of species, so we need to check that pedigree IDs are available for each individual for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Haplosome *value = (Haplosome *)(p_values[value_index]); Species &species = value->individual_->subpopulation_->species_; if (!species.PedigreesEnabledByUser() && !species.RecordingTreeSequence()) EIDOS_TERMINATION << "ERROR (Haplosome::GetProperty): property haplosomePedigreeID is not available because neither pedigree recording nor tree-sequence recording has been enabled." << EidosTerminate(); int_result->set_int_no_check(value->haplosome_id_, value_index); } } return int_result; } EidosValue *Haplosome::GetProperty_Accelerated_chromosomeSubposition(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Haplosome *value = (Haplosome *)(p_values[value_index]); int64_t subposition_value = (uint64_t)(value->chromosome_subposition_); int_result->set_int_no_check(subposition_value, value_index); } return int_result; } EidosValue *Haplosome::GetProperty_Accelerated_isNullHaplosome(EidosObject **p_values, size_t p_values_size) { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Haplosome *value = (Haplosome *)(p_values[value_index]); logical_result->set_logical_no_check(value->mutrun_count_ == 0, value_index); } return logical_result; } EidosValue *Haplosome::GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Haplosome *value = (Haplosome *)(p_values[value_index]); slim_usertag_t tag_value = value->tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Haplosome::GetProperty): property tag accessed on haplosome before being set." << EidosTerminate(); int_result->set_int_no_check(tag_value, value_index); } return int_result; } void Haplosome::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { switch (p_property_id) { case gID_tag: // ACCELERATED { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; Individual::s_any_haplosome_tag_set_ = true; return; } default: { return super::SetProperty(p_property_id, p_value); } } } void Haplosome::SetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { Individual::s_any_haplosome_tag_set_ = true; // SLiMCastToUsertagTypeOrRaise() is a no-op at present if (p_source_size == 1) { int64_t source_value = p_source.IntAtIndex_NOCAST(0, nullptr); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Haplosome *)(p_values[value_index]))->tag_value_ = source_value; } else { const int64_t *source_data = p_source.IntData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Haplosome *)(p_values[value_index]))->tag_value_ = source_data[value_index]; } } EidosValue_SP Haplosome::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { //case gID_containsMarkerMutation: return ExecuteMethod_Accelerated_containsMarkerMutation(p_method_id, p_arguments, p_interpreter); //case gID_containsMutations: return ExecuteMethod_Accelerated_containsMutations(p_method_id, p_arguments, p_interpreter); //case gID_countOfMutationsOfType: return ExecuteMethod_Accelerated_countOfMutationsOfType(p_method_id, p_arguments, p_interpreter); case gID_mutationsOfType: return ExecuteMethod_mutationsOfType(p_method_id, p_arguments, p_interpreter); case gID_nucleotides: return ExecuteMethod_nucleotides(p_method_id, p_arguments, p_interpreter); case gID_positionsOfMutationsOfType: return ExecuteMethod_positionsOfMutationsOfType(p_method_id, p_arguments, p_interpreter); case gID_sumOfMutationsOfType: return ExecuteMethod_sumOfMutationsOfType(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } // ********************* - (Nlo$)containsMarkerMutation(io$ mutType, integer$ position, [returnMutation$ = F]) // EidosValue_SP Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation(EidosObject **p_elements, size_t p_elements_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *mutType_value = p_arguments[0].get(); EidosValue *position_value = p_arguments[1].get(); EidosValue *returnMutation_value = p_arguments[2].get(); if (p_elements_size) { // SPECIES CONSISTENCY CHECK Species *haplosomes_species = Community::SpeciesForHaplosomesVector((Haplosome **)p_elements, (int)p_elements_size); if (!haplosomes_species) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation): containsMarkerMutation() requires that all target haplosomes belong to the same species." << EidosTerminate(); haplosomes_species->population_.CheckForDeferralInHaplosomesVector((Haplosome **)p_elements, p_elements_size, "Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation"); Species &species = *haplosomes_species; MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species.community_, &species, "containsMarkerMutation()"); // SPECIES CONSISTENCY CHECK slim_position_t marker_position = SLiMCastToPositionTypeOrRaise(position_value->IntAtIndex_NOCAST(0, nullptr)); eidos_logical_t returnMutation = returnMutation_value->LogicalAtIndex_NOCAST(0, nullptr); if (p_elements_size == 1) { // separate singleton case to return gStaticEidosValue_LogicalT / gStaticEidosValue_LogicalF Haplosome *element = (Haplosome *)(p_elements[0]); if (!element->IsNull()) { Chromosome *chromosome = element->AssociatedChromosome(); slim_position_t last_position = chromosome->last_position_; if (marker_position > last_position) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation): containsMarkerMutation() position " << marker_position << " is past the end of the chromosome for the haplosome." << EidosTerminate(); Mutation *mut = element->mutation_with_type_and_position(mutation_type_ptr, marker_position, last_position); if (returnMutation == false) return (mut ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); else return (mut ? EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(mut, gSLiM_Mutation_Class)) : (EidosValue_SP)gStaticEidosValueNULL); } } else if (returnMutation == false) { // We will return a logical vector, T/F for each target haplosome; parallelized EidosValue_Logical *result_logical_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_elements_size); bool null_haplosome_seen = false; EIDOS_THREAD_COUNT(gEidos_OMP_threads_CONTAINS_MARKER_MUT); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(p_elements_size) firstprivate(p_elements, mutation_type_ptr, marker_position, last_position, result_logical_vec) reduction(||: null_haplosome_seen) if(p_elements_size >= EIDOS_OMPMIN_CONTAINS_MARKER_MUT) num_threads(thread_count) for (size_t element_index = 0; element_index < p_elements_size; ++element_index) { Haplosome *element = (Haplosome *)(p_elements[element_index]); if (element->IsNull()) { null_haplosome_seen = true; continue; } Chromosome *chromosome = element->AssociatedChromosome(); slim_position_t last_position = chromosome->last_position_; if (marker_position > last_position) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation): containsMarkerMutation() position " << marker_position << " is past the end of the chromosome for the haplosome." << EidosTerminate(); Mutation *mut = element->mutation_with_type_and_position(mutation_type_ptr, marker_position, last_position); result_logical_vec->set_logical_no_check(mut != nullptr, element_index); } if (!null_haplosome_seen) return EidosValue_SP(result_logical_vec); } else // (returnMutation == true) { // We will return an object vector, one Mutation (or NULL) for each target haplosome; not parallelized, for now EidosValue_Object *result_obj_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class))->reserve(p_elements_size); bool null_haplosome_seen = false; for (size_t element_index = 0; element_index < p_elements_size; ++element_index) { Haplosome *element = (Haplosome *)(p_elements[element_index]); if (element->IsNull()) { null_haplosome_seen = true; continue; } Chromosome *chromosome = element->AssociatedChromosome(); slim_position_t last_position = chromosome->last_position_; if (marker_position > last_position) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation): containsMarkerMutation() position " << marker_position << " is past the end of the chromosome for the haplosome." << EidosTerminate(); Mutation *mut = element->mutation_with_type_and_position(mutation_type_ptr, marker_position, last_position); if (mut) result_obj_vec->push_object_element_RR(mut); } if (!null_haplosome_seen) return EidosValue_SP(result_obj_vec); } // we drop through to here when a null haplosome is encountered EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation): containsMarkerMutation() cannot be called on a null haplosome." << EidosTerminate(); } else { return gStaticEidosValue_Logical_ZeroVec; } } // ********************* - (logical)containsMutations(object mutations) // EidosValue_SP Haplosome::ExecuteMethod_Accelerated_containsMutations(EidosObject **p_elements, size_t p_elements_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (p_elements_size) { // SPECIES CONSISTENCY CHECK Species *haplosomes_species = Community::SpeciesForHaplosomesVector((Haplosome **)p_elements, (int)p_elements_size); if (!haplosomes_species) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMutations): containsMutations() requires that all target haplosomes belong to the same species." << EidosTerminate(); haplosomes_species->population_.CheckForDeferralInHaplosomesVector((Haplosome **)p_elements, p_elements_size, "Haplosome::ExecuteMethod_Accelerated_containsMutations"); EidosValue *mutations_value = p_arguments[0].get(); int mutations_count = mutations_value->Count(); if (mutations_count > 0) { Species *mutations_species = Community::SpeciesForMutations(mutations_value); if (mutations_species != haplosomes_species) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMutations): containsMutations() requires that all mutations belong to the same species as the target haplosomes." << EidosTerminate(); } if ((mutations_count == 1) && (p_elements_size == 1)) { // We want to be smart enough to return gStaticEidosValue_LogicalT or gStaticEidosValue_LogicalF in the singleton/singleton case Mutation *mut = (Mutation *)(mutations_value->ObjectElementAtIndex_NOCAST(0, nullptr)); Haplosome *element = (Haplosome *)(p_elements[0]); if (element->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMutations): containsMutations() cannot be called on a null haplosome." << EidosTerminate(); // BCH 11/24/2024: I've gone back and forth on whether it should be an error to ask whether a mutation // associated with chromosome A is in a haplosome associated with chromosome B. For now I'm going to // say it is an error, and that can be relaxed later if it becomes clear that it is too strict. It // does seem like it is a question that indicates a fundamental logic flaw, like asking whether a // mutation that belongs to species A is in a haplosome that belongs to species B. if (mut->chromosome_index_ != element->chromosome_index_) { //return gStaticEidosValue_LogicalF; EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMutations): containsMutations() requires that all mutations are associated with the same chromosome as the target haplosomes. (If this requirement makes life difficult, it could be relaxed if necessary; but it seems useful for catching logic errors. Note that the containsMutations() method of Individual does not have this restriction.)" << EidosTerminate(); } bool contained = element->contains_mutation(mut); return (contained ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } else { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_elements_size * mutations_count); EidosValue_SP result(logical_result); int64_t result_index = 0; EidosObject * const *mutations_data = mutations_value->ObjectData(); for (size_t element_index = 0; element_index < p_elements_size; ++element_index) { Haplosome *element = (Haplosome *)(p_elements[element_index]); if (element->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMutations): containsMutations() cannot be called on a null haplosome." << EidosTerminate(); for (int value_index = 0; value_index < mutations_count; ++value_index) { Mutation *mut = (Mutation *)mutations_data[value_index]; // BCH 11/24/2024: I've gone back and forth on whether it should be an error to ask whether a mutation // associated with chromosome A is in a haplosome associated with chromosome B. For now I'm going to // say it is an error, and that can be relaxed later if it becomes clear that it is too strict. It // does seem like it is a question that indicates a fundamental logic flaw, like asking whether a // mutation that belongs to species A is in a haplosome that belongs to species B. if (mut->chromosome_index_ != element->chromosome_index_) { //logical_result->set_logical_no_check(false, result_index++); //continue; EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_containsMutations): containsMutations() requires that all mutations are associated with the same chromosome as the target haplosomes. (If this requirement makes life difficult, it could be relaxed if necessary; but it seems useful for catching logic errors. Note that the containsMutations() method of Individual does not have this restriction.)" << EidosTerminate(); } bool contained = element->contains_mutation(mut); logical_result->set_logical_no_check(contained, result_index++); } } return result; } } else { return gStaticEidosValue_Logical_ZeroVec; } } // ********************* - (integer$)countOfMutationsOfType(io$ mutType) // EidosValue_SP Haplosome::ExecuteMethod_Accelerated_countOfMutationsOfType(EidosObject **p_elements, size_t p_elements_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (p_elements_size == 0) return gStaticEidosValue_Integer_ZeroVec; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForHaplosomesVector((Haplosome **)p_elements, (int)p_elements_size); if (species == nullptr) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_countOfMutationsOfType): countOfMutationsOfType() requires that mutType belongs to the same species as the target individual." << EidosTerminate(); species->population_.CheckForDeferralInHaplosomesVector((Haplosome **)p_elements, p_elements_size, "Haplosome::ExecuteMethod_Accelerated_countOfMutationsOfType"); EidosValue *mutType_value = p_arguments[0].get(); MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species->community_, species, "countOfMutationsOfType()"); // SPECIES CONSISTENCY CHECK // Count the number of mutations of the given type const int32_t mutrun_count = ((Haplosome *)(p_elements[0]))->mutrun_count_; Mutation *mut_block_ptr = gSLiM_Mutation_Block; EidosValue_Int *integer_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_elements_size); bool saw_error = false; EIDOS_THREAD_COUNT(gEidos_OMP_threads_G_COUNT_OF_MUTS_OF_TYPE); #pragma omp parallel for schedule(dynamic, 1) default(none) shared(p_elements_size) firstprivate(p_elements, mut_block_ptr, mutation_type_ptr, integer_result, mutrun_count) reduction(||: saw_error) if(p_elements_size >= EIDOS_OMPMIN_G_COUNT_OF_MUTS_OF_TYPE) num_threads(thread_count) for (size_t element_index = 0; element_index < p_elements_size; ++element_index) { Haplosome *element = (Haplosome *)(p_elements[element_index]); if (element->IsNull()) { saw_error = true; continue; } int match_count = 0; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = element->mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) if ((mut_block_ptr + mut_ptr[mut_index])->mutation_type_ptr_ == mutation_type_ptr) ++match_count; } integer_result->set_int_no_check(match_count, element_index); } if (saw_error) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_Accelerated_countOfMutationsOfType): countOfMutationsOfType() cannot be called on a null haplosome." << EidosTerminate(); return EidosValue_SP(integer_result); } // ********************* - (object)mutationsOfType(io$ mutType) // EidosValue_SP Haplosome::ExecuteMethod_mutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *mutType_value = p_arguments[0].get(); if (IsDeferred()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_mutationsOfType): the mutations of deferred haplosomes cannot be accessed." << EidosTerminate(); if (IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_mutationsOfType): mutationsOfType() cannot be called on a null haplosome." << EidosTerminate(); Species &species = individual_->subpopulation_->species_; MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species.community_, &species, "mutationsOfType()"); // SPECIES CONSISTENCY CHECK // We want to return a singleton if we can, but we also want to avoid scanning through all our mutations twice. // We do this by not creating a vector until we see the second match; with one match, we make a singleton. Mutation *mut_block_ptr = gSLiM_Mutation_Block; Mutation *first_match = nullptr; EidosValue_Object *vec = nullptr; EidosValue_SP result_SP; int run_index; for (run_index = 0; run_index < mutrun_count_; ++run_index) { const MutationRun *mutrun = mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) { Mutation *mut = (mut_block_ptr + mut_ptr[mut_index]); if (mut->mutation_type_ptr_ == mutation_type_ptr) { if (!vec) { if (!first_match) first_match = mut; else { vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class)); result_SP = EidosValue_SP(vec); vec->push_object_element_RR(first_match); vec->push_object_element_RR(mut); first_match = nullptr; } } else { vec->push_object_element_RR(mut); } } } } // Now return the appropriate return value if (first_match) { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(first_match, gSLiM_Mutation_Class)); } else { if (!vec) { vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class)); result_SP = EidosValue_SP(vec); } return result_SP; } } // ********************* – (is)nucleotides([Ni$ start = NULL], [Ni$ end = NULL], [s$ format = "string"]) // EidosValue_SP Haplosome::ExecuteMethod_nucleotides(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (IsDeferred()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_nucleotides): the mutations of deferred haplosomes cannot be accessed." << EidosTerminate(); Species *species = &individual_->subpopulation_->species_; Chromosome *chromosome = AssociatedChromosome(); slim_position_t last_position = chromosome->last_position_; if (!species->IsNucleotideBased()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_nucleotides): nucleotides() may only be called in nucleotide-based models." << EidosTerminate(); NucleotideArray *sequence = chromosome->AncestralSequence(); EidosValue *start_value = p_arguments[0].get(); EidosValue *end_value = p_arguments[1].get(); int64_t start = (start_value->Type() == EidosValueType::kValueNULL) ? 0 : start_value->IntAtIndex_NOCAST(0, nullptr); int64_t end = (end_value->Type() == EidosValueType::kValueNULL) ? last_position : end_value->IntAtIndex_NOCAST(0, nullptr); if ((start < 0) || (end < 0) || (start > last_position) || (end > last_position) || (start > end)) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_nucleotides): start and end must be within the chromosome's extent, and start must be <= end." << EidosTerminate(); if (((std::size_t)start >= sequence->size()) || ((std::size_t)end >= sequence->size())) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_nucleotides): (internal error) start and end must be within the ancestral sequence's length." << EidosTerminate(); int64_t length = end - start + 1; if (length > INT_MAX) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_nucleotides): the returned vector would exceed the maximum vector length in Eidos." << EidosTerminate(); EidosValue_String *format_value = (EidosValue_String *)p_arguments[2].get(); const std::string &format = format_value->StringRefAtIndex_NOCAST(0, nullptr); if (format == "codon") { EidosValue_SP codon_value = sequence->NucleotidesAsCodonVector(start, end, /* p_force_vector */ true); // patch the sequence with nucleotide mutations // no singleton case; we force a vector return from NucleotidesAsCodonVector() for simplicity int64_t *int_vec = ((EidosValue_Int *)(codon_value.get()))->data_mutable(); HaplosomeWalker walker(this); walker.MoveToPosition(start); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= start is guaranteed by MoveToPosition() if (pos > end) break; int8_t nuc = mut->nucleotide_; if (nuc != -1) { // We have a nucleotide-based mutation within the sequence range. We need to get the current codon value, // deconstruct it into nucleotides, replace the overlaid nucleotide, reconstruct the codon value, and put // it back into the codon vector. int64_t codon_index = (pos - start) / 3; int codon_offset = (pos - start) % 3; int codon = (int)int_vec[codon_index]; if (codon_offset == 0) { codon = codon & 0x0F; codon |= (nuc * 16); } else if (codon_offset == 1) { codon = codon & 0x33; codon |= (nuc * 4); } else { codon = codon & 0x3C; codon |= nuc; } int_vec[codon_index] = codon; } walker.NextMutation(); } return codon_value; } else if (format == "string") { EidosValue_SP string_value = sequence->NucleotidesAsStringSingleton(start, end); // patch the sequence with nucleotide mutations if (start == end) { // singleton case: replace string_value entirely HaplosomeWalker walker(this); walker.MoveToPosition(start); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= start is guaranteed by MoveToPosition() if (pos > end) break; int8_t nuc = mut->nucleotide_; if (nuc != -1) { if (nuc == 0) string_value = gStaticEidosValue_StringA; else if (nuc == 1) string_value = gStaticEidosValue_StringC; else if (nuc == 2) string_value = gStaticEidosValue_StringG; else /* (nuc == 3) */ string_value = gStaticEidosValue_StringT; } walker.NextMutation(); } } else { // vector case: replace the appropriate character in string_value std::string &string_string = ((EidosValue_String *)(string_value.get()))->StringData_Mutable()[0]; char *string_ptr = &string_string[0]; // data() returns a const pointer, but this is safe in C++11 and later HaplosomeWalker walker(this); walker.MoveToPosition(start); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= start is guaranteed by MoveToPosition() if (pos > end) break; int8_t nuc = mut->nucleotide_; if (nuc != -1) string_ptr[pos - start] = gSLiM_Nucleotides[nuc]; walker.NextMutation(); } } return string_value; } else if (format == "integer") { EidosValue_SP integer_value = sequence->NucleotidesAsIntegerVector(start, end); // patch the sequence with nucleotide mutations if (start == end) { // singleton case: replace integer_value entirely HaplosomeWalker walker(this); walker.MoveToPosition(start); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= start is guaranteed by MoveToPosition() if (pos > end) break; int8_t nuc = mut->nucleotide_; if (nuc != -1) { if (nuc == 0) integer_value = gStaticEidosValue_Integer0; else if (nuc == 1) integer_value = gStaticEidosValue_Integer1; else if (nuc == 2) integer_value = gStaticEidosValue_Integer2; else /* (nuc == 3) */ integer_value = gStaticEidosValue_Integer3; } walker.NextMutation(); } } else { // vector case: replace the appropriate element in integer_value int64_t *int_vec = ((EidosValue_Int *)(integer_value.get()))->data_mutable(); HaplosomeWalker walker(this); walker.MoveToPosition(start); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= start is guaranteed by MoveToPosition() if (pos > end) break; int8_t nuc = mut->nucleotide_; if (nuc != -1) int_vec[pos-start] = (int)nuc; walker.NextMutation(); } } return integer_value; } else if (format == "char") { EidosValue_SP char_value = sequence->NucleotidesAsStringVector(start, end); // patch the sequence with nucleotide mutations if (start == end) { // singleton case: replace char_value entirely HaplosomeWalker walker(this); walker.MoveToPosition(start); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= start is guaranteed by MoveToPosition() if (pos > end) break; int8_t nuc = mut->nucleotide_; if (nuc != -1) { if (nuc == 0) char_value = gStaticEidosValue_StringA; else if (nuc == 1) char_value = gStaticEidosValue_StringC; else if (nuc == 2) char_value = gStaticEidosValue_StringG; else /* (nuc == 3) */ char_value = gStaticEidosValue_StringT; } walker.NextMutation(); } } else { // vector case: replace the appropriate element in char_value std::string *char_vec = char_value->StringData_Mutable(); HaplosomeWalker walker(this); walker.MoveToPosition(start); while (!walker.Finished()) { Mutation *mut = walker.CurrentMutation(); slim_position_t pos = mut->position_; // pos >= start is guaranteed by MoveToPosition() if (pos > end) break; int8_t nuc = mut->nucleotide_; if (nuc != -1) char_vec[pos - start] = gSLiM_Nucleotides[nuc]; walker.NextMutation(); } } return char_value; } EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_nucleotides): parameter format must be either 'string', 'char', 'integer', or 'codon'." << EidosTerminate(); } // ********************* - (integer)positionsOfMutationsOfType(io$ mutType) // EidosValue_SP Haplosome::ExecuteMethod_positionsOfMutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *mutType_value = p_arguments[0].get(); if (IsDeferred()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_positionsOfMutationsOfType): the mutations of deferred haplosomes cannot be accessed." << EidosTerminate(); if (IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_positionsOfMutationsOfType): positionsOfMutationsOfType() cannot be called on a null haplosome." << EidosTerminate(); Species &species = individual_->subpopulation_->species_; MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species.community_, &species, "positionsOfMutationsOfType()"); // SPECIES CONSISTENCY CHECK // Return the positions of mutations of the given type EidosValue_Int *int_result = new (gEidosValuePool->AllocateChunk()) EidosValue_Int(); Mutation *mut_block_ptr = gSLiM_Mutation_Block; for (int run_index = 0; run_index < mutrun_count_; ++run_index) { const MutationRun *mutrun = mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) { Mutation *mutation = mut_block_ptr + mut_ptr[mut_index]; if (mutation->mutation_type_ptr_ == mutation_type_ptr) int_result->push_int(mutation->position_); } } return EidosValue_SP(int_result); } // ********************* - (integer$)sumOfMutationsOfType(io$ mutType) // EidosValue_SP Haplosome::ExecuteMethod_sumOfMutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *mutType_value = p_arguments[0].get(); if (IsDeferred()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_sumOfMutationsOfType): the mutations of deferred haplosomes cannot be accessed." << EidosTerminate(); if (IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome::ExecuteMethod_sumOfMutationsOfType): sumOfMutationsOfType() cannot be called on a null haplosome." << EidosTerminate(); Species &species = individual_->subpopulation_->species_; MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species.community_, &species, "sumOfMutationsOfType()"); // SPECIES CONSISTENCY CHECK // Sum the selection coefficients of mutations of the given type Mutation *mut_block_ptr = gSLiM_Mutation_Block; double selcoeff_sum = 0.0; int mutrun_count = mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = mutruns_[run_index]; int haplosome1_count = mutrun->size(); const MutationIndex *haplosome_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < haplosome1_count; ++mut_index) { Mutation *mut_ptr = mut_block_ptr + haplosome_ptr[mut_index]; if (mut_ptr->mutation_type_ptr_ == mutation_type_ptr) selcoeff_sum += mut_ptr->selection_coeff_; } } return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(selcoeff_sum)); } // print the sample represented by haplosomes, using SLiM's own format void Haplosome::PrintHaplosomes_SLiM(std::ostream &p_out, std::vector &p_haplosomes, bool p_output_object_tags) { Mutation *mut_block_ptr = gSLiM_Mutation_Block; slim_popsize_t sample_size = (slim_popsize_t)p_haplosomes.size(); // get the polymorphisms within the sample PolymorphismMap polymorphisms; for (slim_popsize_t s = 0; s < sample_size; s++) { Haplosome &haplosome = *p_haplosomes[s]; if (haplosome.IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_SLiM): cannot output null haplosomes." << EidosTerminate(); for (int run_index = 0; run_index < haplosome.mutrun_count_; ++run_index) { const MutationRun *mutrun = haplosome.mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) AddMutationToPolymorphismMap(&polymorphisms, mut_block_ptr + mut_ptr[mut_index]); } } // print the sample's polymorphisms; NOTE the output format changed due to the addition of mutation_id_, BCH 11 June 2016 // NOTE the output format changed due to the addition of the nucleotide, BCH 2 March 2019 p_out << "Mutations:" << std::endl; for (const PolymorphismPair &polymorphism_pair : polymorphisms) { if (p_output_object_tags) polymorphism_pair.second.Print_ID_Tag(p_out); else polymorphism_pair.second.Print_ID(p_out); } // print the sample's haplosomes p_out << "Haplosomes:" << std::endl; for (slim_popsize_t j = 0; j < sample_size; j++) // go through all haplosomes { Haplosome &haplosome = *p_haplosomes[j]; Individual *individual = haplosome.individual_; if (!individual) EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_SLiM): (internal error) missing individual for haplosome." << EidosTerminate(); slim_popsize_t index = individual->index_; if (index == -1) EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_SLiM): haplosomes being output must be visible in a subpopulation (i.e., may not belong to new juveniles)." << EidosTerminate(); Subpopulation *subpop = individual->subpopulation_; if (!subpop) EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_SLiM): (internal error) missing subpopulation for individual." << EidosTerminate(); // BCH 2/9/2025: For SLiM 5, we now print the subpopulation id and the // individual index for the haplosome, telling the user where each // haplosome came from; probably not useful, but more useful than before p_out << 'p' << subpop->subpopulation_id_ << ":i" << index; if (p_output_object_tags) { if (haplosome.tag_value_ == SLIM_TAG_UNSET_VALUE) p_out << " ?"; else p_out << ' ' << haplosome.tag_value_; } for (int run_index = 0; run_index < haplosome.mutrun_count_; ++run_index) { const MutationRun *mutrun = haplosome.mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) { slim_polymorphismid_t polymorphism_id = FindMutationInPolymorphismMap(polymorphisms, mut_block_ptr + mut_ptr[mut_index]); if (polymorphism_id == -1) EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_SLiM): (internal error) polymorphism not found." << EidosTerminate(); p_out << " " << polymorphism_id; } } p_out << std::endl; } } // print the sample represented by haplosomes, using "ms" format void Haplosome::PrintHaplosomes_MS(std::ostream &p_out, std::vector &p_haplosomes, const Chromosome &p_chromosome, bool p_filter_monomorphic) { Mutation *mut_block_ptr = gSLiM_Mutation_Block; slim_popsize_t sample_size = (slim_popsize_t)p_haplosomes.size(); // BCH 7 Nov. 2016: sort the polymorphisms by position since that is the expected sort // order in MS output. In other types of output, sorting by the mutation id seems to // be fine. std::vector sorted_polymorphisms; { // get the polymorphisms within the sample PolymorphismMap polymorphisms; for (slim_popsize_t s = 0; s < sample_size; s++) { Haplosome &haplosome = *p_haplosomes[s]; if (haplosome.IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_MS): cannot output null haplosomes." << EidosTerminate(); for (int run_index = 0; run_index < haplosome.mutrun_count_; ++run_index) { const MutationRun *mutrun = haplosome.mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) AddMutationToPolymorphismMap(&polymorphisms, mut_block_ptr + mut_ptr[mut_index]); } } for (const PolymorphismPair &polymorphism_pair : polymorphisms) sorted_polymorphisms.emplace_back(polymorphism_pair.second); std::sort(sorted_polymorphisms.begin(), sorted_polymorphisms.end()); } // if requested, remove polymorphisms that are not polymorphic within the sample if (p_filter_monomorphic) { std::vector filtered_polymorphisms; for (Polymorphism &p : sorted_polymorphisms) if (p.prevalence_ != sample_size) filtered_polymorphisms.emplace_back(p); std::swap(sorted_polymorphisms, filtered_polymorphisms); } // make a hash table that looks up the genotype string position from a mutation pointer #if EIDOS_ROBIN_HOOD_HASHING robin_hood::unordered_flat_map genotype_string_positions; //typedef robin_hood::pair MAP_PAIR; #elif STD_UNORDERED_MAP_HASHING std::unordered_map genotype_string_positions; //typedef std::pair MAP_PAIR; #endif int genotype_string_position = 0; try { for (const Polymorphism &polymorphism : sorted_polymorphisms) genotype_string_positions.emplace(polymorphism.mutation_ptr_, genotype_string_position++); } catch (...) { EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_MS): (internal error) SLiM encountered a raise from an internal hash table; please report this." << EidosTerminate(nullptr); } // print header p_out << "//" << std::endl << "segsites: " << sorted_polymorphisms.size() << std::endl; // print the sample's positions if (sorted_polymorphisms.size() > 0) { // Save flags/precision std::ios_base::fmtflags oldflags = p_out.flags(); std::streamsize oldprecision = p_out.precision(); p_out << std::fixed << std::setprecision(15); // BCH 26 Jan. 2020: increasing this from 7 to 10, so longer chromosomes work; maybe this should be a parameter? // BCH 23 July 2020: increasing from 10 to 15, which is the limit of double-precision floats anyway // Output positions p_out << "positions:"; for (const Polymorphism &polymorphism : sorted_polymorphisms) p_out << " " << static_cast(polymorphism.mutation_ptr_->position_) / p_chromosome.last_position_; // this prints positions as being in the interval [0,1], which Philipp decided was the best policy p_out << std::endl; // Restore flags/precision p_out.flags(oldflags); p_out.precision(oldprecision); } // print the sample's genotypes for (slim_popsize_t j = 0; j < sample_size; j++) // go through all individuals { Haplosome &haplosome = *p_haplosomes[j]; std::string genotype(sorted_polymorphisms.size(), '0'); // fill with 0s for (int run_index = 0; run_index < haplosome.mutrun_count_; ++run_index) { const MutationRun *mutrun = haplosome.mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) { const Mutation *mutation = mut_block_ptr + mut_ptr[mut_index]; auto found_position = genotype_string_positions.find(mutation); // BCH 4/24/2019: when p_filter_monomorphic is true, mutations in a given haplosome may not exist in the position map if (found_position != genotype_string_positions.end()) genotype.replace(found_position->second, 1, "1"); } } p_out << genotype << std::endl; } } inline void EmitHaplosomeCall_Nuc_Simplify(std::ostream &p_out, const Haplosome &haplosome, std::vector &nuc_based, slim_position_t mut_position, int *allele_index_for_nuc) { // Find and emit the nuc-based mut contained by this haplosome, if any. If more than one nuc-based mut is contained, it is an error. int contained_mut_index = -1; for (int muts_index = 0; muts_index < (int)nuc_based.size(); ++muts_index) { const Mutation *mutation = nuc_based[muts_index]->mutation_ptr_; if (haplosome.contains_mutation(mutation)) { if (contained_mut_index == -1) contained_mut_index = muts_index; else EIDOS_TERMINATION << "ERROR (EmitHaplosomeCall_Nuc): more than one nucleotide-based mutation encountered at the same position (" << mut_position << ") in the same haplosome; the nucleotide cannot be called." << EidosTerminate(); } } if (contained_mut_index == -1) p_out << '0'; else p_out << allele_index_for_nuc[nuc_based[contained_mut_index]->mutation_ptr_->nucleotide_]; } inline void EmitHaplosomeCall_Nuc(std::ostream &p_out, const Haplosome &haplosome, std::vector &nuc_based, slim_position_t mut_position) { // Find and emit the nuc-based mut contained by this haplosome, if any. If more than one nuc-based mut is contained, it is an error. int contained_mut_index = -1; for (int muts_index = 0; muts_index < (int)nuc_based.size(); ++muts_index) { const Mutation *mutation = nuc_based[muts_index]->mutation_ptr_; if (haplosome.contains_mutation(mutation)) { if (contained_mut_index == -1) contained_mut_index = muts_index; else EIDOS_TERMINATION << "ERROR (EmitHaplosomeCall_Nuc): more than one nucleotide-based mutation encountered at the same position (" << mut_position << ") in the same haplosome; the nucleotide cannot be called." << EidosTerminate(); } } if (contained_mut_index == -1) p_out << '0'; else p_out << (contained_mut_index + 1); } // print the sample represented by haplosomes, using "vcf" format // the haplosomes will all belong to a single chromosome, p_chromosome, and may include null haplosomes // depending on the intrinsic ploidy of p_chromosome the calls will be diploid or haploid; if diploid, // calls where one of a pair of haplosomes is null will be emitted as a haploid call; if all haplosomes // for a given individual are null, the call emitted will be "~". void Haplosome::PrintHaplosomes_VCF(std::ostream &p_out, std::vector &p_haplosomes, const Chromosome &p_chromosome, bool p_groupAsIndividuals, bool p_output_multiallelics, bool p_simplify_nucs, bool p_output_nonnucs) { Species &species = p_chromosome.species_; bool nucleotide_based = species.IsNucleotideBased(); bool pedigrees_enabled = species.PedigreesEnabledByUser(); slim_popsize_t haplosome_count = (slim_popsize_t)p_haplosomes.size(); slim_popsize_t individual_count; // get information about the chromosome we're writing, which determines whether an "individual" in the VCF is one haplosome or two ChromosomeType chromosome_type = p_chromosome.Type(); int intrinsic_ploidy = p_chromosome.IntrinsicPloidy(); if (!p_groupAsIndividuals) intrinsic_ploidy = 1; // if groupAsIndividuals is false, we just act as though the chromosome is haploid if (intrinsic_ploidy == 2) { if (haplosome_count % 2 == 1) EIDOS_TERMINATION << "ERROR (Haplosome::PrintHaplosomes_VCF): Haplosome vector must be an even length for chromosome type \"" << chromosome_type << "\", since haplosomes are paired into individuals." << EidosTerminate(); individual_count = haplosome_count / 2; } else { individual_count = haplosome_count; } // print the VCF header p_out << "##fileformat=VCFv4.2" << std::endl; { time_t rawtime; struct tm timeinfo; char buffer[25]; // should never be more than 10, in fact, plus a null time(&rawtime); localtime_r(&rawtime, &timeinfo); strftime(buffer, 25, "%Y%m%d", &timeinfo); p_out << "##fileDate=" << std::string(buffer) << std::endl; } p_out << "##source=SLiM" << std::endl; // BCH 10 July 2019: output haplosome pedigree IDs, if available, for all of the haplosomes being output. // It would be nice to be able to output individual pedigree IDs, but since we are working with a // vector of haplosomes there is no guarantee that the pairs of haplosomes here come from the same individuals. if (pedigrees_enabled && (haplosome_count > 0)) { p_out << "##slimHaplosomePedigreeIDs="; for (slim_popsize_t haplosome_index = 0; haplosome_index < haplosome_count; haplosome_index++) { if (haplosome_index > 0) p_out << ","; p_out << p_haplosomes[haplosome_index]->haplosome_id_; } p_out << std::endl; } // BCH 6 March 2019: Note that all of the INFO fields that provide per-mutation information have been // changed from a Number of 1 to a Number of ., since in nucleotide-based models we can call more than // one allele in a single call line (unlike in non-nucleotide-based models). p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; // Note that at present we do not output the hemizygous dominance coefficient; too edge p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; // changed to ticks for 4.0, and changed "GO" to "TO" p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; if (p_output_multiallelics && !nucleotide_based) p_out << "##INFO=" << std::endl; if (nucleotide_based) p_out << "##INFO=" << std::endl; if (p_output_nonnucs && nucleotide_based) p_out << "##INFO=" << std::endl; p_out << "##FORMAT=" << std::endl; p_out << "##contig=" << std::endl; p_out << "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT"; for (slim_popsize_t individual_index = 0; individual_index < individual_count; individual_index++) p_out << "\ti" << individual_index; p_out << std::endl; Haplosome::_PrintVCF(p_out, (const Haplosome **)p_haplosomes.data(), haplosome_count, p_chromosome, p_groupAsIndividuals, p_simplify_nucs, p_output_nonnucs, p_output_multiallelics); } void Haplosome::_PrintVCF(std::ostream &p_out, const Haplosome **p_haplosomes, int64_t p_haplosomes_count, const Chromosome &p_chromosome, bool p_groupAsIndividuals, bool p_simplify_nucs, bool p_output_nonnucs, bool p_output_multiallelics) { ChromosomeType chromosome_type = p_chromosome.Type(); int intrinsic_ploidy = p_chromosome.IntrinsicPloidy(); Species &species = p_chromosome.species_; bool nucleotide_based = species.IsNucleotideBased(); NucleotideArray *ancestral_seq = p_chromosome.AncestralSequence(); Mutation *mut_block_ptr = gSLiM_Mutation_Block; int64_t individual_count; // if groupAsIndividuals is false, we just act as though the chromosome is haploid // this option is not available for Individual::PrintIndividuals_VCF() since it doesn't // make sense for individual-based output, only single-chromosome haplosome-based output if (!p_groupAsIndividuals) intrinsic_ploidy = 1; if (intrinsic_ploidy == 2) { if (p_haplosomes_count % 2 == 1) EIDOS_TERMINATION << "ERROR (Haplosome::_PrintVCF): Haplosome vector must be an even length for chromosome type \"" << chromosome_type << "\", since haplosomes are paired into individuals." << EidosTerminate(); individual_count = p_haplosomes_count / 2; } else { individual_count = p_haplosomes_count; } // get the polymorphisms within the sample PolymorphismMap polymorphisms; for (slim_popsize_t haplosome_index = 0; haplosome_index < p_haplosomes_count; haplosome_index++) { const Haplosome &haplosome = *p_haplosomes[haplosome_index]; if (!haplosome.IsNull()) { for (int run_index = 0; run_index < haplosome.mutrun_count_; ++run_index) { const MutationRun *mutrun = haplosome.mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) AddMutationToPolymorphismMap(&polymorphisms, mut_block_ptr + mut_ptr[mut_index]); } } } // We want to output polymorphisms sorted by position (starting in SLiM 3.3), to facilitate // calling all of the nucleotide mutations at a given position with a single call line. std::vector sorted_polymorphisms; for (const PolymorphismPair &polymorphism_pair : polymorphisms) sorted_polymorphisms.emplace_back(polymorphism_pair.second); std::sort(sorted_polymorphisms.begin(), sorted_polymorphisms.end()); // Print a line for each mutation. Note that we do NOT treat multiple mutations at the same position at being different alleles, // output on the same line. This is because a single individual can carry more than one mutation at the same position, so it is // not really a question of different alleles; if there are N mutations at a given position, there are 2^N possible "alleles", // which is just silly to try to wedge into VCF format. So instead, we output each mutation as a separate line, and we tag lines // for positions that carry more than one mutation with the MULTIALLELIC flag so they can be filtered out if they bother the user. // BCH 6 March 2019: The above comment remains true in non-nucleotide-based models. In nucleotide-based models, the nucleotide- // based mutations at a given position are all output as a single call line, and then any non-nucleotide-based mutations are // emitted as separated call lines after that that are marked NONNUC (unless p_output_nonnucs is false, in which case they are // simply suppressed). for (auto polyiter = sorted_polymorphisms.begin(); polyiter != sorted_polymorphisms.end(); ) { // Assemble vectors of all the nuc-based and non-nuc-based mutations at this position; we will emit them all at once std::vector nuc_based, nonnuc_based; slim_position_t mut_position = polyiter->mutation_ptr_->position_; while (true) { // Eat polymorphism entries in sorted_polymorphisms as long as they're at the same position, until the end Polymorphism &polymorphism = *polyiter; const Mutation *mutation = polyiter->mutation_ptr_; if (mutation->position_ == mut_position) { if (mutation->mutation_type_ptr_->nucleotide_based_) nuc_based.emplace_back(&polymorphism); else nonnuc_based.emplace_back(&polymorphism); } else { break; } // Next polymorphism polyiter++; if (polyiter == sorted_polymorphisms.end()) break; } // Emit the nucleotide-based mutations at this position as a single call line if (nucleotide_based && (nuc_based.size() > 0)) { // Get the ancestral nucleotide at this position; this will be index 0 // Indices 1..n will be used for the corresponding mutations in nonnuc_based // Note that this means it is int ancestral_nuc_index = ancestral_seq->NucleotideAtIndex(mut_position); // 0..3 for ACGT // Emit a single call line for all of the nucleotide-based mutations if (p_simplify_nucs) { // We are requested to simplify the nucleotide state; any mutations with the ancestral nucleotide will be considered part of the // ancestral state, and any mutations with matching nucleotide will be lumped together; SLiM state will not be emitted // We tally up the total prevalence of each nucleotide, ignoring the ancestral nucleotide. slim_refcount_t total_prevalence[4] = {0, 0, 0, 0}; int allele_index_for_nuc[4] = {-1, -1 -1, -1}; for (Polymorphism *polymorphism : nuc_based) { int derived_nuc_index = (unsigned char)polymorphism->mutation_ptr_->nucleotide_; if (derived_nuc_index != ancestral_nuc_index) total_prevalence[derived_nuc_index] += polymorphism->prevalence_; } // Assign genotype call indexes for the four nucleotides, based upon which ones have prevalence > 0 allele_index_for_nuc[ancestral_nuc_index] = 0; // emit 0 for any mutations with a back-mutation int next_allele_index = 1; // 0 is ancestral for (int nuc_index = 0; nuc_index < 4; ++nuc_index) { if (total_prevalence[nuc_index] > 0) allele_index_for_nuc[nuc_index] = next_allele_index++; } // If the only segregating alleles are back-mutations, we don't need to emit this call line at all if (total_prevalence[0] + total_prevalence[1] + total_prevalence[2] + total_prevalence[3] != 0) { // emit CHROM ("1"), POS, ID (".") // BCH 2/3/2025: we now emit the chromosome's symbol in the CHROM field, introducing a minor // backward compatibility break; it used to be "1" by default, now it is "A" by default, but // this is easy to fix by calling initializeChromosome() explicitly and supplying symbol="1" p_out << p_chromosome.Symbol() << "\t" << (mut_position + 1) << "\t.\t"; // +1 because VCF uses 1-based positions // emit REF ("A" etc.) p_out << gSLiM_Nucleotides[ancestral_nuc_index]; p_out << "\t"; // emit ALT ("T" etc.) bool firstEmitted = true; for (int nuc_index=0; nuc_index < 4; ++nuc_index) { if (total_prevalence[nuc_index] > 0) { if (!firstEmitted) p_out << ','; firstEmitted = false; p_out << gSLiM_Nucleotides[nuc_index]; } } // emit QUAL (1000), FILTER (PASS) p_out << "\t1000\tPASS\t"; // emit the INFO fields and the Genotype marker; note mutation-specific fields are omitted since we are aggregating p_out << "AC="; firstEmitted = true; for (int nuc_prevalence : total_prevalence) { if (nuc_prevalence > 0) { if (!firstEmitted) p_out << ','; firstEmitted = false; p_out << nuc_prevalence; } } p_out << ";DP=1000;"; p_out << "AA=" << gSLiM_Nucleotides[ancestral_nuc_index]; p_out << "\tGT"; // emit the individual calls if (intrinsic_ploidy == 1) { // intrinsically haploid chromosome; one haplosome per individual for (slim_popsize_t individual_index = 0; individual_index < individual_count; individual_index++) { const Haplosome &haplosome = *p_haplosomes[individual_index]; // BCH 2/4/2025: If the haplosome is null, we now emit a "~" character if (haplosome.IsNull()) { p_out << "\t~"; continue; } // haploid call p_out << '\t'; EmitHaplosomeCall_Nuc_Simplify(p_out, haplosome, nuc_based, mut_position, allele_index_for_nuc); } } else { // intrinsically diploid chromosome; two haplosomes per individual for (slim_popsize_t individual_index = 0; individual_index < individual_count; individual_index++) { const Haplosome &haplosome1 = *p_haplosomes[(size_t)individual_index * 2]; const Haplosome &haplosome2 = *p_haplosomes[(size_t)individual_index * 2 + 1]; bool haplosome1_null = haplosome1.IsNull(), haplosome2_null = haplosome2.IsNull(); // BCH 2/4/2025: If both haplosomes are null, we now emit a "~" character if (haplosome1_null && haplosome2_null) { p_out << "\t~"; continue; } // diploid call unless hemizygous, producing a haploid call (and losing which haplosome was null) p_out << '\t'; if (!haplosome1_null) EmitHaplosomeCall_Nuc_Simplify(p_out, haplosome1, nuc_based, mut_position, allele_index_for_nuc); if (!haplosome1_null && !haplosome2_null) // emit a separator for a diploid call p_out << '|'; if (!haplosome2_null) EmitHaplosomeCall_Nuc_Simplify(p_out, haplosome2, nuc_based, mut_position, allele_index_for_nuc); } } p_out << std::endl; } } else { // emit CHROM ("1"), POS, ID (".") // BCH 2/3/2025: we now emit the chromosome's symbol in the CHROM field, introducing a minor // backward compatibility break; it used to be "1" by default, now it is "A" by default, but // this is easy to fix by calling initializeChromosome() explicitly and supplying symbol="1" p_out << p_chromosome.Symbol() << "\t" << (mut_position + 1) << "\t.\t"; // +1 because VCF uses 1-based positions // emit REF ("A" etc.) p_out << gSLiM_Nucleotides[ancestral_nuc_index]; p_out << "\t"; // emit ALT ("T" etc.) for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << gSLiM_Nucleotides[polymorphism->mutation_ptr_->nucleotide_]; } // emit QUAL (1000), FILTER (PASS) p_out << "\t1000\tPASS\t"; // emit the INFO fields and the Genotype marker p_out << "MID="; for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << polymorphism->mutation_ptr_->mutation_id_; } p_out << ";"; p_out << "S="; for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << polymorphism->mutation_ptr_->selection_coeff_; } p_out << ";"; p_out << "DOM="; for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << polymorphism->mutation_ptr_->mutation_type_ptr_->dominance_coeff_; } p_out << ";"; p_out << "PO="; for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << polymorphism->mutation_ptr_->subpop_index_; } p_out << ";"; p_out << "TO="; for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << polymorphism->mutation_ptr_->origin_tick_; } p_out << ";"; p_out << "MT="; for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << polymorphism->mutation_ptr_->mutation_type_ptr_->mutation_type_id_; } p_out << ";"; p_out << "AC="; for (Polymorphism *polymorphism : nuc_based) { if (polymorphism != nuc_based.front()) p_out << ','; p_out << polymorphism->prevalence_; } p_out << ";"; p_out << "DP=1000;"; p_out << "AA=" << gSLiM_Nucleotides[ancestral_nuc_index]; p_out << "\tGT"; // emit the individual calls if (intrinsic_ploidy == 1) { // intrinsically haploid chromosome; one haplosome per individual for (slim_popsize_t individual_index = 0; individual_index < individual_count; individual_index++) { const Haplosome &haplosome = *p_haplosomes[individual_index]; // BCH 2/4/2025: If the haplosome is null, we now emit a "~" character if (haplosome.IsNull()) { p_out << "\t~"; continue; } // haploid call p_out << '\t'; EmitHaplosomeCall_Nuc(p_out, haplosome, nuc_based, mut_position); } } else { // intrinsically diploid chromosome; two haplosomes per individual for (slim_popsize_t individual_index = 0; individual_index < individual_count; individual_index++) { const Haplosome &haplosome1 = *p_haplosomes[(size_t)individual_index * 2]; const Haplosome &haplosome2 = *p_haplosomes[(size_t)individual_index * 2 + 1]; bool haplosome1_null = haplosome1.IsNull(), haplosome2_null = haplosome2.IsNull(); // BCH 2/4/2025: If both haplosomes are null, we now emit a "~" character if (haplosome1_null && haplosome2_null) { p_out << "\t~"; continue; } // diploid call unless hemizygous, producing a haploid call (and losing which haplosome was null) p_out << '\t'; if (!haplosome1.IsNull()) EmitHaplosomeCall_Nuc(p_out, haplosome1, nuc_based, mut_position); if (!haplosome1_null && !haplosome2_null) // emit a separator for a diploid call p_out << '|'; if (!haplosome2.IsNull()) EmitHaplosomeCall_Nuc(p_out, haplosome2, nuc_based, mut_position); } } p_out << std::endl; } } // Emit the non-nucleotide-based mutations at this position as individual call lines, each as an A->T mutation // We do this if outputNonnucleotides==T, or if we are non-nucleotide-based (in which case outputNonnucleotides is ignored) if (p_output_nonnucs || !nucleotide_based) { for (Polymorphism *polymorphism : nonnuc_based) { const Mutation *mutation = polymorphism->mutation_ptr_; // Count the mutations at the given position to determine if we are multiallelic int allele_count = (int)nonnuc_based.size(); // Output this mutation if (1) we are outputting multiallelics in a non-nuc-based model, or (2) we are a nuc-based model (regardless of allele count), or (3) it is not multiallelic if (p_output_multiallelics || nucleotide_based || (allele_count == 1)) { // emit CHROM ("1"), POS, ID ("."), REF ("A"), and ALT ("T") // BCH 2/3/2025: we now emit the chromosome's symbol in the CHROM field, introducing a minor // backward compatibility break; it used to be "1" by default, now it is "A" by default, but // this is easy to fix by calling initializeChromosome() explicitly and supplying symbol="1" p_out << p_chromosome.Symbol() << "\t" << (mut_position + 1) << "\t.\tA\tT"; // +1 because VCF uses 1-based positions // emit QUAL (1000), FILTER (PASS) p_out << "\t1000\tPASS\t"; // emit the INFO fields and the Genotype marker p_out << "MID=" << mutation->mutation_id_ << ";"; p_out << "S=" << mutation->selection_coeff_ << ";"; p_out << "DOM=" << mutation->mutation_type_ptr_->dominance_coeff_ << ";"; p_out << "PO=" << mutation->subpop_index_ << ";"; p_out << "TO=" << mutation->origin_tick_ << ";"; p_out << "MT=" << mutation->mutation_type_ptr_->mutation_type_id_ << ";"; p_out << "AC=" << polymorphism->prevalence_ << ";"; p_out << "DP=1000"; if (!nucleotide_based && (allele_count > 1)) // output MULTIALLELIC flags only in non-nuc-based models p_out << ";MULTIALLELIC"; if (nucleotide_based && p_output_nonnucs) p_out << ";NONNUC"; p_out << "\tGT"; // emit the individual calls if (intrinsic_ploidy == 1) { for (slim_popsize_t individual_index = 0; individual_index < individual_count; individual_index++) { const Haplosome &haplosome = *p_haplosomes[individual_index]; // BCH 2/4/2025: If the haplosome is null, we now emit a "~" character if (haplosome.IsNull()) { p_out << "\t~"; continue; } // haploid call p_out << (haplosome.contains_mutation(mutation) ? "\t1" : "\t0"); } } else { for (slim_popsize_t individual_index = 0; individual_index < individual_count; individual_index++) { const Haplosome &haplosome1 = *p_haplosomes[(size_t)individual_index * 2]; const Haplosome &haplosome2 = *p_haplosomes[(size_t)individual_index * 2 + 1]; bool haplosome1_null = haplosome1.IsNull(), haplosome2_null = haplosome2.IsNull(); // BCH 2/4/2025: If both haplosomes are null, we now emit a "~" character if (haplosome1_null && haplosome2_null) { p_out << "\t~"; continue; } else if (haplosome1_null) { // hemizygous; we emit this as haploid (losing which haplosome was null) p_out << (haplosome2.contains_mutation(mutation) ? "\t1" : "\t0"); } else if (haplosome2_null) { // hemizygous; we emit this as haploid (losing which haplosome was null) p_out << (haplosome1.contains_mutation(mutation) ? "\t1" : "\t0"); } else { bool haplosome1_has_mut = haplosome1.contains_mutation(mutation); bool haplosome2_has_mut = haplosome2.contains_mutation(mutation); if (haplosome1_has_mut && haplosome2_has_mut) p_out << "\t1|1"; else if (haplosome1_has_mut) p_out << "\t1|0"; else if (haplosome2_has_mut) p_out << "\t0|1"; else p_out << "\t0|0"; } } } p_out << std::endl; } } } // polyiter already points to the mutation at the next position, or to end(), so we don't advance it here } } size_t Haplosome::MemoryUsageForMutrunBuffers(void) { if (mutruns_ == run_buffer_) return 0; else return mutrun_count_ * sizeof(MutationRun *); } // // Haplosome_Class // #pragma mark - #pragma mark Haplosome_Class #pragma mark - EidosClass *gSLiM_Haplosome_Class = nullptr; const std::vector *Haplosome_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("Haplosome_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_chromosome, true, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Chromosome_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_chromosomeSubposition, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Haplosome::GetProperty_Accelerated_chromosomeSubposition)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_haplosomePedigreeID,true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Haplosome::GetProperty_Accelerated_haplosomePedigreeID)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_individual, true, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Individual_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_isNullHaplosome, true, kEidosValueMaskLogical | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Haplosome::GetProperty_Accelerated_isNullHaplosome)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutationCount, true, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_mutations, true, kEidosValueMaskObject, gSLiM_Mutation_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Haplosome::GetProperty_Accelerated_tag)->DeclareAcceleratedSet(Haplosome::SetProperty_Accelerated_tag)); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *Haplosome_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("Haplosome_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_addMutations, kEidosValueMaskVOID))->AddObject("mutations", gSLiM_Mutation_Class)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_addNewDrawnMutation, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddIntObject("mutationType", gSLiM_MutationType_Class)->AddInt("position")->AddIntObject_ON("originSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)->AddIntString_ON("nucleotide", gStaticEidosValueNULL)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_addNewMutation, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddIntObject("mutationType", gSLiM_MutationType_Class)->AddNumeric("selectionCoeff")->AddInt("position")->AddIntObject_ON("originSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)->AddIntString_ON("nucleotide", gStaticEidosValueNULL)); methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_containsMarkerMutation, kEidosValueMaskLogical | kEidosValueMaskSingleton | kEidosValueMaskNULL | kEidosValueMaskObject, gSLiM_Mutation_Class))->AddIntObject_S("mutType", gSLiM_MutationType_Class)->AddInt_S("position")->AddLogical_OS("returnMutation", gStaticEidosValue_LogicalF))->DeclareAcceleratedImp(Haplosome::ExecuteMethod_Accelerated_containsMarkerMutation)); methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_containsMutations, kEidosValueMaskLogical))->AddObject("mutations", gSLiM_Mutation_Class))->DeclareAcceleratedImp(Haplosome::ExecuteMethod_Accelerated_containsMutations)); methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_countOfMutationsOfType, kEidosValueMaskInt | kEidosValueMaskSingleton))->AddIntObject_S("mutType", gSLiM_MutationType_Class))->DeclareAcceleratedImp(Haplosome::ExecuteMethod_Accelerated_countOfMutationsOfType)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_positionsOfMutationsOfType, kEidosValueMaskInt))->AddIntObject_S("mutType", gSLiM_MutationType_Class)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_mutationCountsInHaplosomes, kEidosValueMaskInt))->AddObject_ON("mutations", gSLiM_Mutation_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_mutationFrequenciesInHaplosomes, kEidosValueMaskFloat))->AddObject_ON("mutations", gSLiM_Mutation_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_mutationsOfType, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddIntObject_S("mutType", gSLiM_MutationType_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_nucleotides, kEidosValueMaskInt | kEidosValueMaskString))->AddInt_OSN(gEidosStr_start, gStaticEidosValueNULL)->AddInt_OSN(gEidosStr_end, gStaticEidosValueNULL)->AddString_OS("format", EidosValue_String_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("string")))); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_readHaplosomesFromMS, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddString_S(gEidosStr_filePath)->AddIntObject_S("mutationType", gSLiM_MutationType_Class)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_readHaplosomesFromVCF, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddString_S(gEidosStr_filePath)->AddIntObject_OSN("mutationType", gSLiM_MutationType_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_removeMutations, kEidosValueMaskVOID))->AddObject_ON("mutations", gSLiM_Mutation_Class, gStaticEidosValueNULL)->AddLogical_OS("substitute", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_outputHaplosomesToMS, kEidosValueMaskVOID))->AddString_OSN(gEidosStr_filePath, gStaticEidosValueNULL)->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddLogical_OS("filterMonomorphic", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_outputHaplosomesToVCF, kEidosValueMaskVOID))->AddString_OSN(gEidosStr_filePath, gStaticEidosValueNULL)->AddLogical_OS("outputMultiallelics", gStaticEidosValue_LogicalT)->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddLogical_OS("simplifyNucleotides", gStaticEidosValue_LogicalF)->AddLogical_OS("outputNonnucleotides", gStaticEidosValue_LogicalT)->AddLogical_OS("groupAsIndividuals", gStaticEidosValue_LogicalT)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_outputHaplosomes, kEidosValueMaskVOID))->AddString_OSN(gEidosStr_filePath, gStaticEidosValueNULL)->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddLogical_OS("objectTags", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_sumOfMutationsOfType, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddIntObject_S("mutType", gSLiM_MutationType_Class)); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } EidosValue_SP Haplosome_Class::ExecuteClassMethod(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { switch (p_method_id) { case gID_addMutations: return ExecuteMethod_addMutations(p_method_id, p_target, p_arguments, p_interpreter); case gID_addNewDrawnMutation: case gID_addNewMutation: return ExecuteMethod_addNewMutation(p_method_id, p_target, p_arguments, p_interpreter); case gID_mutationCountsInHaplosomes: case gID_mutationFrequenciesInHaplosomes: return ExecuteMethod_mutationFreqsCountsInHaplosomes(p_method_id, p_target, p_arguments, p_interpreter); case gID_outputHaplosomes: case gID_outputHaplosomesToMS: case gID_outputHaplosomesToVCF: return ExecuteMethod_outputX(p_method_id, p_target, p_arguments, p_interpreter); case gID_readHaplosomesFromMS: return ExecuteMethod_readHaplosomesFromMS(p_method_id, p_target, p_arguments, p_interpreter); case gID_readHaplosomesFromVCF: return ExecuteMethod_readHaplosomesFromVCF(p_method_id, p_target, p_arguments, p_interpreter); case gID_removeMutations: return ExecuteMethod_removeMutations(p_method_id, p_target, p_arguments, p_interpreter); default: return super::ExecuteClassMethod(p_method_id, p_target, p_arguments, p_interpreter); } } // ********************* + (void)addMutations(object mutations) // EidosValue_SP Haplosome_Class::ExecuteMethod_addMutations(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_target, p_arguments, p_interpreter) EidosValue *mutations_value = p_arguments[0].get(); // FIXME this method should be optimized for large-scale bulk addition in the same // way that addNewMutation() and addNewDrawnMutation() now are. BCH 29 Oct 2017 int target_size = p_target->Count(); if (target_size == 0) return gStaticEidosValueVOID; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForHaplosomes(p_target); if (!species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): " << "addMutations() requires that all target haplosomes belong to the same species." << EidosTerminate(); species->population_.CheckForDeferralInHaplosomes(p_target, "Haplosome_Class::ExecuteMethod_addMutations"); Community &community = species->community_; // All haplosomes must belong to the same chromosome, and all mutations being added must belong to that chromosome too. // It's important that a mismatch result in an error; attempts to add mutations to chromosomes inconsistently should be flagged. int mutations_count = mutations_value->Count(); Mutation * const *mutations = (Mutation * const *)mutations_value->ObjectData(); Haplosome * const *targets = (Haplosome * const *)p_target->ObjectData(); Haplosome *haplosome_0 = targets[0]; slim_chromosome_index_t chromosome_index = haplosome_0->chromosome_index_; if (species->Chromosomes().size() > 1) { // We have to check for consistency if there's more than one chromosome for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) if (targets[haplosome_index]->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): " << "addMutations() requires that all target haplosomes are associated with the same chromosome." << EidosTerminate(); for (int value_index = 0; value_index < mutations_count; ++value_index) if (mutations[value_index]->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): " << "addMutations() requires that all mutations to be added are associated with the same chromosome as the target haplosomes." << EidosTerminate(); } Chromosome *chromosome = species->Chromosomes()[chromosome_index]; // use the 0th haplosome in the target to find out what the mutation run length is, so we can calculate run indices slim_position_t mutrun_length = haplosome_0->mutrun_length_; // check that the individuals that mutations are being added to have age == 0, in nonWF models, to prevent tree sequence inconsistencies (see issue #102) if ((community.ModelType() == SLiMModelType::kModelTypeNonWF) && species->RecordingTreeSequence()) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets[haplosome_index]; Individual *target_individual = target_haplosome->OwningIndividual(); if (target_individual->age_ > 0) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): " << "addMutations() cannot add mutations to individuals of age > 0 when tree-sequence recording is enabled, to prevent internal inconsistencies." << EidosTerminate(); } } // check for other semantic issues Population &pop = species->population_; species->CheckMutationStackPolicy(); // TIMING RESTRICTION if (!community.warned_early_mutation_add_) { if ((community.CycleStage() == SLiMCycleStage::kWFStage0ExecuteFirstScripts) || (community.CycleStage() == SLiMCycleStage::kWFStage1ExecuteEarlyScripts)) { if (!gEidosSuppressWarnings) { p_interpreter.ErrorOutputStream() << "#WARNING (Haplosome_Class::ExecuteMethod_addMutations): addMutations() should probably not be called from a first() or early() event in a WF model; the added mutation(s) will not influence fitness values during offspring generation." << std::endl; community.warned_early_mutation_add_ = true; } } // Note that there is no equivalent problem in nonWF models, because fitness values are used for survival, // not reproduction, and there is no event stage in the tick cycle that splits fitness from survival. } // TIMING RESTRICTION if (community.executing_species_ == species) { if (community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosModifyChildCallback) { // Check that we're not inside a modifyChild() callback, or if we are, that the only haplosomes being modified belong to the new child. // This prevents problems with retracting the proposed child when tree-sequence recording is enabled; other extraneous changes must // not be backed out, and it's hard to separate, e.g., what's a child-related new mutation from an extraneously added new mutation. // Note that the other Haplosome methods that add/remove mutations perform the same check, and should be maintained in parallel. Individual *focal_modification_child = community.focal_modification_child_; if (focal_modification_child) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets[haplosome_index]; if (target_haplosome->individual_ != focal_modification_child) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): addMutations() cannot be called on the currently executing species from within a modifyChild() callback to modify any haplosomes except those of the focal child being generated." << EidosTerminate(); } } } else if ((community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosRecombinationCallback) || (community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosMutationCallback)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): addMutations() cannot be called on the currently executing species from within a recombination() or mutation() callback." << EidosTerminate(); } // check that the same haplosome is not included more than once as a target, which we don't allow; we use patch_pointer as scratch for (int target_index = 0; target_index < target_size; ++target_index) { Haplosome *target_haplosome = targets[target_index]; if (target_haplosome->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): addMutations() cannot be called on a null haplosome." << EidosTerminate(); target_haplosome->scratch_ = 1; } for (int target_index = 0; target_index < target_size; ++target_index) { Haplosome *target_haplosome = targets[target_index]; if (target_haplosome->scratch_ != 1) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): addMutations() cannot be called on the same haplosome more than once (you must eliminate duplicates in the target vector)." << EidosTerminate(); target_haplosome->scratch_ = 0; } // Construct a vector of mutations to add that is sorted by position std::vector mutations_to_add; for (int value_index = 0; value_index < mutations_count; ++value_index) { Mutation *mut_to_add = mutations[value_index]; if ((mut_to_add->state_ == MutationState::kFixedAndSubstituted) || (mut_to_add->state_ == MutationState::kRemovedWithSubstitution)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): addMutations() cannot add a mutation that has already been fixed/substituted." << EidosTerminate(); mutations_to_add.emplace_back(mut_to_add); } std::sort(mutations_to_add.begin(), mutations_to_add.end(), [ ](Mutation *i1, Mutation *i2) {return i1->position_ < i2->position_;}); // SPECIES CONSISTENCY CHECK if (mutations_count > 0) { Species *mutations_species = Community::SpeciesForMutations(mutations_value); if (mutations_species != species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addMutations): addMutations() requires that all mutations belong to the same species as the target haplosomes." << EidosTerminate(); } // TREE SEQUENCE RECORDING // First, pre-plan the positions of new tree-seq derived states in anticipation of doing the addition. We have to check // whether the mutation being added is already present, to avoid recording a new derived state identical to the old one state. // The algorithm used here, with HaplosomeWalker, depends upon the fact that we just sorted the mutations to add by position. // However, we do still have to think about multiple muts being added at the same position, and existing stacked mutations. bool recording_tree_sequence_mutations = species->RecordingTreeSequenceMutations(); std::vector>> new_derived_state_positions; if (recording_tree_sequence_mutations) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets[haplosome_index]; HaplosomeWalker walker(target_haplosome); slim_position_t last_added_pos = -1; for (Mutation *mut : mutations_to_add) { slim_position_t mut_pos = mut->position_; // We don't care about other mutations at an already recorded position; move on if (mut_pos == last_added_pos) continue; // Advance the walker until it is at or after the mutation's position while (!walker.Finished()) { if (walker.Position() >= mut_pos) break; walker.NextMutation(); } // If walker is finished, or is now after the mutation's position, drop through to add this position if (!walker.Finished() && (walker.Position() == mut_pos)) { // If the mutation is already present, somewhere at this position, then we don't need to record it if (walker.MutationIsStackedAtCurrentPosition(mut)) continue; // The mutation is not already present, so we need to record it; drop through to the addition code } // we have decided that the new derived state at this position will need to be recorded, so note that if (last_added_pos == -1) { // no pair entry in new_derived_state_positions yet, so make a new pair entry for this haplosome new_derived_state_positions.emplace_back(target_haplosome, std::vector(1, mut_pos)); } else { // we have an existing pair entry for this haplosome, so add this position to its position vector std::pair> &haplosome_entry = new_derived_state_positions.back(); std::vector &haplosome_list = haplosome_entry.second; haplosome_list.emplace_back(mut_pos); } last_added_pos = mut_pos; } } } // Now handle the mutations to add, broken into bulk operations according to the mutation run they fall into slim_mutrun_index_t last_handled_mutrun_index = -1; for (int value_index = 0; value_index < mutations_count; ++value_index) { Mutation *next_mutation = mutations_to_add[value_index]; const slim_position_t pos = next_mutation->position_; slim_mutrun_index_t mutrun_index = (slim_mutrun_index_t)(pos / mutrun_length); if (mutrun_index <= last_handled_mutrun_index) continue; // We have not yet processed this mutation run; do this mutation run index as a bulk operation int64_t operation_id = MutationRun::GetNextOperationID(); Haplosome::BulkOperationStart(operation_id, mutrun_index); MutationRunContext &mutrun_context = chromosome->ChromosomeMutationRunContextForMutationRunIndex(mutrun_index); for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets[haplosome_index]; // See if WillModifyRunForBulkOperation() can short-circuit the operation for us MutationRun *target_run = target_haplosome->WillModifyRunForBulkOperation(operation_id, mutrun_index, mutrun_context); if (target_run) { for (int mut_index = value_index; mut_index < mutations_count; ++mut_index) { Mutation *mut_to_add = mutations_to_add[mut_index]; const slim_position_t add_pos = mut_to_add->position_; // since we're in sorted order by position, as soon as we leave the current mutation run we're done if (add_pos / mutrun_length != mutrun_index) break; if (target_run->enforce_stack_policy_for_addition(mut_to_add->position_, mut_to_add->mutation_type_ptr_)) { target_run->insert_sorted_mutation_if_unique(mut_to_add->BlockIndex()); // No need to add the mutation to the registry; how would the user ever get a Mutation that was not already in it? // Similarly, no need to check and set pure_neutral_ and all_pure_neutral_DFE_; the mutation is already in the system } } } } Haplosome::BulkOperationEnd(operation_id, mutrun_index); // now we have handled all mutations at this index (and all previous indices) last_handled_mutrun_index = mutrun_index; // invalidate cached mutation refcounts; refcounts have changed pop.InvalidateMutationReferencesCache(); } // TREE SEQUENCE RECORDING // Now that all the bulk operations are done, record all the new derived states. BCH 6/12/2021: Note that if a mutation to be added // was rejected by stacking policy 'f' above, it will still get recorded here with a new derived state, which will be identical to the // previous derived state. This is maybe a bug, but nobody has complained and it looks hard to fix. People don't use policy 'f' much. if (recording_tree_sequence_mutations) { for (std::pair> &haplosome_pair : new_derived_state_positions) { Haplosome *target_haplosome = haplosome_pair.first; std::vector &haplosome_positions = haplosome_pair.second; for (slim_position_t position : haplosome_positions) species->RecordNewDerivedState(target_haplosome, position, *target_haplosome->derived_mutation_ids_at_position(position)); } } return gStaticEidosValueVOID; } // ********************* + (object)addNewDrawnMutation(io mutationType, integer position, [Nio originSubpop = NULL], [Nis nucleotide = NULL]) // ********************* + (object)addNewMutation(io mutationType, numeric selectionCoeff, integer position, [Nio originSubpop = NULL], [Nis nucleotide = NULL]) // EidosValue_SP Haplosome_Class::ExecuteMethod_addNewMutation(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_target, p_arguments, p_interpreter) #ifdef __clang_analyzer__ assert(((p_method_id == gID_addNewDrawnMutation) && (p_arguments.size() == 5)) || ((p_method_id == gID_addNewMutation) && (p_arguments.size() == 6))); #endif EidosValue *arg_muttype = p_arguments[0].get(); EidosValue *arg_selcoeff = (p_method_id == gID_addNewDrawnMutation ? nullptr : p_arguments[1].get()); EidosValue *arg_position = (p_method_id == gID_addNewDrawnMutation ? p_arguments[1].get() : p_arguments[2].get()); EidosValue *arg_origin_subpop = (p_method_id == gID_addNewDrawnMutation ? p_arguments[2].get() : p_arguments[3].get()); EidosValue *arg_nucleotide = (p_method_id == gID_addNewDrawnMutation ? p_arguments[3].get() : p_arguments[4].get()); int target_size = p_target->Count(); if (target_size == 0) return gStaticEidosValueNULLInvisible; // this is almost an error condition, since a mutation was expected to be added and none was std::string method_name = EidosStringRegistry::StringForGlobalStringID(p_method_id); method_name.append("()"); // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForHaplosomes(p_target); if (!species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires that all target haplosomes belong to the same species." << EidosTerminate(); species->population_.CheckForDeferralInHaplosomes(p_target, "Haplosome_Class::ExecuteMethod_addNewMutation"); Community &community = species->community_; // All haplosomes must belong to the same chromosome. It's important that a mismatch result in an error; // attempts to add mutations to chromosomes inconsistently should be flagged. Haplosome * const *targets = (Haplosome * const *)p_target->ObjectData(); Haplosome *haplosome_0 = targets[0]; slim_chromosome_index_t chromosome_index = haplosome_0->chromosome_index_; if (species->Chromosomes().size() > 1) { // We have to check for consistency if there's more than one chromosome for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) if (targets[haplosome_index]->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires that all target haplosomes are associated with the same chromosome." << EidosTerminate(); } Chromosome *chromosome = species->Chromosomes()[chromosome_index]; // get the 0th haplosome in the target to find out what the mutation run length is, so we can calculate run indices int mutrun_count = haplosome_0->mutrun_count_; slim_position_t mutrun_length = haplosome_0->mutrun_length_; // check that the individuals that mutations are being added to have age == 0, in nonWF models, to prevent tree sequence inconsistencies (see issue #102) if ((community.ModelType() == SLiMModelType::kModelTypeNonWF) && species->RecordingTreeSequence()) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets[haplosome_index]; Individual *target_individual = target_haplosome->OwningIndividual(); if (target_individual->age_ > 0) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " cannot add mutations to individuals of age > 0 when tree-sequence recording is enabled, to prevent internal inconsistencies." << EidosTerminate(); } } // check for other semantic issues Population &pop = species->population_; bool nucleotide_based = species->IsNucleotideBased(); if (!nucleotide_based && (arg_nucleotide->Type() != EidosValueType::kValueNULL)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires nucleotide to be NULL in non-nucleotide-based models." << EidosTerminate(); species->CheckMutationStackPolicy(); // TIMING RESTRICTION if (!community.warned_early_mutation_add_) { if ((community.CycleStage() == SLiMCycleStage::kWFStage0ExecuteFirstScripts) || (community.CycleStage() == SLiMCycleStage::kWFStage1ExecuteEarlyScripts)) { if (!gEidosSuppressWarnings) { p_interpreter.ErrorOutputStream() << "#WARNING (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " should probably not be called from a first() or early() event in a WF model; the added mutation will not influence fitness values during offspring generation." << std::endl; community.warned_early_mutation_add_ = true; } } // Note that there is no equivalent problem in nonWF models, because fitness values are used for survival, // not reproduction, and there is no event stage in the tick cycle that splits fitness from survival. } // TIMING RESTRICTION if (community.executing_species_ == species) { if (community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosModifyChildCallback) { // Check that we're not inside a modifyChild() callback, or if we are, that the only haplosomes being modified belong to the new child. // This prevents problems with retracting the proposed child when tree-sequence recording is enabled; other extraneous changes must // not be backed out, and it's hard to separate, e.g., what's a child-related new mutation from an extraneously added new mutation. // Note that the other Haplosome methods that add/remove mutations perform the same check, and should be maintained in parallel. Individual *focal_modification_child = community.focal_modification_child_; if (focal_modification_child) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets[haplosome_index]; if (target_haplosome->individual_ != focal_modification_child) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " cannot be called on the currently executing species from within a modifyChild() callback to modify any haplosomes except those of the focal child being generated." << EidosTerminate(); } } } else if ((community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosRecombinationCallback) || (community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosMutationCallback)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " cannot be called on the currently executing species from within a recombination() or mutation() callback." << EidosTerminate(); } // position and originSubpop can now be either singletons or vectors of matching length or NULL; check them all int muttype_count = arg_muttype->Count(); int selcoeff_count = (arg_selcoeff ? arg_selcoeff->Count() : 0); int position_count = arg_position->Count(); int origin_subpop_count = arg_origin_subpop->Count(); int nucleotide_count = arg_nucleotide->Count(); if (arg_origin_subpop->Type() == EidosValueType::kValueNULL) origin_subpop_count = 1; if (arg_nucleotide->Type() == EidosValueType::kValueNULL) nucleotide_count = 1; int count_to_add = std::max({muttype_count, selcoeff_count, position_count, origin_subpop_count, nucleotide_count}); if (((muttype_count != 1) && (muttype_count != count_to_add)) || (arg_selcoeff && (selcoeff_count != 1) && (selcoeff_count != count_to_add)) || ((position_count != 1) && (position_count != count_to_add)) || ((origin_subpop_count != 1) && (origin_subpop_count != count_to_add)) || ((nucleotide_count != 1) && (nucleotide_count != count_to_add))) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires that mutationType, " << ((p_method_id == gID_addNewMutation) ? "selectionCoeff, " : "") << "position, originSubpop, and nucleotide be either (1) singleton, or (2) equal in length to the other non-singleton argument(s), or (3) NULL, for originSubpop and nucleotide." << EidosTerminate(); EidosValue_Object_SP retval(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class)); if (count_to_add == 0) return retval; // before proceeding, let's check that all positions supplied are valid, so we don't need to worry about it below // would be better not to call IntAtIndex_NOCAST() multiple times for the same position, but that will not be the majority of our time anyway... slim_position_t last_position = chromosome->last_position_; for (int position_index = 0; position_index < position_count; ++position_index) { slim_position_t position = SLiMCastToPositionTypeOrRaise(arg_position->IntAtIndex_NOCAST(position_index, nullptr)); if (position > last_position) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " position " << position << " is past the end of the chromosome." << EidosTerminate(); } // similarly, check nucleotide values for validity uint8_t *nucleotide_lookup = NucleotideArray::NucleotideCharToIntLookup(); if (arg_nucleotide->Type() == EidosValueType::kValueNULL) { // If nucleotide is NULL, all mutation types supplied must be non-nucleotide-based for (int muttype_index = 0; muttype_index < muttype_count; ++muttype_index) { MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(arg_muttype, muttype_index, &community, species, method_name.c_str()); // SPECIES CONSISTENCY CHECK if (mutation_type_ptr->nucleotide_based_) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires nucleotide to be non-NULL when nucleotide-based mutation types are used." << EidosTerminate(); } } else { // If nucleotide is non-NULL, all mutation types supplied must be nucleotide-based for (int muttype_index = 0; muttype_index < muttype_count; ++muttype_index) { MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(arg_muttype, muttype_index, &community, species, method_name.c_str()); // SPECIES CONSISTENCY CHECK if (!mutation_type_ptr->nucleotide_based_) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires nucleotide to be NULL when non-nucleotide-based mutation types are used." << EidosTerminate(); } // And then nucleotide values must also be within bounds if (arg_nucleotide->Type() == EidosValueType::kValueInt) { for (int nucleotide_index = 0; nucleotide_index < nucleotide_count; ++nucleotide_index) { int64_t nuc_int = arg_nucleotide->IntAtIndex_NOCAST(nucleotide_index, nullptr); if ((nuc_int < 0) || (nuc_int > 3)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires integer nucleotide values to be in [0,3]." << EidosTerminate(); } } else if (arg_nucleotide->Type() == EidosValueType::kValueString) { for (int nucleotide_index = 0; nucleotide_index < nucleotide_count; ++nucleotide_index) { uint8_t nuc = nucleotide_lookup[(unsigned char)(arg_nucleotide->StringAtIndex_NOCAST(nucleotide_index, nullptr)[0])]; if (nuc > 3) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires string nucleotide values to be 'A', 'C', 'G', or 'T'." << EidosTerminate(); } } } // check that the same haplosome is not included more than once as a target, which we don't allow; we use patch_pointer as scratch for (int target_index = 0; target_index < target_size; ++target_index) { Haplosome *target_haplosome = targets[target_index]; if (target_haplosome->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " cannot be called on a null haplosome." << EidosTerminate(); target_haplosome->scratch_ = 1; } for (int target_index = 0; target_index < target_size; ++target_index) { Haplosome *target_haplosome = targets[target_index]; if (target_haplosome->scratch_ != 1) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " cannot be called on the same haplosome more than once (you must eliminate duplicates in the target vector)." << EidosTerminate(); target_haplosome->scratch_ = 0; } // each bulk operation is performed on a single mutation run, so we need to figure out which runs we're influencing std::vector mutrun_indexes; if (mutrun_count == 1) { // if we have just a single mutrun, we can avoid the sorting and uniquing; all valid positions are in mutrun 0 mutrun_indexes.emplace_back(0); } else { for (int pos_index = 0; pos_index < position_count; ++pos_index) { slim_position_t position = SLiMCastToPositionTypeOrRaise(arg_position->IntAtIndex_NOCAST(pos_index, nullptr)); mutrun_indexes.emplace_back((slim_mutrun_index_t)(position / mutrun_length)); } std::sort(mutrun_indexes.begin(), mutrun_indexes.end()); mutrun_indexes.resize(std::distance(mutrun_indexes.begin(), std::unique(mutrun_indexes.begin(), mutrun_indexes.end()))); } // for the singleton case for each of the parameters, get all the info MutationType *singleton_mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(arg_muttype, 0, &community, species, method_name.c_str()); // SPECIES CONSISTENCY CHECK double singleton_selection_coeff = (arg_selcoeff ? arg_selcoeff->NumericAtIndex_NOCAST(0, nullptr) : 0.0); slim_position_t singleton_position = SLiMCastToPositionTypeOrRaise(arg_position->IntAtIndex_NOCAST(0, nullptr)); slim_tick_t origin_tick = community.Tick(); slim_objectid_t singleton_origin_subpop_id; if (arg_origin_subpop->Type() == EidosValueType::kValueNULL) { singleton_origin_subpop_id = -1; // We set the origin subpopulation based on the first haplosome in the target if (target_size >= 1) { Haplosome *first_target = targets[0]; singleton_origin_subpop_id = first_target->individual_->subpopulation_->subpopulation_id_; } } else if (arg_origin_subpop->Type() == EidosValueType::kValueInt) { singleton_origin_subpop_id = SLiMCastToObjectidTypeOrRaise(arg_origin_subpop->IntAtIndex_NOCAST(0, nullptr)); } else { Subpopulation *origin_subpop = ((Subpopulation *)(arg_origin_subpop->ObjectElementAtIndex_NOCAST(0, nullptr))); // SPECIES CONSISTENCY CHECK if (&origin_subpop->species_ != species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_addNewMutation): " << method_name << " requires that originSubpop belong to the same species as the target haplosomes." << EidosTerminate(); singleton_origin_subpop_id = origin_subpop->subpopulation_id_; } int64_t singleton_nucleotide; if (arg_nucleotide->Type() == EidosValueType::kValueNULL) singleton_nucleotide = -1; else if (arg_nucleotide->Type() == EidosValueType::kValueInt) singleton_nucleotide = arg_nucleotide->IntAtIndex_NOCAST(0, nullptr); else singleton_nucleotide = nucleotide_lookup[(unsigned char)(arg_nucleotide->StringAtIndex_NOCAST(0, nullptr)[0])]; // ok, now loop to add the mutations in a single bulk operation per mutation run bool recording_tree_sequence_mutations = species->RecordingTreeSequenceMutations(); for (slim_mutrun_index_t mutrun_index : mutrun_indexes) { int64_t operation_id = MutationRun::GetNextOperationID(); std::vector mutations_to_add; // Before starting the bulk operation for this mutation run, construct all of the mutations and add them all to the registry, etc. // It is possible that some mutations will not actually be added to any haplosome, due to stacking; they will be cleared from the // registry as lost mutations in the next cycle. All mutations are returned to the user, whether actually added or not. MutationType *mutation_type_ptr = singleton_mutation_type_ptr; double selection_coeff = singleton_selection_coeff; slim_position_t position = singleton_position; slim_objectid_t origin_subpop_id = singleton_origin_subpop_id; int64_t nucleotide = singleton_nucleotide; for (int mut_parameter_index = 0; mut_parameter_index < count_to_add; ++mut_parameter_index) { if (position_count != 1) position = SLiMCastToPositionTypeOrRaise(arg_position->IntAtIndex_NOCAST(mut_parameter_index, nullptr)); // check that this mutation will be added to this mutation run if (position / mutrun_length == mutrun_index) { if (muttype_count != 1) mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(arg_muttype, mut_parameter_index, &community, species, method_name.c_str()); // SPECIES CONSISTENCY CHECK if (selcoeff_count != 1) { if (arg_selcoeff) selection_coeff = arg_selcoeff->NumericAtIndex_NOCAST(mut_parameter_index, nullptr); else selection_coeff = mutation_type_ptr->DrawSelectionCoefficient(); } if (origin_subpop_count != 1) { if (arg_origin_subpop->Type() == EidosValueType::kValueInt) origin_subpop_id = SLiMCastToObjectidTypeOrRaise(arg_origin_subpop->IntAtIndex_NOCAST(mut_parameter_index, nullptr)); else origin_subpop_id = ((Subpopulation *)(arg_origin_subpop->ObjectElementAtIndex_NOCAST(mut_parameter_index, nullptr)))->subpopulation_id_; } if (nucleotide_count != 1) { // Already checked for validity above if (arg_nucleotide->Type() == EidosValueType::kValueInt) nucleotide = arg_nucleotide->IntAtIndex_NOCAST(mut_parameter_index, nullptr); else nucleotide = nucleotide_lookup[(unsigned char)(arg_nucleotide->StringAtIndex_NOCAST(mut_parameter_index, nullptr)[0])]; } MutationIndex new_mut_index = SLiM_NewMutationFromBlock(); Mutation *new_mut = new (gSLiM_Mutation_Block + new_mut_index) Mutation(mutation_type_ptr, chromosome->Index(), position, selection_coeff, origin_subpop_id, origin_tick, (int8_t)nucleotide); // This mutation type might not be used by any genomic element type (i.e. might not already be vetted), so we need to check and set pure_neutral_ // The selection coefficient might have been supplied by the user (i.e., not be from the mutation type's DFE), so we set all_pure_neutral_DFE_ also if (selection_coeff != 0.0) { species->pure_neutral_ = false; mutation_type_ptr->all_pure_neutral_DFE_ = false; } // add to the registry, return value, haplosome, etc. if (new_mut->state_ != MutationState::kInRegistry) pop.MutationRegistryAdd(new_mut); retval->push_object_element_RR(new_mut); mutations_to_add.emplace_back(new_mut_index); } } // BCH 18 January 2020: If a vector of positions was provided, mutations_to_add might be out of sorted // order, which is expected below by clear_set_and_merge(), so we sort here if ((position_count != 1) && (mutations_to_add.size() > 1)) { Mutation *mut_block_ptr = gSLiM_Mutation_Block; std::sort(mutations_to_add.begin(), mutations_to_add.end(), [mut_block_ptr](MutationIndex i1, MutationIndex i2) {return (mut_block_ptr + i1)->position_ < (mut_block_ptr + i2)->position_;}); } // Now start the bulk operation and add mutations_to_add to every target haplosome Haplosome::BulkOperationStart(operation_id, mutrun_index); MutationRunContext &mutrun_context = chromosome->ChromosomeMutationRunContextForMutationRunIndex(mutrun_index); for (int target_index = 0; target_index < target_size; ++target_index) { Haplosome *target_haplosome = targets[target_index]; // See if WillModifyRunForBulkOperation() can short-circuit the operation for us const MutationRun *original_run = target_haplosome->mutruns_[mutrun_index]; MutationRun *modifiable_mutrun = target_haplosome->WillModifyRunForBulkOperation(operation_id, mutrun_index, mutrun_context); if (modifiable_mutrun) { // We merge the original run (which has not yet been freed!) and mutations_to_add into modifiable_mutrun modifiable_mutrun->clear_set_and_merge(*original_run, mutations_to_add); } // TREE SEQUENCE RECORDING // whether WillModifyRunForBulkOperation() short-circuited the addition or not, we need to notify the tree seq code // BCH 6/12/2021: We only need to record a derived state once per position, even if there were multiple adds at that position. // This prevents redundant derived states from being recorded; see discussion in https://github.com/MesserLab/SLiM/issues/195 if (recording_tree_sequence_mutations) { MutationIndex *muts = mutations_to_add.data(); MutationIndex *muts_end = muts + mutations_to_add.size(); slim_position_t previous_position = -1; while (muts != muts_end) { Mutation *mut = gSLiM_Mutation_Block + *(muts++); slim_position_t pos = mut->position_; if (pos != previous_position) { species->RecordNewDerivedState(target_haplosome, pos, *target_haplosome->derived_mutation_ids_at_position(pos)); previous_position = pos; } } } } Haplosome::BulkOperationEnd(operation_id, mutrun_index); // invalidate cached mutation refcounts; refcounts have changed pop.InvalidateMutationReferencesCache(); } return retval; } // ********************* + (float)mutationFrequenciesInHaplosomes([No mutations = NULL]) // ********************* + (integer)mutationCountsInHaplosomes([No mutations = NULL]) // EidosValue_SP Haplosome_Class::ExecuteMethod_mutationFreqsCountsInHaplosomes(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *mutations_value = p_arguments[0].get(); // get our target vector, handle the zero-length case, and do a pre-check for null haplosomes so we don't have to worry about it later on slim_refcount_t target_size = (slim_refcount_t)p_target->Count(); if (target_size == 0) { // With a zero-length target, frequencies are undefined so it is an error; to keep life simple, we make it an error for counts too EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_mutationFreqsCountsInHaplosomes): " << EidosStringRegistry::StringForGlobalStringID(p_method_id) << "() cannot calculate counts/frequencies in a zero-length Haplosome vector (divide by zero)." << EidosTerminate(); } THREAD_SAFETY_IN_ACTIVE_PARALLEL("Haplosome_Class::ExecuteMethod_mutationFreqsCountsInHaplosomes(): usage of statics"); Haplosome * const *target_data = (Haplosome * const *)p_target->ObjectData(); for (int target_index = 0; target_index < target_size; ++target_index) if (target_data[target_index]->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_mutationFreqsCountsInHaplosomes): " << EidosStringRegistry::StringForGlobalStringID(p_method_id) << "() cannot be called on a null haplosome." << EidosTerminate(); // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForHaplosomesVector(target_data, target_size); if (!species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_mutationFreqsCountsInHaplosomes): " << EidosStringRegistry::StringForGlobalStringID(p_method_id) << "() requires that all target haplosomes belong to a single species." << EidosTerminate(); if (mutations_value->Count() >= 1) { Species *mut_species = Community::SpeciesForMutations(mutations_value); if (mut_species != species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_mutationFreqsCountsInHaplosomes): " << EidosStringRegistry::StringForGlobalStringID(p_method_id) << "() requires that all mutations belong to the same species as the target haplosomes." << EidosTerminate(); } // Note that we allow the haplosomes and mutations to be associated with more than one chromosome here; // you can pass a mix, and for each mutation you get its frequency in the haplosomes for its chromosome. // If you pass NULL, all mutations are used, which can be confusing in the multi-chromosome case if you // passed a vector of haplosomes that are all for a specific chromosome. In that case, you shouldn't // pass NULL for mutations, but rather use sim.subsetMutations() to get the mutations for your focal // chromosome, probably. species->population_.CheckForDeferralInHaplosomes(p_target, "Haplosome_Class::ExecuteMethod_mutationFreqsCountsInHaplosomes"); Population &population = species->population_; // Have the Population tally for the target haplosomes population.TallyMutationReferencesAcrossHaplosomes(target_data, target_size); // Use the back-end code in Population to do the counting; TallyMutationReferencesAcrossHaplosomes() // should have set the total haplosome count correctly for the given haplosome sample. Note that a // sample of mutations can be passed that belongs to a variety of different chromosomes; in this case, // each chromosome's total haplosome count should reflect the number of haplosomes in the sample // that belong to that chromosome, so the frequencies should be correct in that sense. if (p_method_id == gID_mutationFrequenciesInHaplosomes) return population.Eidos_FrequenciesForTalliedMutations(mutations_value); else return population.Eidos_CountsForTalliedMutations(mutations_value); } // ********************* + (void)outputHaplosomes([Ns$ filePath = NULL], [logical$ append=F], [logical$ objectTags = F]) // ********************* + (void)outputHaplosomesToMS([Ns$ filePath = NULL], [logical$ append=F], [logical$ filterMonomorphic = F]) // ********************* + (void)outputHaplosomesToVCF([Ns$ filePath = NULL], [logical$ outputMultiallelics = T], [logical$ append=F], [logical$ simplifyNucleotides = F], [logical$ outputNonnucleotides = T], [logical$ groupAsIndividuals = T]) // EidosValue_SP Haplosome_Class::ExecuteMethod_outputX(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_target, p_arguments, p_interpreter) EidosValue *filePath_value = p_arguments[0].get(); EidosValue *outputMultiallelics_value = ((p_method_id == gID_outputHaplosomesToVCF) ? p_arguments[1].get() : nullptr); EidosValue *append_value = ((p_method_id == gID_outputHaplosomesToVCF) ? p_arguments[2].get() : p_arguments[1].get()); EidosValue *filterMonomorphic_value = ((p_method_id == gID_outputHaplosomesToMS) ? p_arguments[2].get() : nullptr); EidosValue *simplifyNucleotides_value = ((p_method_id == gID_outputHaplosomesToVCF) ? p_arguments[3].get() : nullptr); EidosValue *outputNonnucleotides_value = ((p_method_id == gID_outputHaplosomesToVCF) ? p_arguments[4].get() : nullptr); EidosValue *groupAsIndividuals_value = ((p_method_id == gID_outputHaplosomesToVCF) ? p_arguments[5].get() : nullptr); EidosValue *objectTags_value = ((p_method_id == gID_outputHaplosomes) ? p_arguments[2].get() : nullptr); // default to outputting multiallelic positions (used by VCF output only) bool output_multiallelics = true; if (p_method_id == gID_outputHaplosomesToVCF) output_multiallelics = outputMultiallelics_value->LogicalAtIndex_NOCAST(0, nullptr); bool simplify_nucs = false; if (p_method_id == gID_outputHaplosomesToVCF) simplify_nucs = simplifyNucleotides_value->LogicalAtIndex_NOCAST(0, nullptr); bool output_nonnucs = true; if (p_method_id == gID_outputHaplosomesToVCF) output_nonnucs = outputNonnucleotides_value->LogicalAtIndex_NOCAST(0, nullptr); bool group_as_individuals = true; if (p_method_id == gID_outputHaplosomesToVCF) group_as_individuals = groupAsIndividuals_value->LogicalAtIndex_NOCAST(0, nullptr); // figure out if we're filtering out mutations that are monomorphic within the sample (MS output only) bool filter_monomorphic = false; if (p_method_id == gID_outputHaplosomesToMS) filter_monomorphic = filterMonomorphic_value->LogicalAtIndex_NOCAST(0, nullptr); bool output_object_tags = (objectTags_value ? objectTags_value->LogicalAtIndex_NOCAST(0, nullptr) : false); // Get all the haplosomes we're sampling from p_target; they must all be in the same species, which we determine here // We require at least one haplosome because otherwise we can't determine the species int sample_size = p_target->Count(); std::vector haplosomes; if (sample_size <= 0) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_outputX): output of a zero-length haplosome vector is illegal; at least one haplosome is required for output." << EidosTerminate(); Haplosome **target_haplosomes = (Haplosome **)p_target->ObjectData(); Species *species = &target_haplosomes[0]->individual_->subpopulation_->species_; for (int index = 0; index < sample_size; ++index) { Haplosome *haplosome = target_haplosomes[index]; Species *haplosome_species = &haplosome->individual_->subpopulation_->species_; if (species != haplosome_species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_outputX): all haplosomes for output must belong to the same species." << EidosTerminate(); haplosomes.emplace_back(haplosome); } species->population_.CheckForDeferralInHaplosomes(p_target, "Haplosome_Class::ExecuteMethod_outputX"); Community &community = species->community_; // We infer the chromosome from the haplosomes, and in a multi-chrom species all the haplosomes must belong to it. slim_chromosome_index_t chromosome_index = haplosomes[0]->chromosome_index_; const std::vector &chromosomes = species->Chromosomes(); Chromosome *chromosome = chromosomes[chromosome_index]; if (chromosomes.size() > 1) { for (Haplosome *haplosome : haplosomes) if (haplosome->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_outputX): all haplosomes for output must be associated with the same chromosome." << EidosTerminate(); } // Now handle stream/file output and dispatch to the actual print method if (filePath_value->Type() == EidosValueType::kValueNULL) { // before writing anything, erase a progress line if we've got one up, to try to make a clean slate Eidos_EraseProgress(); // If filePath is NULL, output to our output stream std::ostream &output_stream = p_interpreter.ExecutionOutputStream(); // For the output stream, we put out a descriptive SLiM-style header for all output types // BCH 2/2/2025: added the cycle count here after the tick; it was already documented as being here! // BCH 2/2/2025: added the chromosome symbol in the header; it is redundant for SLiM-format output, // but useful for MS and VCF; I decided to put it in all three for consistency across formats // BCH 2/7/2025: changed GS/GM/GV to HS/HM/HV, for the genome -> haplosome transition output_stream << "#OUT: " << community.Tick() << " " << species->Cycle() << " H"; if (p_method_id == gID_outputHaplosomes) output_stream << "S"; else if (p_method_id == gID_outputHaplosomesToMS) output_stream << "M"; else if (p_method_id == gID_outputHaplosomesToVCF) output_stream << "V"; output_stream << " " << sample_size; if (chromosomes.size() > 1) { output_stream << " " << chromosome->Type(); // chromosome type, with >1 chromosome output_stream << " \"" << chromosome->Symbol() << "\""; // chromosome symbol, with >1 chromosome } output_stream << std::endl; // Call out to print the actual sample if (p_method_id == gID_outputHaplosomes) Haplosome::PrintHaplosomes_SLiM(output_stream, haplosomes, output_object_tags); else if (p_method_id == gID_outputHaplosomesToMS) Haplosome::PrintHaplosomes_MS(output_stream, haplosomes, *chromosome, filter_monomorphic); else if (p_method_id == gID_outputHaplosomesToVCF) Haplosome::PrintHaplosomes_VCF(output_stream, haplosomes, *chromosome, group_as_individuals, output_multiallelics, simplify_nucs, output_nonnucs); } else { // Otherwise, output to filePath std::string outfile_path = Eidos_ResolvedPath(filePath_value->StringAtIndex_NOCAST(0, nullptr)); bool append = append_value->LogicalAtIndex_NOCAST(0, nullptr); std::ofstream outfile; outfile.open(outfile_path.c_str(), append ? (std::ios_base::app | std::ios_base::out) : std::ios_base::out); if (outfile.is_open()) { switch (p_method_id) { case gID_outputHaplosomes: // For file output, we put out the descriptive SLiM-style header only for SLiM-format output // BCH 2/2/2025: added the cycle count here after the tick; it was already documented as being here! // BCH 2/2/2025: added the chromosome symbol in the header; it is redundant for SLiM-format output, // but useful for MS and VCF; I decided to put it in all three for consistency across formats // BCH 2/7/2025: changed GS/GM/GV to HS/HM/HV, for the genome -> haplosome transition outfile << "#OUT: " << community.Tick() << " " << species->Cycle() << " HS " << sample_size; if (chromosomes.size() > 1) { outfile << " " << chromosome->Type(); // chromosome type, with >1 chromosome outfile << " \"" << chromosome->Symbol() << "\""; // chromosome symbol, with >1 chromosome } outfile << " " << outfile_path << std::endl; Haplosome::PrintHaplosomes_SLiM(outfile, haplosomes, output_object_tags); break; case gID_outputHaplosomesToMS: Haplosome::PrintHaplosomes_MS(outfile, haplosomes, *chromosome, filter_monomorphic); break; case gID_outputHaplosomesToVCF: Haplosome::PrintHaplosomes_VCF(outfile, haplosomes, *chromosome, group_as_individuals, output_multiallelics, simplify_nucs, output_nonnucs); break; default: EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_outputX): (internal error) unhandled case." << EidosTerminate(); } outfile.close(); } else { EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_outputX): could not open " << outfile_path << "." << EidosTerminate(); } } return gStaticEidosValueVOID; } // ********************* + (o)readHaplosomesFromMS(s$ filePath = NULL, io mutationType) // EidosValue_SP Haplosome_Class::ExecuteMethod_readHaplosomesFromMS(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_interpreter) THREAD_SAFETY_IN_ACTIVE_PARALLEL("Haplosome_Class::ExecuteMethod_readHaplosomesFromMS(): SLiM global state read"); EidosValue *filePath_value = p_arguments[0].get(); EidosValue *mutationType_value = p_arguments[1].get(); Community &community = SLiM_GetCommunityFromInterpreter(p_interpreter); std::string file_path = Eidos_ResolvedPath(Eidos_StripTrailingSlash(filePath_value->StringAtIndex_NOCAST(0, nullptr))); MutationType *mutation_type_ptr = nullptr; if (mutationType_value->Type() != EidosValueType::kValueNULL) mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutationType_value, 0, &community, nullptr, "ExecuteMethod_readHaplosomesFromMS()"); // this dictates the focal species if (!mutation_type_ptr) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): mutation type not found." << EidosTerminate(); // Get the species of interest from the mutation type; we will check that all target haplosomes belong to it below Species &species = mutation_type_ptr->species_; Population &pop = species.population_; bool recording_mutations = species.RecordingTreeSequenceMutations(); bool nucleotide_based = species.IsNucleotideBased(); int target_size = p_target->Count(); if (target_size <= 0) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): readHaplosomesFromMS() requires at least one target haplosome." << EidosTerminate(); // SPECIES CONSISTENCY CHECK Species *target_species = Community::SpeciesForHaplosomes(p_target); if (target_species != &species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): readHaplosomesFromMS() requires that all target haplosomes belong to the same species." << EidosTerminate(); species.population_.CheckForDeferralInHaplosomes(p_target, "Haplosome_Class::ExecuteMethod_readHaplosomesFromMS"); // For MS input, we need to know the chromosome to calculate positions from the normalized interval [0, 1]. // We infer it from the haplosomes, and in a multi-chromosome species all the haplosomes must belong to it. Haplosome * const *targets_data = (Haplosome * const *)p_target->ObjectData(); slim_chromosome_index_t chromosome_index = targets_data[0]->chromosome_index_; const std::vector &chromosomes = species.Chromosomes(); Chromosome *chromosome = chromosomes[chromosome_index]; if (chromosomes.size() > 1) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) if (targets_data[haplosome_index]->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): for readHaplosomesFromMS(), all target haplosomes must be associated with the same chromosome." << EidosTerminate(); } slim_position_t last_position = chromosome->last_position_; // Parse the whole input file and retain the information from it std::ifstream infile(file_path); if (!infile) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): could not read file at path " << file_path << "." << EidosTerminate(); std::string line, sub; int parse_state = 0; int segsites = -1; std::vector positions; std::vector calls; while (!infile.eof()) { getline(infile, line); if ((line.length() == 0) || (line.find("//") == 0)) continue; switch (parse_state) { case 0: { // Expecting "segsites: x" std::istringstream iss(line); iss >> sub; if (sub != "segsites:") EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): expecting 'segsites:', found '" << sub << "'." << EidosTerminate(); if (!(iss >> sub)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): missing segsites value." << EidosTerminate(); int64_t segsites_long = EidosInterpreter::NonnegativeIntegerForString(sub, nullptr); if ((segsites_long <= 0) || (segsites_long > 1000000)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): readMS() requires segsites in (0,1000000]." << EidosTerminate(); segsites = (int)segsites_long; if (iss >> sub) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): malformed segsites line; additional content after segsites value." << EidosTerminate(); parse_state = 1; break; } case 1: { // Expecting "positions: a b c..." std::istringstream iss(line); iss >> sub; if (sub != "positions:") EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): expecting 'positions:', found '" << sub << "'." << EidosTerminate(); for (int pos_index = 0; pos_index < segsites; ++pos_index) { if (!(iss >> sub)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): missing positions value." << EidosTerminate(); double pos_double = EidosInterpreter::FloatForString(sub, nullptr); if ((pos_double < 0.0) || (pos_double > 1.0)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): readMS() requires positions in [0,1]." << EidosTerminate(); // BCH 26 Jan. 2020: There is a little subtlety here. This equation, round(pos * L), provides // the exact inverse of what outputHaplosomesToMS() / outputMSSample() do, so it should exactly recover // positions written out by SLiM in MS format (modulo numerical error). However, it results // in half as much "mutational density" at positions 0 and L as at other positions, if the // positions are uniformly distributed in [0,1] rather than originating in SLiM. In that case, // min(floor(pos*(L+1)), L) would be better. Maybe this choice ought to be an optional logical // parameter to readHaplosomesFromMS(), but nobody has complained yet, so I'm ignoring it for now; if // you expect to get exact discrete base positions you shouldn't be using MS format anyway... positions.emplace_back((slim_position_t)round(pos_double * last_position)); } if (iss >> sub) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): malformed positions line; additional content after last expected position." << EidosTerminate(); parse_state = 2; break; } case 2: { // Expecting "001010011001101111010..." of length segsites if (line.find_first_not_of("01") != std::string::npos) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): call lines must be composed entirely of 0 and 1." << EidosTerminate(); if ((int)line.length() != segsites) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): call lines must be equal in length to the segsites value." << EidosTerminate(); calls.emplace_back(line); break; } default: EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): (internal error) unhandled case." << EidosTerminate(); } } infile.close(); if ((int)calls.size() != target_size) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): target haplosome vector has size " << target_size << " but " << calls.size() << " call lines found." << EidosTerminate(); // Instantiate the mutations; NOTE THAT THE STACKING POLICY IS NOT CHECKED HERE, AS THIS IS NOT CONSIDERED THE ADDITION OF A MUTATION! std::vector mutation_indices; EidosRNG_32_bit &rng_32 = EIDOS_32BIT_RNG(omp_get_thread_num()); for (int mut_index = 0; mut_index < segsites; ++mut_index) { slim_position_t position = positions[mut_index]; double selection_coeff = mutation_type_ptr->DrawSelectionCoefficient(); slim_objectid_t subpop_index = -1; slim_tick_t origin_tick = community.Tick(); int8_t nucleotide = -1; if (nucleotide_based && mutation_type_ptr->nucleotide_based_) { // select a nucleotide that is different from the ancestral state at this position int8_t ancestral = (int8_t)chromosome->AncestralSequence()->NucleotideAtIndex(position); nucleotide = (int8_t)Eidos_rng_interval_uint32(rng_32, 3); // 0, 1, 2 if (nucleotide == ancestral) nucleotide++; } MutationIndex new_mut_index = SLiM_NewMutationFromBlock(); Mutation *new_mut = new (gSLiM_Mutation_Block + new_mut_index) Mutation(mutation_type_ptr, chromosome->Index(), position, selection_coeff, subpop_index, origin_tick, nucleotide); // This mutation type might not be used by any genomic element type (i.e. might not already be vetted), so we need to check and set pure_neutral_ if (selection_coeff != 0.0) { species.pure_neutral_ = false; // the selection coefficient was drawn from the mutation type's DFE, so there is no need to set all_pure_neutral_DFE_ //mutation_type_ptr->all_pure_neutral_DFE_ = false; } // add it to our local map, so we can find it when making haplosomes, and to the population's mutation registry pop.MutationRegistryAdd(new_mut); mutation_indices.emplace_back(new_mut_index); } // Sort the mutations by position so we can add them in order, and make an "order" vector for accessing calls in the sorted order Mutation *mut_block_ptr = gSLiM_Mutation_Block; std::vector order_vec = EidosSortIndexes(positions); std::sort(mutation_indices.begin(), mutation_indices.end(), [mut_block_ptr](MutationIndex i1, MutationIndex i2) {return (mut_block_ptr + i1)->position_ < (mut_block_ptr + i2)->position_;}); // Add the mutations to the target haplosomes, recording a new derived state with each addition #ifndef _OPENMP MutationRunContext &mutrun_context = chromosome->ChromosomeMutationRunContextForThread(omp_get_thread_num()); // when not parallel, we have only one MutationRunContext #endif for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *haplosome = targets_data[haplosome_index]; if (haplosome->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromMS): readHaplosomesFromMS() does not allow null haplosomes in the target haplosome vector." << EidosTerminate(); bool haplosome_started_empty = (haplosome->mutation_count() == 0); slim_position_t mutrun_length = haplosome->mutrun_length_; slim_mutrun_index_t current_run_index = -1; MutationRun *current_mutrun = nullptr; std::string &haplosome_string = calls[haplosome_index]; for (int segsite_index = 0; segsite_index < segsites; ++segsite_index) { int64_t call_index = order_vec[segsite_index]; char call = haplosome_string[call_index]; if (call == '1') { MutationIndex mut_index = mutation_indices[segsite_index]; Mutation *mut = mut_block_ptr + mut_index; slim_position_t mut_pos = mut->position_; slim_mutrun_index_t mut_mutrun_index = (slim_mutrun_index_t)(mut_pos / mutrun_length); if (mut_mutrun_index != current_run_index) { #ifdef _OPENMP // When parallel, the MutationRunContext depends upon the position in the haplosome MutationRunContext &mutrun_context = species.ChromosomeMutationRunContextForMutationRunIndex(mut_mutrun_index); #endif current_run_index = mut_mutrun_index; // We use WillModifyRun() because these are existing haplosomes we didn't create, and their runs may be shared; we have // no way to tell. We avoid making excessive mutation run copies by calling this only once per mutrun per haplosome. current_mutrun = haplosome->WillModifyRun(mut_mutrun_index, mutrun_context); } // If the haplosome started empty, we can add mutations to the end with emplace_back(); if it did not, then they need to be inserted if (haplosome_started_empty) current_mutrun->emplace_back(mut_index); else current_mutrun->insert_sorted_mutation(mut_index); if (recording_mutations) species.RecordNewDerivedState(haplosome, mut_pos, *haplosome->derived_mutation_ids_at_position(mut_pos)); } } } // Return the instantiated mutations int mutation_count = (int)mutation_indices.size(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class))->resize_no_initialize_RR(mutation_count); for (int mut_index = 0; mut_index < mutation_count; ++mut_index) vec->set_object_element_no_check_no_previous_RR(mut_block_ptr + mutation_indices[mut_index], mut_index); return EidosValue_Object_SP(vec); } // ********************* + (o)readHaplosomesFromVCF(s$ filePath = NULL, [Nio mutationType = NULL]) // EidosValue_SP Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_interpreter) // BEWARE: This method shares a great deal of code with Individual_Class::ExecuteMethod_readIndividualsFromVCF(). Maintain in parallel. THREAD_SAFETY_IN_ACTIVE_PARALLEL("Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF(): SLiM global state read"); EidosValue *filePath_value = p_arguments[0].get(); EidosValue *mutationType_value = p_arguments[1].get(); // SPECIES CONSISTENCY CHECK if (p_target->Count() == 0) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): " << "readHaplosomesFromVCF() requires a target Haplosome vector of length 1 or more, so that the species of the target can be determined." << EidosTerminate(); Species *species = Community::SpeciesForHaplosomes(p_target); if (!species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): " << "readHaplosomesFromVCF() requires that all target haplosomes belong to the same species." << EidosTerminate(); species->population_.CheckForDeferralInHaplosomes(p_target, "Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF"); // All haplosomes must belong to the same chromosome, and in multichrom models the CHROM field must match its symbol const std::vector &chromosomes = species->Chromosomes(); bool model_is_multi_chromosome = (chromosomes.size() > 1); Haplosome * const *targets_data = (Haplosome * const *)p_target->ObjectData(); int target_size = p_target->Count(); Haplosome *haplosome_0 = targets_data[0]; slim_chromosome_index_t chromosome_index = haplosome_0->chromosome_index_; Chromosome *chromosome = chromosomes[chromosome_index]; std::string chromosome_symbol = chromosome->Symbol(); if (species->Chromosomes().size() > 1) { // We have to check for consistency if there's more than one chromosome for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) if (targets_data[haplosome_index]->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): " << "readHaplosomesFromVCF() requires that all target haplosomes are associated with the same chromosome." << EidosTerminate(); } Community &community = species->community_; Population &pop = species->population_; slim_position_t last_position = chromosome->last_position_; bool recording_mutations = species->RecordingTreeSequenceMutations(); bool nucleotide_based = species->IsNucleotideBased(); std::string file_path = Eidos_ResolvedPath(Eidos_StripTrailingSlash(filePath_value->StringAtIndex_NOCAST(0, nullptr))); MutationType *default_mutation_type_ptr = nullptr; if (mutationType_value->Type() != EidosValueType::kValueNULL) default_mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutationType_value, 0, &community, species, "readHaplosomesFromVCF()"); // SPECIES CONSISTENCY CHECK // Parse the whole input file and retain the information from it std::ifstream infile(file_path); if (!infile) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): could not read file at path " << file_path << "." << EidosTerminate(); std::string line, sub; int parse_state = 0; int sample_id_count = 0; bool info_MID_defined = false, info_S_defined = false, info_DOM_defined = false, info_PO_defined = false; bool info_GO_defined = false, info_TO_defined = false, info_MT_defined = false, /*info_AA_defined = false,*/ info_NONNUC_defined = false; std::vector> call_lines; while (!infile.eof()) { getline(infile, line); switch (parse_state) { case 0: { // In header, parsing ## lines, until we get to the #CHROM line; the point of this is that we only want to interpret // INFO fields like MID, S, etc. as having their SLiM-specific meaning if their SLiM-specific definition is present if (line.compare(0, 2, "##") == 0) { if (line == "##INFO=") info_MID_defined = true; if (line == "##INFO=") info_S_defined = true; if (line == "##INFO=") info_DOM_defined = true; if (line == "##INFO=") info_PO_defined = true; if (line == "##INFO=") info_GO_defined = true; if (line == "##INFO=") info_TO_defined = true; // SLiM 4 emits TO (tick) instead of GO (generation) if (line == "##INFO=") info_MT_defined = true; /*if (line == "##INFO=") info_AA_defined = true;*/ // this one is standard, so we don't require this definition if (line == "##INFO=") info_NONNUC_defined = true; } else if (line.compare(0, 1, "#") == 0) { static const char *header_fields[9] = {"CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT"}; std::istringstream iss(line); iss.get(); // eat the initial # // verify that the expected standard columns are present for (const char *header_field : header_fields) { if (!(iss >> sub)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): missing VCF header '" << header_field << "'." << EidosTerminate(); if (sub != header_field) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): expected VCF header '" << header_field << "', saw '" << sub << "'." << EidosTerminate(); } // the remaining columns are sample IDs; we don't care what they are, we just count them while (iss >> sub) sample_id_count++; // now the remainder of the file should be call lines parse_state = 1; } else { EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): unexpected line in VCF header: '" << line << "'." << EidosTerminate(); } break; } case 1: { // In call lines, fields are separated by tabs, and could theoretically contain spaces; here we just read a whole line, // extract the position field for the mutation, and save the line indexed by its mutation's position for later handling if (line.length() == 0) break; std::istringstream iss(line); std::getline(iss, sub, '\t'); // CHROM if (model_is_multi_chromosome) { // in multi-chromosome models the CHROM value must match the associated chromosome of the haplosomes if (sub != chromosome_symbol) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): the CHROM field's value (\"" << sub << "\") in a call line does not match the symbol (\"" << chromosome_symbol << "\") for the focal chromosome with which the target haplosomes are associated. In multi-chromosome models, the CHROM field is required to match the chromosome symbol to prevent bugs." << EidosTerminate(); } else { // in single-chromosome models the CHROM value must be consistent across the whole file, but need not match if (call_lines.size() == 0) chromosome_symbol = sub; // first call line's CHROM symbol gets remembered else if (sub != chromosome_symbol) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): the CHROM field's value (\"" << sub << "\") in a call line does not match the initial CHROM field's value (\"" << chromosome_symbol << "\"). In single-chromosome models, the CHROM field is required to have a single consistent value across all call lines to prevent bugs." << EidosTerminate(); } std::getline(iss, sub, '\t'); // POS int64_t pos = EidosInterpreter::NonnegativeIntegerForString(sub, nullptr) - 1; // -1 because VCF uses 1-based positions if ((pos < 0) || (pos > last_position)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file POS value " << pos << " out of range." << EidosTerminate(); call_lines.emplace_back(pos, line); break; } default: EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): (internal error) unhandled case." << EidosTerminate(); } } infile.close(); // sort the call lines by position, so that we can add them to empty haplosomes efficiently std::sort(call_lines.begin(), call_lines.end(), [ ](const std::pair &l1, const std::pair &l2) {return l1.first < l2.first;}); // cache target haplosomes and determine whether they are initially empty, in which case we can do fast mutation addition with emplace_back() std::vector targets; std::vector target_last_mutrun_modified; std::vector target_last_mutrun; bool all_target_haplosomes_started_empty = true; for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *haplosome = targets_data[haplosome_index]; // null haplosomes are silently excluded from the target list, for convenience if (!haplosome->IsNull()) { if (haplosome->mutation_count() != 0) all_target_haplosomes_started_empty = false; targets.emplace_back(haplosome); target_last_mutrun_modified.emplace_back(-1); target_last_mutrun.emplace_back(nullptr); } } target_size = (int)targets.size(); // adjust for possible exclusion of null haplosomes // parse all the call lines, instantiate their mutations, and add the mutations to the target haplosomes #ifndef _OPENMP MutationRunContext &mutrun_context = chromosome->ChromosomeMutationRunContextForThread(omp_get_thread_num()); // when not parallel, we have only one MutationRunContext #endif std::vector mutation_indices; bool has_initial_mutations = (gSLiM_next_mutation_id != 0); for (std::pair &call_line : call_lines) { slim_position_t mut_position = call_line.first; std::istringstream iss(call_line.second); std::string ref_str, alt_str, info_str; std::getline(iss, sub, '\t'); // CHROM; don't care (already checked it above) std::getline(iss, sub, '\t'); // POS; already fetched std::getline(iss, sub, '\t'); // ID; don't care std::getline(iss, ref_str, '\t'); // REF std::getline(iss, alt_str, '\t'); // ALT std::getline(iss, sub, '\t'); // QUAL; don't care std::getline(iss, sub, '\t'); // FILTER; don't care std::getline(iss, info_str, '\t'); // INFO std::getline(iss, sub, '\t'); // FORMAT; don't care (GT must be first, according to the standard; we don't check) // parse/validate the REF nucleotide int8_t ref_nuc; if (ref_str == "A") ref_nuc = 0; else if (ref_str == "C") ref_nuc = 1; else if (ref_str == "G") ref_nuc = 2; else if (ref_str == "T") ref_nuc = 3; else EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file REF value must be A/C/G/T." << EidosTerminate(); // parse/validate the ALT nucleotides std::vector alt_substrs = Eidos_string_split(alt_str, ","); std::vector alt_nucs; for (std::string &alt_substr : alt_substrs) { if (alt_substr == "A") alt_nucs.emplace_back(0); else if (alt_substr == "C") alt_nucs.emplace_back(1); else if (alt_substr == "G") alt_nucs.emplace_back(2); else if (alt_substr == "T") alt_nucs.emplace_back(3); else EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file ALT value must be A/C/G/T." << EidosTerminate(); } std::size_t alt_allele_count = alt_nucs.size(); // parse/validate the INFO fields that we recognize std::vector info_substrs = Eidos_string_split(info_str, ";"); std::vector info_mutids; std::vector info_selcoeffs; std::vector info_domcoeffs; std::vector info_poporigin; std::vector info_tickorigin; std::vector info_muttype; int8_t info_ancestral_nuc = -1; bool info_is_nonnuc = false; for (std::string &info_substr : info_substrs) { if (info_MID_defined && (info_substr.compare(0, 4, "MID=") == 0)) // Mutation ID { std::vector value_substrs = Eidos_string_split(info_substr.substr(4), ","); for (std::string &value_substr : value_substrs) info_mutids.emplace_back((slim_mutationid_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); if (info_mutids.size() && has_initial_mutations) { if (!gEidosSuppressWarnings) { if (!community.warned_readFromVCF_mutIDs_unused_) { p_interpreter.ErrorOutputStream() << "#WARNING (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): readHaplosomesFromVCF(): the VCF file specifies mutation IDs with the MID field, but some mutation IDs have already been used so uniqueness cannot be guaranteed. Use of mutation IDs is therefore disabled; mutations will not receive the mutation ID requested in the file. To fix this warning, remove the MID field from the VCF file before reading. To get readHaplosomesFromVCF() to use the specified mutation IDs, load the VCF file into a model that has never simulated a mutation, and has therefore not used any mutation IDs." << std::endl; community.warned_readFromVCF_mutIDs_unused_ = true; } } // disable use of MID for this read info_MID_defined = false; info_mutids.clear(); } } else if (info_S_defined && (info_substr.compare(0, 2, "S=") == 0)) // Selection Coefficient { std::vector value_substrs = Eidos_string_split(info_substr.substr(2), ","); for (std::string &value_substr : value_substrs) info_selcoeffs.emplace_back(EidosInterpreter::FloatForString(value_substr, nullptr)); } else if (info_DOM_defined && (info_substr.compare(0, 4, "DOM=") == 0)) // Dominance Coefficient { std::vector value_substrs = Eidos_string_split(info_substr.substr(4), ","); for (std::string &value_substr : value_substrs) info_domcoeffs.emplace_back(EidosInterpreter::FloatForString(value_substr, nullptr)); } else if (info_PO_defined && (info_substr.compare(0, 3, "PO=") == 0)) // Population of Origin { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_poporigin.emplace_back((slim_objectid_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (info_TO_defined && (info_substr.compare(0, 3, "TO=") == 0)) // Tick of Origin { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_tickorigin.emplace_back((slim_tick_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (info_GO_defined && (info_substr.compare(0, 3, "GO=") == 0)) // Generation of Origin - emitted by SLiM 3, treated as TO here { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_tickorigin.emplace_back((slim_tick_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (info_MT_defined && (info_substr.compare(0, 3, "MT=") == 0)) // Mutation Type { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_muttype.emplace_back((slim_objectid_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (/* info_AA_defined && */ (info_substr.compare(0, 3, "AA=") == 0)) // Ancestral Allele; definition not required since it is a standard field { std::string aa_str = info_substr.substr(3); if (aa_str == "A") info_ancestral_nuc = 0; else if (aa_str == "C") info_ancestral_nuc = 1; else if (aa_str == "G") info_ancestral_nuc = 2; else if (aa_str == "T") info_ancestral_nuc = 3; else EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file AA value must be A/C/G/T." << EidosTerminate(); } else if (info_NONNUC_defined && (info_substr == "NONNUC")) // Non-nucleotide-based { info_is_nonnuc = true; } if ((info_mutids.size() != 0) && (info_mutids.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file unexpected value count for MID field." << EidosTerminate(); if ((info_selcoeffs.size() != 0) && (info_selcoeffs.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file unexpected value count for S field." << EidosTerminate(); if ((info_domcoeffs.size() != 0) && (info_domcoeffs.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file unexpected value count for DOM field." << EidosTerminate(); if ((info_poporigin.size() != 0) && (info_poporigin.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file unexpected value count for PO field." << EidosTerminate(); if ((info_tickorigin.size() != 0) && (info_tickorigin.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file unexpected value count for GO or TO field." << EidosTerminate(); if ((info_muttype.size() != 0) && (info_muttype.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file unexpected value count for MT field." << EidosTerminate(); } // read the genotype data for each sample id, which might be diploid or haploid, and might have data beyond GT std::vector genotype_calls; for (int sample_index = 0; sample_index < sample_id_count; ++sample_index) { if (iss.eof()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file call line ended unexpectedly before the last sample." << EidosTerminate(); std::getline(iss, sub, '\t'); // extract just the GT field if others are present std::size_t colon_pos = sub.find_first_of(':'); if (colon_pos != std::string::npos) sub = sub.substr(0, colon_pos); // separate haploid calls that are joined by | or /; this is the hotspot of the whole method, so we try to be efficient here bool call_handled = false; if ((sub.length() == 3) && ((sub[1] == '|') || (sub[1] == '/'))) { // diploid, both single-digit char sub_ch1 = sub[0]; char sub_ch2 = sub[2]; if ((sub_ch1 >= '0') && (sub_ch1 <= '9') && (sub_ch2 >= '0') && (sub_ch2 <= '9')) { int genotype_call1 = (int)(sub_ch1 - '0'); int genotype_call2 = (int)(sub_ch2 - '0'); if ((genotype_call1 < 0) || (genotype_call1 > (int)alt_allele_count) || (genotype_call2 < 0) || (genotype_call2 > (int)alt_allele_count)) // 0 is REF, 1..n are ALT alleles EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file call out of range (does not correspond to a REF or ALT allele in the call line)." << EidosTerminate(); genotype_calls.emplace_back(genotype_call1); genotype_calls.emplace_back(genotype_call2); call_handled = true; } } else if (sub.length() == 1) { char sub_ch = sub[0]; if (sub_ch == '~') { // If the call is ~, a tilde, it indicates that no genetic information is present // (this is the case for a female if we're reading Y-chromosome data, for example). // We do not add anything to genotype_calls; it is as if this call does not exist. // Note that this is not part of the VCF standard; it had to be invented for SLiM. call_handled = true; } else { // haploid, single-digit if ((sub_ch >= '0') && (sub_ch <= '9')) { int genotype_call = (int)(sub_ch - '0'); if ((genotype_call < 0) || (genotype_call > (int)alt_allele_count)) // 0 is REF, 1..n are ALT alleles EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file call out of range (does not correspond to a REF or ALT allele in the call line)." << EidosTerminate(); genotype_calls.emplace_back(genotype_call); call_handled = true; } } } if (!call_handled) { std::vector genotype_substrs; if (sub.find('|') != std::string::npos) genotype_substrs = Eidos_string_split(sub, "|"); // phased else if (sub.find('/') != std::string::npos) genotype_substrs = Eidos_string_split(sub, "/"); // unphased; we don't worry about that else genotype_substrs.emplace_back(sub); // haploid, presumably if ((genotype_substrs.size() < 1) || (genotype_substrs.size() > 2)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file genotype calls must be diploid or haploid; " << genotype_substrs.size() << " calls found in one sample." << EidosTerminate(); // extract the calls' integer values, validate them, and keep them; we don't care which call was in which sample, we just preserve their order for (std::string &genotype_substr : genotype_substrs) { std::size_t genotype_call = EidosInterpreter::NonnegativeIntegerForString(genotype_substr, nullptr); if (/*(genotype_call < 0) ||*/ (genotype_call > alt_allele_count)) // 0 is REF, 1..n are ALT alleles EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file call out of range (does not correspond to a REF or ALT allele in the call line)." << EidosTerminate(); genotype_calls.emplace_back((int)genotype_call); } } } if (!iss.eof()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file call line has unexpected entries following the last sample." << EidosTerminate(); if ((int)genotype_calls.size() != target_size) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): target haplosome vector has size " << target_size << " but " << genotype_calls.size() << " calls were found in one call line." << EidosTerminate(); // We have one call for each non-null target haplosome, so the requirement for this function is met. // Note that there is no checking that a ~ matches the position of a null haplosome in the target // vector; we have no concept of "individuals", we just match haplosomes to calls for each line. // The Individual version of readHaplosomesFromVCF() can be smarter, since it understands individuals. // instantiate the mutations involved in this call line; the REF allele represents no mutation, ALT alleles are each separate mutations std::vector alt_allele_mut_indices; for (std::size_t alt_allele_index = 0; alt_allele_index < alt_allele_count; ++alt_allele_index) { // figure out the mutation type; if specified with MT, look it up, otherwise use the default supplied MutationType *mutation_type_ptr = default_mutation_type_ptr; if (info_muttype.size() > 0) { slim_objectid_t mutation_type_id = info_muttype[alt_allele_index]; mutation_type_ptr = species->MutationTypeWithID(mutation_type_id); if (!mutation_type_ptr) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file MT field references a mutation type m" << mutation_type_id << " that is not defined." << EidosTerminate(); } if (!mutation_type_ptr) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file MT field missing, but no default mutation type was supplied in the mutationType parameter." << EidosTerminate(); // check the dominance coefficient of DOM against that of the mutation type if (info_domcoeffs.size() > 0) { if (std::abs(info_domcoeffs[alt_allele_index] - mutation_type_ptr->dominance_coeff_) > 0.0001) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): VCF file DOM field specifies a dominance coefficient " << info_domcoeffs[alt_allele_index] << " that differs from the mutation type's dominance coefficient of " << mutation_type_ptr->dominance_coeff_ << "." << EidosTerminate(); } // get the selection coefficient from S, or draw one double selection_coeff; if (info_selcoeffs.size() > 0) selection_coeff = info_selcoeffs[alt_allele_index]; else selection_coeff = mutation_type_ptr->DrawSelectionCoefficient(); // get the subpop index from PO, or set to -1; no bounds checking on this slim_objectid_t subpop_index = -1; if (info_poporigin.size() > 0) subpop_index = info_poporigin[alt_allele_index]; // get the origin tick from gO, or set to the current tick; no bounds checking on this slim_tick_t origin_tick; if (info_tickorigin.size() > 0) origin_tick = info_tickorigin[alt_allele_index]; else origin_tick = community.Tick(); // figure out the nucleotide and do nucleotide-related checks int8_t alt_allele_nuc = alt_nucs[alt_allele_index]; // must be defined, in all cases, but might be ignored int8_t nucleotide; if (nucleotide_based) { if (info_NONNUC_defined) { // We are reading a SLiM-generated VCF file that uses NONNUC to designate non-nucleotide-based mutations if (info_is_nonnuc) { // This call line is marked NONNUC, so there is no associated nucleotide; check against the mutation type if (mutation_type_ptr->nucleotide_based_) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): a mutation marked NONNUC cannot use a nucleotide-based mutation type." << EidosTerminate(); nucleotide = -1; } else { // This call line is not marked NONNUC, so it represents nucleotide-based alleles if (!mutation_type_ptr->nucleotide_based_) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): a nucleotide-based mutation cannot use a non-nucleotide-based mutation type." << EidosTerminate(); if (ref_nuc != info_ancestral_nuc) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): the REF nucleotide does not match the AA nucleotide." << EidosTerminate(); int8_t ancestral = (int8_t)chromosome->AncestralSequence()->NucleotideAtIndex(mut_position); if (ancestral != ref_nuc) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): the REF/AA nucleotide does not match the ancestral nucleotide at the same position; a matching ancestral nucleotide sequence must be set prior to calling readHaplosomesFromVCF()." << EidosTerminate(); nucleotide = alt_allele_nuc; } } else { // We are reading a generic VCF file that does not use NONNUC, so we follow the mutation type's lead; if it is nucleotide-based, we use the nucleotide specified if (mutation_type_ptr->nucleotide_based_) { // The mutation type is nucleotide-based, so use the nucleotide specified; in this case we ignore REF and AA, however nucleotide = alt_allele_nuc; } else { // The mutation type is non-nucleotide-based, so we ignore the nucleotide supplied, as well as REF/AA nucleotide = -1; } } } else { // We are a non-nucleotide-based model, so NONNUC should not be defined; we do not understand nucleotides and will ignore them if (info_NONNUC_defined) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF): cannot read a VCF file generated by a nucleotide-based model into a non-nucleotide-based model." << EidosTerminate(); nucleotide = -1; } // instantiate the mutation with the values decided upon MutationIndex new_mut_index = SLiM_NewMutationFromBlock(); Mutation *new_mut; if (info_mutids.size() > 0) { // a mutation ID was supplied; we use it blindly, having checked above that we are in the case where this is legal slim_mutationid_t mut_mutid = info_mutids[alt_allele_index]; new_mut = new (gSLiM_Mutation_Block + new_mut_index) Mutation(mut_mutid, mutation_type_ptr, chromosome->Index(), mut_position, selection_coeff, subpop_index, origin_tick, nucleotide); } else { // no mutation ID supplied, so use whatever is next new_mut = new (gSLiM_Mutation_Block + new_mut_index) Mutation(mutation_type_ptr, chromosome->Index(), mut_position, selection_coeff, subpop_index, origin_tick, nucleotide); } // This mutation type might not be used by any genomic element type (i.e. might not already be vetted), so we need to check and set pure_neutral_ // The selection coefficient might have been supplied by the user (i.e., not be from the mutation type's DFE), so we set all_pure_neutral_DFE_ also if (selection_coeff != 0.0) { species->pure_neutral_ = false; mutation_type_ptr->all_pure_neutral_DFE_ = false; } // add it to our local map, so we can find it when making haplosomes, and to the population's mutation registry pop.MutationRegistryAdd(new_mut); alt_allele_mut_indices.emplace_back(new_mut_index); mutation_indices.emplace_back(new_mut_index); } // add the mutations to the appropriate haplosomes and record the new derived states for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { int call = genotype_calls[haplosome_index]; if (call != 0) { Haplosome *haplosome = targets[haplosome_index]; slim_mutrun_index_t &haplosome_last_mutrun_modified = target_last_mutrun_modified[haplosome_index]; MutationRun *&haplosome_last_mutrun = target_last_mutrun[haplosome_index]; slim_position_t mutrun_length = haplosome->mutrun_length_; MutationIndex mut_index = alt_allele_mut_indices[call - 1]; slim_mutrun_index_t mut_mutrun_index = (slim_mutrun_index_t)(mut_position / mutrun_length); if (mut_mutrun_index != haplosome_last_mutrun_modified) { #ifdef _OPENMP // When parallel, the MutationRunContext depends upon the position in the haplosome MutationRunContext &mutrun_context = species->ChromosomeMutationRunContextForMutationRunIndex(mut_mutrun_index); #endif // We use WillModifyRun() because these are existing haplosomes we didn't create, and their runs may be shared; we have // no way to tell. We avoid making excessive mutation run copies by calling this only once per mutrun per haplosome. haplosome_last_mutrun = haplosome->WillModifyRun(mut_mutrun_index, mutrun_context); haplosome_last_mutrun_modified = mut_mutrun_index; } // If the haplosome started empty, we can add mutations to the end with emplace_back(); if it did not, then they need to be inserted if (all_target_haplosomes_started_empty) haplosome_last_mutrun->emplace_back(mut_index); else haplosome_last_mutrun->insert_sorted_mutation(mut_index); if (recording_mutations) species->RecordNewDerivedState(haplosome, mut_position, *haplosome->derived_mutation_ids_at_position(mut_position)); } } } // Return the instantiated mutations Mutation *mut_block_ptr = gSLiM_Mutation_Block; int mutation_count = (int)mutation_indices.size(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class))->resize_no_initialize_RR(mutation_count); for (int mut_index = 0; mut_index < mutation_count; ++mut_index) vec->set_object_element_no_check_no_previous_RR(mut_block_ptr + mutation_indices[mut_index], mut_index); return EidosValue_Object_SP(vec); } // ********************* + (void)removeMutations([No mutations = NULL], [logical$ substitute = F]) // EidosValue_SP Haplosome_Class::ExecuteMethod_removeMutations(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_target, p_arguments, p_interpreter) EidosValue *mutations_value = p_arguments[0].get(); EidosValue *substitute_value = p_arguments[1].get(); Mutation *mut_block_ptr = gSLiM_Mutation_Block; int target_size = p_target->Count(); if (target_size == 0) return gStaticEidosValueVOID; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForHaplosomes(p_target); if (!species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() requires that all target haplosomes belong to the same species." << EidosTerminate(); // All haplosomes must belong to the same chromosome, and all mutations being added must belong to that chromosome too. // It's important that a mismatch result in an error; attempts to add mutations to chromosomes inconsistently should be flagged. int mutations_count = mutations_value->Count(); Haplosome * const *targets_data = (Haplosome * const *)p_target->ObjectData(); Haplosome *haplosome_0 = targets_data[0]; slim_chromosome_index_t chromosome_index = haplosome_0->chromosome_index_; if (species->Chromosomes().size() > 1) { // We have to check for consistency if there's more than one chromosome for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) if (targets_data[haplosome_index]->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() requires that all target haplosomes are associated with the same chromosome." << EidosTerminate(); if (mutations_value->Type() != EidosValueType::kValueNULL) { Mutation * const *mutations = (Mutation * const *)mutations_value->ObjectData(); for (int value_index = 0; value_index < mutations_count; ++value_index) if (mutations[value_index]->chromosome_index_ != chromosome_index) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() requires that all mutations to be removed are associated with the same chromosome as the target haplosomes." << EidosTerminate(); } } Chromosome *chromosome = species->Chromosomes()[chromosome_index]; species->population_.CheckForDeferralInHaplosomes(p_target, "Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF"); Community &community = species->community_; Population &pop = species->population_; slim_tick_t tick = community.Tick(); bool create_substitutions = substitute_value->LogicalAtIndex_NOCAST(0, nullptr); bool recording_tree_sequence_mutations = species->RecordingTreeSequenceMutations(); bool any_nonneutral_removed = false; // Use the 0th haplosome in the target to find out what the mutation run length is, so we can calculate run indices slim_position_t mutrun_length = haplosome_0->mutrun_length_; // TIMING RESTRICTION if (community.executing_species_ == species) { if (community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosModifyChildCallback) { // Check that we're not inside a modifyChild() callback, or if we are, that the only haplosomes being modified belong to the new child. // This prevents problems with retracting the proposed child when tree-sequence recording is enabled; other extraneous changes must // not be backed out, and it's hard to separate, e.g., what's a child-related new mutation from an extraneously added new mutation. // Note that the other Haplosome methods that add/remove mutations perform the same check, and should be maintained in parallel. Individual *focal_modification_child = community.focal_modification_child_; if (focal_modification_child) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets_data[haplosome_index]; if (target_haplosome->individual_ != focal_modification_child) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() cannot be called on the currently executing species from within a modifyChild() callback to modify any haplosomes except those of the focal child being generated." << EidosTerminate(); } } // This is actually only a problem when tree recording is on, but for consistency we outlaw it in all cases. When a substitution // is created, it is added to the derived state of every haplosome, which is a side effect that can't be retracted if the modifyChild() // callback rejects the proposed child, so it has to be prohibited up front. Anyway it would be a very strange thing to do. if (create_substitutions) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() cannot be called on the currently executing species from within a modifyChild() callback to create a substitution, because that would have side effects on haplosomes other than those of the focal child being generated." << EidosTerminate(); } else if ((community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosRecombinationCallback) || (community.executing_block_type_ == SLiMEidosBlockType::SLiMEidosMutationCallback)) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() cannot be called on the currently executing species from within a recombination() or mutation() callback." << EidosTerminate(); } if (mutations_value->Type() == EidosValueType::kValueNULL) { // This is the "remove all mutations" case, for which we have no vector of mutations to remove. In this case we do the tree // sequence recording first, since we can add new empty derived states just at the locations where mutations presently exist. // Then we just go through and clear out the target haplosomes. if (create_substitutions) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): in removeMutations() substitute may not be T if mutations is NULL; an explicit vector of mutations to be substituted must be supplied." << EidosTerminate(); // TREE SEQUENCE RECORDING if (recording_tree_sequence_mutations) { const std::vector empty_mut_vector; for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets_data[haplosome_index]; if (target_haplosome->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() cannot be called on a null haplosome. This error may be due to a break in backward compatibility in SLiM 3.7 involving addRecombinant() with haploid models; if that seems likely, please see the release notes." << EidosTerminate(); for (HaplosomeWalker target_walker(target_haplosome); !target_walker.Finished(); target_walker.NextMutation()) { Mutation *mut = target_walker.CurrentMutation(); slim_position_t pos = mut->position_; species->RecordNewDerivedState(target_haplosome, pos, empty_mut_vector); } } } // Fetch haplosome pointers and check for null haplosomes up front std::vectortarget_haplosomes; for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets_data[haplosome_index]; if (target_haplosome->IsNull()) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() cannot be called on a null haplosome. This error may be due to a break in backward compatibility in SLiM 3.7 involving addRecombinant() with haploid models; if that seems likely, please see the release notes." << EidosTerminate(); target_haplosomes.push_back(target_haplosome); } // Now remove all mutations; we don't use bulk operations here because it is simpler to just set them all to the same empty run // BCH 8/12/2021: fixing this code to only reset mutation runs if they presently contain a mutation; do nothing for empty mutruns // This avoids a bunch of mutrun thrash in haploid models that remove all mutations from the second haplosome in modifyChild(), etc. int mutrun_count = haplosome_0->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { MutationRun *shared_empty_run = nullptr; // different shared empty run for each mutrun index for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = target_haplosomes[haplosome_index]; const MutationRun *mutrun = target_haplosome->mutruns_[run_index]; if (mutrun->size()) { // Allocate the shared empty run lazily, since we might not need it (if we're removing mutations from haplosomes that are empty already) if (!shared_empty_run) { MutationRunContext &mutrun_context = chromosome->ChromosomeMutationRunContextForMutationRunIndex(run_index); shared_empty_run = MutationRun::NewMutationRun(mutrun_context); } target_haplosome->mutruns_[run_index] = shared_empty_run; } } } // invalidate cached mutation refcounts; refcounts have changed pop.InvalidateMutationReferencesCache(); // in this code path we just assume that nonneutral mutations might have been removed any_nonneutral_removed = true; } else { // If the user is creating substitutions for mutations, we now check for consistency at the end of the cycle, so that // we don't have a mutation still segregating while a substitution for it has also been created; see CheckMutationRegistry() // BCH 9/24/2021: Note that we cannot do the opposite check: checking that we only substitute a mutation when it has, in fact, // fixed. We can't do that because there are models, such as the PAR (pseudo-autosomal region) recipe, that have different // fixation frequences for different parts of the haplosome because multiple chromosomes of different ploidy are being simulated. // Until we support multiple chromosomes more intrinsically, and can do that properly, substitution at less than frequency // 1.0 must be supported. Note that this also means that we have to record new derived states here, because in some cases // (those same cases), the derived state for some haplosomes will have changed; if we do multiple chromosomes properly some day, // the recording of new derived states can probably be removed here, since no genetic state will have changed. if (create_substitutions) pop.SetMutationRegistryNeedsCheck(); // SPECIES CONSISTENCY CHECK if (mutations_count) { Species *mutations_species = Community::SpeciesForMutations(mutations_value); if (mutations_species != species) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() requires that all mutations belong to the same species as the target haplosomes." << EidosTerminate(); } // Construct a vector of mutations to remove that is sorted by position std::vector mutations_to_remove; Mutation * const *mutations_data = (Mutation * const *)mutations_value->ObjectData(); for (int value_index = 0; value_index < mutations_count; ++value_index) { Mutation *mut = mutations_data[value_index]; if (mut->state_ != MutationState::kInRegistry) EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() cannot remove mutations that are not currently segregating (i.e., have either been fixed/substituted or lost)." << EidosTerminate(); if (create_substitutions) mut->state_ = MutationState::kRemovedWithSubstitution; // mark removed/substituted mutations with a special state so they get handled correctly later mutations_to_remove.emplace_back(mut); if (mut->selection_coeff_ != 0.0) any_nonneutral_removed = true; } std::sort(mutations_to_remove.begin(), mutations_to_remove.end(), [ ](Mutation *i1, Mutation *i2) {return i1->position_ < i2->position_;}); // TREE SEQUENCE RECORDING // First, pre-plan the positions of new tree-seq derived states in anticipation of doing the removal. We have to check // whether the mutation being added is already absent, to avoid recording a new derived state identical to the old one state. // The algorithm used here, with HaplosomeWalker, depends upon the fact that we just sorted the mutations to add by position. // However, we do still have to think about multiple muts being removed from the same position, and existing stacked mutations. // Note that when we are not creating substitutions, the haplosomes that possess a given mutation are the ones that change; but // when we are creating substitutions, the haplosomes that do *not* possess a given mutation are the ones that change, because // they gain the substitutuon, whereas the other haplosomes lose the mutation and gain the substitution, and are thus unchanged. std::vector>> new_derived_state_positions; if (recording_tree_sequence_mutations) { for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets_data[haplosome_index]; HaplosomeWalker walker(target_haplosome); slim_position_t last_added_pos = -1; for (Mutation *mut : mutations_to_remove) { slim_position_t mut_pos = mut->position_; // We don't care about other mutations at an already recorded position; move on if (mut_pos == last_added_pos) continue; // Advance the walker until it is at or after the mutation's position while (!walker.Finished()) { if (walker.Position() >= mut_pos) break; walker.NextMutation(); } // If the derived state won't change for this position, then we don't need to record it; whether the // state will change depends upon whether the mutation is present, and whether we're substituting bool mutation_present = !walker.Finished() && (walker.Position() == mut_pos) && walker.MutationIsStackedAtCurrentPosition(mut); if ((create_substitutions && mutation_present) || (!create_substitutions && !mutation_present)) continue; // we have decided that the new derived state at this position will need to be recorded, so note that if (last_added_pos == -1) { // no pair entry in new_derived_state_positions yet, so make a new pair entry for this haplosome new_derived_state_positions.emplace_back(target_haplosome, std::vector(1, mut_pos)); } else { // we have an existing pair entry for this haplosome, so add this position to its position vector std::pair> &haplosome_entry = new_derived_state_positions.back(); std::vector &haplosome_list = haplosome_entry.second; haplosome_list.emplace_back(mut_pos); } last_added_pos = mut_pos; } } } // Create substitutions for the mutations being removed, if requested. // Note we don't test for the mutations actually being fixed; that is the caller's responsibility to manage. if (create_substitutions) { for (int value_index = 0; value_index < mutations_count; ++value_index) { Mutation *mut = mutations_data[value_index]; Substitution *sub = new Substitution(*mut, tick); // TREE SEQUENCE RECORDING // When doing tree recording, we additionally keep all fixed mutations (their ids) in a multimap indexed by their position // This allows us to find all the fixed mutations at a given position quickly and easily, for calculating derived states if (species->RecordingTreeSequence()) pop.treeseq_substitutions_map_.emplace(mut->position_, sub); pop.substitutions_.emplace_back(sub); } // TREE SEQUENCE RECORDING // When doing tree recording, if a given mutation is converted to a substitution by script, it may not be contained in some // haplosomes, but at the moment it fixes it is then considered to belong to the derived state of every non-null haplosome. We // therefore need to go through all of the non-target haplosomes and record their new derived states at all positions // containing a new substitution. If they already had a copy of the mutation, and didn't have it removed here, that is // pretty weird, but in practice it just means that their derived state will contain that mutation id twice – once for the // segregating mutation they still contain, and once for the new substitution. We don't check for that case, but it should // just work automatically. if (recording_tree_sequence_mutations) { int haplosome_count_per_individual = species->HaplosomeCountPerIndividual(); // Mark all non-null haplosomes in the simulation that are not among the target haplosomes for (auto subpop_pair : species->population_.subpops_) { Subpopulation *subpop = subpop_pair.second; for (Individual *ind : subpop->parent_individuals_) { for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = ind->haplosomes_[haplosome_index]; haplosome->scratch_ = (haplosome->IsNull() ? 0 : 1); } } } for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) targets_data[haplosome_index]->scratch_ = 0; // Figure out the unique chromosome positions that have changed (the uniqued set of mutation positions) std::vector unique_positions; slim_position_t last_pos = -1; for (Mutation *mut : mutations_to_remove) { slim_position_t pos = mut->position_; if (pos != last_pos) { unique_positions.emplace_back(pos); last_pos = pos; } } // Loop through those haplosomes and log the new derived state at each (unique) position for (auto subpop_pair : species->population_.subpops_) { Subpopulation *subpop = subpop_pair.second; for (Individual *ind : subpop->parent_individuals_) { for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = ind->haplosomes_[haplosome_index]; if (haplosome->scratch_ == 1) { for (slim_position_t position : unique_positions) species->RecordNewDerivedState(haplosome, position, *haplosome->derived_mutation_ids_at_position(position)); haplosome->scratch_ = 0; } } } } } } // Now handle the mutations to remove, broken into bulk operations according to the mutation run they fall into slim_mutrun_index_t last_handled_mutrun_index = -1; for (int value_index = 0; value_index < mutations_count; ++value_index) { Mutation *next_mutation = mutations_to_remove[value_index]; const slim_position_t pos = next_mutation->position_; slim_mutrun_index_t mutrun_index = (slim_mutrun_index_t)(pos / mutrun_length); if (mutrun_index <= last_handled_mutrun_index) continue; // We have not yet processed this mutation run; do this mutation run index as a bulk operation int64_t operation_id = MutationRun::GetNextOperationID(); Haplosome::BulkOperationStart(operation_id, mutrun_index); MutationRunContext &mutrun_context = chromosome->ChromosomeMutationRunContextForMutationRunIndex(mutrun_index); for (int haplosome_index = 0; haplosome_index < target_size; ++haplosome_index) { Haplosome *target_haplosome = targets_data[haplosome_index]; if (target_haplosome->IsNull()) { Haplosome::BulkOperationEnd(operation_id, mutrun_index); // clean up for SLiMgui EIDOS_TERMINATION << "ERROR (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() cannot be called on a null haplosome. This error may be due to a break in backward compatibility in SLiM 3.7 involving addRecombinant() with haploid models; if that seems likely, please see the release notes." << EidosTerminate(); } // See if WillModifyRunForBulkOperation() can short-circuit the operation for us MutationRun *mutrun = target_haplosome->WillModifyRunForBulkOperation(operation_id, mutrun_index, mutrun_context); if (mutrun) { // Remove the specified mutations; see RemoveFixedMutations for the origins of this code MutationIndex *haplosome_iter = mutrun->begin_pointer(); MutationIndex *haplosome_backfill_iter = mutrun->begin_pointer(); MutationIndex *haplosome_max = mutrun->end_pointer(); // haplosome_iter advances through the mutation list; for each entry it hits, the entry is either removed (skip it) or not removed (copy it backward to the backfill pointer) while (haplosome_iter != haplosome_max) { MutationIndex candidate_mutation = *haplosome_iter; const slim_position_t candidate_pos = (mut_block_ptr + candidate_mutation)->position_; bool should_remove = false; for (int mut_index = value_index; mut_index < mutations_count; ++mut_index) { Mutation *mut_to_remove = mutations_to_remove[mut_index]; MutationIndex mut_to_remove_index = mut_to_remove->BlockIndex(); if (mut_to_remove_index == candidate_mutation) { should_remove = true; break; } // since we're in sorted order by position, as soon as we pass the candidate we're done if (mut_to_remove->position_ > candidate_pos) break; } if (should_remove) { // Removed mutation; we want to omit it, so we just advance our pointer ++haplosome_iter; } else { // Unremoved mutation; we want to keep it, so we copy it backward and advance our backfill pointer as well as haplosome_iter if (haplosome_backfill_iter != haplosome_iter) *haplosome_backfill_iter = *haplosome_iter; ++haplosome_backfill_iter; ++haplosome_iter; } } // excess mutations at the end have been copied back already; we just adjust mutation_count_ and forget about them mutrun->set_size(mutrun->size() - (int)(haplosome_iter - haplosome_backfill_iter)); } } Haplosome::BulkOperationEnd(operation_id, mutrun_index); // now we have handled all mutations at this index (and all previous indices) last_handled_mutrun_index = mutrun_index; // invalidate cached mutation refcounts; refcounts have changed pop.InvalidateMutationReferencesCache(); } // TREE SEQUENCE RECORDING // Now that all the bulk operations are done, record all the new derived states if (recording_tree_sequence_mutations) { for (std::pair> &haplosome_pair : new_derived_state_positions) { Haplosome *target_haplosome = haplosome_pair.first; std::vector &haplosome_positions = haplosome_pair.second; for (slim_position_t position : haplosome_positions) species->RecordNewDerivedState(target_haplosome, position, *target_haplosome->derived_mutation_ids_at_position(position)); } } } // TIMING RESTRICTION // issue a warning if removeMutations() was called at a questionable time, but only if the mutations removed were non-neutral // BCH: added the !create_substitutions check; if a substitution is being created, then it can be assumed that the mutation is fixed // in the model and is thus deemed by the model to make no difference to fitness outcomes (mutations that matter to fitness outcomes // should not be removed when they fix anyway). This is maybe not absolutely 100% clear, but models that handle their own fixation, // like haploid models and haplodiploid models, should not have to see/suppress this warning. if (any_nonneutral_removed && !create_substitutions && !community.warned_early_mutation_remove_) { if ((community.CycleStage() == SLiMCycleStage::kWFStage0ExecuteFirstScripts) || (community.CycleStage() == SLiMCycleStage::kWFStage1ExecuteEarlyScripts)) { if (!gEidosSuppressWarnings) { p_interpreter.ErrorOutputStream() << "#WARNING (Haplosome_Class::ExecuteMethod_removeMutations): removeMutations() should probably not be called from a first() or early() event in a WF model; the removed mutation(s) will still influence fitness values during offspring generation." << std::endl; community.warned_early_mutation_remove_ = true; } } // Note that there is no equivalent problem in nonWF models, because fitness values are used for survival, // not reproduction, and there is no event stage in the tick cycle that splits fitness from survival. } return gStaticEidosValueVOID; } // // HaplosomeWalker // #pragma mark - #pragma mark HaplosomeWalker #pragma mark - void HaplosomeWalker::NextMutation(void) { // the !mutrun_ptr_ is actually not necessary, but ASAN wants it to be here... if (!mutrun_ptr_ || (++mutrun_ptr_ >= mutrun_end_)) { // finished the current mutation, so move to the next until we find a mutation do { if (++mutrun_index_ >= haplosome_->mutrun_count_) { // finished all mutation runs, so we're done mutation_ = nullptr; return; } const MutationRun *mutrun = haplosome_->mutruns_[mutrun_index_]; mutrun_ptr_ = mutrun->begin_pointer_const(); mutrun_end_ = mutrun->end_pointer_const(); } while (mutrun_ptr_ == mutrun_end_); } mutation_ = gSLiM_Mutation_Block + *mutrun_ptr_; } void HaplosomeWalker::MoveToPosition(slim_position_t p_position) { // Move the the first mutation that is at or after the given position. Using this to move to a // given position is more efficient than iteratively advancing mutation by mutation, because // we can (1) go to the correct mutation run directly, and (2) do a binary search inside the // mutation run for the correct position. Haplosome *haplosome = haplosome_; // start at the mutrun dictated by the position we are moving to; positions < 0 start at 0 mutrun_index_ = (int32_t)(p_position / haplosome->mutrun_length_); if (mutrun_index_ < 0) mutrun_index_ = 0; while (true) { // if the mutrun is past the end of the last mutrun, we're done if (mutrun_index_ >= haplosome->mutrun_count_) { mutation_ = nullptr; return; } // get the information on the mutrun const MutationRun *mutrun = haplosome->mutruns_[mutrun_index_]; mutrun_ptr_ = mutrun->begin_pointer_const(); mutrun_end_ = mutrun->end_pointer_const(); // if the mutrun is empty, we will need to move to the next mutrun to find a mutation if (mutrun_ptr_ == mutrun_end_) mutrun_index_++; else break; } // if the mutation found is at or after the requested position, we are already done mutation_ = gSLiM_Mutation_Block + *mutrun_ptr_; if (mutation_->position_ >= p_position) return; // otherwise, we are in the correct mutrun for the position, but the requested position // still lies ahead of us, so we have to advance until we reach or pass the position // FIXME should do a binary search inside the mutation run instead do NextMutation(); while (!Finished() && (Position() < p_position)); } bool HaplosomeWalker::MutationIsStackedAtCurrentPosition(Mutation *p_search_mut) { // We have reached some chromosome position (presumptively the *first mutation* at that position, // which we do not check here), and the caller wants to know if a given mutation, which is // located at that position, is contained in this haplosome. This requires that we look ahead // through all of the mutations at the current position, since they are not sorted. We are // guaranteed to stay inside the current mutation run, though, so this lookahead is simple. if (Finished()) EIDOS_TERMINATION << "ERROR (HaplosomeWalker::MutationIsStackedAtCurrentPosition): (internal error) MutationIsStackedAtCurrentPosition() called on a finished walker." << EidosTerminate(); if (!p_search_mut) EIDOS_TERMINATION << "ERROR (HaplosomeWalker::MutationIsStackedAtCurrentPosition): (internal error) MutationIsStackedAtCurrentPosition() called with a nullptr mutation to search for." << EidosTerminate(); slim_position_t pos = mutation_->position_; if (p_search_mut->position_ != pos) EIDOS_TERMINATION << "ERROR (HaplosomeWalker::MutationIsStackedAtCurrentPosition): (internal error) MutationIsStackedAtCurrentPosition() called with a mutation that is not at the current walker position." << EidosTerminate(); for (const MutationIndex *search_ptr_ = mutrun_ptr_; search_ptr_ != mutrun_end_; ++search_ptr_) { MutationIndex mutindex = *search_ptr_; Mutation *mut = gSLiM_Mutation_Block + mutindex; if (mut == p_search_mut) return true; if (mut->position_ != pos) break; } return false; } bool HaplosomeWalker::IdenticalAtCurrentPositionTo(HaplosomeWalker &p_other_walker) { if (Finished()) EIDOS_TERMINATION << "ERROR (HaplosomeWalker::IdenticalAtCurrentPositionTo): (internal error) IdenticalAtCurrentPositionTo() called on a finished walker." << EidosTerminate(); if (p_other_walker.Finished()) EIDOS_TERMINATION << "ERROR (HaplosomeWalker::IdenticalAtCurrentPositionTo): (internal error) IdenticalAtCurrentPositionTo() called on a finished walker." << EidosTerminate(); if (Position() != p_other_walker.Position()) EIDOS_TERMINATION << "ERROR (HaplosomeWalker::IdenticalAtCurrentPositionTo): (internal error) IdenticalAtCurrentPositionTo() called with walkers at different positions." << EidosTerminate(); // If the two walkers are using the same mutation run, they are identical by definition if (mutrun_ptr_ == p_other_walker.mutrun_ptr_) return true; // If their current mutation differs, then the positions are not identical if (mutation_ != p_other_walker.mutation_) return false; // Scan forward as long as we are still within the same position slim_position_t pos = mutation_->position_; const MutationIndex *search_ptr_1 = mutrun_ptr_ + 1; const MutationIndex *search_ptr_2 = p_other_walker.mutrun_ptr_ + 1; do { Mutation *mut_1 = (search_ptr_1 != mutrun_end_) ? (gSLiM_Mutation_Block + *search_ptr_1) : nullptr; Mutation *mut_2 = (search_ptr_2 != p_other_walker.mutrun_end_) ? (gSLiM_Mutation_Block + *search_ptr_2) : nullptr; bool has_mut_at_position_1 = (mut_1) ? (mut_1->position_ == pos) : false; bool has_mut_at_position_2 = (mut_2) ? (mut_2->position_ == pos) : false; // If we have left the current position for both, simultaneously, then the positions are identical if (!has_mut_at_position_1 && !has_mut_at_position_2) return true; // If we left the current position for one but not for the other, then the positions differ if (!has_mut_at_position_1 || !has_mut_at_position_2) return false; // We are still within the current position in both, so if the mutations we reached differ then the positions differ if (mut_1 != mut_2) return false; // Otherwise we saw identical mutations at the position, and we should continue scanning ++search_ptr_1; ++search_ptr_2; } while (true); } int8_t HaplosomeWalker::NucleotideAtCurrentPosition(void) { if (Finished()) EIDOS_TERMINATION << "ERROR (HaplosomeWalker::NucleotideAtCurrentPosition): (internal error) NucleotideAtCurrentPosition() called on a finished walker." << EidosTerminate(); // First check the mutation we're at, since we already have it int8_t nuc = mutation_->nucleotide_; if (nuc != -1) return nuc; // Then scan forward as long as we are still within the same position slim_position_t pos = mutation_->position_; for (const MutationIndex *search_ptr_ = mutrun_ptr_ + 1; search_ptr_ != mutrun_end_; ++search_ptr_) { MutationIndex mutindex = *search_ptr_; Mutation *mut = gSLiM_Mutation_Block + mutindex; if (mut->position_ != pos) return -1; nuc = mut->nucleotide_; if (nuc != -1) return nuc; } return -1; } ================================================ FILE: core/haplosome.h ================================================ // // haplosome.h // SLiM // // Created by Ben Haller on 12/13/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class Haplosome represents a particular haplosome, defined as a vector of mutations. Each individual in the simulation has a haplosome, which determines that individual's fitness (from the fitness effects of all of the mutations possessed). */ // This include is up here because I had to jump through some hoops to break a #include loop between this and chromosome.h; // forward declaration wasn't enough, because both classes have inline methods that need the other type's definition. #include "chromosome.h" #ifndef __SLiM__haplosome__ #define __SLiM__haplosome__ #include "mutation.h" #include "slim_globals.h" #include "eidos_value.h" #include "mutation_run.h" #include #include #include //TREE SEQUENCE //INCLUDE JEROME's TABLES API #include "../treerec/tskit/tables.h" #include "eidos_globals.h" #if EIDOS_ROBIN_HOOD_HASHING #include "robin_hood.h" typedef robin_hood::unordered_flat_map SLiMBulkOperationHashTable; typedef robin_hood::pair SLiMBulkOperationPair; #elif STD_UNORDERED_MAP_HASHING #include typedef std::unordered_map SLiMBulkOperationHashTable; typedef std::pair SLiMBulkOperationPair; #endif class Species; class Population; class Subpopulation; class Individual; class Individual_Class; class HaplosomeWalker; extern EidosClass *gSLiM_Haplosome_Class; // Haplosome now keeps an array of MutationRun objects, and those objects actually hold the mutations of the haplosome. This design // allows multiple Haplosome objects to share the same runs of mutations, for speed in copying runs during offspring generation. // The number of runs can be determined at runtime, ideally as a function of the chromosome length, mutation rate, and recombination // rate; the goal is to make it so runs are short enough that events inside them are relatively rare (allowing them to be copied // often during reproduction), but long enough that they contain enough mutations to make all this machinery worthwhile; if they // are usually empty then we're actually doing more work than we were before! // Each haplosome knows the number of runs and the run length, but it is the same for every haplosome in a given simulation. The whole // simulation can be scaled up or down by splitting or joining mutation runs; this is done by the mutation-run experiment code. // The array of runs is malloced; since this is two mallocs per individual, this should not be unacceptable overhead, and it avoids // hard-coding of a maximum number of runs, wasting memory on unused pointers, etc. For null haplosomes the runs pointer is nullptr, // so if we screw up our null haplosome checks we will get a hard crash, which is not a bad thing. // BCH 4/19/2023: Note that Haplosome's MutationRun objects are now handled using const pointers. This reflects the fact that, in // general, MutationRuns in a Haplosome may be shared with other Haplosomes, and so should not be modified. The underlying objects are // not const, in fact, they are just handled through const pointers to enforce immutability once they are added to a haplosome. This // means that there are certain places in the code where we cast away the constness, when we know that a MutationRun is not, in // fact, shared by more than one haplosome; but in general we do not do that, because if the run is shared then modifying it is unsafe. // See mutation_run.h for further comments on this scheme, which replaces a previous scheme based on the refcounts of the runs. // BCH 5 May 2017: Well, it turns out that allocating the array of runs is in fact substantial overhead in some cases, so let's try // to avoid it. We can keep an internal buffer of mutation run pointers, which we can use as long as we are within the buffer size. // Using a size of 1 for now, since larger sizes increase memory usage substantially for some models, and also slow us down somehow. // BCH 22 April 2023: I tried removing this internal buffer, to see if it is really worthwhile. It is. I think having the pointer // to the MutationRun in the same memory block really helps a lot with memory locality. #define SLIM_HAPLOSOME_MUTRUN_BUFSIZE 1 class Haplosome : public EidosObject { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosObject super; private: EidosValue_SP self_value_; // cached EidosValue object for speed #ifdef SLIMGUI public: #else private: #endif slim_chromosome_index_t /* uint8_t */ chromosome_index_; // the index of this haplosome's chromosome uint8_t chromosome_subposition_; // 0 for the first haplosome for the chromosome, 1 for the second int8_t scratch_; // temporary scratch space that can be used locally in algorithms // 1 BYTE UNUSED HERE! int32_t mutrun_count_; // number of runs being used; 0 for a null haplosome, otherwise >= 1 slim_position_t mutrun_length_; // the length, in base pairs, of each run; the last run may not use its full length const MutationRun *run_buffer_[SLIM_HAPLOSOME_MUTRUN_BUFSIZE]; // an internal buffer used to avoid allocation and memory nonlocality for simple models const MutationRun **mutruns_; // mutation runs; nullptr if a null haplosome OR an empty haplosome Individual *individual_; // NOT OWNED: the Individual this haplosome belongs to slim_usertag_t tag_value_ = SLIM_TAG_UNSET_VALUE; // a user-defined tag value // TREE SEQUENCE RECORDING slim_haplosomeid_t haplosome_id_; // a unique id assigned by SLiM, as a side effect of pedigree recording, that never changes // note this is shared by all haplosomes at the same position (1st/2nd) in the same individual // Bulk operation optimization; see WillModifyRunForBulkOperation(). The idea is to keep track of changes to MutationRun // objects in a bulk operation, and short-circuit the operation for all haplosomes with the same initial MutationRun (since // the bulk operation will produce the same product MutationRun given the same initial MutationRun). Note this is shared by all species. static int64_t s_bulk_operation_id_; static slim_mutrun_index_t s_bulk_operation_mutrun_index_; static SLiMBulkOperationHashTable s_bulk_operation_runs_; public: Haplosome(const Haplosome &p_original) = delete; Haplosome& operator= (const Haplosome &p_original) = delete; // tags to indicate construction of a null vs. non-null haplosome, since the constructors take the same arguments // see https://stackoverflow.com/a/46775828/2752221 for this very nice solution with zero runtime overhead (no if) struct NullHaplosome final {}; struct NonNullHaplosome final {}; // make a null haplosome; the Haplosome::NullHaplosome{} parameter is just a tag to select this constructor // this constructor is for internal use only, and does not set chromosome_subposition_; use NewHaplosome_NULL() inline Haplosome(NullHaplosome, Individual *p_individual, Chromosome *p_chromosome) : chromosome_index_(p_chromosome->Index()), mutrun_count_(0), mutrun_length_(0), mutruns_(nullptr), individual_(p_individual), haplosome_id_(-1) { }; // make a non-null haplosome; the Haplosome::NonNullHaplosome{} parameter is just a tag to select this constructor // this constructor is for internal use only, and does not set chromosome_subposition_; use NewHaplosome_NONNULL() inline Haplosome(NonNullHaplosome, Individual *p_individual, Chromosome *p_chromosome) : chromosome_index_(p_chromosome->Index()), mutrun_count_(p_chromosome->mutrun_count_), mutrun_length_(p_chromosome->mutrun_length_), individual_(p_individual), haplosome_id_(-1) { if (mutrun_count_ <= SLIM_HAPLOSOME_MUTRUN_BUFSIZE) { mutruns_ = run_buffer_; #if SLIM_CLEAR_HAPLOSOMES EIDOS_BZERO(run_buffer_, SLIM_HAPLOSOME_MUTRUN_BUFSIZE * sizeof(const MutationRun *)); #endif } else { #if SLIM_CLEAR_HAPLOSOMES mutruns_ = (const MutationRun **)calloc(mutrun_count_, sizeof(const MutationRun *)); #else mutruns_ = (const MutationRun **)malloc(mutrun_count_ * sizeof(const MutationRun *)); #endif } }; ~Haplosome(void); inline __attribute__((always_inline)) slim_haplosomeid_t HaplosomeID(void) { return haplosome_id_; } inline __attribute__((always_inline)) void SetHaplosomeID(slim_haplosomeid_t p_new_id) { haplosome_id_ = p_new_id; } // should basically never be called inline __attribute__((always_inline)) Individual *OwningIndividual(void) { return individual_; } inline __attribute__((always_inline)) const Individual *OwningIndividual(void) const { return individual_; } Chromosome *AssociatedChromosome(void) const; inline __attribute__((always_inline)) slim_position_t MutrunLength(void) const { return mutrun_length_; } void NullHaplosomeAccessError(void) const __attribute__((__noreturn__)) __attribute__((cold)) __attribute__((analyzer_noreturn)); // prints an error message, a stacktrace, and exits; called only for DEBUG inline __attribute__((always_inline)) bool IsNull(void) const // returns true if the haplosome is a null (placeholder) haplosome, false otherwise { return (mutrun_count_ == 0); // null haplosomes have a mutrun count of 0 } inline __attribute__((always_inline)) bool IsDeferred(void) const // returns true if the haplosome is deferred haplosome (not yet generated), false otherwise { return ((mutrun_count_ != 0) && mutruns_ && !mutruns_[0]); // when deferred, non-null haplosomes have a non-zero mutrun count but are cleared to nullptr } void MakeNull(void) __attribute__((cold)); // transform into a null haplosome // used to re-initialize haplosomes to a new state, reusing them for efficiency void ReinitializeHaplosomeToNull(Individual *individual); void ReinitializeHaplosomeToNonNull(Individual *individual, Chromosome *p_chromosome); #if DEBUG static void DebugCheckStructureMatch(Haplosome *hapA, Haplosome *hapB, Chromosome *p_chromosome) { // This does a consistency check that two haplosomes (parent and child) match each other and, // if they are non-null, the expectated mutrun count/length passed in (from a chromosome) // It is used in the WF "munge" methods that munge an existing individual into a new child if ((hapA->IsNull() != hapB->IsNull()) || (!hapA->IsNull() && ((hapA->mutrun_count_ != p_chromosome->mutrun_count_) || (hapA->mutrun_length_ != p_chromosome->mutrun_length_) || (hapB->mutrun_count_ != p_chromosome->mutrun_count_) || (hapB->mutrun_length_ != p_chromosome->mutrun_length_)))) EIDOS_TERMINATION << "ERROR (Haplosome::CheckStructureMatch): (internal error) haplosome structure does not match!" << EidosTerminate(); } static void DebugCheckStructureMatch(Haplosome *hapA, Haplosome *hapB, Haplosome *hapC, Chromosome *p_chromosome) { // This does a consistency check that two haplosomes (parent and child) match each other and, // if they are non-null, the expectated mutrun count/length passed in (from a chromosome) // It is used in the WF "munge" methods that munge an existing individual into a new child if (((hapA->IsNull() != hapB->IsNull()) || (hapA->IsNull() != hapC->IsNull())) || (!hapA->IsNull() && ((hapA->mutrun_count_ != p_chromosome->mutrun_count_) || (hapA->mutrun_length_ != p_chromosome->mutrun_length_) || (hapB->mutrun_count_ != p_chromosome->mutrun_count_) || (hapB->mutrun_length_ != p_chromosome->mutrun_length_) || (hapC->mutrun_count_ != p_chromosome->mutrun_count_) || (hapC->mutrun_length_ != p_chromosome->mutrun_length_)))) EIDOS_TERMINATION << "ERROR (Haplosome::CheckStructureMatch): (internal error) haplosome structure does not match!" << EidosTerminate(); } #else static inline void DebugCheckStructureMatch(Haplosome *, Haplosome *, Chromosome *) {} static inline void DebugCheckStructureMatch(Haplosome *, Haplosome *, Haplosome *, Chromosome *) {} #endif // This should be called before starting to define a mutation run from scratch, as the crossover-mutation code does. It will // discard the current MutationRun and start over from scratch with a unique, new MutationRun which is returned by the call. // Note that there is a _LOCKED version of this below, which locks around the use of the allocation pool. inline MutationRun *WillCreateRun(int p_run_index, MutationRunContext &p_mutrun_context) { #if DEBUG if (p_run_index < 0) EIDOS_TERMINATION << "ERROR (Haplosome::WillCreateRun): (internal error) attempt to create a negative-index run." << EidosTerminate(); if (p_run_index >= mutrun_count_) EIDOS_TERMINATION << "ERROR (Haplosome::WillCreateRun): (internal error) attempt to create an out-of-index run." << EidosTerminate(); #endif MutationRun *new_run = MutationRun::NewMutationRun(p_mutrun_context); // take from shared pool of used objects mutruns_[p_run_index] = new_run; return new_run; } inline MutationRun *WillCreateRun_LOCKED(int p_run_index, MutationRunContext &p_mutrun_context) { #if DEBUG if (p_run_index < 0) EIDOS_TERMINATION << "ERROR (Haplosome::WillCreateRun_LOCKED): (internal error) attempt to create a negative-index run." << EidosTerminate(); if (p_run_index >= mutrun_count_) EIDOS_TERMINATION << "ERROR (Haplosome::WillCreateRun_LOCKED): (internal error) attempt to create an out-of-index run." << EidosTerminate(); #endif MutationRun *new_run = MutationRun::NewMutationRun_LOCKED(p_mutrun_context); // take from shared pool of used objects mutruns_[p_run_index] = new_run; return new_run; } // This should be called before modifying the run at a given index. It will replicate the run to produce a single-referenced copy, // thus guaranteeing that the run can be modified legally. The _UNSHARED version avoids making that copy unless the run is empty, // based on a guarantee from the caller that the run is already single-referenced unless it is empty; this variant is used in code // that loads new genetic data into initially empty haplosomes. WillModifyRun() used to test the use count of the run to see whether // a copy was needed or not, but that is no longer possible in the new mutation run design where use counts are only valid after tallying. MutationRun *WillModifyRun(slim_mutrun_index_t p_run_index, MutationRunContext &p_mutrun_context); MutationRun *WillModifyRun_UNSHARED(slim_mutrun_index_t p_run_index, MutationRunContext &p_mutrun_context); // This is an alternate version of WillModifyRun(). It labels the upcoming modification as being the result of a bulk operation // being applied across multiple haplosomes, such that identical input haplosomes will produce identical output haplosomes, such as adding // the same mutation to all target haplosomes. It returns T if the caller needs to actually perform the operation on this haplosome, // or F if this call performed the run for the caller (because the operation had already been performed on an identical haplosome). // The goal is that haplosomes that share the same mutation run should continue to share the same mutation run after being processed // by a bulk operation using this method. A bit strange, but potentially important for efficiency. Note that this method knows // nothing about the operation being performed; it just plays around with MutationRun pointers, recognizing when the runs are // identical. The first call for a new operation ID will always return a pointer, and the caller will then perform the operation; // subsequent calls for haplosomes with the same starting MutationRun will substitute the same final MutationRun and return nullptr. static void BulkOperationStart(int64_t p_operation_id, slim_mutrun_index_t p_mutrun_index); MutationRun *WillModifyRunForBulkOperation(int64_t p_operation_id, slim_mutrun_index_t p_mutrun_index, MutationRunContext &p_mutrun_context); static void BulkOperationEnd(int64_t p_operation_id, slim_mutrun_index_t p_mutrun_index); // Remove all mutations in p_haplosome that have a state_ of MutationState::kFixedAndSubstituted, indicating that they have fixed void RemoveFixedMutations(int64_t p_operation_id, slim_mutrun_index_t p_mutrun_index) { #if DEBUG if (mutrun_count_ == 0) NullHaplosomeAccessError(); #endif // Remove all fixed mutations from the given mutation run index. Note that we cast away the const on our // mutation runs here. This method is called only when fixed mutations are being removed from *all* haplosomes, // so the fact that it modifies other haplosomes that share this mutation run is a feature, not a bug. See // Population::RemoveAllFixedMutations() for further context on this. MutationRun *mutrun = const_cast(mutruns_[p_mutrun_index]); mutrun->RemoveFixedMutations(p_operation_id); } // TallyHaplosomeReferences_Checkback() counts up the total MutationRun references, using their usage counts, as a checkback void TallyHaplosomeReferences_Checkback(slim_refcount_t *p_mutrun_ref_tally, slim_refcount_t *p_mutrun_tally, int64_t p_operation_id); inline __attribute__((always_inline)) int mutation_count(void) const // used to be called size(); renamed to avoid confusion with MutationRun::size() and break code using the wrong method { #if DEBUG if (mutrun_count_ == 0) NullHaplosomeAccessError(); #endif if (mutrun_count_ == 1) { return run_buffer_[0]->size(); } else { int mut_count = 0; for (int run_index = 0; run_index < mutrun_count_; ++run_index) mut_count += mutruns_[run_index]->size(); return mut_count; } } #if SLIM_CLEAR_HAPLOSOMES // BCH 10/15/2024: clearing haplosomes to nullptr is no longer required; it just slows us down. inline __attribute__((always_inline)) void clear_to_nullptr(void) { // It is legal to call this method on null haplosomes, for speed/simplicity; it does no harm EIDOS_BZERO(mutruns_, mutrun_count_ * sizeof(const MutationRun *)); } inline void check_cleared_to_nullptr(void) { // It is legal to call this method on null haplosomes, for speed/simplicity; it does no harm for (int run_index = 0; run_index < mutrun_count_; ++run_index) if (mutruns_[run_index]) EIDOS_TERMINATION << "ERROR (Haplosome::check_cleared_to_nullptr): (internal error) haplosome should be cleared but is not." << EidosTerminate(); } #endif inline __attribute__((always_inline)) bool contains_mutation(const Mutation *p_mut) const { #if DEBUG if (mutrun_count_ == 0) NullHaplosomeAccessError(); #endif return mutruns_[p_mut->position_ / mutrun_length_]->contains_mutation(p_mut); } inline __attribute__((always_inline)) Mutation *mutation_with_type_and_position(MutationType *p_mut_type, slim_position_t p_position, slim_position_t p_last_position) { #if DEBUG if (mutrun_count_ == 0) NullHaplosomeAccessError(); #endif return mutruns_[p_position / mutrun_length_]->mutation_with_type_and_position(p_mut_type, p_position, p_last_position); } inline void copy_from_haplosome(const Haplosome &p_source_haplosome) { if (p_source_haplosome.IsNull()) { // p_original is a null haplosome, so make ourselves null too, if we aren't already MakeNull(); } else { #if DEBUG if (mutrun_count_ == 0) NullHaplosomeAccessError(); #endif #if DEBUG if ((mutrun_count_ != p_source_haplosome.mutrun_count_) || (mutrun_length_ != p_source_haplosome.mutrun_length_)) EIDOS_TERMINATION << "ERROR (Haplosome::copy_from_haplosome): (internal error) assignment from haplosome with different count/length." << EidosTerminate(); #endif if (mutrun_count_ == 1) { // This optimization does seem to make a significant difference... run_buffer_[0] = p_source_haplosome.mutruns_[0]; } else { memcpy(mutruns_, p_source_haplosome.mutruns_, mutrun_count_ * sizeof(const MutationRun *)); } } // DO NOT copy the subpop pointer! That is not part of the genetic state of the haplosome, // it's a back-pointer to the Subpopulation that owns this haplosome, and never changes! // subpop_ = p_source_haplosome.subpop_; } inline const std::vector *derived_mutation_ids_at_position(slim_position_t p_position) const { slim_mutrun_index_t run_index = (slim_mutrun_index_t)(p_position / mutrun_length_); return mutruns_[run_index]->derived_mutation_ids_at_position(p_position); } void record_derived_states(Species *p_species) const; // print the sample represented by haplosomes, using SLiM's own format static void PrintHaplosomes_SLiM(std::ostream &p_out, std::vector &p_haplosomes, bool p_output_object_tags); // print the sample represented by haplosomes, using "ms" format static void PrintHaplosomes_MS(std::ostream &p_out, std::vector &p_haplosomes, const Chromosome &p_chromosome, bool p_filter_monomorphic); // print the sample represented by haplosomes, using "vcf" format static void PrintHaplosomes_VCF(std::ostream &p_out, std::vector &p_haplosomes, const Chromosome &p_chromosome, bool groupAsIndividuals, bool p_output_multiallelics, bool p_simplify_nucs, bool p_output_nonnucs); static void _PrintVCF(std::ostream &p_out, const Haplosome **p_haplosomes, int64_t p_haplosomes_count, const Chromosome &p_chromosome, bool p_groupAsIndividuals, bool p_simplify_nucs, bool p_output_nonnucs, bool p_output_multiallelics); // Memory usage tallying, for outputUsage() size_t MemoryUsageForMutrunBuffers(void); // // Eidos support // void GenerateCachedEidosValue(void); inline __attribute__((always_inline)) EidosValue_SP CachedEidosValue(void) { if (!self_value_) GenerateCachedEidosValue(); return self_value_; }; virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; static EidosValue_SP ExecuteMethod_Accelerated_containsMarkerMutation(EidosObject **p_values, size_t p_values_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); static EidosValue_SP ExecuteMethod_Accelerated_containsMutations(EidosObject **p_values, size_t p_values_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); static EidosValue_SP ExecuteMethod_Accelerated_countOfMutationsOfType(EidosObject **p_values, size_t p_values_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_mutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_nucleotides(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_positionsOfMutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_sumOfMutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); // Accelerated property access; see class EidosObject for comments on this mechanism static EidosValue *GetProperty_Accelerated_haplosomePedigreeID(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_chromosomeSubposition(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_isNullHaplosome(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size); // Accelerated property writing; see class EidosObject for comments on this mechanism static void SetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); friend class Haplosome_Class; // With the new mutation run structure, the simplest course of action is to just let some SLiM classes delve // in Haplosome directly; we really don't want to get into trying to define an iterator that loops over mutation // runs, etc., transparently and pretends that a haplosome is just a single bag of mutations. // FIXME well, now we do in fact have the HaplosomeWalker iterator class below, which works nicely, so some // of the code that messes around inside Haplosome's internals can probably be switched over to using it... friend Species; friend Population; friend Subpopulation; friend Chromosome; friend Individual; friend Individual_Class; friend HaplosomeWalker; }; class Haplosome_Class : public EidosClass { private: typedef EidosClass super; public: Haplosome_Class(const Haplosome_Class &p_original) = delete; // no copy-construct Haplosome_Class& operator=(const Haplosome_Class&) = delete; // no copying inline Haplosome_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; virtual EidosValue_SP ExecuteClassMethod(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const override; EidosValue_SP ExecuteMethod_addMutations(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_addNewMutation(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_mutationFreqsCountsInHaplosomes(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_outputX(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_readHaplosomesFromMS(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_readHaplosomesFromVCF(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_removeMutations(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; }; // This class allows clients of Haplosome to walk the mutations inside a haplosome without needing to know about MutationRun. class HaplosomeWalker { private: Haplosome *haplosome_; // the haplosome being walked int32_t mutrun_index_; // the mutation run index we're presently traversing const MutationIndex *mutrun_ptr_; // a pointer to the current element in the mutation run const MutationIndex *mutrun_end_; // an end pointer for the mutation run Mutation *mutation_; // the current mutation pointer, or nullptr if we have reached the end of the haplosome public: HaplosomeWalker(void) = delete; HaplosomeWalker(const HaplosomeWalker &p_original) = default; HaplosomeWalker& operator= (const HaplosomeWalker &p_original) = default; inline HaplosomeWalker(Haplosome *p_haplosome) : haplosome_(p_haplosome), mutrun_index_(-1), mutrun_ptr_(nullptr), mutrun_end_(nullptr), mutation_(nullptr) { NextMutation(); }; HaplosomeWalker(HaplosomeWalker&&) = default; inline ~HaplosomeWalker(void) {}; inline Haplosome *WalkerHaplosome(void) { return haplosome_; } inline Mutation *CurrentMutation(void) { return mutation_; } inline bool Finished(void) { return (mutation_ == nullptr); } inline slim_position_t Position(void) { return mutation_->position_; } // must be sure the walker is not finished! void NextMutation(void); void MoveToPosition(slim_position_t p_position); bool MutationIsStackedAtCurrentPosition(Mutation *p_search_mut); // scans for the given mutation in any slot at the current position bool IdenticalAtCurrentPositionTo(HaplosomeWalker &p_other_walker); // compares stacked mutations between walkers (reordered is not identical) int8_t NucleotideAtCurrentPosition(void); }; #endif /* defined(__SLiM__haplosome__) */ ================================================ FILE: core/individual.cpp ================================================ // // individual.cpp // SLiM // // Created by Ben Haller on 6/10/16. // Copyright (c) 2016-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "individual.h" #include "subpopulation.h" #include "species.h" #include "community.h" #include "eidos_property_signature.h" #include "eidos_call_signature.h" #include "polymorphism.h" #include #include #include #include #include #include #pragma mark - #pragma mark Individual #pragma mark - // A global counter used to assign all Individual objects a unique ID slim_pedigreeid_t gSLiM_next_pedigree_id = 0; // Static member bools that track whether any individual has ever sustained a particular type of change bool Individual::s_any_individual_color_set_ = false; bool Individual::s_any_individual_dictionary_set_ = false; bool Individual::s_any_individual_tag_set_ = false; bool Individual::s_any_individual_tagF_set_ = false; bool Individual::s_any_individual_tagL_set_ = false; bool Individual::s_any_haplosome_tag_set_ = false; bool Individual::s_any_individual_fitness_scaling_set_ = false; // individual first, haplosomes later; this is the new multichrom paradigm // BCH 10/12/2024: Note that this will rarely be called after simulation startup; see NewSubpopIndividual() Individual::Individual(Subpopulation *p_subpopulation, slim_popsize_t p_individual_index, IndividualSex p_sex, slim_age_t p_age, double p_fitness, float p_mean_parent_age) : #ifdef SLIMGUI color_set_(false), #endif mean_parent_age_(p_mean_parent_age), pedigree_id_(-1), pedigree_p1_(-1), pedigree_p2_(-1), pedigree_g1_(-1), pedigree_g2_(-1), pedigree_g3_(-1), pedigree_g4_(-1), reproductive_output_(0), sex_(p_sex), migrant_(false), killed_(false), cached_fitness_UNSAFE_(p_fitness), #ifdef SLIMGUI cached_unscaled_fitness_(p_fitness), #endif age_(p_age), index_(p_individual_index), subpopulation_(p_subpopulation) { // Set up our haplosomes with nullptr values initially. If we have 0/1/2 haplosomes total, we use our // internal buffer for speed; avoid malloc/free entirely, and even more important, get the memory // locality of having the haplosome pointers right inside the individual itself. Otherwise, we alloc // an external buffer, which entails fetching a new cache line to go through the indirection. int haplosome_count_per_individual = subpopulation_->HaplosomeCountPerIndividual(); if (haplosome_count_per_individual <= 2) { hapbuffer_[0] = nullptr; hapbuffer_[1] = nullptr; haplosomes_ = hapbuffer_; } else { haplosomes_ = (Haplosome **)calloc(haplosome_count_per_individual, sizeof(Haplosome *)); } // Initialize tag values to the "unset" value tag_value_ = SLIM_TAG_UNSET_VALUE; tagF_value_ = SLIM_TAGF_UNSET_VALUE; tagL0_set_ = false; tagL1_set_ = false; tagL2_set_ = false; tagL3_set_ = false; tagL4_set_ = false; // Initialize x/y/z to 0.0, only when leak-checking (they show up as used before initialized in Valgrind) #if SLIM_LEAK_CHECKING spatial_x_ = 0.0; spatial_y_ = 0.0; spatial_z_ = 0.0; #endif } Individual::~Individual(void) { // BCH 10/6/2024: Individual now owns the haplosomes inside it (a policy change for multichrom) // BCH 10/12/2024: Note that this might no longer be called except at simulation end; see FreeSubpopIndividual() Subpopulation *subpop = subpopulation_; // The subpopulation_ pointer is set to nullptr when an individual is placed in individuals_junkyard_; // in that case, its haplosomes have already been freed, so this loop does not need to run. if (subpopulation_) { const std::vector &chromosome_for_haplosome_index = subpopulation_->species_.ChromosomesForHaplosomeIndices(); int haplosome_count_per_individual = subpop->HaplosomeCountPerIndividual(); Haplosome **haplosomes = haplosomes_; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; ++haplosome_index) { Haplosome *haplosome = haplosomes[haplosome_index]; // haplosome pointers can be nullptr, if an individual has already freed its haplosome objects; // this happens when an individual is placed in individuals_junkyard_, in particular if (haplosome) { Chromosome *chromosome = chromosome_for_haplosome_index[haplosome_index]; chromosome->FreeHaplosome(haplosome); } } } if (haplosomes_ != hapbuffer_) free(haplosomes_); #if DEBUG haplosomes_ = nullptr; #endif } #if DEBUG void Individual::AddHaplosomeAtIndex(Haplosome *p_haplosome, int p_index) { int haplosome_count_per_individual = subpopulation_->HaplosomeCountPerIndividual(); if ((p_index < 0) || (p_index >= haplosome_count_per_individual)) EIDOS_TERMINATION << "ERROR (Individual::AddHaplosomeAtIndex): (internal error) haplosome index " << p_index << " out of range." << EidosTerminate(); // in DEBUG haplosomes_ should be zero-filled; when not in DEBUG, it may not be! if (haplosomes_[p_index]) EIDOS_TERMINATION << "ERROR (Individual::AddHaplosomeAtIndex): (internal error) haplosome index " << p_index << " already filled." << EidosTerminate(); // the haplosome should already know that it belongs to the individual; this method just makes the individual aware of that if (p_haplosome->individual_ != this) EIDOS_TERMINATION << "ERROR (Individual::AddHaplosomeAtIndex): (internal error) haplosome individual_ pointer not set up." << EidosTerminate(); haplosomes_[p_index] = p_haplosome; } #endif void Individual::AppendHaplosomesForChromosomes(EidosValue_Object *vec, std::vector &chromosome_indices, int64_t index, bool includeNulls) { Species &species = subpopulation_->species_; for (slim_chromosome_index_t chromosome_index : chromosome_indices) { Chromosome *chromosome = species.Chromosomes()[chromosome_index]; int first_haplosome_index = species.FirstHaplosomeIndices()[chromosome_index]; switch (chromosome->Type()) { // diploid chromosome types, where we will use index if supplied case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kX_XSexChromosome: case ChromosomeType::kZ_ZSexChromosome: { if ((index == -1) || (index == 0)) { Haplosome *haplosome = haplosomes_[first_haplosome_index]; if (includeNulls || !haplosome->IsNull()) vec->push_object_element_NORR(haplosome); } if ((index == -1) || (index == 1)) { Haplosome *haplosome = haplosomes_[first_haplosome_index+1]; if (includeNulls || !haplosome->IsNull()) vec->push_object_element_NORR(haplosome); } break; } // haploid chromosome types, where index is ignored case ChromosomeType::kH_HaploidAutosome: case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kHF_HaploidFemaleInherited: case ChromosomeType::kFL_HaploidFemaleLine: case ChromosomeType::kHM_HaploidMaleInherited: case ChromosomeType::kML_HaploidMaleLine: case ChromosomeType::kHNull_HaploidAutosomeWithNull: // the null is just ignored by this code { Haplosome *haplosome = haplosomes_[first_haplosome_index]; if (includeNulls || !haplosome->IsNull()) vec->push_object_element_NORR(haplosome); break; } // haploid chromosome types with a null haplosome first; index is ignored case ChromosomeType::kNullY_YSexChromosomeWithNull: { Haplosome *haplosome = haplosomes_[first_haplosome_index+1]; // the (possibly) non-null haplosome if (includeNulls || !haplosome->IsNull()) vec->push_object_element_NORR(haplosome); break; } } } } static inline bool _InPedigree(slim_pedigreeid_t A, slim_pedigreeid_t A_P1, slim_pedigreeid_t A_P2, slim_pedigreeid_t A_G1, slim_pedigreeid_t A_G2, slim_pedigreeid_t A_G3, slim_pedigreeid_t A_G4, slim_pedigreeid_t B) { if (B == -1) return false; if ((A == B) || (A_P1 == B) || (A_P2 == B) || (A_G1 == B) || (A_G2 == B) || (A_G3 == B) || (A_G4 == B)) return true; return false; } static double _Relatedness(slim_pedigreeid_t A, slim_pedigreeid_t A_P1, slim_pedigreeid_t A_P2, slim_pedigreeid_t A_G1, slim_pedigreeid_t A_G2, slim_pedigreeid_t A_G3, slim_pedigreeid_t A_G4, slim_pedigreeid_t B, slim_pedigreeid_t B_P1, slim_pedigreeid_t B_P2, slim_pedigreeid_t B_G1, slim_pedigreeid_t B_G2, slim_pedigreeid_t B_G3, slim_pedigreeid_t B_G4) { if ((A == -1) || (B == -1)) { // Unknown pedigree IDs do not match anybody return 0.0; } else if (A == B) { // An individual matches itself with relatedness 1.0 return 1.0; } else { double out = 0.0; if (_InPedigree(B, B_P1, B_P2, B_G1, B_G2, B_G3, B_G4, A)) // if A is in B... { out += _Relatedness(A, A_P1, A_P2, A_G1, A_G2, A_G3, A_G4, B_P1, B_G1, B_G2, -1, -1, -1, -1) / 2.0; out += _Relatedness(A, A_P1, A_P2, A_G1, A_G2, A_G3, A_G4, B_P2, B_G3, B_G4, -1, -1, -1, -1) / 2.0; } else { out += _Relatedness(A_P1, A_G1, A_G2, -1, -1, -1, -1, B, B_P1, B_P2, B_G1, B_G2, B_G3, B_G4) / 2.0; out += _Relatedness(A_P2, A_G3, A_G4, -1, -1, -1, -1, B, B_P1, B_P2, B_G1, B_G2, B_G3, B_G4) / 2.0; } return out; } } double Individual::_Relatedness(slim_pedigreeid_t A, slim_pedigreeid_t A_P1, slim_pedigreeid_t A_P2, slim_pedigreeid_t A_G1, slim_pedigreeid_t A_G2, slim_pedigreeid_t A_G3, slim_pedigreeid_t A_G4, slim_pedigreeid_t B, slim_pedigreeid_t B_P1, slim_pedigreeid_t B_P2, slim_pedigreeid_t B_G1, slim_pedigreeid_t B_G2, slim_pedigreeid_t B_G3, slim_pedigreeid_t B_G4, IndividualSex A_sex, IndividualSex B_sex, ChromosomeType p_chromosome_type) { // This version of _Relatedness() corrects for the sex chromosome case. It should be regarded as the top-level internal API here. // This is separate from RelatednessToIndividual(), and implemented as a static member function, for unit testing; we want an // API that unit tests can call without needing to actually have a constructed Individual object. // Correct for sex-chromosome simulations; the only individuals that count are those that pass on the sex chromosome to the // child. We can do that here since we know that the first parent of a given individual is female and the second is male. // If individuals are cloning, then both parents will be the same sex as the offspring, in fact, but we still want to // treat it the same I think (?). For example, a male offspring from biparental mating inherits an X from its female // parent only; a male offspring from cloning still inherits only one sex chromosome from its parent, so the same correction // seems appropriate still. #if DEBUG if ((p_chromosome_type != ChromosomeType::kA_DiploidAutosome) && ((A_sex == IndividualSex::kHermaphrodite) || (B_sex == IndividualSex::kHermaphrodite))) EIDOS_TERMINATION << "ERROR (Individual::_Relatedness): (internal error) hermaphrodites cannot exist when modeling a sex chromosome" << EidosTerminate(); if (((A_sex == IndividualSex::kHermaphrodite) && (B_sex != IndividualSex::kHermaphrodite)) || ((A_sex != IndividualSex::kHermaphrodite) && (B_sex == IndividualSex::kHermaphrodite))) EIDOS_TERMINATION << "ERROR (Individual::_Relatedness): (internal error) hermaphrodites cannot coexist with males and females" << EidosTerminate(); if (((A_sex == IndividualSex::kMale) && (B_P1 == A) && (B_P1 != B_P2)) || ((B_sex == IndividualSex::kMale) && (A_P1 == B) && (A_P1 != A_P2)) || ((A_sex == IndividualSex::kFemale) && (B_P2 == A) && (B_P2 != B_P1)) || ((B_sex == IndividualSex::kFemale) && (A_P2 == B) && (A_P2 != A_P1))) EIDOS_TERMINATION << "ERROR (Individual::_Relatedness): (internal error) a male was indicated as a first parent, or a female as second parent, without clonality" << EidosTerminate(); #endif switch (p_chromosome_type) { case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kH_HaploidAutosome: { // No intervention needed (we assume that inheritance was normal, without null haplosomes) // For "H", recombination is possible if there are two parents, so this is the same as "A" break; } case ChromosomeType::kHNull_HaploidAutosomeWithNull: { // For "H-", the second parent should always match the first (by cloning), but we make sure of it B_P1 = A_P1; B_P2 = A_P2; B_G1 = A_G1; B_G2 = A_G2; B_G3 = A_G3; B_G4 = A_G4; break; } case ChromosomeType::kX_XSexChromosome: { // Whichever sex A is, its second parent (A_P2) is male and so its male parent (A_G4) gave A_P2 a Y, not an X A_G4 = A_G3; if (A_sex == IndividualSex::kMale) { // If A is male, its second parent (male) gave it a Y, not an X A_P2 = A_P1; A_G3 = A_G1; A_G4 = A_G2; } // Whichever sex B is, its second parent (B_P2) is male and so its male parent (B_G4) gave B_P2 a Y, not an X B_G4 = B_G3; if (B_sex == IndividualSex::kMale) { // If B is male, its second parent (male) gave it a Y, not an X B_P2 = B_P1; B_G3 = B_G1; B_G4 = B_G2; } break; } case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kNullY_YSexChromosomeWithNull: case ChromosomeType::kML_HaploidMaleLine: { // When modeling the Y, females have no relatedness to anybody else except themselves, defined as 1.0 for consistency if ((A_sex == IndividualSex::kFemale) || (B_sex == IndividualSex::kFemale)) { if (A == B) return 1.0; return 0.0; } // The female parents (A_P1 and B_P1) and their parents, and female grandparents (A_G3 and B_G3), do not contribute A_G3 = A_G4; A_P1 = A_P2; A_G1 = A_G3; A_G2 = A_G4; B_G3 = B_G4; B_P1 = B_P2; B_G1 = B_G3; B_G2 = B_G4; break; } case ChromosomeType::kHM_HaploidMaleInherited: { // inherited from the male parent, so only the male (second) parents count // BCH 27 August 2025: Note that HM is now legal in non-sexual models; "male" just means "second" A_G3 = A_G4; A_P1 = A_P2; A_G1 = A_G3; A_G2 = A_G4; B_G3 = B_G4; B_P1 = B_P2; B_G1 = B_G3; B_G2 = B_G4; break; } case ChromosomeType::kZ_ZSexChromosome: { // Whichever sex A is, its first parent (A_P1) is female and so its female parent (A_G1) gave A_P1 a W, not a Z A_G1 = A_G2; if (A_sex == IndividualSex::kFemale) { // If A is female, its first parent (female) gave it a W, not a Z A_P1 = A_P2; A_G1 = A_G3; A_G2 = A_G4; } // Whichever sex B is, its first parent (B_P1) is female and so its female parent (B_G1) gave B_P1 a W, not a Z B_G1 = B_G2; if (B_sex == IndividualSex::kFemale) { // If B is female, its first parent (female) gave it a W, not a Z B_P1 = B_P2; B_G1 = B_G3; B_G2 = B_G4; } break; } case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kFL_HaploidFemaleLine: { // When modeling the W, males have no relatedness to anybody else except themselves, defined as 1.0 for consistency if ((A_sex == IndividualSex::kMale) || (B_sex == IndividualSex::kMale)) { if (A == B) return 1.0; return 0.0; } // The male parents (A_P2 and B_P2) and their parents, and male grandparents (A_G2 and B_G2), do not contribute A_G2 = A_G1; A_P2 = A_P1; A_G3 = A_G1; A_G4 = A_G2; B_G2 = B_G1; B_P2 = B_P1; B_G3 = B_G1; B_G4 = B_G2; break; } case ChromosomeType::kHF_HaploidFemaleInherited: { // inherited from the female parent, so only the female (first) parents count // BCH 27 August 2025: Note that HF is now legal in non-sexual models; "female" just means "first" A_G2 = A_G1; A_P2 = A_P1; A_G3 = A_G1; A_G4 = A_G2; B_G2 = B_G1; B_P2 = B_P1; B_G3 = B_G1; B_G4 = B_G2; break; } } return ::_Relatedness(A, A_P1, A_P2, A_G1, A_G2, A_G3, A_G4, B, B_P1, B_P2, B_G1, B_G2, B_G3, B_G4); } double Individual::RelatednessToIndividual(Individual &p_ind, ChromosomeType p_chromosome_type) { // So, the goal is to calculate A and B's relatedness, given pedigree IDs for themselves and (perhaps) for their parents and // grandparents. Note that a pedigree ID of -1 means "no information"; for a given cycle, information should either be // available for everybody, or for nobody (the latter occurs when that cycle is prior to the start of forward simulation). // So we have these ancestry trees: // // G1 G2 G3 G4 G5 G6 G7 G8 // \ / \ / \ / \ / // P1 P2 P3 P4 // \ / \ / // \ / \ / // \ / \ / // A B // // If A and B are same individual, the relatedness is 1.0. Otherwise, we need to determine the amount of consanguinity between // A and B. If A is a parent of B (P3 or P4), their relatedness is 0.5; if A is a grandparent of B (G5/G6/G7/G8), then their // relatedness is 0.25. A could also appear in B's tree more than once, but A cannot be its own parent. So if A==P3, then A // cannot also be G5 or G6, and indeed, we do not need to look at G5 or G6 at all; the fact that A==P3 tells us everything we // we need to know about that half of B's tree, with a contribution of 0.5. But it could *additionally* be true that A==P4, // giving another 0.5 for 1.0 total; or that A==G7, for 0.25; or that A==G8, for 0.25; for that A==G7 *and* A==G8, for 0.5, // making 1.0 total. Basically, whenever you see A at a given position you do not need to look further upward from that node, // but you must still look at other nodes. To do this properly, recursion is the simplest approach; this algorithm is thanks // to Peter Ralph. // Individual &indA = *this, &indB = p_ind; slim_pedigreeid_t A = indA.pedigree_id_; slim_pedigreeid_t A_P1 = indA.pedigree_p1_; slim_pedigreeid_t A_P2 = indA.pedigree_p2_; slim_pedigreeid_t A_G1 = indA.pedigree_g1_; slim_pedigreeid_t A_G2 = indA.pedigree_g2_; slim_pedigreeid_t A_G3 = indA.pedigree_g3_; slim_pedigreeid_t A_G4 = indA.pedigree_g4_; slim_pedigreeid_t B = indB.pedigree_id_; slim_pedigreeid_t B_P1 = indB.pedigree_p1_; slim_pedigreeid_t B_P2 = indB.pedigree_p2_; slim_pedigreeid_t B_G1 = indB.pedigree_g1_; slim_pedigreeid_t B_G2 = indB.pedigree_g2_; slim_pedigreeid_t B_G3 = indB.pedigree_g3_; slim_pedigreeid_t B_G4 = indB.pedigree_g4_; return _Relatedness(A, A_P1, A_P2, A_G1, A_G2, A_G3, A_G4, B, B_P1, B_P2, B_G1, B_G2, B_G3, B_G4, indA.sex_, indB.sex_, p_chromosome_type); } int Individual::_SharedParentCount(slim_pedigreeid_t X_P1, slim_pedigreeid_t X_P2, slim_pedigreeid_t Y_P1, slim_pedigreeid_t Y_P2) { // This is the top-level internal API here. It is separate from RelatednessToIndividual(), and // implemented as a static member function, for unit testing; we want an // API that unit tests can call without needing to actually have a constructed Individual object. // If one individual is missing parent information, return 0 if ((X_P1 == -1) || (X_P2 == -1) || (Y_P1 == -1) || (Y_P2 == -1)) return 0; // If both parents match, in one way or another, then they must be full siblings if ((X_P1 == Y_P1) && (X_P2 == Y_P2)) return 2; if ((X_P1 == Y_P2) && (X_P2 == Y_P1)) return 2; // Otherwise, if one parent matches, they must be half siblings if ((X_P1 == Y_P1) || (X_P1 == Y_P2) || (X_P2 == Y_P1) || (X_P2 == Y_P2)) return 1; // Otherwise, they are not siblings return 0; } int Individual::SharedParentCountWithIndividual(Individual &p_ind) { // This is much simpler than Individual::RelatednessToIndividual(); we just want the shared parent count. That is // defined, for two individuals X and Y with parents in {A, B, C, D}, as: // // AB CD -> 0 (no shared parents) // AB CC -> 0 (no shared parents) // AB AC -> 1 (half siblings) // AB AA -> 1 (half siblings) // AA AB -> 1 (half siblings) // AB AB -> 2 (full siblings) // AB BA -> 2 (full siblings) // AA AA -> 2 (full siblings) // // If X is itself a parent of Y, or vice versa, that is irrelevant for this method; we are not measuring // consanguinity here. // Individual &indX = *this, &indY = p_ind; slim_pedigreeid_t X_P1 = indX.pedigree_p1_; slim_pedigreeid_t X_P2 = indX.pedigree_p2_; slim_pedigreeid_t Y_P1 = indY.pedigree_p1_; slim_pedigreeid_t Y_P2 = indY.pedigree_p2_; return _SharedParentCount(X_P1, X_P2, Y_P1, Y_P2); } // print a vector of individuals, with all mutations and all haplosomes, to a stream // this takes a focal chromosome; if nullptr, data from all chromosomes is printed void Individual::PrintIndividuals_SLiM(std::ostream &p_out, const Individual **p_individuals, int64_t p_individuals_count, Species &species, bool p_output_spatial_positions, bool p_output_ages, bool p_output_ancestral_nucs, bool p_output_pedigree_ids, bool p_output_object_tags, bool p_output_substitutions, Chromosome *p_focal_chromosome) { Population &population = species.population_; Community &community = species.community_; if (population.child_generation_valid_) EIDOS_TERMINATION << "ERROR (Individual::PrintIndividuals_SLiM): (internal error) called with child generation active!." << EidosTerminate(); #if DO_MEMORY_CHECKS // This method can burn a huge amount of memory and get us killed, if we have a maximum memory usage. It's nice to // try to check for that and terminate with a proper error message, to help the user diagnose the problem. int mem_check_counter = 0, mem_check_mod = 100; if (eidos_do_memory_checks) Eidos_CheckRSSAgainstMax("Individual::PrintIndividuals_SLiM", "(The memory usage was already out of bounds on entry.)"); #endif // this method now handles outputFull() as well as outputIndividuals() bool output_full_population = (p_individuals == nullptr); if (output_full_population) { // We need to set up an individuals vector that contains all individuals, so we can share code below int64_t total_population_size = 0; for (const std::pair &subpop_pair : population.subpops_) { Subpopulation *subpop = subpop_pair.second; slim_popsize_t subpop_size = subpop->parent_subpop_size_; total_population_size += subpop_size; } p_individuals = (const Individual **)malloc(total_population_size * sizeof(Individual *)); p_individuals_count = total_population_size; const Individual **ind_buffer_ptr = p_individuals; for (const std::pair &subpop_pair : population.subpops_) { Subpopulation *subpop = subpop_pair.second; slim_popsize_t subpop_size = subpop->parent_subpop_size_; for (slim_popsize_t i = 0; i < subpop_size; i++) // go through all children *(ind_buffer_ptr++) = subpop->parent_individuals_[i]; } } // write the #OUT line p_out << "#OUT: " << community.Tick() << " " << species.Cycle() << (output_full_population ? " A" : " IS") << std::endl; // Figure out spatial position output. If it was not requested, then we don't do it, and that's fine. If it // was requested, then we output the number of spatial dimensions we're configured for (which might be zero). int spatial_output_count = (p_output_spatial_positions ? species.SpatialDimensionality() : 0); // Figure out age output. If it was not requested, don't do it; if it was requested, do it if we use a nonWF model. int age_output_count = (p_output_ages && (species.model_type_ == SLiMModelType::kModelTypeNonWF)) ? 1 : 0; // Starting in SLiM 2.3, we output a version indicator at the top of the file so we can decode different // versions, etc. Starting in SLiM 5, the version number is again synced with PrintAllBinary() (skipping // over 7 directly to 8), and the crazy four-way version number scheme that encoded flags is gone. See // PrintAllBinary() for the version history; but with version 8 we break backward compatibility anyway. p_out << "Version: 8" << std::endl; // Starting with version 8 (SLiM 5.0), we write out some flags; this information used to be incorporated into // the version number, which was gross. Now we write out flags for all optional output that is enabled. // Reading code can assume that if a flag is not present, that output is not present. bool has_nucleotides = species.IsNucleotideBased(); bool output_ancestral_nucs = has_nucleotides && p_output_ancestral_nucs; p_out << "Flags:"; if (spatial_output_count) p_out << " SPACE=" << spatial_output_count; if (age_output_count) p_out << " AGES"; if (p_output_pedigree_ids) p_out << " PEDIGREES"; if (has_nucleotides) p_out << " NUC"; if (output_ancestral_nucs) p_out << " ANC_SEQ"; if (p_output_object_tags) p_out << " OBJECT_TAGS"; if (p_output_substitutions) p_out << " SUBSTITUTIONS"; p_out << std::endl; // Output populations first, for outputFull() only if (output_full_population) { p_out << "Populations:" << std::endl; for (const std::pair &subpop_pair : population.subpops_) { Subpopulation *subpop = subpop_pair.second; slim_popsize_t subpop_size = subpop->parent_subpop_size_; double subpop_sex_ratio; if (species.model_type_ == SLiMModelType::kModelTypeWF) { subpop_sex_ratio = subpop->parent_sex_ratio_; } else { // We want to output empty (but not removed) subpops, so we use a sex ratio of 0.0 to prevent div by 0 if (subpop->parent_subpop_size_ == 0) subpop_sex_ratio = 0.0; else subpop_sex_ratio = 1.0 - (subpop->parent_first_male_index_ / (double)subpop->parent_subpop_size_); } p_out << "p" << subpop_pair.first << " " << subpop_size; // SEX ONLY if (subpop->sex_enabled_) p_out << " S " << subpop_sex_ratio; else p_out << " H"; if (p_output_object_tags) { if (subpop->tag_value_ == SLIM_TAG_UNSET_VALUE) p_out << " ?"; else p_out << ' ' << subpop->tag_value_; } p_out << std::endl; #if DO_MEMORY_CHECKS if (eidos_do_memory_checks) { mem_check_counter++; if (mem_check_counter % mem_check_mod == 0) Eidos_CheckRSSAgainstMax("Individual::PrintIndividuals_SLiM", "(Out of memory while outputting population list.)"); } #endif } } // print all individuals; this used to come after the Mutations: section, but now mutations are per-chromosome, // whereas the list of individuals is invariant across all of the chromosomes printed, and so must come before p_out << "Individuals:" << std::endl; THREAD_SAFETY_IN_ACTIVE_PARALLEL("Individual::PrintIndividuals_SLiM(): usage of statics"); static char double_buf[40]; for (int64_t individual_index = 0; individual_index < p_individuals_count; ++individual_index) { const Individual &individual = *(p_individuals[individual_index]); Subpopulation *subpop = individual.subpopulation_; slim_popsize_t index_in_subpop = individual.index_; if (!subpop || (index_in_subpop == -1)) { if (output_full_population) free(p_individuals); EIDOS_TERMINATION << "ERROR (Individual::PrintIndividuals_SLiM): target individuals must be visible in a subpopulation (i.e., may not be new juveniles)." << EidosTerminate(); } p_out << "p" << subpop->subpopulation_id_ << ":i" << index_in_subpop; // individual identifier // BCH 9/13/2020: adding individual pedigree IDs, for SLiM 3.5, format version 5/6 if (p_output_pedigree_ids) p_out << " " << individual.PedigreeID(); p_out << ' ' << individual.sex_; // BCH 2/5/2025: Before version 8, we emitted haplosome identifiers here, like "p1:16" and // "p1:17", but now that we have multiple chromosomes that really isn't helpful; removing // them. In the Haplosomes section we will now just identify the individual; that suffices. // output spatial position if requested; BCH 22 March 2019 switch to full precision for this, for accurate reloading if (spatial_output_count) { if (spatial_output_count >= 1) { snprintf(double_buf, 40, "%.*g", EIDOS_DBL_DIGS, individual.spatial_x_); // necessary precision for non-lossiness p_out << " " << double_buf; } if (spatial_output_count >= 2) { snprintf(double_buf, 40, "%.*g", EIDOS_DBL_DIGS, individual.spatial_y_); // necessary precision for non-lossiness p_out << " " << double_buf; } if (spatial_output_count >= 3) { snprintf(double_buf, 40, "%.*g", EIDOS_DBL_DIGS, individual.spatial_z_); // necessary precision for non-lossiness p_out << " " << double_buf; } } // output ages if requested if (age_output_count) p_out << " " << individual.age_; // output individual tags if requested if (p_output_object_tags) { if (individual.tag_value_ == SLIM_TAG_UNSET_VALUE) p_out << " ?"; else p_out << ' ' << individual.tag_value_; if (individual.tagF_value_ == SLIM_TAGF_UNSET_VALUE) p_out << " ?"; else { snprintf(double_buf, 40, "%.*g", EIDOS_DBL_DIGS, individual.tagF_value_); // necessary precision for non-lossiness p_out << " " << double_buf; } if (individual.tagL0_set_) p_out << ' ' << (individual.tagL0_value_ ? 'T' : 'F'); else p_out << " ?"; if (individual.tagL1_set_) p_out << ' ' << (individual.tagL1_value_ ? 'T' : 'F'); else p_out << " ?"; if (individual.tagL2_set_) p_out << ' ' << (individual.tagL2_value_ ? 'T' : 'F'); else p_out << " ?"; if (individual.tagL3_set_) p_out << ' ' << (individual.tagL3_value_ ? 'T' : 'F'); else p_out << " ?"; if (individual.tagL4_set_) p_out << ' ' << (individual.tagL4_value_ ? 'T' : 'F'); else p_out << " ?"; } p_out << std::endl; #if DO_MEMORY_CHECKS if (eidos_do_memory_checks) { mem_check_counter++; if (mem_check_counter % mem_check_mod == 0) Eidos_CheckRSSAgainstMax("Population::PrintAll", "(Out of memory while printing individuals.)"); } #endif } // Loop over chromosomes and output data for each const std::vector &chromosomes = species.Chromosomes(); for (Chromosome *chromosome : chromosomes) { // if we have a focal chromosome, skip all the other chromosomes if (p_focal_chromosome && (chromosome != p_focal_chromosome)) continue; // write information about the chromosome; note that we write the chromosome symbol, but PrintAllBinary() does not slim_chromosome_index_t chromosome_index = chromosome->Index(); p_out << "Chromosome: " << (uint32_t)chromosome_index << " " << chromosome->Type() << " " << chromosome->ID() << " " << chromosome->last_position_ << " \"" << chromosome->Symbol() << "\""; if (p_output_object_tags) { if (chromosome->tag_value_ == SLIM_TAG_UNSET_VALUE) p_out << " ?"; else p_out << ' ' << chromosome->tag_value_; } p_out << std::endl; int first_haplosome_index = species.FirstHaplosomeIndices()[chromosome_index]; int last_haplosome_index = species.LastHaplosomeIndices()[chromosome_index]; PolymorphismMap polymorphisms; Mutation *mut_block_ptr = gSLiM_Mutation_Block; // add all polymorphisms for this chromosome for (int64_t individual_index = 0; individual_index < p_individuals_count; ++individual_index) { const Individual *ind = p_individuals[individual_index]; Haplosome **haplosomes = ind->haplosomes_; for (int haplosome_index = first_haplosome_index; haplosome_index <= last_haplosome_index; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) AddMutationToPolymorphismMap(&polymorphisms, mut_block_ptr + mut_ptr[mut_index]); } #if DO_MEMORY_CHECKS if (eidos_do_memory_checks) { mem_check_counter++; if (mem_check_counter % mem_check_mod == 0) Eidos_CheckRSSAgainstMax("Population::PrintAll", "(Out of memory while assembling polymorphisms.)"); } #endif } } // print all polymorphisms p_out << "Mutations:" << std::endl; for (const PolymorphismPair &polymorphism_pair : polymorphisms) { // NOTE this added mutation_id_, BCH 11 June 2016 // NOTE the output format changed due to the addition of the nucleotide, BCH 2 March 2019 if (p_output_object_tags) polymorphism_pair.second.Print_ID_Tag(p_out); else polymorphism_pair.second.Print_ID(p_out); #if DO_MEMORY_CHECKS if (eidos_do_memory_checks) { mem_check_counter++; if (mem_check_counter % mem_check_mod == 0) Eidos_CheckRSSAgainstMax("Population::PrintAll", "(Out of memory while printing polymorphisms.)"); } #endif } // print all haplosomes p_out << "Haplosomes:" << std::endl; for (int64_t individual_index = 0; individual_index < p_individuals_count; ++individual_index) { const Individual *ind = p_individuals[individual_index]; Haplosome **haplosomes = ind->haplosomes_; for (int haplosome_index = first_haplosome_index; haplosome_index <= last_haplosome_index; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; // i used to be the haplosome index, now it is the individual index; we will have one or // two lines with this individual index, depending on the intrinsic ploidy of the chromosome // since we changed from a haplosome index to an individual index, we now emit an "i", // just follow the same convention as the Individuals section p_out << "p" << ind->subpopulation_->subpopulation_id_ << ":i" << ind->index_; if (p_output_object_tags) { if (haplosome->tag_value_ == SLIM_TAG_UNSET_VALUE) p_out << " ?"; else p_out << ' ' << haplosome->tag_value_; } if (haplosome->IsNull()) { p_out << " "; } else { int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; int mut_count = mutrun->size(); const MutationIndex *mut_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < mut_count; ++mut_index) { slim_polymorphismid_t polymorphism_id = FindMutationInPolymorphismMap(polymorphisms, mut_block_ptr + mut_ptr[mut_index]); if (polymorphism_id == -1) EIDOS_TERMINATION << "ERROR (Population::PrintAll): (internal error) polymorphism not found." << EidosTerminate(); p_out << " " << polymorphism_id; } } } p_out << std::endl; #if DO_MEMORY_CHECKS if (eidos_do_memory_checks) { mem_check_counter++; if (mem_check_counter % mem_check_mod == 0) Eidos_CheckRSSAgainstMax("Population::PrintAll", "(Out of memory while printing haplosomes.)"); } #endif } } // print ancestral sequence if (output_ancestral_nucs) { p_out << "Ancestral sequence:" << std::endl; p_out << *(chromosome->AncestralSequence()); // operator<< above ends with a newline; here we add another, which the read code // can use to recognize that the nucleotide sequence has ended, even without an EOF p_out << std::endl; } } // Output substitutions at the end if requested; see Species::ExecuteMethod_outputFixedMutations() if (output_full_population && p_output_substitutions) { p_out << "Substitutions:" << std::endl; std::vector &subs = population.substitutions_; for (unsigned int i = 0; i < subs.size(); i++) { p_out << i << " "; if (p_output_object_tags) subs[i]->PrintForSLiMOutput_Tag(p_out); else subs[i]->PrintForSLiMOutput(p_out); #if DO_MEMORY_CHECKS if (eidos_do_memory_checks) { mem_check_counter++; if (mem_check_counter % mem_check_mod == 0) Eidos_CheckRSSAgainstMax("Species::ExecuteMethod_outputFixedMutations", "(outputFixedMutations(): Out of memory while outputting substitution objects.)"); } #endif } } // if we malloced a buffer of individuals above, free it now if (output_full_population) free(p_individuals); } void Individual::PrintIndividuals_VCF(std::ostream &p_out, const Individual **p_individuals, int64_t p_individuals_count, Species &p_species, bool p_output_multiallelics, bool p_simplify_nucs, bool p_output_nonnucs, Chromosome *p_focal_chromosome) { const std::vector &chromosomes = p_species.Chromosomes(); bool nucleotide_based = p_species.IsNucleotideBased(); bool pedigrees_enabled = p_species.PedigreesEnabledByUser(); // print the VCF header p_out << "##fileformat=VCFv4.2" << std::endl; { time_t rawtime; struct tm timeinfo; char buffer[25]; // should never be more than 10, in fact, plus a null time(&rawtime); localtime_r(&rawtime, &timeinfo); strftime(buffer, 25, "%Y%m%d", &timeinfo); p_out << "##fileDate=" << std::string(buffer) << std::endl; } p_out << "##source=SLiM" << std::endl; // BCH 2/11/2025: Unlike Haplosome::PrintHaplosomes_VCF(), we can print individual pedigree IDs, // since we are working with a vector of individuals, not a vector of haplosomes. if (pedigrees_enabled && (p_individuals_count > 0)) { p_out << "##slimIndividualPedigreeIDs="; for (int64_t individual_index = 0; individual_index < p_individuals_count; ++individual_index) { if (individual_index > 0) p_out << ","; p_out << p_individuals[individual_index]->pedigree_id_; } p_out << std::endl; } // BCH 6 March 2019: Note that all of the INFO fields that provide per-mutation information have been // changed from a Number of 1 to a Number of ., since in nucleotide-based models we can call more than // one allele in a single call line (unlike in non-nucleotide-based models). p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; // Note that at present we do not output the hemizygous dominance coefficient; too edge p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; // changed to ticks for 4.0, and changed "GO" to "TO" p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; p_out << "##INFO=" << std::endl; if (p_output_multiallelics && !nucleotide_based) p_out << "##INFO=" << std::endl; if (nucleotide_based) p_out << "##INFO=" << std::endl; if (p_output_nonnucs && nucleotide_based) p_out << "##INFO=" << std::endl; p_out << "##FORMAT=" << std::endl; p_out << "##contig=" << std::endl; p_out << "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT"; // When printing individual identifiers, we print the actual identifiers like p1:i17, // unlike Population::PrintSample_VCF() and Haplosome_Class::ExecuteMethod_outputX() for (slim_popsize_t individual_index = 0; individual_index < p_individuals_count; individual_index++) { const Individual &ind = *p_individuals[individual_index]; slim_popsize_t index_in_subpop = ind.index_; Subpopulation *subpop = ind.subpopulation_; if (!subpop || (index_in_subpop == -1)) EIDOS_TERMINATION << "ERROR (Individual::PrintIndividuals_VCF): target individuals must be visible in a subpopulation (i.e., may not be a new juvenile)." << EidosTerminate(); p_out << "\tp" << subpop->subpopulation_id_ << ":i" << index_in_subpop; } p_out << std::endl; for (Chromosome *chromosome : chromosomes) { if (p_focal_chromosome && (chromosome != p_focal_chromosome)) continue; slim_chromosome_index_t chromosome_index = chromosome->Index(); int intrinsic_ploidy = chromosome->IntrinsicPloidy(); int first_haplosome_index_ = p_species.FirstHaplosomeIndices()[chromosome_index]; int last_haplosome_index_ = p_species.LastHaplosomeIndices()[chromosome_index]; int64_t haplosome_count = p_individuals_count * intrinsic_ploidy; // assemble a vector of haplosomes, allowing us to share code with Haplosome::PrintHaplosomes_VCF() const Haplosome **haplosomes_buffer = (const Haplosome **)malloc(haplosome_count * sizeof(Haplosome *)); int64_t haplosome_buffer_index = 0; for (int64_t individual_index = 0; individual_index < p_individuals_count; ++individual_index) { const Individual &ind = *p_individuals[individual_index]; for (slim_popsize_t i = first_haplosome_index_; i <= last_haplosome_index_; i++) haplosomes_buffer[haplosome_buffer_index++] = ind.haplosomes_[i]; } Haplosome::_PrintVCF(p_out, haplosomes_buffer, haplosome_count, *chromosome, /* p_groupAsIndividuals*/ true, p_simplify_nucs, p_output_nonnucs, p_output_multiallelics); } } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - void Individual::GenerateCachedEidosValue(void) { // Note that this cache cannot be invalidated as long as a symbol table might exist that this value has been placed into self_value_ = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_Individual_Class)); } const EidosClass *Individual::Class(void) const { return gSLiM_Individual_Class; } void Individual::Print(std::ostream &p_ostream) const { if (killed_) p_ostream << Class()->ClassNameForDisplay() << ""; else p_ostream << Class()->ClassNameForDisplay() << "subpopulation_id_ << ":i" << index_ << ">"; } EidosValue_SP Individual::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_subpopulation: // ACCELERATED { if (killed_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property subpopulation is not available for individuals that have been killed; they have no subpopulation." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(subpopulation_, gSLiM_Subpopulation_Class)); } case gID_index: // ACCELERATED { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(index_)); } case gID_haplosomes: // ACCELERATED { int haplosome_count_per_individual = subpopulation_->HaplosomeCountPerIndividual(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class))->resize_no_initialize(haplosome_count_per_individual); Haplosome **haplosomes = haplosomes_; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; vec->set_object_element_no_check_NORR(haplosome, haplosome_index); } return EidosValue_SP(vec); } case gID_haplosomesNonNull: // ACCELERATED { int haplosome_count_per_individual = subpopulation_->HaplosomeCountPerIndividual(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class))->reserve(haplosome_count_per_individual); Haplosome **haplosomes = haplosomes_; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; if (!haplosome->IsNull()) vec->push_object_element_no_check_NORR(haplosome); } return EidosValue_SP(vec); } case gID_haploidGenome1: // ACCELERATED case gID_haploidGenome1NonNull: // ACCELERATED /* A vector of all Haplosome objects associated with this individual that are attributed to its first parent (the female parent, in sexual models). This method assumes the individual was generated by the typical method for each chromosome type, as explained below; it does not trace back the true ancestry of each haplosome. The semantics of this are more obvious for some chromosome types than others, depending on the inheritance pattern of the chromosome as described in initializeChromosome(). For chromosomes with two associated haplosomes (types "A", "X", "Z", "H-", and "-Y"), the first haplosome is assumed to be from the first parent, and is thus included, whereas the second haplosome is assumed to be from the second parent and is thus not included. For chromosomes with one associated haplosome that is inherited from the female parent in one way or another (types "W", "HF", and "FL"), that haplosome is always included. For type "H", the single haplosome is assumed to have come from the first parent (since clonal inheritance is the common case), and so is included. Other chromosome types ("Y", "HM", "ML") are never included. See also the haploidGenome1NonNull property and the haplosomesForChromosomes() method. */ { bool allowNullHaplosomes = (p_property_id == gID_haploidGenome1); // the semantics of this property assume that the individual was generated by a biparental cross int haplosome_count_per_individual = subpopulation_->HaplosomeCountPerIndividual(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class))->reserve(haplosome_count_per_individual); Haplosome **haplosomes = haplosomes_; int haplosome_index = 0; for (Chromosome *chromosome : subpopulation_->species_.Chromosomes()) { switch (chromosome->Type()) { // chromosomes that have two haplosomes, the first of which is from the first parent case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kX_XSexChromosome: case ChromosomeType::kZ_ZSexChromosome: case ChromosomeType::kNullY_YSexChromosomeWithNull: case ChromosomeType::kHNull_HaploidAutosomeWithNull: // assumed to follow the same pattern { Haplosome *haplosome = haplosomes[haplosome_index]; if (allowNullHaplosomes || !haplosome->IsNull()) vec->push_object_element_no_check_NORR(haplosome); haplosome_index += 2; break; } // chromosomes that have one haplosome, from the female parent case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kHF_HaploidFemaleInherited: case ChromosomeType::kFL_HaploidFemaleLine: case ChromosomeType::kH_HaploidAutosome: // assumed to follow the same pattern, so included { Haplosome *haplosome = haplosomes[haplosome_index]; if (allowNullHaplosomes || !haplosome->IsNull()) vec->push_object_element_no_check_NORR(haplosome); haplosome_index += 1; break; } // chromosomes that have one haplosome, from the male parent case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kHM_HaploidMaleInherited: case ChromosomeType::kML_HaploidMaleLine: { haplosome_index += 1; break; } } } return EidosValue_SP(vec); } case gID_haploidGenome2: // ACCELERATED case gID_haploidGenome2NonNull: // ACCELERATED /* A vector of all Haplosome objects associated with this individual that are attributed to its second parent (the male parent, in sexual models). This method assumes the individual was generated by the typical method for each chromosome type, as explained below; it does not trace back the true ancestry of each haplosome. The semantics of this are more obvious for some chromosome types than others, depending on the inheritance pattern of the chromosome as described in initializeChromosome(). For chromosomes with two associated haplosomes (types "A", "X", "Z", "H-", and"-Y"), the second haplosome is assumed to be from the second parent, and is thus included, whereas the first haplosome is assumed to be from the first parent and is thus not included. For chromosomes with one associated haplosome that is inherited from the male parent in one way or another (types "Y", "HM", and "ML"), that haplosome is always included. For type "H", the single haplosome is assumed to have come from the first parent (since clonal inheritance is the common case), and so is not included. Other chromosome types ("W", "HF", "FL") are never included. See also the haploidGenome2NonNull property and the haplosomesForChromosomes() method. */ { bool allowNullHaplosomes = (p_property_id == gID_haploidGenome2); // the semantics of this property assume that the individual was generated by a biparental cross int haplosome_count_per_individual = subpopulation_->HaplosomeCountPerIndividual(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class))->reserve(haplosome_count_per_individual); Haplosome **haplosomes = haplosomes_; int haplosome_index = 0; for (Chromosome *chromosome : subpopulation_->species_.Chromosomes()) { switch (chromosome->Type()) { // chromosomes that have two haplosomes, the second of which is from the second parent case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kX_XSexChromosome: case ChromosomeType::kZ_ZSexChromosome: case ChromosomeType::kNullY_YSexChromosomeWithNull: case ChromosomeType::kHNull_HaploidAutosomeWithNull: // assumed to follow the same pattern { Haplosome *haplosome = haplosomes[haplosome_index+1]; if (allowNullHaplosomes || !haplosome->IsNull()) vec->push_object_element_no_check_NORR(haplosome); haplosome_index += 2; break; } // chromosomes that have one haplosome, from the female parent case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kHF_HaploidFemaleInherited: case ChromosomeType::kFL_HaploidFemaleLine: case ChromosomeType::kH_HaploidAutosome: // assumed to follow the same pattern, so skipped { haplosome_index += 1; break; } // chromosomes that have one haplosome, from the male parent case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kHM_HaploidMaleInherited: case ChromosomeType::kML_HaploidMaleLine: { Haplosome *haplosome = haplosomes[haplosome_index]; if (allowNullHaplosomes || !haplosome->IsNull()) vec->push_object_element_no_check_NORR(haplosome); haplosome_index += 1; break; } } } return EidosValue_SP(vec); } case gID_sex: { static EidosValue_SP static_sex_string_H; static EidosValue_SP static_sex_string_F; static EidosValue_SP static_sex_string_M; static EidosValue_SP static_sex_string_O; #pragma omp critical (GetProperty_sex_cache) { if (!static_sex_string_H) { static_sex_string_H = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("H")); static_sex_string_F = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("F")); static_sex_string_M = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("M")); static_sex_string_O = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String("?")); } } switch (sex_) { case IndividualSex::kHermaphrodite: return static_sex_string_H; case IndividualSex::kFemale: return static_sex_string_F; case IndividualSex::kMale: return static_sex_string_M; default: return static_sex_string_O; } } case gID_age: // ACCELERATED { if (age_ == -1) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property age is not available in WF models." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(age_)); } case gID_meanParentAge: { if (mean_parent_age_ == -1) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property meanParentAge is not available in WF models." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(mean_parent_age_)); } case gID_pedigreeID: // ACCELERATED { Species &species = subpopulation_->species_; if (!species.PedigreesEnabledByUser() && !species.RecordingTreeSequence()) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property pedigreeID is not available because neither pedigree recording nor tree-sequence recording has been enabled." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(pedigree_id_)); } case gID_pedigreeParentIDs: { if (!subpopulation_->species_.PedigreesEnabledByUser()) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property pedigreeParentIDs is not available because pedigree recording has not been enabled." << EidosTerminate(); EidosValue_Int *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(2); vec->set_int_no_check(pedigree_p1_, 0); vec->set_int_no_check(pedigree_p2_, 1); return EidosValue_SP(vec); } case gID_pedigreeGrandparentIDs: { if (!subpopulation_->species_.PedigreesEnabledByUser()) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property pedigreeGrandparentIDs is not available because pedigree recording has not been enabled." << EidosTerminate(); EidosValue_Int *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(4); vec->set_int_no_check(pedigree_g1_, 0); vec->set_int_no_check(pedigree_g2_, 1); vec->set_int_no_check(pedigree_g3_, 2); vec->set_int_no_check(pedigree_g4_, 3); return EidosValue_SP(vec); } case gID_reproductiveOutput: // ACCELERATED { if (!subpopulation_->species_.PedigreesEnabledByUser()) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property reproductiveOutput is not available because pedigree recording has not been enabled." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(reproductive_output_)); } case gID_spatialPosition: // ACCELERATED { Species &species = subpopulation_->species_; switch (species.SpatialDimensionality()) { case 0: EIDOS_TERMINATION << "ERROR (Individual::GetProperty): position cannot be accessed in non-spatial simulations." << EidosTerminate(); case 1: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(spatial_x_)); case 2: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float{spatial_x_, spatial_y_}); case 3: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float{spatial_x_, spatial_y_, spatial_z_}); default: return gStaticEidosValueNULL; // never hit; here to make the compiler happy } } case gID_uniqueMutations: { Species &species = subpopulation_->species_; int haplosome_count_per_individual = species.HaplosomeCountPerIndividual(); int total_mutation_count = 0; subpopulation_->population_.CheckForDeferralInHaplosomesVector(haplosomes_, haplosome_count_per_individual, "Individual::GetProperty"); for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = haplosomes_[haplosome_index]; if (!haplosome->IsNull()) total_mutation_count += haplosome->mutation_count(); } // We reserve a vector large enough to hold all the mutations from all haplosomes; probably usually overkill, but it does little harm EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class)); EidosValue_SP result_SP = EidosValue_SP(vec); if (total_mutation_count == 0) return result_SP; vec->reserve(total_mutation_count); Mutation *mut_block_ptr = gSLiM_Mutation_Block; for (Chromosome *chromosome : species.Chromosomes()) { int first_haplosome_index = species.FirstHaplosomeIndices()[chromosome->Index()]; int last_haplosome_index = species.LastHaplosomeIndices()[chromosome->Index()]; if (first_haplosome_index == last_haplosome_index) { // haploid chromosomes contain unique mutations by definition; add them all Haplosome *haplosome1 = haplosomes_[first_haplosome_index]; if (!haplosome1->IsNull()) { int mutrun_count = haplosome1->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun1 = haplosome1->mutruns_[run_index]; int g1_size = mutrun1->size(); int g1_index = 0; while (g1_index < g1_size) { MutationIndex mut = (*mutrun1)[g1_index++]; vec->push_object_element_no_check_RR(mut_block_ptr + mut); } } } } else { // diploid chromosomes require uniquing logic Haplosome *haplosome1 = haplosomes_[first_haplosome_index]; Haplosome *haplosome2 = haplosomes_[last_haplosome_index]; int haplosome1_size = (haplosome1->IsNull() ? 0 : haplosome1->mutation_count()); int haplosome2_size = (haplosome2->IsNull() ? 0 : haplosome2->mutation_count()); if (haplosome1_size + haplosome2_size > 0) { int mutrun_count = (haplosome1_size ? haplosome1->mutrun_count_ : haplosome2->mutrun_count_); for (int run_index = 0; run_index < mutrun_count; ++run_index) { // We want to interleave mutations from the two haplosomes, keeping only the uniqued mutations. For a given position, we take mutations // from g1 first, and then look at the mutations in g2 at the same position and add them if they are not in g1. const MutationRun *mutrun1 = (haplosome1_size ? haplosome1->mutruns_[run_index] : nullptr); const MutationRun *mutrun2 = (haplosome2_size ? haplosome2->mutruns_[run_index] : nullptr); int g1_size = (mutrun1 ? mutrun1->size() : 0); int g2_size = (mutrun2 ? mutrun2->size() : 0); int g1_index = 0, g2_index = 0; if (g1_size && g2_size) { // Get the position of the mutations at g1_index and g2_index MutationIndex g1_mut = (*mutrun1)[g1_index], g2_mut = (*mutrun2)[g2_index]; slim_position_t pos1 = (mut_block_ptr + g1_mut)->position_, pos2 = (mut_block_ptr + g2_mut)->position_; // Process mutations as long as both haplosomes still have mutations left in them do { if (pos1 < pos2) { vec->push_object_element_no_check_RR(mut_block_ptr + g1_mut); // Move to the next mutation in g1 if (++g1_index >= g1_size) break; g1_mut = (*mutrun1)[g1_index]; pos1 = (mut_block_ptr + g1_mut)->position_; } else if (pos1 > pos2) { vec->push_object_element_no_check_RR(mut_block_ptr + g2_mut); // Move to the next mutation in g2 if (++g2_index >= g2_size) break; g2_mut = (*mutrun2)[g2_index]; pos2 = (mut_block_ptr + g2_mut)->position_; } else { // pos1 == pos2; copy mutations from g1 until we are done with this position, then handle g2 slim_position_t focal_pos = pos1; int first_index = g1_index; bool done = false; while (pos1 == focal_pos) { vec->push_object_element_no_check_RR(mut_block_ptr + g1_mut); // Move to the next mutation in g1 if (++g1_index >= g1_size) { done = true; break; } g1_mut = (*mutrun1)[g1_index]; pos1 = (mut_block_ptr + g1_mut)->position_; } // Note that we may be done with g1 here, so be careful int last_index_plus_one = g1_index; while (pos2 == focal_pos) { int check_index; for (check_index = first_index; check_index < last_index_plus_one; ++check_index) if ((*mutrun1)[check_index] == g2_mut) break; // If the check indicates that g2_mut is not in g1, we copy it over if (check_index == last_index_plus_one) vec->push_object_element_no_check_RR(mut_block_ptr + g2_mut); // Move to the next mutation in g2 if (++g2_index >= g2_size) { done = true; break; } g2_mut = (*mutrun2)[g2_index]; pos2 = (mut_block_ptr + g2_mut)->position_; } // Note that we may be done with both g1 and/or g2 here; if so, done will be set and we will break out if (done) break; } } while (true); } // Finish off any tail ends, which must be unique and sorted already while (g1_index < g1_size) vec->push_object_element_no_check_RR(mut_block_ptr + (*mutrun1)[g1_index++]); while (g2_index < g2_size) vec->push_object_element_no_check_RR(mut_block_ptr + (*mutrun2)[g2_index++]); } } } } return result_SP; } // variables case gEidosID_color: { // as of SLiM 4.0.1, we construct a color string from the RGB values, which will // not necessarily be what the user set, but will represent the same color #ifdef SLIMGUI if (!color_set_) return gStaticEidosValue_StringEmpty; char hex_chars[8]; Eidos_GetColorString(colorR_, colorG_, colorB_, hex_chars); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(std::string(hex_chars))); #else // BCH 3/23/2025: color variables now only exist in SLiMgui, to save on memory footprint return gStaticEidosValue_StringEmpty; #endif } case gID_tag: // ACCELERATED { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tag accessed on individual before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } case gID_tagF: // ACCELERATED { double tagF_value = tagF_value_; if (tagF_value == SLIM_TAGF_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagF accessed on individual before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(tagF_value)); } case gID_tagL0: // ACCELERATED { if (!tagL0_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL0 accessed on individual before being set." << EidosTerminate(); return (tagL0_value_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } case gID_tagL1: // ACCELERATED { if (!tagL1_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL1 accessed on individual before being set." << EidosTerminate(); return (tagL1_value_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } case gID_tagL2: // ACCELERATED { if (!tagL2_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL2 accessed on individual before being set." << EidosTerminate(); return (tagL2_value_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } case gID_tagL3: // ACCELERATED { if (!tagL3_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL3 accessed on individual before being set." << EidosTerminate(); return (tagL3_value_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } case gID_tagL4: // ACCELERATED { if (!tagL4_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL4 accessed on individual before being set." << EidosTerminate(); return (tagL4_value_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } case gID_migrant: // ACCELERATED { return (migrant_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } case gID_fitnessScaling: // ACCELERATED { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(fitness_scaling_)); } case gEidosID_x: // ACCELERATED { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(spatial_x_)); } case gEidosID_y: // ACCELERATED { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(spatial_y_)); } case gEidosID_z: // ACCELERATED { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(spatial_z_)); } // These properties are presently undocumented, used for testing purposes, but maybe they are useful to others? // They provide x/y/z as pairs or a triplet, whether the model is spatial or not, regardless of dimensionality case gEidosID_xy: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float({spatial_x_, spatial_y_})); } case gEidosID_xz: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float({spatial_x_, spatial_z_})); } case gEidosID_yz: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float({spatial_y_, spatial_z_})); } case gEidosID_xyz: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float({spatial_x_, spatial_y_, spatial_z_})); } // all others, including gID_none default: return super::GetProperty(p_property_id); } } EidosValue *Individual::GetProperty_Accelerated_index(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); int_result->set_int_no_check(value->index_, value_index); } return int_result; } EidosValue *Individual::GetProperty_Accelerated_pedigreeID(EidosObject **p_values, size_t p_values_size) { Species *consensus_species = Community::SpeciesForIndividualsVector((Individual **)p_values, (int)p_values_size); EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); if (p_values_size == 0) return int_result; if (consensus_species) { // check that pedigree IDs are enabled, once Species &species = ((Individual *)(p_values[0]))->subpopulation_->species_; if (!species.PedigreesEnabledByUser() && !species.RecordingTreeSequence()) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property pedigreeID is not available because neither pedigree recording nor tree-sequence recording has been enabled." << EidosTerminate(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); int_result->set_int_no_check(value->pedigree_id_, value_index); } } else { // we have a mix of species, so we need to check that pedigree IDs are available for each individual for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Species &species = value->subpopulation_->species_; if (!species.PedigreesEnabledByUser() && !species.RecordingTreeSequence()) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property pedigreeID is not available because neither pedigree recording nor tree-sequence recording has been enabled." << EidosTerminate(); int_result->set_int_no_check(value->pedigree_id_, value_index); } } return int_result; } EidosValue *Individual::GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); slim_usertag_t tag_value = value->tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tag accessed on individual before being set." << EidosTerminate(); int_result->set_int_no_check(tag_value, value_index); } return int_result; } EidosValue *Individual::GetProperty_Accelerated_age(EidosObject **p_values, size_t p_values_size) { if ((p_values_size > 0) && (((Individual *)(p_values[0]))->subpopulation_->community_.ModelType() == SLiMModelType::kModelTypeWF)) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property age is not available in WF models." << EidosTerminate(); EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); int_result->set_int_no_check(value->age_, value_index); } return int_result; } EidosValue *Individual::GetProperty_Accelerated_reproductiveOutput(EidosObject **p_values, size_t p_values_size) { if ((p_values_size > 0) && !((Individual *)(p_values[0]))->subpopulation_->species_.PedigreesEnabledByUser()) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property reproductiveOutput is not available because pedigree recording has not been enabled." << EidosTerminate(); EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); int_result->set_int_no_check(value->reproductive_output_, value_index); } return int_result; } EidosValue *Individual::GetProperty_Accelerated_tagF(EidosObject **p_values, size_t p_values_size) { EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); double tagF_value = value->tagF_value_; if (tagF_value == SLIM_TAGF_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagF accessed on individual before being set." << EidosTerminate(); float_result->set_float_no_check(tagF_value, value_index); } return float_result; } EidosValue *Individual::GetProperty_Accelerated_tagL0(EidosObject **p_values, size_t p_values_size) { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); if (!value->tagL0_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL0 accessed on individual before being set." << EidosTerminate(); logical_result->set_logical_no_check(value->tagL0_value_, value_index); } return logical_result; } EidosValue *Individual::GetProperty_Accelerated_tagL1(EidosObject **p_values, size_t p_values_size) { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); if (!value->tagL1_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL1 accessed on individual before being set." << EidosTerminate(); logical_result->set_logical_no_check(value->tagL1_value_, value_index); } return logical_result; } EidosValue *Individual::GetProperty_Accelerated_tagL2(EidosObject **p_values, size_t p_values_size) { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); if (!value->tagL2_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL2 accessed on individual before being set." << EidosTerminate(); logical_result->set_logical_no_check(value->tagL2_value_, value_index); } return logical_result; } EidosValue *Individual::GetProperty_Accelerated_tagL3(EidosObject **p_values, size_t p_values_size) { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); if (!value->tagL3_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL3 accessed on individual before being set." << EidosTerminate(); logical_result->set_logical_no_check(value->tagL3_value_, value_index); } return logical_result; } EidosValue *Individual::GetProperty_Accelerated_tagL4(EidosObject **p_values, size_t p_values_size) { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); if (!value->tagL4_set_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property tagL4 accessed on individual before being set." << EidosTerminate(); logical_result->set_logical_no_check(value->tagL4_value_, value_index); } return logical_result; } EidosValue *Individual::GetProperty_Accelerated_migrant(EidosObject **p_values, size_t p_values_size) { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); logical_result->set_logical_no_check(value->migrant_, value_index); } return logical_result; } EidosValue *Individual::GetProperty_Accelerated_fitnessScaling(EidosObject **p_values, size_t p_values_size) { EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); float_result->set_float_no_check(value->fitness_scaling_, value_index); } return float_result; } EidosValue *Individual::GetProperty_Accelerated_x(EidosObject **p_values, size_t p_values_size) { EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); float_result->set_float_no_check(value->spatial_x_, value_index); } return float_result; } EidosValue *Individual::GetProperty_Accelerated_y(EidosObject **p_values, size_t p_values_size) { EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); float_result->set_float_no_check(value->spatial_y_, value_index); } return float_result; } EidosValue *Individual::GetProperty_Accelerated_z(EidosObject **p_values, size_t p_values_size) { EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); float_result->set_float_no_check(value->spatial_z_, value_index); } return float_result; } EidosValue *Individual::GetProperty_Accelerated_spatialPosition(EidosObject **p_values, size_t p_values_size) { Species *consensus_species = Community::SpeciesForIndividualsVector((Individual **)p_values, (int)p_values_size); EidosValue_Float *float_result; if (consensus_species) { // All individuals belong to the same species (common case), so we have the same dimensionality for all int dimensionality = consensus_species->SpatialDimensionality(); if (dimensionality == 0) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): position cannot be accessed in non-spatial simulations." << EidosTerminate(); float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(p_values_size * dimensionality); if (dimensionality == 1) { for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); float_result->set_float_no_check(value->spatial_x_, value_index); } } else if (dimensionality == 2) { size_t result_index = 0; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); float_result->set_float_no_check(value->spatial_x_, result_index++); float_result->set_float_no_check(value->spatial_y_, result_index++); } } else // dimensionality == 3 { size_t result_index = 0; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); float_result->set_float_no_check(value->spatial_x_, result_index++); float_result->set_float_no_check(value->spatial_y_, result_index++); float_result->set_float_no_check(value->spatial_z_, result_index++); } } } else { // Mixed-species group, so we have to figure out the dimensionality for each individual separately // FIXME: Do we really want to allow this? seems crazy - how would the user actually use this? float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float()); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Species &species = value->subpopulation_->species_; switch (species.SpatialDimensionality()) { case 0: EIDOS_TERMINATION << "ERROR (Individual::GetProperty): position cannot be accessed in non-spatial simulations." << EidosTerminate(); case 1: float_result->push_float(value->spatial_x_); break; case 2: float_result->push_float(value->spatial_x_); float_result->push_float(value->spatial_y_); break; case 3: float_result->push_float(value->spatial_x_); float_result->push_float(value->spatial_y_); float_result->push_float(value->spatial_z_); break; default: break; // never hit; here to make the compiler happy } } } return float_result; } EidosValue *Individual::GetProperty_Accelerated_subpopulation(EidosObject **p_values, size_t p_values_size) { EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Subpopulation_Class))->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); if (value->killed_) EIDOS_TERMINATION << "ERROR (Individual::GetProperty): property subpopulation is not available for individuals that have been killed; they have no subpopulation." << EidosTerminate(); object_result->set_object_element_no_check_NORR(value->subpopulation_, value_index); } return object_result; } EidosValue *Individual::GetProperty_Accelerated_haploidGenome1(EidosObject **p_values, size_t p_values_size) { const Individual **individuals_buffer = (const Individual **)p_values; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector(individuals_buffer, (int)p_values_size); if (!species) return nullptr; // defer to GetProperty(); different species will have different chromosomes const std::vector &chromosomes = species->Chromosomes(); if (chromosomes.size() != 1) return nullptr; // defer to GetProperty(); multiple chromosomes need to be looped through Chromosome *chromosome = chromosomes[0]; EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class)); switch (chromosome->Type()) { // chromosomes that have two haplosomes, the first of which is from the first parent case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kX_XSexChromosome: case ChromosomeType::kZ_ZSexChromosome: case ChromosomeType::kNullY_YSexChromosomeWithNull: case ChromosomeType::kHNull_HaploidAutosomeWithNull: // assumed to follow the same pattern { object_result->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); object_result->set_object_element_no_check_NORR(value->haplosomes_[0], value_index); } return object_result; } // chromosomes that have one haplosome, from the female parent case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kHF_HaploidFemaleInherited: case ChromosomeType::kFL_HaploidFemaleLine: case ChromosomeType::kH_HaploidAutosome: // assumed to follow the same pattern, so included { object_result->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); object_result->set_object_element_no_check_NORR(value->haplosomes_[0], value_index); } return object_result; } // chromosomes that have one haplosome, from the male parent case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kHM_HaploidMaleInherited: case ChromosomeType::kML_HaploidMaleLine: { return object_result; // zero-length return } } // some compilers warn if this is not here, even though the switch above handles all ChromosomeType values EIDOS_TERMINATION << "ERROR (Individual::GetProperty_Accelerated_haploidGenome1): (internal error) chromosome type not handled." << EidosTerminate(); } EidosValue *Individual::GetProperty_Accelerated_haploidGenome1NonNull(EidosObject **p_values, size_t p_values_size) { const Individual **individuals_buffer = (const Individual **)p_values; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector(individuals_buffer, (int)p_values_size); if (!species) return nullptr; // defer to GetProperty(); different species will have different chromosomes const std::vector &chromosomes = species->Chromosomes(); if (chromosomes.size() != 1) return nullptr; // defer to GetProperty(); multiple chromosomes need to be looped through Chromosome *chromosome = chromosomes[0]; EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class)); switch (chromosome->Type()) { // chromosomes that have two haplosomes, the first of which is from the first parent case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kX_XSexChromosome: case ChromosomeType::kZ_ZSexChromosome: case ChromosomeType::kNullY_YSexChromosomeWithNull: case ChromosomeType::kHNull_HaploidAutosomeWithNull: // assumed to follow the same pattern { object_result->reserve(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Haplosome *haplosome = value->haplosomes_[0]; if (!haplosome->IsNull()) object_result->push_object_element_no_check_NORR(haplosome); } return object_result; } // chromosomes that have one haplosome, from the female parent case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kHF_HaploidFemaleInherited: case ChromosomeType::kFL_HaploidFemaleLine: case ChromosomeType::kH_HaploidAutosome: // assumed to follow the same pattern, so included { object_result->reserve(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Haplosome *haplosome = value->haplosomes_[0]; if (!haplosome->IsNull()) object_result->push_object_element_no_check_NORR(haplosome); } return object_result; } // chromosomes that have one haplosome, from the male parent case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kHM_HaploidMaleInherited: case ChromosomeType::kML_HaploidMaleLine: { return object_result; // zero-length return } } // some compilers warn if this is not here, even though the switch above handles all ChromosomeType values EIDOS_TERMINATION << "ERROR (Individual::GetProperty_Accelerated_haploidGenome1NonNull): (internal error) chromosome type not handled." << EidosTerminate(); } EidosValue *Individual::GetProperty_Accelerated_haploidGenome2(EidosObject **p_values, size_t p_values_size) { const Individual **individuals_buffer = (const Individual **)p_values; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector(individuals_buffer, (int)p_values_size); if (!species) return nullptr; // defer to GetProperty(); different species will have different chromosomes const std::vector &chromosomes = species->Chromosomes(); if (chromosomes.size() != 1) return nullptr; // defer to GetProperty(); multiple chromosomes need to be looped through Chromosome *chromosome = chromosomes[0]; EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class)); switch (chromosome->Type()) { // chromosomes that have two haplosomes, the first of which is from the first parent case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kX_XSexChromosome: case ChromosomeType::kZ_ZSexChromosome: case ChromosomeType::kNullY_YSexChromosomeWithNull: case ChromosomeType::kHNull_HaploidAutosomeWithNull: // assumed to follow the same pattern { object_result->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); object_result->set_object_element_no_check_NORR(value->haplosomes_[1], value_index); } return object_result; } // chromosomes that have one haplosome, from the female parent case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kHF_HaploidFemaleInherited: case ChromosomeType::kFL_HaploidFemaleLine: case ChromosomeType::kH_HaploidAutosome: // assumed to follow the same pattern, so included { return object_result; // zero-length return } // chromosomes that have one haplosome, from the male parent case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kHM_HaploidMaleInherited: case ChromosomeType::kML_HaploidMaleLine: { object_result->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); object_result->set_object_element_no_check_NORR(value->haplosomes_[0], value_index); } return object_result; } } // some compilers warn if this is not here, even though the switch above handles all ChromosomeType values EIDOS_TERMINATION << "ERROR (Individual::GetProperty_Accelerated_haploidGenome2): (internal error) chromosome type not handled." << EidosTerminate(); } EidosValue *Individual::GetProperty_Accelerated_haploidGenome2NonNull(EidosObject **p_values, size_t p_values_size) { const Individual **individuals_buffer = (const Individual **)p_values; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector(individuals_buffer, (int)p_values_size); if (!species) return nullptr; // defer to GetProperty(); different species will have different chromosomes const std::vector &chromosomes = species->Chromosomes(); if (chromosomes.size() != 1) return nullptr; // defer to GetProperty(); multiple chromosomes need to be looped through Chromosome *chromosome = chromosomes[0]; EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class)); switch (chromosome->Type()) { // chromosomes that have two haplosomes, the first of which is from the first parent case ChromosomeType::kA_DiploidAutosome: case ChromosomeType::kX_XSexChromosome: case ChromosomeType::kZ_ZSexChromosome: case ChromosomeType::kNullY_YSexChromosomeWithNull: case ChromosomeType::kHNull_HaploidAutosomeWithNull: // assumed to follow the same pattern { object_result->reserve(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Haplosome *haplosome = value->haplosomes_[1]; if (!haplosome->IsNull()) object_result->push_object_element_no_check_NORR(haplosome); } return object_result; } // chromosomes that have one haplosome, from the female parent case ChromosomeType::kW_WSexChromosome: case ChromosomeType::kHF_HaploidFemaleInherited: case ChromosomeType::kFL_HaploidFemaleLine: case ChromosomeType::kH_HaploidAutosome: // assumed to follow the same pattern, so included { return object_result; // zero-length return } // chromosomes that have one haplosome, from the male parent case ChromosomeType::kY_YSexChromosome: case ChromosomeType::kHM_HaploidMaleInherited: case ChromosomeType::kML_HaploidMaleLine: { object_result->reserve(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Haplosome *haplosome = value->haplosomes_[0]; if (!haplosome->IsNull()) object_result->push_object_element_no_check_NORR(haplosome); } return object_result; } } // some compilers warn if this is not here, even though the switch above handles all ChromosomeType values EIDOS_TERMINATION << "ERROR (Individual::GetProperty_Accelerated_haploidGenome2NonNull): (internal error) chromosome type not handled." << EidosTerminate(); } EidosValue *Individual::GetProperty_Accelerated_haplosomes(EidosObject **p_values, size_t p_values_size) { const Individual **individuals_buffer = (const Individual **)p_values; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector(individuals_buffer, (int)p_values_size); if (!species) return nullptr; // defer to GetProperty(); different species will have different chromosomes int haplosome_count_per_individual = species->HaplosomeCountPerIndividual(); EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class))->resize_no_initialize(p_values_size * haplosome_count_per_individual); size_t result_index = 0; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Haplosome **haplosomes = value->haplosomes_; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; object_result->set_object_element_no_check_NORR(haplosome, result_index++); } } return object_result; } EidosValue *Individual::GetProperty_Accelerated_haplosomesNonNull(EidosObject **p_values, size_t p_values_size) { const Individual **individuals_buffer = (const Individual **)p_values; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector(individuals_buffer, (int)p_values_size); if (!species) return nullptr; // defer to GetProperty(); different species will have different chromosomes int haplosome_count_per_individual = species->HaplosomeCountPerIndividual(); EidosValue_Object *object_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class))->reserve(p_values_size * haplosome_count_per_individual); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *value = (Individual *)(p_values[value_index]); Haplosome **haplosomes = value->haplosomes_; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = haplosomes[haplosome_index]; if (!haplosome->IsNull()) object_result->push_object_element_no_check_NORR(haplosome); } } return object_result; } void Individual::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { case gEidosID_color: // ACCELERATED { #ifdef SLIMGUI // BCH 3/23/2025: color variables now only exist in SLiMgui, to save on memory footprint const std::string &color_string = ((EidosValue_String &)p_value).StringRefAtIndex_NOCAST(0, nullptr); if (color_string.empty()) { color_set_ = false; } else { Eidos_GetColorComponents(color_string, &colorR_, &colorG_, &colorB_); color_set_ = true; s_any_individual_color_set_ = true; // keep track of the fact that an individual's color has been set } #endif return; } case gID_tag: // ACCELERATED { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; s_any_individual_tag_set_ = true; return; } case gID_tagF: // ACCELERATED { tagF_value_ = p_value.FloatAtIndex_NOCAST(0, nullptr); s_any_individual_tagF_set_ = true; return; } case gID_tagL0: // ACCELERATED { eidos_logical_t value = p_value.LogicalAtIndex_NOCAST(0, nullptr); tagL0_set_ = true; tagL0_value_ = value; s_any_individual_tagL_set_ = true; return; } case gID_tagL1: // ACCELERATED { eidos_logical_t value = p_value.LogicalAtIndex_NOCAST(0, nullptr); tagL1_set_ = true; tagL1_value_ = value; s_any_individual_tagL_set_ = true; return; } case gID_tagL2: // ACCELERATED { eidos_logical_t value = p_value.LogicalAtIndex_NOCAST(0, nullptr); tagL2_set_ = true; tagL2_value_ = value; s_any_individual_tagL_set_ = true; return; } case gID_tagL3: // ACCELERATED { eidos_logical_t value = p_value.LogicalAtIndex_NOCAST(0, nullptr); tagL3_set_ = true; tagL3_value_ = value; s_any_individual_tagL_set_ = true; return; } case gID_tagL4: // ACCELERATED { eidos_logical_t value = p_value.LogicalAtIndex_NOCAST(0, nullptr); tagL4_set_ = true; tagL4_value_ = value; s_any_individual_tagL_set_ = true; return; } case gID_fitnessScaling: // ACCELERATED { fitness_scaling_ = p_value.FloatAtIndex_NOCAST(0, nullptr); Individual::s_any_individual_fitness_scaling_set_ = true; if ((fitness_scaling_ < 0.0) || (std::isnan(fitness_scaling_))) EIDOS_TERMINATION << "ERROR (Individual::SetProperty): property fitnessScaling must be >= 0.0." << EidosTerminate(); return; } case gEidosID_x: // ACCELERATED { spatial_x_ = p_value.FloatAtIndex_NOCAST(0, nullptr); return; } case gEidosID_y: // ACCELERATED { spatial_y_ = p_value.FloatAtIndex_NOCAST(0, nullptr); return; } case gEidosID_z: // ACCELERATED { spatial_z_ = p_value.FloatAtIndex_NOCAST(0, nullptr); return; } case gID_age: // ACCELERATED { slim_age_t value = SLiMCastToAgeTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); age_ = value; return; } // all others, including gID_none default: return super::SetProperty(p_property_id, p_value); } } void Individual::SetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { s_any_individual_tag_set_ = true; // SLiMCastToUsertagTypeOrRaise() is a no-op at present if (p_source_size == 1) { int64_t source_value = p_source.IntAtIndex_NOCAST(0, nullptr); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->tag_value_ = source_value; } else { const int64_t *source_data = p_source.IntData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->tag_value_ = source_data[value_index]; } } void Individual::SetProperty_Accelerated_tagF(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { s_any_individual_tagF_set_ = true; // SLiMCastToUsertagTypeOrRaise() is a no-op at present if (p_source_size == 1) { double source_value = p_source.FloatAtIndex_NOCAST(0, nullptr); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->tagF_value_ = source_value; } else { const double *source_data = p_source.FloatData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->tagF_value_ = source_data[value_index]; } } void Individual::SetProperty_Accelerated_tagL0(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { s_any_individual_tagL_set_ = true; const eidos_logical_t *source_data = p_source.LogicalData(); if (p_source_size == 1) { eidos_logical_t source_value = source_data[0]; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL0_set_ = true; individual->tagL0_value_ = source_value; } } else { for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL0_set_ = true; individual->tagL0_value_ = source_data[value_index]; } } } void Individual::SetProperty_Accelerated_tagL1(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { s_any_individual_tagL_set_ = true; const eidos_logical_t *source_data = p_source.LogicalData(); if (p_source_size == 1) { eidos_logical_t source_value = source_data[0]; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL1_set_ = true; individual->tagL1_value_ = source_value; } } else { for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL1_set_ = true; individual->tagL1_value_ = source_data[value_index]; } } } void Individual::SetProperty_Accelerated_tagL2(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { s_any_individual_tagL_set_ = true; const eidos_logical_t *source_data = p_source.LogicalData(); if (p_source_size == 1) { eidos_logical_t source_value = source_data[0]; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL2_set_ = true; individual->tagL2_value_ = source_value; } } else { for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL2_set_ = true; individual->tagL2_value_ = source_data[value_index]; } } } void Individual::SetProperty_Accelerated_tagL3(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { s_any_individual_tagL_set_ = true; const eidos_logical_t *source_data = p_source.LogicalData(); if (p_source_size == 1) { eidos_logical_t source_value = source_data[0]; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL3_set_ = true; individual->tagL3_value_ = source_value; } } else { for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL3_set_ = true; individual->tagL3_value_ = source_data[value_index]; } } } void Individual::SetProperty_Accelerated_tagL4(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { s_any_individual_tagL_set_ = true; const eidos_logical_t *source_data = p_source.LogicalData(); if (p_source_size == 1) { eidos_logical_t source_value = source_data[0]; for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL4_set_ = true; individual->tagL4_value_ = source_value; } } else { for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->tagL4_set_ = true; individual->tagL4_value_ = source_data[value_index]; } } } bool Individual::_SetFitnessScaling_1(double source_value, EidosObject **p_values, size_t p_values_size) { if ((source_value < 0.0) || (std::isnan(source_value))) return true; // Note that parallelization here only helps on machines with very high memory bandwidth, // because this loop spends all of its time writing to memory. It also introduces a // potential race condition if the same Individual is referenced more than once in // p_values; that is considered a bug in the user's script, and we could check for it // in DEBUG mode if we wanted to. EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_FITNESS_SCALE_1); #pragma omp parallel for simd schedule(simd:static) default(none) shared(p_values_size) firstprivate(p_values, source_value) if(parallel:p_values_size >= EIDOS_OMPMIN_SET_FITNESS_SCALE_1) num_threads(thread_count) for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->fitness_scaling_ = source_value; return false; } bool Individual::_SetFitnessScaling_N(const double *source_data, EidosObject **p_values, size_t p_values_size) { bool saw_error = false; // deferred raises for OpenMP compliance // Note that parallelization here only helps on machines with very high memory bandwidth, // because this loop spends all of its time writing to memory. It also introduces a // potential race condition if the same Individual is referenced more than once in // p_values; that is considered a bug in the user's script, and we could check for it // in DEBUG mode if we wanted to. EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_FITNESS_SCALE_2); #pragma omp parallel for schedule(static) default(none) shared(p_values_size) firstprivate(p_values, source_data) reduction(||: saw_error) if(p_values_size >= EIDOS_OMPMIN_SET_FITNESS_SCALE_2) num_threads(thread_count) for (size_t value_index = 0; value_index < p_values_size; ++value_index) { double source_value = source_data[value_index]; if ((source_value < 0.0) || (std::isnan(source_value))) saw_error = true; ((Individual *)(p_values[value_index]))->fitness_scaling_ = source_value; } return saw_error; } void Individual::SetProperty_Accelerated_fitnessScaling(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { Individual::s_any_individual_fitness_scaling_set_ = true; bool needs_raise = false; if (p_source_size == 1) { double source_value = p_source.FloatAtIndex_NOCAST(0, nullptr); needs_raise = _SetFitnessScaling_1(source_value, p_values, p_values_size); } else { const double *source_data = p_source.FloatData(); needs_raise = _SetFitnessScaling_N(source_data, p_values, p_values_size); } if (needs_raise) EIDOS_TERMINATION << "ERROR (Individual::SetProperty_Accelerated_fitnessScaling): property fitnessScaling must be >= 0.0." << EidosTerminate(); } void Individual::SetProperty_Accelerated_x(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { if (p_source_size == 1) { double source_value = p_source.FloatAtIndex_NOCAST(0, nullptr); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->spatial_x_ = source_value; } else { const double *source_data = p_source.FloatData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->spatial_x_ = source_data[value_index]; } } void Individual::SetProperty_Accelerated_y(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { if (p_source_size == 1) { double source_value = p_source.FloatAtIndex_NOCAST(0, nullptr); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->spatial_y_ = source_value; } else { const double *source_data = p_source.FloatData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->spatial_y_ = source_data[value_index]; } } void Individual::SetProperty_Accelerated_z(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { if (p_source_size == 1) { double source_value = p_source.FloatAtIndex_NOCAST(0, nullptr); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->spatial_z_ = source_value; } else { const double *source_data = p_source.FloatData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->spatial_z_ = source_data[value_index]; } } void Individual::SetProperty_Accelerated_color(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { #pragma unused (p_values, p_values_size, p_source, p_source_size) #ifdef SLIMGUI // BCH 3/23/2025: color variables now only exist in SLiMgui, to save on memory footprint if (p_source_size == 1) { const std::string &source_value = ((EidosValue_String &)p_source).StringRefAtIndex_NOCAST(0, nullptr); if (source_value.empty()) { for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->color_set_ = false; } else { uint8_t color_red, color_green, color_blue; Eidos_GetColorComponents(source_value, &color_red, &color_green, &color_blue); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); individual->colorR_ = color_red; individual->colorG_ = color_green; individual->colorB_ = color_blue; individual->color_set_ = true; } s_any_individual_color_set_ = true; // keep track of the fact that an individual's color has been set } } else { const std::string *source_data = p_source.StringData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { Individual *individual = ((Individual *)(p_values[value_index])); const std::string &source_value = source_data[value_index]; if (source_value.empty()) { individual->color_set_ = false; } else { Eidos_GetColorComponents(source_value, &individual->colorR_, &individual->colorG_, &individual->colorB_); individual->color_set_ = true; s_any_individual_color_set_ = true; // keep track of the fact that an individual's color has been set } } } #endif } void Individual::SetProperty_Accelerated_age(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size) { if (p_source_size == 1) { int64_t source_value = p_source.IntAtIndex_NOCAST(0, nullptr); slim_age_t source_age = SLiMCastToAgeTypeOrRaise(source_value); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->age_ = source_age; } else { const int64_t *source_data = p_source.IntData(); for (size_t value_index = 0; value_index < p_values_size; ++value_index) ((Individual *)(p_values[value_index]))->age_ = SLiMCastToAgeTypeOrRaise(source_data[value_index]); } } EidosValue_SP Individual::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_containsMutations: return ExecuteMethod_containsMutations(p_method_id, p_arguments, p_interpreter); //case gID_countOfMutationsOfType: return ExecuteMethod_Accelerated_countOfMutationsOfType(p_method_id, p_arguments, p_interpreter); case gID_haplosomesForChromosomes: return ExecuteMethod_haplosomesForChromosomes(p_method_id, p_arguments, p_interpreter); case gID_relatedness: return ExecuteMethod_relatedness(p_method_id, p_arguments, p_interpreter); case gID_sharedParentCount: return ExecuteMethod_sharedParentCount(p_method_id, p_arguments, p_interpreter); //case gID_sumOfMutationsOfType: return ExecuteMethod_Accelerated_sumOfMutationsOfType(p_method_id, p_arguments, p_interpreter); case gID_uniqueMutationsOfType: return ExecuteMethod_uniqueMutationsOfType(p_method_id, p_arguments, p_interpreter); case gID_mutationsFromHaplosomes: return ExecuteMethod_mutationsFromHaplosomes(p_method_id, p_arguments, p_interpreter); default: { // In a sense, we here "subclass" EidosDictionaryUnretained to override setValue(); we set a flag remembering that // an individual's dictionary has been modified, and then we call "super" for the usual behavior. if (p_method_id == gEidosID_setValue) s_any_individual_dictionary_set_ = true; return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } } // ********************* - (logical)containsMutations(object mutations) // EidosValue_SP Individual::ExecuteMethod_containsMutations(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) int haplosome_count_per_individual = subpopulation_->species_.HaplosomeCountPerIndividual(); subpopulation_->population_.CheckForDeferralInHaplosomesVector(haplosomes_, haplosome_count_per_individual, "Individual::ExecuteMethod_containsMutations"); EidosValue *mutations_value = p_arguments[0].get(); int mutations_count = mutations_value->Count(); if (mutations_count == 0) return gStaticEidosValue_Logical_ZeroVec; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForMutations(mutations_value); if (species != &subpopulation_->species_) EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_containsMutations): containsMutations() requires that all mutations belong to the same species as the target individual." << EidosTerminate(); if (mutations_count == 1) { // treat the singleton case separately to return gStaticEidosValue_LogicalT / gStaticEidosValue_LogicalF Mutation *mut = (Mutation *)(mutations_value->ObjectElementAtIndex_NOCAST(0, nullptr)); slim_chromosome_index_t mut_chrom_index = mut->chromosome_index_; int first_haplosome_index = species->FirstHaplosomeIndices()[mut_chrom_index]; int last_haplosome_index = species->LastHaplosomeIndices()[mut_chrom_index]; for (int haplosome_index = first_haplosome_index; haplosome_index <= last_haplosome_index; ++haplosome_index) { Haplosome *haplosome = haplosomes_[haplosome_index]; if (!haplosome->IsNull() && haplosome->contains_mutation(mut)) return gStaticEidosValue_LogicalT; } return gStaticEidosValue_LogicalF; } else { EidosValue_Logical *logical_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Logical())->resize_no_initialize(mutations_count); Mutation * const *mutations = (Mutation * const *)mutations_value->ObjectData(); for (int value_index = 0; value_index < mutations_count; ++value_index) { Mutation *mut = mutations[value_index]; slim_chromosome_index_t mut_chrom_index = mut->chromosome_index_; int first_haplosome_index = species->FirstHaplosomeIndices()[mut_chrom_index]; int last_haplosome_index = species->LastHaplosomeIndices()[mut_chrom_index]; for (int haplosome_index = first_haplosome_index; haplosome_index <= last_haplosome_index; ++haplosome_index) { Haplosome *haplosome = haplosomes_[haplosome_index]; if (!haplosome->IsNull() && haplosome->contains_mutation(mut)) { logical_result->set_logical_no_check(true, value_index); continue; } logical_result->set_logical_no_check(false, value_index); } } return EidosValue_SP(logical_result); } } // ********************* - (integer$)countOfMutationsOfType(io$ mutType) // EidosValue_SP Individual::ExecuteMethod_Accelerated_countOfMutationsOfType(EidosObject **p_elements, size_t p_elements_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (p_elements_size == 0) return gStaticEidosValue_Integer_ZeroVec; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector((Individual **)p_elements, (int)p_elements_size); if (species == nullptr) EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_Accelerated_countOfMutationsOfType): countOfMutationsOfType() requires that mutType belongs to the same species as the target individual." << EidosTerminate(); species->population_.CheckForDeferralInIndividualsVector((Individual **)p_elements, p_elements_size, "Individual::ExecuteMethod_Accelerated_countOfMutationsOfType"); EidosValue *mutType_value = p_arguments[0].get(); MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species->community_, species, "countOfMutationsOfType()"); // SPECIES CONSISTENCY CHECK // Count the number of mutations of the given type Mutation *mut_block_ptr = gSLiM_Mutation_Block; EidosValue_Int *integer_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_elements_size); int haplosome_count_per_individual = species->HaplosomeCountPerIndividual(); EIDOS_THREAD_COUNT(gEidos_OMP_threads_I_COUNT_OF_MUTS_OF_TYPE); #pragma omp parallel for schedule(dynamic, 1) default(none) shared(p_elements_size) firstprivate(p_elements, mut_block_ptr, mutation_type_ptr, integer_result) if(p_elements_size >= EIDOS_OMPMIN_I_COUNT_OF_MUTS_OF_TYPE) num_threads(thread_count) for (size_t element_index = 0; element_index < p_elements_size; ++element_index) { Individual *element = (Individual *)(p_elements[element_index]); int match_count = 0; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = element->haplosomes_[haplosome_index]; if (!haplosome->IsNull()) { int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; int haplosome1_count = mutrun->size(); const MutationIndex *haplosome1_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < haplosome1_count; ++mut_index) if ((mut_block_ptr + haplosome1_ptr[mut_index])->mutation_type_ptr_ == mutation_type_ptr) ++match_count; } } } integer_result->set_int_no_check(match_count, element_index); } return EidosValue_SP(integer_result); } // ********************* - (object)haplosomesForChromosomes([Niso chromosomes = NULL], [Ni$ index = NULL], [logical$ includeNulls = T]) // EidosValue_SP Individual::ExecuteMethod_haplosomesForChromosomes(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *chromosomes_value = p_arguments[0].get(); EidosValue *index_value = p_arguments[1].get(); EidosValue *includeNulls_value = p_arguments[2].get(); // assemble a vector of chromosome indices we're fetching Species &species = subpopulation_->species_; std::vector chromosome_indices; species.GetChromosomeIndicesFromEidosValue(chromosome_indices, chromosomes_value); // get index and includeNulls int64_t index = -1; // for NULL if (index_value->Type() == EidosValueType::kValueInt) { index = index_value->IntAtIndex_NOCAST(0, nullptr); if ((index != 0) && (index != 1)) EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_haplosomesForChromosomes): haplosomesForChromosomes() requires that index is 0, 1, or NULL." << EidosTerminate(); } bool includeNulls = includeNulls_value->LogicalAtIndex_NOCAST(0, nullptr); // fetch the requested haplosomes EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Haplosome_Class)); AppendHaplosomesForChromosomes(vec, chromosome_indices, index, includeNulls); return EidosValue_SP(vec); } // ********************* - (float)relatedness(object individuals, [Niso$ chromosome = NULL]) // EidosValue_SP Individual::ExecuteMethod_relatedness(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *individuals_value = p_arguments[0].get(); EidosValue *chromosome_value = p_arguments[1].get(); int individuals_count = individuals_value->Count(); if (individuals_count == 0) return gStaticEidosValue_Float_ZeroVec; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividuals(individuals_value); if (species != &subpopulation_->species_) EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_relatedness): relatedness() requires that all individuals belong to the same species as the target individual." << EidosTerminate(); Chromosome *chromosome = species->GetChromosomeFromEidosValue(chromosome_value); if (!chromosome) { if (species->Chromosomes().size() == 1) chromosome = species->Chromosomes()[0]; else if (species->Chromosomes().size() > 1) EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_relatedness): relatedness() requires the chromosome to be specified in multi-chromosome models." << EidosTerminate(); } // in a no-genetics model, the chromosome parameter must be NULL, so chromosome will be nullptr, and we assume type "A" ChromosomeType chromosome_type = (chromosome ? chromosome->Type() : ChromosomeType::kA_DiploidAutosome); bool pedigree_tracking_enabled = subpopulation_->species_.PedigreesEnabledByUser(); EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(individuals_count); Individual * const *individuals_data = (Individual * const *)individuals_value->ObjectData(); if (pedigree_tracking_enabled) { // this parallelizes the case of one_individual.relatedness(many_individuals) // it would be nice to also parallelize the case of many_individuals.relatedness(one_individual); that would require accelerating this method EIDOS_THREAD_COUNT(gEidos_OMP_threads_RELATEDNESS); #pragma omp parallel for schedule(dynamic, 128) default(none) shared(individuals_count, individuals_data) firstprivate(float_result) if(individuals_count >= EIDOS_OMPMIN_RELATEDNESS) num_threads(thread_count) for (int value_index = 0; value_index < individuals_count; ++value_index) { Individual *ind = individuals_data[value_index]; double relatedness = RelatednessToIndividual(*ind, chromosome_type); float_result->set_float_no_check(relatedness, value_index); } } else { for (int value_index = 0; value_index < individuals_count; ++value_index) { Individual *ind = individuals_data[value_index]; double relatedness = (ind == this) ? 1.0 : 0.0; float_result->set_float_no_check(relatedness, value_index); } } return EidosValue_SP(float_result); } // ********************* - (integer)sharedParentCount(o individuals) // EidosValue_SP Individual::ExecuteMethod_sharedParentCount(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *individuals_value = p_arguments[0].get(); int individuals_count = individuals_value->Count(); // SPECIES CONSISTENCY CHECK if (individuals_count > 0) { Species *species = Community::SpeciesForIndividuals(individuals_value); if (species != &subpopulation_->species_) EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_sharedParentCount): sharedParentCount() requires that all individuals belong to the same species as the target individual." << EidosTerminate(); } bool pedigree_tracking_enabled = subpopulation_->species_.PedigreesEnabledByUser(); EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(individuals_count); Individual * const *individuals = (Individual * const *)individuals_value->ObjectData(); if (pedigree_tracking_enabled) { // FIXME needs parallelization, see relatedness() for (int value_index = 0; value_index < individuals_count; ++value_index) { Individual *ind = individuals[value_index]; int shared_count = SharedParentCountWithIndividual(*ind); int_result->set_int_no_check(shared_count, value_index); } } else { for (int value_index = 0; value_index < individuals_count; ++value_index) { Individual *ind = individuals[value_index]; int shared_count = (ind == this) ? 2.0 : 0.0; int_result->set_int_no_check(shared_count, value_index); } } return EidosValue_SP(int_result); } // ********************* - (integer$)sumOfMutationsOfType(io$ mutType) // EidosValue_SP Individual::ExecuteMethod_Accelerated_sumOfMutationsOfType(EidosObject **p_elements, size_t p_elements_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (p_elements_size == 0) return gStaticEidosValue_Float_ZeroVec; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector((Individual **)p_elements, (int)p_elements_size); if (species == nullptr) EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_Accelerated_sumOfMutationsOfType): sumOfMutationsOfType() requires that mutType belongs to the same species as the target individual." << EidosTerminate(); species->population_.CheckForDeferralInIndividualsVector((Individual **)p_elements, p_elements_size, "Individual::ExecuteMethod_Accelerated_sumOfMutationsOfType"); EidosValue *mutType_value = p_arguments[0].get(); MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species->community_, species, "sumOfMutationsOfType()"); // SPECIES CONSISTENCY CHECK // Sum the selection coefficients of mutations of the given type Mutation *mut_block_ptr = gSLiM_Mutation_Block; EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(p_elements_size); int haplosome_count_per_individual = species->HaplosomeCountPerIndividual(); EIDOS_THREAD_COUNT(gEidos_OMP_threads_SUM_OF_MUTS_OF_TYPE); #pragma omp parallel for schedule(dynamic, 1) default(none) shared(p_elements_size) firstprivate(p_elements, mut_block_ptr, mutation_type_ptr, float_result) if(p_elements_size >= EIDOS_OMPMIN_SUM_OF_MUTS_OF_TYPE) num_threads(thread_count) for (size_t element_index = 0; element_index < p_elements_size; ++element_index) { Individual *element = (Individual *)(p_elements[element_index]); double selcoeff_sum = 0.0; for (int haplosome_index = 0; haplosome_index < haplosome_count_per_individual; haplosome_index++) { Haplosome *haplosome = element->haplosomes_[haplosome_index]; if (!haplosome->IsNull()) { int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; int haplosome1_count = mutrun->size(); const MutationIndex *haplosome1_ptr = mutrun->begin_pointer_const(); for (int mut_index = 0; mut_index < haplosome1_count; ++mut_index) { Mutation *mut_ptr = mut_block_ptr + haplosome1_ptr[mut_index]; if (mut_ptr->mutation_type_ptr_ == mutation_type_ptr) selcoeff_sum += mut_ptr->selection_coeff_; } } } } float_result->set_float_no_check(selcoeff_sum, element_index); } return EidosValue_SP(float_result); } // ********************* - (object)uniqueMutationsOfType(io$ mutType) // EidosValue_SP Individual::ExecuteMethod_uniqueMutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // NOTE: this method has been deprecated in favor of mutationsFromHaplosomes() int haplosome_count_per_individual = subpopulation_->species_.HaplosomeCountPerIndividual(); subpopulation_->population_.CheckForDeferralInHaplosomesVector(haplosomes_, haplosome_count_per_individual, "Individual::ExecuteMethod_uniqueMutationsOfType"); EidosValue *mutType_value = p_arguments[0].get(); Species &species = subpopulation_->species_; MutationType *mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species.community_, &species, "uniqueMutationsOfType()"); // SPECIES CONSISTENCY CHECK // This code is adapted from uniqueMutations and follows its logic closely // We try to reserve a vector large enough to hold all the mutations; probably usually overkill, but it does little harm // Note that since we do not *always* reserve, we have to use push_object_element() below to check for space EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class)); EidosValue_SP result_SP = EidosValue_SP(vec); bool only_haploid_haplosomes = true; size_t vec_reserve_size = 0; for (Chromosome *chromosome : species.Chromosomes()) { int first_haplosome_index = species.FirstHaplosomeIndices()[chromosome->Index()]; int last_haplosome_index = species.LastHaplosomeIndices()[chromosome->Index()]; if (first_haplosome_index == last_haplosome_index) { // haploid chromosomes contain unique mutations by definition; add them all Haplosome *haplosome1 = haplosomes_[first_haplosome_index]; if (!haplosome1->IsNull()) vec_reserve_size += haplosome1->mutation_count(); } else { // diploid chromsomes where one is empty/null also contain unique mutations by definition Haplosome *haplosome1 = haplosomes_[first_haplosome_index]; Haplosome *haplosome2 = haplosomes_[last_haplosome_index]; int haplosome1_size = (haplosome1->IsNull() ? 0 : haplosome1->mutation_count()); int haplosome2_size = (haplosome2->IsNull() ? 0 : haplosome2->mutation_count()); if (haplosome1_size == 0) { vec_reserve_size += haplosome2_size; } else if (haplosome2_size == 0) { vec_reserve_size += haplosome1_size; } else { vec_reserve_size += (haplosome1_size + haplosome2_size); only_haploid_haplosomes = false; } } } if (vec_reserve_size == 0) return result_SP; if (only_haploid_haplosomes || (vec_reserve_size < 100)) // an arbitrary limit, but we don't want to make something *too* unnecessarily big... vec->reserve(vec_reserve_size); Mutation *mut_block_ptr = gSLiM_Mutation_Block; for (Chromosome *chromosome : species.Chromosomes()) { int first_haplosome_index = species.FirstHaplosomeIndices()[chromosome->Index()]; int last_haplosome_index = species.LastHaplosomeIndices()[chromosome->Index()]; if (first_haplosome_index == last_haplosome_index) { // haploid chromosomes contain unique mutations by definition; add them all Haplosome *haplosome1 = haplosomes_[first_haplosome_index]; if (!haplosome1->IsNull()) { int mutrun_count = haplosome1->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun1 = haplosome1->mutruns_[run_index]; int g1_size = mutrun1->size(); int g1_index = 0; while (g1_index < g1_size) { MutationIndex mut = (*mutrun1)[g1_index++]; if ((mut_block_ptr + mut)->mutation_type_ptr_ == mutation_type_ptr) vec->push_object_element_RR(mut_block_ptr + mut); } } } } else { // diploid chromosomes require uniquing logic Haplosome *haplosome1 = haplosomes_[first_haplosome_index]; Haplosome *haplosome2 = haplosomes_[last_haplosome_index]; int haplosome1_size = (haplosome1->IsNull() ? 0 : haplosome1->mutation_count()); int haplosome2_size = (haplosome2->IsNull() ? 0 : haplosome2->mutation_count()); int mutrun_count = (haplosome1_size ? haplosome1->mutrun_count_ : haplosome2->mutrun_count_); for (int run_index = 0; run_index < mutrun_count; ++run_index) { // We want to interleave mutations from the two haplosomes, keeping only the uniqued mutations. For a given position, we take mutations // from g1 first, and then look at the mutations in g2 at the same position and add them if they are not in g1. const MutationRun *mutrun1 = (haplosome1_size ? haplosome1->mutruns_[run_index] : nullptr); const MutationRun *mutrun2 = (haplosome2_size ? haplosome2->mutruns_[run_index] : nullptr); int g1_size = (mutrun1 ? mutrun1->size() : 0); int g2_size = (mutrun2 ? mutrun2->size() : 0); int g1_index = 0, g2_index = 0; if (g1_size && g2_size) { MutationIndex g1_mut = (*mutrun1)[g1_index], g2_mut = (*mutrun2)[g2_index]; // At this point, we need to loop forward in g1 and g2 until we have found mutations of the right type in both while ((mut_block_ptr + g1_mut)->mutation_type_ptr_ != mutation_type_ptr) { if (++g1_index >= g1_size) break; g1_mut = (*mutrun1)[g1_index]; } while ((mut_block_ptr + g2_mut)->mutation_type_ptr_ != mutation_type_ptr) { if (++g2_index >= g2_size) break; g2_mut = (*mutrun2)[g2_index]; } if ((g1_index < g1_size) && (g2_index < g2_size)) { slim_position_t pos1 = (mut_block_ptr + g1_mut)->position_; slim_position_t pos2 = (mut_block_ptr + g2_mut)->position_; // Process mutations as long as both haplosomes still have mutations left in them do { // Now we have mutations of the right type, so we can start working with them by position if (pos1 < pos2) { vec->push_object_element_RR(mut_block_ptr + g1_mut); // Move to the next mutation in g1 loopback1: if (++g1_index >= g1_size) break; g1_mut = (*mutrun1)[g1_index]; if ((mut_block_ptr + g1_mut)->mutation_type_ptr_ != mutation_type_ptr) goto loopback1; pos1 = (mut_block_ptr + g1_mut)->position_; } else if (pos1 > pos2) { vec->push_object_element_RR(mut_block_ptr + g2_mut); // Move to the next mutation in g2 loopback2: if (++g2_index >= g2_size) break; g2_mut = (*mutrun2)[g2_index]; if ((mut_block_ptr + g2_mut)->mutation_type_ptr_ != mutation_type_ptr) goto loopback2; pos2 = (mut_block_ptr + g2_mut)->position_; } else { // pos1 == pos2; copy mutations from g1 until we are done with this position, then handle g2 slim_position_t focal_pos = pos1; int first_index = g1_index; bool done = false; while (pos1 == focal_pos) { vec->push_object_element_RR(mut_block_ptr + g1_mut); // Move to the next mutation in g1 loopback3: if (++g1_index >= g1_size) { done = true; break; } g1_mut = (*mutrun1)[g1_index]; if ((mut_block_ptr + g1_mut)->mutation_type_ptr_ != mutation_type_ptr) goto loopback3; pos1 = (mut_block_ptr + g1_mut)->position_; } // Note that we may be done with g1 here, so be careful int last_index_plus_one = g1_index; while (pos2 == focal_pos) { int check_index; for (check_index = first_index; check_index < last_index_plus_one; ++check_index) if ((*mutrun1)[check_index] == g2_mut) break; // If the check indicates that g2_mut is not in g1, we copy it over if (check_index == last_index_plus_one) vec->push_object_element_RR(mut_block_ptr + g2_mut); // Move to the next mutation in g2 loopback4: if (++g2_index >= g2_size) { done = true; break; } g2_mut = (*mutrun2)[g2_index]; if ((mut_block_ptr + g2_mut)->mutation_type_ptr_ != mutation_type_ptr) goto loopback4; pos2 = (mut_block_ptr + g2_mut)->position_; } // Note that we may be done with both g1 and/or g2 here; if so, done will be set and we will break out if (done) break; } } while (true); } } // Finish off any tail ends, which must be unique and sorted already while (g1_index < g1_size) { MutationIndex mut = (*mutrun1)[g1_index++]; if ((mut_block_ptr + mut)->mutation_type_ptr_ == mutation_type_ptr) vec->push_object_element_RR(mut_block_ptr + mut); } while (g2_index < g2_size) { MutationIndex mut = (*mutrun2)[g2_index++]; if ((mut_block_ptr + mut)->mutation_type_ptr_ == mutation_type_ptr) vec->push_object_element_RR(mut_block_ptr + mut); } } } } return result_SP; /* A SLiM model to test the above code: initialize() { initializeMutationRate(1e-5); initializeMutationType("m1", 0.5, "f", 0.0); initializeMutationType("m2", 0.5, "f", 0.0); initializeMutationType("m3", 0.5, "f", 0.0); initializeGenomicElementType("g1", c(m1, m2, m3), c(1.0, 1.0, 1.0)); initializeGenomicElement(g1, 0, 99999); initializeRecombinationRate(1e-8); } 1 early() { sim.addSubpop("p1", 500); } 1:20000 late() { for (i in p1.individuals) { // check m1 um1 = i.uniqueMutationsOfType(m1); um2 = sortBy(unique(i.haplosomes.mutationsOfType(m1)), "position"); if (!identical(um1.position, um2.position)) { print("Mismatch for m1!"); print(um1.position); print(um2.position); } } } */ } // ********************* - (object)mutationsFromHaplosomes(string$ category, [Nio$ mutType = NULL], [Niso chromosomes = NULL]) // EidosValue_SP Individual::ExecuteMethod_mutationsFromHaplosomes(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *category_value = p_arguments[0].get(); EidosValue *mutType_value = p_arguments[1].get(); EidosValue *chromosomes_value = p_arguments[2].get(); Species &species = subpopulation_->species_; // parse category typedef enum _SLiMMutationFilteringCategory { kFilterUnique, kFilterHomozygous, kFilterHeterozygous, kFilterHemizygous, kFilterAll } SLiMMutationFilteringCategory; SLiMMutationFilteringCategory category; std::string category_string = category_value->StringAtIndex_NOCAST(0, nullptr); if (category_string == "unique") category = SLiMMutationFilteringCategory::kFilterUnique; else if (category_string == "homozygous") category = SLiMMutationFilteringCategory::kFilterHomozygous; else if (category_string == "heterozygous") category = SLiMMutationFilteringCategory::kFilterHeterozygous; else if (category_string == "hemizygous") category = SLiMMutationFilteringCategory::kFilterHemizygous; else if (category_string == "all") category = SLiMMutationFilteringCategory::kFilterAll; else EIDOS_TERMINATION << "ERROR (Individual::ExecuteMethod_mutationsFromHaplosomes): mutationsFromHaplosomes() requires that category is 'unique', 'homozygous', 'heterozygous', 'hemizygous', or 'all'." << EidosTerminate(); // parse mutType MutationType *mutation_type_ptr = nullptr; // used if mutType_value is NULL, to indicate applicability to all mutation types if (mutType_value->Type() != EidosValueType::kValueNULL) { mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutType_value, 0, &species.community_, &species, "mutationsFromHaplosomes()"); // SPECIES CONSISTENCY CHECK } // parse chromosomes std::vector chromosome_indices; species.GetChromosomeIndicesFromEidosValue(chromosome_indices, chromosomes_value); // loop through the chromosomes EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class)); EidosValue_SP result_SP = EidosValue_SP(vec); Mutation *mut_block_ptr = gSLiM_Mutation_Block; for (slim_chromosome_index_t chromosome_index : chromosome_indices) { Chromosome *chromosome = species.Chromosomes()[chromosome_index]; int first_haplosome_index = species.FirstHaplosomeIndices()[chromosome_index]; int last_haplosome_index = species.LastHaplosomeIndices()[chromosome_index]; if (chromosome->IntrinsicPloidy() == 1) { // the chromosome is intrinsically haploid; add its mutations if category applies if ((category != SLiMMutationFilteringCategory::kFilterUnique) && (category != SLiMMutationFilteringCategory::kFilterHomozygous) && (category != SLiMMutationFilteringCategory::kFilterAll)) continue; Haplosome *haplosome = haplosomes_[first_haplosome_index]; if (haplosome->IsNull()) continue; int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun1 = haplosome->mutruns_[run_index]; int g1_size = mutrun1->size(); int g1_index = 0; while (g1_index < g1_size) { MutationIndex mut_index = (*mutrun1)[g1_index++]; if (!mutation_type_ptr || ((mut_block_ptr + mut_index)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + mut_index); } } } else { // the chromosome is intrinsically diploid Haplosome *haplosome1 = haplosomes_[first_haplosome_index]; Haplosome *haplosome2 = haplosomes_[last_haplosome_index]; if (haplosome1->IsNull() && haplosome2->IsNull()) { // both haplosomes are null; skip this chromosome continue; } else if (haplosome1->IsNull() || haplosome2->IsNull()) { // exactly one haplosome is null; hemizygous case if ((category != SLiMMutationFilteringCategory::kFilterUnique) && (category != SLiMMutationFilteringCategory::kFilterHemizygous) && (category != SLiMMutationFilteringCategory::kFilterAll)) continue; Haplosome *haplosome = haplosome1->IsNull() ? haplosome2 : haplosome1; int mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun1 = haplosome->mutruns_[run_index]; int g1_size = mutrun1->size(); int g1_index = 0; while (g1_index < g1_size) { MutationIndex mut_index = (*mutrun1)[g1_index++]; if (!mutation_type_ptr || ((mut_block_ptr + mut_index)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + mut_index); } } } else { // two non-null haplosomes; run through them in synchrony // this code is adapted from Subpopulation::_Fitness_DiploidChromosome() if ((category != SLiMMutationFilteringCategory::kFilterUnique) && (category != SLiMMutationFilteringCategory::kFilterHomozygous) && (category != SLiMMutationFilteringCategory::kFilterHeterozygous) && (category != SLiMMutationFilteringCategory::kFilterAll)) continue; // set flags that we can quickly check for whether we are pushing particular mutations or not bool push_homozygous = ((category == SLiMMutationFilteringCategory::kFilterHomozygous) || (category == SLiMMutationFilteringCategory::kFilterUnique) || (category == SLiMMutationFilteringCategory::kFilterAll)); bool push_heterozygous = ((category == SLiMMutationFilteringCategory::kFilterHeterozygous) || (category == SLiMMutationFilteringCategory::kFilterUnique) || (category == SLiMMutationFilteringCategory::kFilterAll)); // both haplosomes are being modeled, so we need to scan through and figure out which mutations are heterozygous and which are homozygous const int32_t mutrun_count = haplosome1->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun1 = haplosome1->mutruns_[run_index]; const MutationRun *mutrun2 = haplosome2->mutruns_[run_index]; // Read directly from the MutationRun buffers const MutationIndex *haplosome1_iter = mutrun1->begin_pointer_const(); const MutationIndex *haplosome2_iter = mutrun2->begin_pointer_const(); const MutationIndex *haplosome1_max = mutrun1->end_pointer_const(); const MutationIndex *haplosome2_max = mutrun2->end_pointer_const(); // first, handle the situation before either haplosome iterator has reached the end of its haplosome, for simplicity/speed if (haplosome1_iter != haplosome1_max && haplosome2_iter != haplosome2_max) { MutationIndex haplosome1_mutindex = *haplosome1_iter, haplosome2_mutindex = *haplosome2_iter; slim_position_t haplosome1_iter_position = (mut_block_ptr + haplosome1_mutindex)->position_, haplosome2_iter_position = (mut_block_ptr + haplosome2_mutindex)->position_; do { if (haplosome1_iter_position < haplosome2_iter_position) { // Process a mutation in haplosome1 since it is leading if (push_heterozygous) if (!mutation_type_ptr || ((mut_block_ptr + haplosome1_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome1_mutindex); if (++haplosome1_iter == haplosome1_max) break; else { haplosome1_mutindex = *haplosome1_iter; haplosome1_iter_position = (mut_block_ptr + haplosome1_mutindex)->position_; } } else if (haplosome1_iter_position > haplosome2_iter_position) { // Process a mutation in haplosome2 since it is leading if (push_heterozygous) if (!mutation_type_ptr || ((mut_block_ptr + haplosome2_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome2_mutindex); if (++haplosome2_iter == haplosome2_max) break; else { haplosome2_mutindex = *haplosome2_iter; haplosome2_iter_position = (mut_block_ptr + haplosome2_mutindex)->position_; } } else { // Look for homozygosity: haplosome1_iter_position == haplosome2_iter_position slim_position_t position = haplosome1_iter_position; const MutationIndex *haplosome1_start = haplosome1_iter; // advance through haplosome1 as long as we remain at the same position, handling one mutation at a time do { const MutationIndex *haplosome2_matchscan = haplosome2_iter; // advance through haplosome2 with haplosome2_matchscan, looking for a match for the current mutation in haplosome1, to determine whether we are homozygous or not while (haplosome2_matchscan != haplosome2_max && (mut_block_ptr + *haplosome2_matchscan)->position_ == position) { if (haplosome1_mutindex == *haplosome2_matchscan) { // a homozygous match was found if (push_homozygous) if (!mutation_type_ptr || ((mut_block_ptr + haplosome1_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome1_mutindex); // push a second copy only if we're doing category "all" if (category == SLiMMutationFilteringCategory::kFilterAll) if (!mutation_type_ptr || ((mut_block_ptr + haplosome1_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome1_mutindex); goto homozygousExit1; } haplosome2_matchscan++; } // no match was found, so we are heterozygous if (push_heterozygous) if (!mutation_type_ptr || ((mut_block_ptr + haplosome1_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome1_mutindex); homozygousExit1: if (++haplosome1_iter == haplosome1_max) break; else { haplosome1_mutindex = *haplosome1_iter; haplosome1_iter_position = (mut_block_ptr + haplosome1_mutindex)->position_; } } while (haplosome1_iter_position == position); // advance through haplosome2 as long as we remain at the same position, handling one mutation at a time do { const MutationIndex *haplosome1_matchscan = haplosome1_start; // advance through haplosome1 with haplosome1_matchscan, looking for a match for the current mutation in haplosome2, to determine whether we are homozygous or not while (haplosome1_matchscan != haplosome1_max && (mut_block_ptr + *haplosome1_matchscan)->position_ == position) { if (haplosome2_mutindex == *haplosome1_matchscan) { // a homozygous match was found; we know this match was already found by the haplosome1 loop above goto homozygousExit2; } haplosome1_matchscan++; } // no match was found, so we are heterozygous; we multiply our fitness by the selection coefficient and the dominance coefficient if (push_heterozygous) if (!mutation_type_ptr || ((mut_block_ptr + haplosome2_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome2_mutindex); homozygousExit2: if (++haplosome2_iter == haplosome2_max) break; else { haplosome2_mutindex = *haplosome2_iter; if (!mutation_type_ptr || ((mut_block_ptr + haplosome2_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) haplosome2_iter_position = (mut_block_ptr + haplosome2_mutindex)->position_; } } while (haplosome2_iter_position == position); // break out if either haplosome has reached its end if (haplosome1_iter == haplosome1_max || haplosome2_iter == haplosome2_max) break; } } while (true); } // one or the other haplosome has now reached its end, so now we just need to handle the remaining mutations in the unfinished haplosome #if DEBUG assert(!(haplosome1_iter != haplosome1_max && haplosome2_iter != haplosome2_max)); #endif // if haplosome1 is unfinished, finish it while (haplosome1_iter != haplosome1_max) { MutationIndex haplosome1_mutindex = *haplosome1_iter++; if (push_heterozygous) if (!mutation_type_ptr || ((mut_block_ptr + haplosome1_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome1_mutindex); } // if haplosome2 is unfinished, finish it while (haplosome2_iter != haplosome2_max) { MutationIndex haplosome2_mutindex = *haplosome2_iter++; if (push_heterozygous) if (!mutation_type_ptr || ((mut_block_ptr + haplosome2_mutindex)->mutation_type_ptr_ == mutation_type_ptr)) vec->push_object_element_RR(mut_block_ptr + haplosome2_mutindex); } } } } } return result_SP; } // // Individual_Class // #pragma mark - #pragma mark Individual_Class #pragma mark - EidosClass *gSLiM_Individual_Class = nullptr; const std::vector *Individual_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("Individual_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_subpopulation, true, kEidosValueMaskObject | kEidosValueMaskSingleton, gSLiM_Subpopulation_Class))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_subpopulation)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_index, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_index)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_haplosomes, true, kEidosValueMaskObject, gSLiM_Haplosome_Class))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_haplosomes)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_haplosomesNonNull, true, kEidosValueMaskObject, gSLiM_Haplosome_Class))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_haplosomesNonNull)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_haploidGenome1, true, kEidosValueMaskObject, gSLiM_Haplosome_Class))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_haploidGenome1)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_haploidGenome2, true, kEidosValueMaskObject, gSLiM_Haplosome_Class))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_haploidGenome2)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_haploidGenome1NonNull, true, kEidosValueMaskObject, gSLiM_Haplosome_Class))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_haploidGenome1NonNull)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_haploidGenome2NonNull, true, kEidosValueMaskObject, gSLiM_Haplosome_Class))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_haploidGenome2NonNull)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_sex, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_tag)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_tag)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tagF, false, kEidosValueMaskFloat | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_tagF)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_tagF)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tagL0, false, kEidosValueMaskLogical | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_tagL0)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_tagL0)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tagL1, false, kEidosValueMaskLogical | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_tagL1)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_tagL1)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tagL2, false, kEidosValueMaskLogical | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_tagL2)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_tagL2)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tagL3, false, kEidosValueMaskLogical | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_tagL3)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_tagL3)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tagL4, false, kEidosValueMaskLogical | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_tagL4)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_tagL4)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_migrant, true, kEidosValueMaskLogical | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_migrant)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_fitnessScaling, false, kEidosValueMaskFloat | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_fitnessScaling)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_fitnessScaling)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_x, false, kEidosValueMaskFloat | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_x)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_x)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_y, false, kEidosValueMaskFloat | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_y)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_y)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_z, false, kEidosValueMaskFloat | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_z)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_z)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_xy, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_xz, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_yz, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_xyz, true, kEidosValueMaskFloat))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_age, false, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_age)->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_age)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_meanParentAge, true, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_pedigreeID, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_pedigreeID)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_pedigreeParentIDs, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_pedigreeGrandparentIDs, true, kEidosValueMaskInt))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_reproductiveOutput, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_reproductiveOutput)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_spatialPosition, true, kEidosValueMaskFloat))->DeclareAcceleratedGet(Individual::GetProperty_Accelerated_spatialPosition)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_uniqueMutations, true, kEidosValueMaskObject, gSLiM_Mutation_Class))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_color, false, kEidosValueMaskString | kEidosValueMaskSingleton))->DeclareAcceleratedSet(Individual::SetProperty_Accelerated_color)); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *Individual_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("Individual_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_containsMutations, kEidosValueMaskLogical))->AddObject("mutations", gSLiM_Mutation_Class)); methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_countOfMutationsOfType, kEidosValueMaskInt | kEidosValueMaskSingleton))->AddIntObject_S("mutType", gSLiM_MutationType_Class))->DeclareAcceleratedImp(Individual::ExecuteMethod_Accelerated_countOfMutationsOfType)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_relatedness, kEidosValueMaskFloat))->AddObject("individuals", gSLiM_Individual_Class)->AddArgWithDefault(kEidosValueMaskNULL | kEidosValueMaskInt | kEidosValueMaskString | kEidosValueMaskObject | kEidosValueMaskOptional | kEidosValueMaskSingleton, "chromosome", gSLiM_Chromosome_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_haplosomesForChromosomes, kEidosValueMaskObject, gSLiM_Haplosome_Class))->AddArgWithDefault(kEidosValueMaskNULL | kEidosValueMaskInt | kEidosValueMaskString | kEidosValueMaskObject | kEidosValueMaskOptional, "chromosomes", gSLiM_Chromosome_Class, gStaticEidosValueNULL)->AddInt_OSN("index", gStaticEidosValueNULL)->AddLogical_OS("includeNulls", gStaticEidosValue_LogicalT)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_sharedParentCount, kEidosValueMaskInt))->AddObject("individuals", gSLiM_Individual_Class)); methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_sumOfMutationsOfType, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddIntObject_S("mutType", gSLiM_MutationType_Class))->DeclareAcceleratedImp(Individual::ExecuteMethod_Accelerated_sumOfMutationsOfType)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_uniqueMutationsOfType, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddIntObject_S("mutType", gSLiM_MutationType_Class)->MarkDeprecated()); methods->emplace_back(((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_zygosityOfMutations, kEidosValueMaskInt))->AddObject_ON("mutations", gSLiM_Mutation_Class, gStaticEidosValueNULL)->AddInt_OS("hemizygousValue", gStaticEidosValue_Integer1)->AddInt_OS("haploidValue", gStaticEidosValue_Integer1))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_mutationsFromHaplosomes, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddString_S("category")->AddIntObject_OSN("mutType", gSLiM_MutationType_Class, gStaticEidosValueNULL)->AddArgWithDefault(kEidosValueMaskNULL | kEidosValueMaskInt | kEidosValueMaskString | kEidosValueMaskObject | kEidosValueMaskOptional, "chromosomes", gSLiM_Chromosome_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_outputIndividuals, kEidosValueMaskVOID))->AddString_OSN(gEidosStr_filePath, gStaticEidosValueNULL)->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddArgWithDefault(kEidosValueMaskNULL | kEidosValueMaskInt | kEidosValueMaskString | kEidosValueMaskObject | kEidosValueMaskOptional | kEidosValueMaskSingleton, "chromosome", gSLiM_Chromosome_Class, gStaticEidosValueNULL)->AddLogical_OS("spatialPositions", gStaticEidosValue_LogicalT)->AddLogical_OS("ages", gStaticEidosValue_LogicalT)->AddLogical_OS("ancestralNucleotides", gStaticEidosValue_LogicalF)->AddLogical_OS("pedigreeIDs", gStaticEidosValue_LogicalF)->AddLogical_OS("objectTags", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_outputIndividualsToVCF, kEidosValueMaskVOID))->AddString_OSN(gEidosStr_filePath, gStaticEidosValueNULL)->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddArgWithDefault(kEidosValueMaskNULL | kEidosValueMaskInt | kEidosValueMaskString | kEidosValueMaskObject | kEidosValueMaskOptional | kEidosValueMaskSingleton, "chromosome", gSLiM_Chromosome_Class, gStaticEidosValueNULL)->AddLogical_OS("outputMultiallelics", gStaticEidosValue_LogicalT)->AddLogical_OS("simplifyNucleotides", gStaticEidosValue_LogicalF)->AddLogical_OS("outputNonnucleotides", gStaticEidosValue_LogicalT)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_readIndividualsFromVCF, kEidosValueMaskObject, gSLiM_Mutation_Class))->AddString_S(gEidosStr_filePath)->AddIntObject_OSN("mutationType", gSLiM_MutationType_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosClassMethodSignature *)(new EidosClassMethodSignature(gStr_setSpatialPosition, kEidosValueMaskVOID))->AddFloat("position")); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } EidosValue_SP Individual_Class::ExecuteClassMethod(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { switch (p_method_id) { case gID_outputIndividuals: return ExecuteMethod_outputIndividuals(p_method_id, p_target, p_arguments, p_interpreter); case gID_outputIndividualsToVCF: return ExecuteMethod_outputIndividualsToVCF(p_method_id, p_target, p_arguments, p_interpreter); case gID_readIndividualsFromVCF: return ExecuteMethod_readIndividualsFromVCF(p_method_id, p_target, p_arguments, p_interpreter); case gID_setSpatialPosition: return ExecuteMethod_setSpatialPosition(p_method_id, p_target, p_arguments, p_interpreter); case gID_zygosityOfMutations: return ExecuteMethod_zygosityOfMutations(p_method_id, p_target, p_arguments, p_interpreter); default: { // In a sense, we here "subclass" EidosDictionaryUnretained_Class to override setValuesVectorized(); we set a flag remembering that // an individual's dictionary has been modified, and then we call "super" for the usual behavior. if (p_method_id == gEidosID_setValuesVectorized) Individual::s_any_individual_dictionary_set_ = true; return super::ExecuteClassMethod(p_method_id, p_target, p_arguments, p_interpreter); } } } // ********************* + (void)outputIndividuals([Ns$ filePath = NULL], [logical$ append=F], [Niso$ chromosome = NULL], [logical$ spatialPositions = T], [logical$ ages = T], [logical$ ancestralNucleotides = F], [logical$ pedigreeIDs = F], [logical$ objectTags = F]) // EidosValue_SP Individual_Class::ExecuteMethod_outputIndividuals(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *filePath_value = p_arguments[0].get(); EidosValue *append_value = p_arguments[1].get(); EidosValue *chromosome_value = p_arguments[2].get(); EidosValue *spatialPositions_value = p_arguments[3].get(); EidosValue *ages_value = p_arguments[4].get(); EidosValue *ancestralNucleotides_value = p_arguments[5].get(); EidosValue *pedigreeIDs_value = p_arguments[6].get(); EidosValue *objectTags_value = p_arguments[7].get(); // here we need to require at least one target individual, // do a species consistency check and get the species/community, // and get the vector of individuals that we will pass in // (from the raw data of the EidosValue, no need to copy; but add const) int individuals_count = p_target->Count(); if (individuals_count == 0) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_outputIndividuals): outputIndividuals() cannot be called on a zero-length target vector; at least one individual is required." << EidosTerminate(); const Individual **individuals_buffer = (const Individual **)p_target->ObjectData(); // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividuals(p_target); if (!species) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_outputIndividuals): outputIndividuals() requires that all individuals belong to the same species." << EidosTerminate(); Community &community = species->community_; // TIMING RESTRICTION if (!community.warned_early_output_) { if ((community.CycleStage() == SLiMCycleStage::kWFStage0ExecuteFirstScripts) || (community.CycleStage() == SLiMCycleStage::kWFStage1ExecuteEarlyScripts)) { if (!gEidosSuppressWarnings) { p_interpreter.ErrorOutputStream() << "#WARNING (Individual_Class::ExecuteMethod_outputIndividuals): outputIndividuals() should probably not be called from a first() or early() event in a WF model; the output will reflect state at the beginning of the cycle, not the end." << std::endl; community.warned_early_output_ = true; } } } Chromosome *chromosome = species->GetChromosomeFromEidosValue(chromosome_value); // NULL returns nullptr bool output_spatial_positions = spatialPositions_value->LogicalAtIndex_NOCAST(0, nullptr); bool output_ages = ages_value->LogicalAtIndex_NOCAST(0, nullptr); bool output_ancestral_nucs = ancestralNucleotides_value->LogicalAtIndex_NOCAST(0, nullptr); bool output_pedigree_ids = pedigreeIDs_value->LogicalAtIndex_NOCAST(0, nullptr); bool output_object_tags = objectTags_value->LogicalAtIndex_NOCAST(0, nullptr); if (output_pedigree_ids && !species->PedigreesEnabledByUser()) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_outputIndividuals): outputIndividuals() cannot output pedigree IDs, because pedigree recording has not been enabled." << EidosTerminate(); if (filePath_value->Type() == EidosValueType::kValueNULL) { // before writing anything, erase a progress line if we've got one up, to try to make a clean slate Eidos_EraseProgress(); std::ostream &output_stream = p_interpreter.ExecutionOutputStream(); Individual::PrintIndividuals_SLiM(output_stream, individuals_buffer, individuals_count, *species, output_spatial_positions, output_ages, output_ancestral_nucs, output_pedigree_ids, output_object_tags, /* p_output_substitutions */ false, chromosome); } else { std::string outfile_path = Eidos_ResolvedPath(filePath_value->StringAtIndex_NOCAST(0, nullptr)); bool append = append_value->LogicalAtIndex_NOCAST(0, nullptr); std::ofstream outfile; outfile.open(outfile_path.c_str(), append ? (std::ios_base::app | std::ios_base::out) : std::ios_base::out); if (outfile.is_open()) { Individual::PrintIndividuals_SLiM(outfile, individuals_buffer, individuals_count, *species, output_spatial_positions, output_ages, output_ancestral_nucs, output_pedigree_ids, output_object_tags, /* p_output_substitutions */ false, chromosome); outfile.close(); } else { EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_outputIndividuals): outputIndividuals() could not open " << outfile_path << "." << EidosTerminate(); } } return gStaticEidosValueVOID; } // ********************* + (void)outputIndividualsToVCF([Ns$ filePath = NULL], [logical$ append = F], [Niso$ chromosome = NULL], [logical$ outputMultiallelics = T], [logical$ simplifyNucleotides = F], [logical$ outputNonnucleotides = T]) // EidosValue_SP Individual_Class::ExecuteMethod_outputIndividualsToVCF(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *filePath_value = p_arguments[0].get(); EidosValue *append_value = p_arguments[1].get(); EidosValue *chromosome_value = p_arguments[2].get(); EidosValue *outputMultiallelics_value = p_arguments[3].get(); EidosValue *simplifyNucleotides_value = p_arguments[4].get(); EidosValue *outputNonnucleotides_value = p_arguments[5].get(); // here we need to require at least one target individual, // do a species consistency check and get the species/community, // and get the vector of individuals that we will pass in // (from the raw data of the EidosValue, no need to copy; but add const) int individuals_count = p_target->Count(); if (individuals_count == 0) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_outputIndividualsToVCF): outputIndividualsToVCF() cannot be called on a zero-length target vector; at least one individual is required." << EidosTerminate(); const Individual **individuals_buffer = (const Individual **)p_target->ObjectData(); // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividuals(p_target); if (!species) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_outputIndividualsToVCF): outputIndividualsToVCF() requires that all individuals belong to the same species." << EidosTerminate(); Community &community = species->community_; // TIMING RESTRICTION if (!community.warned_early_output_) { if ((community.CycleStage() == SLiMCycleStage::kWFStage0ExecuteFirstScripts) || (community.CycleStage() == SLiMCycleStage::kWFStage1ExecuteEarlyScripts)) { if (!gEidosSuppressWarnings) { p_interpreter.ErrorOutputStream() << "#WARNING (Individual_Class::ExecuteMethod_outputIndividualsToVCF): outputIndividualsToVCF() should probably not be called from a first() or early() event in a WF model; the output will reflect state at the beginning of the cycle, not the end." << std::endl; community.warned_early_output_ = true; } } } Chromosome *chromosome = species->GetChromosomeFromEidosValue(chromosome_value); // NULL returns nullptr bool output_multiallelics = outputMultiallelics_value->LogicalAtIndex_NOCAST(0, nullptr); bool simplify_nucs = simplifyNucleotides_value->LogicalAtIndex_NOCAST(0, nullptr); bool output_nonnucs = outputNonnucleotides_value->LogicalAtIndex_NOCAST(0, nullptr); if (filePath_value->Type() == EidosValueType::kValueNULL) { // before writing anything, erase a progress line if we've got one up, to try to make a clean slate Eidos_EraseProgress(); std::ostream &output_stream = p_interpreter.ExecutionOutputStream(); // write the #OUT line, for file output only output_stream << "#OUT: " << community.Tick() << " " << species->Cycle() << " IS" << std::endl; Individual::PrintIndividuals_VCF(output_stream, individuals_buffer, individuals_count, *species, output_multiallelics, simplify_nucs, output_nonnucs, chromosome); } else { std::string outfile_path = Eidos_ResolvedPath(filePath_value->StringAtIndex_NOCAST(0, nullptr)); bool append = append_value->LogicalAtIndex_NOCAST(0, nullptr); std::ofstream outfile; outfile.open(outfile_path.c_str(), append ? (std::ios_base::app | std::ios_base::out) : std::ios_base::out); if (outfile.is_open()) { Individual::PrintIndividuals_VCF(outfile, individuals_buffer, individuals_count, *species, output_multiallelics, simplify_nucs, output_nonnucs, chromosome); outfile.close(); } else { EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_outputIndividuals): outputIndividuals() could not open " << outfile_path << "." << EidosTerminate(); } } return gStaticEidosValueVOID; } // this is to avoid copy-pasting the same addition code repeatedly; it is gross, but should get inlined inline __attribute__((always_inline)) static void _AddCallToHaplosome(int call, Haplosome *haplosome, slim_mutrun_index_t &haplosome_last_mutrun_modified, MutationRun *&haplosome_last_mutrun, std::vector &alt_allele_mut_indices, slim_position_t mut_position, Species *species, MutationRunContext *mutrun_context, bool all_target_haplosomes_started_empty, bool recording_mutations) { if (call == 0) return; slim_position_t mutrun_length = haplosome->MutrunLength(); MutationIndex mut_index = alt_allele_mut_indices[call - 1]; slim_mutrun_index_t mut_mutrun_index = (slim_mutrun_index_t)(mut_position / mutrun_length); if (mut_mutrun_index != haplosome_last_mutrun_modified) { #ifdef _OPENMP // When parallel, the MutationRunContext depends upon the position in the haplosome mutrun_context = &species->ChromosomeMutationRunContextForMutationRunIndex(mut_mutrun_index); #endif // We use WillModifyRun() because these are existing haplosomes we didn't create, and their runs may be shared; we have // no way to tell. We avoid making excessive mutation run copies by calling this only once per mutrun per haplosome. haplosome_last_mutrun = haplosome->WillModifyRun(mut_mutrun_index, *mutrun_context); haplosome_last_mutrun_modified = mut_mutrun_index; } // If the haplosome started empty, we can add mutations to the end with emplace_back(); if it did not, then they need to be inserted if (all_target_haplosomes_started_empty) haplosome_last_mutrun->emplace_back(mut_index); else haplosome_last_mutrun->insert_sorted_mutation(mut_index); if (recording_mutations) species->RecordNewDerivedState(haplosome, mut_position, *haplosome->derived_mutation_ids_at_position(mut_position)); } // ********************* + (o)readIndividualsFromVCF(s$ filePath = NULL, [Nio mutationType = NULL]) // EidosValue_SP Individual_Class::ExecuteMethod_readIndividualsFromVCF(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_interpreter) // BEWARE: This method shares a great deal of code with Haplosome_Class::ExecuteMethod_readHaplosomesFromVCF(). Maintain in parallel. THREAD_SAFETY_IN_ACTIVE_PARALLEL("Individual_Class::ExecuteMethod_readIndividualsFromVCF(): SLiM global state read"); EidosValue *filePath_value = p_arguments[0].get(); EidosValue *mutationType_value = p_arguments[1].get(); // SPECIES CONSISTENCY CHECK if (p_target->Count() == 0) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): " << "readIndividualsFromVCF() requires a target Individual vector of length 1 or more, so that the species of the target can be determined." << EidosTerminate(); Species *species = Community::SpeciesForIndividuals(p_target); if (!species) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): " << "readIndividualsFromVCF() requires that all target individuals belong to the same species." << EidosTerminate(); Individual * const *individuals_data = (Individual * const *)p_target->ObjectData(); int individuals_size = p_target->Count(); species->population_.CheckForDeferralInIndividualsVector(individuals_data, individuals_size, "Individual_Class::ExecuteMethod_readIndividualsFromVCF"); const std::vector &chromosomes = species->Chromosomes(); bool model_is_multi_chromosome = (chromosomes.size() > 1); std::string chromosome_symbol; // used in single-chromosome models to check consistency Community &community = species->community_; Population &pop = species->population_; bool recording_mutations = species->RecordingTreeSequenceMutations(); bool nucleotide_based = species->IsNucleotideBased(); std::string file_path = Eidos_ResolvedPath(Eidos_StripTrailingSlash(filePath_value->StringAtIndex_NOCAST(0, nullptr))); bool has_initial_mutations = (gSLiM_next_mutation_id != 0); MutationType *default_mutation_type_ptr = nullptr; if (mutationType_value->Type() != EidosValueType::kValueNULL) default_mutation_type_ptr = SLiM_ExtractMutationTypeFromEidosValue_io(mutationType_value, 0, &community, species, "readIndividualsFromVCF()"); // SPECIES CONSISTENCY CHECK // Parse the whole input file and retain the information from it std::ifstream infile(file_path); if (!infile) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): could not read file at path " << file_path << "." << EidosTerminate(); std::string line, sub; int parse_state = 0; int sample_id_count = 0; bool info_MID_defined = false, info_S_defined = false, info_DOM_defined = false, info_PO_defined = false; bool info_GO_defined = false, info_TO_defined = false, info_MT_defined = false, /*info_AA_defined = false,*/ info_NONNUC_defined = false; // This data structure keeps call lines that we read for each chromosome. They could arrive from the file // in any order; with this we store them by chromosome, and then indexed by their mutation position. std::vector>> call_lines_per_chromosome; call_lines_per_chromosome.resize(chromosomes.size()); // make an empty call_lines vector for each chromosome while (!infile.eof()) { getline(infile, line); switch (parse_state) { case 0: { // In header, parsing ## lines, until we get to the #CHROM line; the point of this is that we only want to interpret // INFO fields like MID, S, etc. as having their SLiM-specific meaning if their SLiM-specific definition is present if (line.compare(0, 2, "##") == 0) { if (line == "##INFO=") info_MID_defined = true; if (line == "##INFO=") info_S_defined = true; if (line == "##INFO=") info_DOM_defined = true; if (line == "##INFO=") info_PO_defined = true; if (line == "##INFO=") info_GO_defined = true; if (line == "##INFO=") info_TO_defined = true; // SLiM 4 emits TO (tick) instead of GO (generation) if (line == "##INFO=") info_MT_defined = true; /*if (line == "##INFO=") info_AA_defined = true;*/ // this one is standard, so we don't require this definition if (line == "##INFO=") info_NONNUC_defined = true; } else if (line.compare(0, 1, "#") == 0) { static const char *header_fields[9] = {"CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT"}; std::istringstream iss(line); iss.get(); // eat the initial # // verify that the expected standard columns are present for (const char *header_field : header_fields) { if (!(iss >> sub)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): missing VCF header '" << header_field << "'." << EidosTerminate(); if (sub != header_field) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): expected VCF header '" << header_field << "', saw '" << sub << "'." << EidosTerminate(); } // the remaining columns are sample IDs; we don't care what they are, we just count them while (iss >> sub) sample_id_count++; if (sample_id_count != individuals_size) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): there are " << sample_id_count << " samples in the VCF file, but " << individuals_size << " target individuals; the number of target individuals must match the number of VCF samples." << EidosTerminate(); // now the remainder of the file should be call lines parse_state = 1; } else { EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): unexpected line in VCF header: '" << line << "'." << EidosTerminate(); } break; } case 1: { // In call lines, fields are separated by tabs, and could theoretically contain spaces; here we just read a whole line, // extract the position field for the mutation, and save the line indexed by its mutation's position for later handling if (line.length() == 0) break; std::istringstream iss(line); std::getline(iss, sub, '\t'); // CHROM Chromosome *chromosome_for_call = species->ChromosomeFromSymbol(sub); if (model_is_multi_chromosome) { // in multi-chromosome models the CHROM value must match match a chromosome in the model if (!chromosome_for_call) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): the CHROM field's value (\"" << sub << "\") in a call line does not match any chromosome symbol for the focal species with which the target individuals are associated. In multi-chromosome models, the CHROM field is required to match a chromosome symbol to prevent bugs." << EidosTerminate(); } else { // in single-chromosome models the CHROM value must be consistent across the whole file, but need not match if (chromosome_symbol.length() == 0) chromosome_symbol = sub; // first call line's CHROM symbol gets remembered else if (sub != chromosome_symbol) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): the CHROM field's value (\"" << sub << "\") in a call line does not match the initial CHROM field's value (\"" << chromosome_symbol << "\"). In single-chromosome models, the CHROM field is required to have a single consistent value across all call lines to prevent bugs." << EidosTerminate(); if (!chromosome_for_call) chromosome_for_call = chromosomes[0]; } slim_chromosome_index_t chromosome_index = chromosome_for_call->Index(); slim_position_t last_position = chromosome_for_call->last_position_; std::getline(iss, sub, '\t'); // POS int64_t pos = EidosInterpreter::NonnegativeIntegerForString(sub, nullptr) - 1; // -1 because VCF uses 1-based positions if ((pos < 0) || (pos > last_position)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file POS value " << pos << " out of range." << EidosTerminate(); std::vector> &call_lines = call_lines_per_chromosome[chromosome_index]; call_lines.emplace_back(pos, line); break; } default: EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): (internal error) unhandled case." << EidosTerminate(); } } infile.close(); // We will keep track of all mutations added, to all chromosomes, and return them as a vector std::vector mutation_indices; // Loop over the call lines for each chromosome, and handle chromosomes one by one // FIXME: from here downwards could probably be parallelized for (size_t chromosome_index = 0; chromosome_index < chromosomes.size(); ++chromosome_index) { std::vector> &call_lines = call_lines_per_chromosome[chromosome_index]; if (call_lines.size() == 0) continue; Chromosome *chromosome = species->Chromosomes()[chromosome_index]; int first_haplosome_index = species->FirstHaplosomeIndices()[chromosome_index]; int last_haplosome_index = species->LastHaplosomeIndices()[chromosome_index]; int intrinsic_ploidy = (last_haplosome_index - first_haplosome_index) + 1; ChromosomeType chromosome_type = chromosome->Type(); // sort call_lines by position, so that we can add them to empty haplosomes efficiently std::sort(call_lines.begin(), call_lines.end(), [ ](const std::pair &l1, const std::pair &l2) {return l1.first < l2.first;}); // cache target haplosomes and determine whether they are initially empty, in which case we can do fast mutation addition with emplace_back() // NOTE that unlike readHaplosomesFromVCF(), we do not exclude null haplosomes here! std::vector haplosomes; std::vector haplosomes_last_mutrun_modified; std::vector haplosomes_last_mutrun; bool all_target_haplosomes_started_empty = true; for (int individuals_index = 0; individuals_index < individuals_size; ++individuals_index) { Individual *ind = individuals_data[individuals_index]; { Haplosome *haplosome = ind->haplosomes_[first_haplosome_index]; if (haplosome->IsNull()) { haplosomes.emplace_back(nullptr); haplosomes_last_mutrun_modified.emplace_back(-1); haplosomes_last_mutrun.emplace_back(nullptr); } else { if (haplosome->mutation_count() != 0) all_target_haplosomes_started_empty = false; haplosomes.emplace_back(haplosome); haplosomes_last_mutrun_modified.emplace_back(-1); haplosomes_last_mutrun.emplace_back(nullptr); } } if (intrinsic_ploidy == 2) { Haplosome *haplosome = ind->haplosomes_[last_haplosome_index]; if (haplosome->IsNull()) { haplosomes.emplace_back(nullptr); haplosomes_last_mutrun_modified.emplace_back(-1); haplosomes_last_mutrun.emplace_back(nullptr); } else { if (haplosome->mutation_count() != 0) all_target_haplosomes_started_empty = false; haplosomes.emplace_back(haplosome); haplosomes_last_mutrun_modified.emplace_back(-1); haplosomes_last_mutrun.emplace_back(nullptr); } } } // parse all the call lines, instantiate their mutations, and add the mutations to the target haplosomes #ifndef _OPENMP MutationRunContext &mutrun_context = chromosome->ChromosomeMutationRunContextForThread(omp_get_thread_num()); // when not parallel, we have only one MutationRunContext #endif for (std::pair &call_line : call_lines) { slim_position_t mut_position = call_line.first; std::istringstream iss(call_line.second); std::string ref_str, alt_str, info_str; std::getline(iss, sub, '\t'); // CHROM; don't care (already checked it above) std::getline(iss, sub, '\t'); // POS; already fetched std::getline(iss, sub, '\t'); // ID; don't care std::getline(iss, ref_str, '\t'); // REF std::getline(iss, alt_str, '\t'); // ALT std::getline(iss, sub, '\t'); // QUAL; don't care std::getline(iss, sub, '\t'); // FILTER; don't care std::getline(iss, info_str, '\t'); // INFO std::getline(iss, sub, '\t'); // FORMAT; don't care (GT must be first, according to the standard; we don't check) // parse/validate the REF nucleotide int8_t ref_nuc; if (ref_str == "A") ref_nuc = 0; else if (ref_str == "C") ref_nuc = 1; else if (ref_str == "G") ref_nuc = 2; else if (ref_str == "T") ref_nuc = 3; else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file REF value must be A/C/G/T." << EidosTerminate(); // parse/validate the ALT nucleotides std::vector alt_substrs = Eidos_string_split(alt_str, ","); std::vector alt_nucs; for (std::string &alt_substr : alt_substrs) { if (alt_substr == "A") alt_nucs.emplace_back(0); else if (alt_substr == "C") alt_nucs.emplace_back(1); else if (alt_substr == "G") alt_nucs.emplace_back(2); else if (alt_substr == "T") alt_nucs.emplace_back(3); else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file ALT value must be A/C/G/T." << EidosTerminate(); } size_t alt_allele_count = alt_nucs.size(); // parse/validate the INFO fields that we recognize std::vector info_substrs = Eidos_string_split(info_str, ";"); std::vector info_mutids; std::vector info_selcoeffs; std::vector info_domcoeffs; std::vector info_poporigin; std::vector info_tickorigin; std::vector info_muttype; int8_t info_ancestral_nuc = -1; bool info_is_nonnuc = false; for (std::string &info_substr : info_substrs) { if (info_MID_defined && (info_substr.compare(0, 4, "MID=") == 0)) // Mutation ID { std::vector value_substrs = Eidos_string_split(info_substr.substr(4), ","); for (std::string &value_substr : value_substrs) info_mutids.emplace_back((slim_mutationid_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); if (info_mutids.size() && has_initial_mutations) { if (!gEidosSuppressWarnings) { if (!community.warned_readFromVCF_mutIDs_unused_) { p_interpreter.ErrorOutputStream() << "#WARNING (Individual_Class::ExecuteMethod_readIndividualsFromVCF): readIndividualsFromVCF(): the VCF file specifies mutation IDs with the MID field, but some mutation IDs have already been used so uniqueness cannot be guaranteed. Use of mutation IDs is therefore disabled; mutations will not receive the mutation ID requested in the file. To fix this warning, remove the MID field from the VCF file before reading. To get readIndividualsFromVCF() to use the specified mutation IDs, load the VCF file into a model that has never simulated a mutation, and has therefore not used any mutation IDs." << std::endl; community.warned_readFromVCF_mutIDs_unused_ = true; } } // disable use of MID for this read info_MID_defined = false; info_mutids.resize(0); } } else if (info_S_defined && (info_substr.compare(0, 2, "S=") == 0)) // Selection Coefficient { std::vector value_substrs = Eidos_string_split(info_substr.substr(2), ","); for (std::string &value_substr : value_substrs) info_selcoeffs.emplace_back(EidosInterpreter::FloatForString(value_substr, nullptr)); } else if (info_DOM_defined && (info_substr.compare(0, 4, "DOM=") == 0)) // Dominance Coefficient { std::vector value_substrs = Eidos_string_split(info_substr.substr(4), ","); for (std::string &value_substr : value_substrs) info_domcoeffs.emplace_back(EidosInterpreter::FloatForString(value_substr, nullptr)); } else if (info_PO_defined && (info_substr.compare(0, 3, "PO=") == 0)) // Population of Origin { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_poporigin.emplace_back((slim_objectid_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (info_TO_defined && (info_substr.compare(0, 3, "TO=") == 0)) // Tick of Origin { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_tickorigin.emplace_back((slim_tick_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (info_GO_defined && (info_substr.compare(0, 3, "GO=") == 0)) // Generation of Origin - emitted by SLiM 3, treated as TO here { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_tickorigin.emplace_back((slim_tick_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (info_MT_defined && (info_substr.compare(0, 3, "MT=") == 0)) // Mutation Type { std::vector value_substrs = Eidos_string_split(info_substr.substr(3), ","); for (std::string &value_substr : value_substrs) info_muttype.emplace_back((slim_objectid_t)EidosInterpreter::NonnegativeIntegerForString(value_substr, nullptr)); } else if (/* info_AA_defined && */ (info_substr.compare(0, 3, "AA=") == 0)) // Ancestral Allele; definition not required since it is a standard field { std::string aa_str = info_substr.substr(3); if (aa_str == "A") info_ancestral_nuc = 0; else if (aa_str == "C") info_ancestral_nuc = 1; else if (aa_str == "G") info_ancestral_nuc = 2; else if (aa_str == "T") info_ancestral_nuc = 3; else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file AA value must be A/C/G/T." << EidosTerminate(); } else if (info_NONNUC_defined && (info_substr == "NONNUC")) // Non-nucleotide-based { info_is_nonnuc = true; } if ((info_mutids.size() != 0) && (info_mutids.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file unexpected value count for MID field." << EidosTerminate(); if ((info_selcoeffs.size() != 0) && (info_selcoeffs.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file unexpected value count for S field." << EidosTerminate(); if ((info_domcoeffs.size() != 0) && (info_domcoeffs.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file unexpected value count for DOM field." << EidosTerminate(); if ((info_poporigin.size() != 0) && (info_poporigin.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file unexpected value count for PO field." << EidosTerminate(); if ((info_tickorigin.size() != 0) && (info_tickorigin.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file unexpected value count for GO or TO field." << EidosTerminate(); if ((info_muttype.size() != 0) && (info_muttype.size() != alt_allele_count)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file unexpected value count for MT field." << EidosTerminate(); } // instantiate the mutations involved in this call line; the REF allele represents no mutation, ALT alleles are each separate mutations std::vector alt_allele_mut_indices; for (std::size_t alt_allele_index = 0; alt_allele_index < alt_allele_count; ++alt_allele_index) { // figure out the mutation type; if specified with MT, look it up, otherwise use the default supplied MutationType *mutation_type_ptr = default_mutation_type_ptr; if (info_muttype.size() > 0) { slim_objectid_t mutation_type_id = info_muttype[alt_allele_index]; mutation_type_ptr = species->MutationTypeWithID(mutation_type_id); if (!mutation_type_ptr) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file MT field references a mutation type m" << mutation_type_id << " that is not defined." << EidosTerminate(); } if (!mutation_type_ptr) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file MT field missing, but no default mutation type was supplied in the mutationType parameter." << EidosTerminate(); // check the dominance coefficient of DOM against that of the mutation type if (info_domcoeffs.size() > 0) { if (std::abs(info_domcoeffs[alt_allele_index] - mutation_type_ptr->dominance_coeff_) > 0.0001) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file DOM field specifies a dominance coefficient " << info_domcoeffs[alt_allele_index] << " that differs from the mutation type's dominance coefficient of " << mutation_type_ptr->dominance_coeff_ << "." << EidosTerminate(); } // get the selection coefficient from S, or draw one double selection_coeff; if (info_selcoeffs.size() > 0) selection_coeff = info_selcoeffs[alt_allele_index]; else selection_coeff = mutation_type_ptr->DrawSelectionCoefficient(); // get the subpop index from PO, or set to -1; no bounds checking on this slim_objectid_t subpop_index = -1; if (info_poporigin.size() > 0) subpop_index = info_poporigin[alt_allele_index]; // get the origin tick from gO, or set to the current tick; no bounds checking on this slim_tick_t origin_tick; if (info_tickorigin.size() > 0) origin_tick = info_tickorigin[alt_allele_index]; else origin_tick = community.Tick(); // figure out the nucleotide and do nucleotide-related checks int8_t alt_allele_nuc = alt_nucs[alt_allele_index]; // must be defined, in all cases, but might be ignored int8_t nucleotide; if (nucleotide_based) { if (info_NONNUC_defined) { // We are reading a SLiM-generated VCF file that uses NONNUC to designate non-nucleotide-based mutations if (info_is_nonnuc) { // This call line is marked NONNUC, so there is no associated nucleotide; check against the mutation type if (mutation_type_ptr->nucleotide_based_) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a mutation marked NONNUC cannot use a nucleotide-based mutation type." << EidosTerminate(); nucleotide = -1; } else { // This call line is not marked NONNUC, so it represents nucleotide-based alleles if (!mutation_type_ptr->nucleotide_based_) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a nucleotide-based mutation cannot use a non-nucleotide-based mutation type." << EidosTerminate(); if (ref_nuc != info_ancestral_nuc) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): the REF nucleotide does not match the AA nucleotide." << EidosTerminate(); int8_t ancestral = (int8_t)chromosome->AncestralSequence()->NucleotideAtIndex(mut_position); if (ancestral != ref_nuc) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): the REF/AA nucleotide does not match the ancestral nucleotide at the same position; a matching ancestral nucleotide sequence must be set prior to calling readIndividualsFromVCF()." << EidosTerminate(); nucleotide = alt_allele_nuc; } } else { // We are reading a generic VCF file that does not use NONNUC, so we follow the mutation type's lead; if it is nucleotide-based, we use the nucleotide specified if (mutation_type_ptr->nucleotide_based_) { // The mutation type is nucleotide-based, so use the nucleotide specified; in this case we ignore REF and AA, however nucleotide = alt_allele_nuc; } else { // The mutation type is non-nucleotide-based, so we ignore the nucleotide supplied, as well as REF/AA nucleotide = -1; } } } else { // We are a non-nucleotide-based model, so NONNUC should not be defined; we do not understand nucleotides and will ignore them if (info_NONNUC_defined) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): cannot read a VCF file generated by a nucleotide-based model into a non-nucleotide-based model." << EidosTerminate(); nucleotide = -1; } // instantiate the mutation with the values decided upon MutationIndex new_mut_index = SLiM_NewMutationFromBlock(); Mutation *new_mut; if (info_mutids.size() > 0) { // a mutation ID was supplied; we use it blindly, having checked above that we are in the case where this is legal slim_mutationid_t mut_mutid = info_mutids[alt_allele_index]; new_mut = new (gSLiM_Mutation_Block + new_mut_index) Mutation(mut_mutid, mutation_type_ptr, chromosome->Index(), mut_position, selection_coeff, subpop_index, origin_tick, nucleotide); } else { // no mutation ID supplied, so use whatever is next new_mut = new (gSLiM_Mutation_Block + new_mut_index) Mutation(mutation_type_ptr, chromosome->Index(), mut_position, selection_coeff, subpop_index, origin_tick, nucleotide); } // This mutation type might not be used by any genomic element type (i.e. might not already be vetted), so we need to check and set pure_neutral_ if (selection_coeff != 0.0) { species->pure_neutral_ = false; mutation_type_ptr->all_pure_neutral_DFE_ = false; } // add it to our local map, so we can find it when making haplosomes, and to the population's mutation registry pop.MutationRegistryAdd(new_mut); alt_allele_mut_indices.emplace_back(new_mut_index); mutation_indices.emplace_back(new_mut_index); } // read the genotype data for each sample id, which might be diploid or haploid, and might have data beyond GT // NOTE: unlike readHaplosomesFromVCF(), we place the mutations directly into the haplosomes, rather than using a genotype_calls vector int haplosomes_index = 0; for (int sample_index = 0; sample_index < sample_id_count; ++sample_index) { if (iss.eof()) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file call line ended unexpectedly before the last sample." << EidosTerminate(); std::getline(iss, sub, '\t'); // extract just the GT field if others are present std::size_t colon_pos = sub.find_first_of(':'); if (colon_pos != std::string::npos) sub = sub.substr(0, colon_pos); // separate haploid calls that are joined by | or /; this is the hotspot of the whole method, so we try to be efficient here bool call_handled = false; int genotype_call1 = -1; int genotype_call2 = -1; if ((sub.length() == 3) && ((sub[1] == '|') || (sub[1] == '/'))) { // diploid, both single-digit char sub_ch1 = sub[0]; char sub_ch2 = sub[2]; if ((sub_ch1 >= '0') && (sub_ch1 <= '9') && (sub_ch2 >= '0') && (sub_ch2 <= '9')) { genotype_call1 = (int)(sub_ch1 - '0'); genotype_call2 = (int)(sub_ch2 - '0'); call_handled = true; } } else if (sub.length() == 1) { char sub_ch = sub[0]; if (sub_ch == '~') { // If the call is ~, a tilde, it indicates that no genetic information is present // (this is the case for a female if we're reading Y-chromosome data, for example). // We leave genotype_call1 and genotype_call2 as -1, indicating no call for either. // Note that this is not part of the VCF standard; it had to be invented for SLiM. call_handled = true; } else { // haploid, single-digit; note we always place this in genotype_call1 if ((sub_ch >= '0') && (sub_ch <= '9')) { genotype_call1 = (int)(sub_ch - '0'); call_handled = true; } } } if (!call_handled) { std::vector genotype_substrs; if (sub.find('|') != std::string::npos) genotype_substrs = Eidos_string_split(sub, "|"); // phased else if (sub.find('/') != std::string::npos) genotype_substrs = Eidos_string_split(sub, "/"); // unphased; we don't worry about that else genotype_substrs.emplace_back(sub); // haploid, presumably if (genotype_substrs.size() == 2) { // diploid genotype_call1 = (int)EidosInterpreter::NonnegativeIntegerForString(genotype_substrs[0], nullptr); genotype_call2 = (int)EidosInterpreter::NonnegativeIntegerForString(genotype_substrs[1], nullptr); } else if (genotype_substrs.size() == 1) { // haploid; note we always place this in genotype_call1 genotype_call1 = (int)EidosInterpreter::NonnegativeIntegerForString(genotype_substrs[0], nullptr); } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file genotype calls must be diploid or haploid; " << genotype_substrs.size() << " calls found in one sample." << EidosTerminate(); } if ((genotype_call1 > (int)alt_allele_count) || (genotype_call2 > (int)alt_allele_count)) // 0 is REF, 1..n are ALT alleles EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file call out of range (does not correspond to a REF or ALT allele in the call line)." << EidosTerminate(); if ((genotype_call2 != -1) && (intrinsic_ploidy == 1)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a diploid call was seen ('" << sub << "') but the focal chromosome for the call (with symbol '" << chromosome->Symbol() << "') is intrinsically haploid." << EidosTerminate(); // add the mutations to the appropriate haplosomes and record the new derived states // this uses the cached haplosome state we set up above, for efficient addition: // //std::vector haplosomes; //std::vector haplosomes_last_mutrun_modified; //std::vector haplosome_last_mutrun; // // remember that haplosomes has nullptr for any null haplosomes, to catch bugs! if (genotype_call1 == -1) { if (genotype_call2 == -1) { // Neither call is present; this occurs with the ~ call, which in not VCF standard // We check that both haplosomes are null; a non-null haplosome should be called // as not having any mutation with 0, not ~. // // We do, however, allow a ~ call for chromosome type 'A' or 'H', and transmogrify // any existing non-null haplosome to null, as long as it is empty. We do not want // to allow that for other chromosome types, since it would require changing the sex. if (intrinsic_ploidy == 2) { Haplosome *haplosome1 = haplosomes[haplosomes_index]; Haplosome *haplosome2 = haplosomes[haplosomes_index + 1]; if (haplosome1 || haplosome2) { if (chromosome_type == ChromosomeType::kA_DiploidAutosome) { if (haplosome1) { if (haplosome1->mutation_count()) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a call of '~' was used for a haplosome that already contains mutations, and thus cannot be made into a null haplosome; use a call of 0, not ~, if the haplosome is not intended to be a null haplosome." << EidosTerminate(); haplosome1->MakeNull(); haplosome1->OwningIndividual()->subpopulation_->has_null_haplosomes_ = true; } if (haplosome2) { if (haplosome2->mutation_count()) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a call of '~' was used for a haplosome that already contains mutations, and thus cannot be made into a null haplosome; use a call of 0, not ~, if the haplosome is not intended to be a null haplosome." << EidosTerminate(); haplosome2->MakeNull(); haplosome2->OwningIndividual()->subpopulation_->has_null_haplosomes_ = true; } } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a call of '~' was used for an individual that has a non-null haplosome; that is not legal." << EidosTerminate(); } // we don't need to do anything; no mutations to add haplosomes_index += 2; } else { Haplosome *haplosome1 = haplosomes[haplosomes_index]; if (haplosome1) { if (chromosome_type == ChromosomeType::kH_HaploidAutosome) { if (haplosome1->mutation_count()) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a call of '~' was used for a haplosome that already contains mutations, and thus cannot be made into a null haplosome; use a call of 0, not ~, if the haplosome is not intended to be a null haplosome." << EidosTerminate(); haplosome1->MakeNull(); haplosome1->OwningIndividual()->subpopulation_->has_null_haplosomes_ = true; } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a call of '~' was used for an individual that has a non-null haplosome; that is not legal." << EidosTerminate(); } // we don't need to do anything; no mutations to add haplosomes_index++; } } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): (internal error) call for position 2 with no call for position 1; that should not occur in the present design." << EidosTerminate(); } else { if (genotype_call2 == -1) { // Haploid call; this could be for an intrinsically haploid chromosome, or // could be indicating that one haplosome of an intrinsically diploid // chromosome is null. For now, we require the null haplosome state to // already be set up. if (intrinsic_ploidy == 2) { if (haplosomes[haplosomes_index]) { // Here, for chromosome type "A" we transmogrify the second haplosome into a // null haplosome as long as it is empty, rather than throwing an error. // The choice to do this to the *second* haplosome is kind of arbitrary; all // we know is that we've got a haploid call for a diploid chromosome. Since // there is no indication in the call syntax, we assume the second haplosome // is the one intended to be null. We could extend the syntax of VCF again // here, like 1|~ versus ~|1 indicating the position of the null, but that // feels like overkill at this time. BCH 3/6/2025. if (haplosomes[haplosomes_index + 1]) { if (chromosome_type == ChromosomeType::kA_DiploidAutosome) { Haplosome *implied_null_haplosome = haplosomes[haplosomes_index + 1]; if (implied_null_haplosome->mutation_count()) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a haploid call implies that an individual's second haplosome for a diploid chromosome is null, but that haplosome already contains mutations, and thus cannot be made into a null haplosome; use a diploid call, if neither haplosome is intended to be a null haplosome." << EidosTerminate(); implied_null_haplosome->MakeNull(); implied_null_haplosome->OwningIndividual()->subpopulation_->has_null_haplosomes_ = true; } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a haploid call is present for an individual that has two non-null haplosomes for the focal chromosome (which is not of type 'A'); that is not legal." << EidosTerminate(); } // add the called mutation to the haplosome at haplosomes_index _AddCallToHaplosome(genotype_call1, haplosomes[haplosomes_index], haplosomes_last_mutrun_modified[haplosomes_index], haplosomes_last_mutrun[haplosomes_index], alt_allele_mut_indices, mut_position, species, &mutrun_context, all_target_haplosomes_started_empty, recording_mutations); } else if (haplosomes[haplosomes_index + 1]) { // add the called mutation to the haplosome at haplosomes_index + 1 _AddCallToHaplosome(genotype_call1, haplosomes[haplosomes_index + 1], haplosomes_last_mutrun_modified[haplosomes_index + 1], haplosomes_last_mutrun[haplosomes_index + 1], alt_allele_mut_indices, mut_position, species, &mutrun_context, all_target_haplosomes_started_empty, recording_mutations); } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a haploid call is present for an individual that has no non-null haplosome for the focal chromosome." << EidosTerminate(); haplosomes_index += 2; } else // intrinsic_ploidy == 1 { if (haplosomes[haplosomes_index]) { // add the called mutation to the haplosome at haplosomes_index _AddCallToHaplosome(genotype_call1, haplosomes[haplosomes_index], haplosomes_last_mutrun_modified[haplosomes_index], haplosomes_last_mutrun[haplosomes_index], alt_allele_mut_indices, mut_position, species, &mutrun_context, all_target_haplosomes_started_empty, recording_mutations); } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a haploid call is present for an individual that has no non-null haplosome for the focal chromosome." << EidosTerminate(); haplosomes_index++; } } else { // Diploid call; must be for an intrinsically diploid chromosome, with no // null haplosomes present in the individual. if (intrinsic_ploidy == 1) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a diploid call is present for an intrinsically haploid focal chromosome." << EidosTerminate(); // add the first called mutation to the haplosome at haplosomes_index if (haplosomes[haplosomes_index]) { _AddCallToHaplosome(genotype_call1, haplosomes[haplosomes_index], haplosomes_last_mutrun_modified[haplosomes_index], haplosomes_last_mutrun[haplosomes_index], alt_allele_mut_indices, mut_position, species, &mutrun_context, all_target_haplosomes_started_empty, recording_mutations); } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a diploid call is present for an individual that has a null haplosome for the focal chromosome." << EidosTerminate(); haplosomes_index++; // add the second called mutation to the haplosome at haplosomes_index if (haplosomes[haplosomes_index]) { _AddCallToHaplosome(genotype_call2, haplosomes[haplosomes_index], haplosomes_last_mutrun_modified[haplosomes_index], haplosomes_last_mutrun[haplosomes_index], alt_allele_mut_indices, mut_position, species, &mutrun_context, all_target_haplosomes_started_empty, recording_mutations); } else EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): a diploid call is present for an individual that has a null haplosome for the focal chromosome." << EidosTerminate(); haplosomes_index++; } } } if (!iss.eof()) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_readIndividualsFromVCF): VCF file call line has unexpected entries following the last sample." << EidosTerminate(); } } // Return the instantiated mutations Mutation *mut_block_ptr = gSLiM_Mutation_Block; int mutation_count = (int)mutation_indices.size(); EidosValue_Object *vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Mutation_Class))->resize_no_initialize_RR(mutation_count); for (int mut_index = 0; mut_index < mutation_count; ++mut_index) vec->set_object_element_no_check_no_previous_RR(mut_block_ptr + mutation_indices[mut_index], mut_index); return EidosValue_Object_SP(vec); } // ********************* – (void)setSpatialPosition(float position) // EidosValue_SP Individual_Class::ExecuteMethod_setSpatialPosition(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *position_value = p_arguments[0].get(); int dimensionality = 0; int value_count = position_value->Count(); int target_size = p_target->Count(); // Determine the spatiality of the individuals involved, and make sure it is the same for all if (target_size >= 1) { Individual * const *targets = (Individual * const *)(p_target->ObjectData()); dimensionality = targets[0]->subpopulation_->species_.SpatialDimensionality(); for (int target_index = 1; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; if (target->subpopulation_->species_.SpatialDimensionality() != dimensionality) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_setSpatialPosition): setSpatialPosition() requires that all individuals in the target vector have the same spatial dimensionality." << EidosTerminate(); } } if ((target_size > 0) && (dimensionality == 0)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_setSpatialPosition): setSpatialPosition() cannot be called in non-spatial simulations." << EidosTerminate(); if ((dimensionality < 0) || (dimensionality > 3)) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_setSpatialPosition): (internal error) unrecognized dimensionality." << EidosTerminate(); if (value_count < dimensionality) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_setSpatialPosition): setSpatialPosition() requires at least as many coordinates as the spatial dimensionality of the simulation." << EidosTerminate(); if (value_count == dimensionality) { // One point is being set across all targets if (target_size >= 1) { // Vector target case, one point Individual * const *targets = (Individual * const *)(p_target->ObjectData()); switch (dimensionality) { case 1: { double x = position_value->FloatAtIndex_NOCAST(0, nullptr); EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_SPATIAL_POS_1_1D); #pragma omp parallel for simd schedule(simd:static) default(none) shared(target_size) firstprivate(targets, x) if(target_size >= EIDOS_OMPMIN_SET_SPATIAL_POS_1_1D) num_threads(thread_count) for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; target->spatial_x_ = x; } break; } case 2: { double x = position_value->FloatAtIndex_NOCAST(0, nullptr); double y = position_value->FloatAtIndex_NOCAST(1, nullptr); EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_SPATIAL_POS_1_2D); #pragma omp parallel for simd schedule(simd:static) default(none) shared(target_size) firstprivate(targets, x, y) if(target_size >= EIDOS_OMPMIN_SET_SPATIAL_POS_1_2D) num_threads(thread_count) for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; target->spatial_x_ = x; target->spatial_y_ = y; } break; } case 3: { double x = position_value->FloatAtIndex_NOCAST(0, nullptr); double y = position_value->FloatAtIndex_NOCAST(1, nullptr); double z = position_value->FloatAtIndex_NOCAST(2, nullptr); EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_SPATIAL_POS_1_3D); #pragma omp parallel for simd schedule(simd:static) default(none) shared(target_size) firstprivate(targets, x, y, z) if(target_size >= EIDOS_OMPMIN_SET_SPATIAL_POS_1_3D) num_threads(thread_count) for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; target->spatial_x_ = x; target->spatial_y_ = y; target->spatial_z_ = z; } break; } default: EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_setSpatialPosition): (internal error) dimensionality out of range." << EidosTerminate(nullptr); } } } else if (value_count == dimensionality * target_size) { // Vector target case, one point per target (so the point vector has to be non-singleton too) Individual * const *targets = (Individual * const *)(p_target->ObjectData()); const double *positions = position_value->FloatData(); #ifdef _OPENMP if (((dimensionality == 1) && (target_size >= EIDOS_OMPMIN_SET_SPATIAL_POS_2_1D)) || ((dimensionality == 2) && (target_size >= EIDOS_OMPMIN_SET_SPATIAL_POS_2_2D)) || ((dimensionality == 3) && (target_size >= EIDOS_OMPMIN_SET_SPATIAL_POS_2_3D))) { switch (dimensionality) { case 1: { EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_SPATIAL_POS_2_1D); #pragma omp parallel for schedule(static) default(none) shared(target_size) firstprivate(targets, positions) num_threads(thread_count) // if(EIDOS_OMPMIN_SET_SPATIAL_POS_2_1D) is above for (int target_index = 0; target_index < target_size; ++target_index) { targets[target_index]->spatial_x_ = positions[target_index]; } break; } case 2: { EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_SPATIAL_POS_2_2D); #pragma omp parallel for schedule(static) default(none) shared(target_size) firstprivate(targets, positions) num_threads(thread_count) // if(EIDOS_OMPMIN_SET_SPATIAL_POS_2_2D) is above for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; const double *target_pos = positions + target_index * 2; target->spatial_x_ = target_pos[0]; target->spatial_y_ = target_pos[1]; } break; } case 3: { EIDOS_THREAD_COUNT(gEidos_OMP_threads_SET_SPATIAL_POS_2_3D); #pragma omp parallel for schedule(static) default(none) shared(target_size) firstprivate(targets, positions) num_threads(thread_count) // if(EIDOS_OMPMIN_SET_SPATIAL_POS_2_3D) is above for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; const double *target_pos = positions + target_index * 3; target->spatial_x_ = target_pos[0]; target->spatial_y_ = target_pos[1]; target->spatial_z_ = target_pos[2]; } break; } } } else #endif { switch (dimensionality) { case 1: { for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; target->spatial_x_ = *(positions++); } break; } case 2: { for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; target->spatial_x_ = *(positions++); target->spatial_y_ = *(positions++); } break; } case 3: { for (int target_index = 0; target_index < target_size; ++target_index) { Individual *target = targets[target_index]; target->spatial_x_ = *(positions++); target->spatial_y_ = *(positions++); target->spatial_z_ = *(positions++); } break; } default: EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_setSpatialPosition): (internal error) dimensionality out of range." << EidosTerminate(nullptr); } } } else { EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_setSpatialPosition): setSpatialPosition() requires the position parameter to contain either one point, or one point per individual (where each point has a number of coordinates equal to the spatial dimensionality of the simulation)." << EidosTerminate(); } return gStaticEidosValueVOID; } // ********************* + (integer)zygosityOfMutations([No mutations = NULL], [integer$ hemizygousValue = 1], [integer$ haploidValue = 1]) // EidosValue_SP Individual_Class::ExecuteMethod_zygosityOfMutations(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_arguments, p_interpreter) Individual **target_individuals = (Individual **)p_target->data(); int target_size = p_target->Count(); if (target_size == 0) return gStaticEidosValue_Integer_ZeroVec; // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividualsVector(target_individuals, target_size); if (species == nullptr) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_zygosityOfMutations): zygosityOfMutations() requires that all target individuals belong to the same species." << EidosTerminate(); species->population_.CheckForDeferralInIndividualsVector(target_individuals, target_size, "Individual_Class::ExecuteMethod_zygosityOfMutations"); const std::vector &chromosomes = species->Chromosomes(); Mutation *mut_block_ptr = gSLiM_Mutation_Block; EidosValue *mutations_value = p_arguments[0].get(); EidosValue *hemizygousValue_value = p_arguments[1].get(); EidosValue *haploidValue_value = p_arguments[2].get(); int64_t hemizygousValue = hemizygousValue_value->IntAtIndex_NOCAST(0, nullptr); int64_t haploidValue = haploidValue_value->IntAtIndex_NOCAST(0, nullptr); std::vector focalMutations; if (mutations_value->Type() == EidosValueType::kValueNULL) { // When assessing all mutations, we assume that all chromosomes need to be scanned int registry_size; const MutationIndex *registry = species->population_.MutationRegistry(®istry_size); focalMutations.resize(registry_size); Mutation **focalMutations_data = focalMutations.data(); for (int registry_index = 0; registry_index < registry_size; ++registry_index) { Mutation *mut = mut_block_ptr + registry[registry_index]; focalMutations_data[registry_index] = mut; } // mark a scratch value inside all chromosomes; 1 indicates the chromosome is active for (Chromosome *chromosome : chromosomes) chromosome->scratch_ = 1; } else { // When assessing a vector of mutations, we first determine which chromosomes we need to scan // In this case we also need to check that all mutations belong to the same species as the individuals if (Community::SpeciesForMutations(mutations_value) != species) EIDOS_TERMINATION << "ERROR (Individual_Class::ExecuteMethod_zygosityOfMutations): zygosityOfMutations() requires that all mutations belong to the same species as the target individuals." << EidosTerminate(); // zero out the scratch_ for all chromosomes; 0 indicates the chromosome is not active for (Chromosome *chromosome : chromosomes) chromosome->scratch_ = 0; Mutation **mutations_data = (Mutation **)mutations_value->ObjectData(); int mutations_count = mutations_value->Count(); focalMutations.resize(mutations_count); Mutation **focalMutations_data = focalMutations.data(); for (int mutations_index = 0; mutations_index < mutations_count; ++mutations_index) { Mutation *mut = mutations_data[mutations_index]; focalMutations_data[mutations_index] = mut; // mark a scratch value inside the associated chromosome; 1 indicates the chromosome is active chromosomes[mut->chromosome_index_]->scratch_ = 1; } } if (focalMutations.size() == 0) return gStaticEidosValue_Integer_ZeroVec; // allocate the result vector EidosValue_Int *integer_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(focalMutations.size() * target_size); int64_t *integer_result_data = integer_result->data_mutable(); // tabulate the results for each individuals const uint8_t PRESENT_HETEROZYGOUS = 1; const uint8_t PRESENT_HOMOZYGOUS = 2; const uint8_t PRESENT_HEMIZYGOUS = 3; const uint8_t PRESENT_HAPLOID = 4; for (int target_index = 0; target_index < target_size; ++target_index) { Individual *individual = target_individuals[target_index]; // first we zero out our zygosity counters; note that if this is parallelized, it will need separate scratch space for each thread for (Mutation *mut : focalMutations) mut->scratch_ = 0; // loop over the active chromosomes for (Chromosome *chromosome : chromosomes) { if (chromosome->scratch_ == 0) continue; unsigned int chromosome_index = chromosome->Index(); if (chromosome->IntrinsicPloidy() == 2) { // intrinsically diploid case Haplosome *haplosome1 = individual->haplosomes_[species->FirstHaplosomeIndices()[chromosome_index]]; Haplosome *haplosome2 = individual->haplosomes_[species->LastHaplosomeIndices()[chromosome_index]]; bool haplosome1_isnull = haplosome1->IsNull(); bool haplosome2_isnull = haplosome2->IsNull(); if (haplosome1_isnull && haplosome2_isnull) continue; if (haplosome1_isnull || haplosome2_isnull) { // hemizygous case Haplosome *haplosome = (haplosome1_isnull ? haplosome2 : haplosome1); const int32_t mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; const MutationIndex *haplosome_iter = mutrun->begin_pointer_const(); const MutationIndex *haplosome_max = mutrun->end_pointer_const(); while (haplosome_iter != haplosome_max) { MutationIndex haplosome_mutation = *haplosome_iter++; Mutation *mutation = (mut_block_ptr + haplosome_mutation); mutation->scratch_ = PRESENT_HEMIZYGOUS; } } } else { // diploid case const int32_t mutrun_count = haplosome1->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun1 = haplosome1->mutruns_[run_index]; const MutationRun *mutrun2 = haplosome2->mutruns_[run_index]; const MutationIndex *haplosome1_iter = mutrun1->begin_pointer_const(); const MutationIndex *haplosome2_iter = mutrun2->begin_pointer_const(); const MutationIndex *haplosome1_max = mutrun1->end_pointer_const(); const MutationIndex *haplosome2_max = mutrun2->end_pointer_const(); if ((haplosome1_iter != haplosome1_max) && (haplosome2_iter != haplosome2_max)) { MutationIndex haplosome1_mutindex = *haplosome1_iter, haplosome2_mutindex = *haplosome2_iter; slim_position_t haplosome1_iter_position = (mut_block_ptr + haplosome1_mutindex)->position_, haplosome2_iter_position = (mut_block_ptr + haplosome2_mutindex)->position_; do { if (haplosome1_iter_position < haplosome2_iter_position) { // Process a mutation in haplosome1 since it is leading (mut_block_ptr + haplosome1_mutindex)->scratch_ = PRESENT_HETEROZYGOUS; if (++haplosome1_iter == haplosome1_max) break; else { haplosome1_mutindex = *haplosome1_iter; haplosome1_iter_position = (mut_block_ptr + haplosome1_mutindex)->position_; } } else if (haplosome1_iter_position > haplosome2_iter_position) { // Process a mutation in haplosome2 since it is leading (mut_block_ptr + haplosome2_mutindex)->scratch_ = PRESENT_HETEROZYGOUS; if (++haplosome2_iter == haplosome2_max) break; else { haplosome2_mutindex = *haplosome2_iter; haplosome2_iter_position = (mut_block_ptr + haplosome2_mutindex)->position_; } } else { // Look for homozygosity: haplosome1_iter_position == haplosome2_iter_position slim_position_t position = haplosome1_iter_position; const MutationIndex *haplosome1_start = haplosome1_iter; // advance through haplosome1 as long as we remain at the same position, handling one mutation at a time do { const MutationIndex *haplosome2_matchscan = haplosome2_iter; // advance through haplosome2 with haplosome2_matchscan, looking for a match for the current mutation in haplosome1, to determine whether we are homozygous or not while (haplosome2_matchscan != haplosome2_max && (mut_block_ptr + *haplosome2_matchscan)->position_ == position) { if (haplosome1_mutindex == *haplosome2_matchscan) { // a match was found, so we record a homozygous state (mut_block_ptr + haplosome1_mutindex)->scratch_ = PRESENT_HOMOZYGOUS; goto homozygousExit1; } haplosome2_matchscan++; } // no match was found, so we are heterozygous (mut_block_ptr + haplosome1_mutindex)->scratch_ = PRESENT_HETEROZYGOUS; homozygousExit1: if (++haplosome1_iter == haplosome1_max) break; else { haplosome1_mutindex = *haplosome1_iter; haplosome1_iter_position = (mut_block_ptr + haplosome1_mutindex)->position_; } } while (haplosome1_iter_position == position); // advance through haplosome2 as long as we remain at the same position, handling one mutation at a time do { const MutationIndex *haplosome1_matchscan = haplosome1_start; // advance through haplosome1 with haplosome1_matchscan, looking for a match for the current mutation in haplosome2, to determine whether we are homozygous or not while ((haplosome1_matchscan != haplosome1_max) && ((mut_block_ptr + *haplosome1_matchscan)->position_ == position)) { if (haplosome2_mutindex == *haplosome1_matchscan) { // a match was found; we know this match was already found by the haplosome1 loop above goto homozygousExit2; } haplosome1_matchscan++; } // no match was found, so we are heterozygous (mut_block_ptr + haplosome2_mutindex)->scratch_ = PRESENT_HETEROZYGOUS; homozygousExit2: if (++haplosome2_iter == haplosome2_max) break; else { haplosome2_mutindex = *haplosome2_iter; haplosome2_iter_position = (mut_block_ptr + haplosome2_mutindex)->position_; } } while (haplosome2_iter_position == position); // break out if either haplosome has reached its end if (haplosome1_iter == haplosome1_max || haplosome2_iter == haplosome2_max) break; } } while (true); } // one or the other haplosome has now reached its end, so now we just need to handle the remaining mutations in the unfinished haplosome #if DEBUG assert(!(haplosome1_iter != haplosome1_max && haplosome2_iter != haplosome2_max)); #endif // if haplosome1 is unfinished, finish it while (haplosome1_iter != haplosome1_max) { MutationIndex haplosome1_mutindex = *haplosome1_iter++; (mut_block_ptr + haplosome1_mutindex)->scratch_ = PRESENT_HETEROZYGOUS; } // if haplosome2 is unfinished, finish it while (haplosome2_iter != haplosome2_max) { MutationIndex haplosome2_mutindex = *haplosome2_iter++; (mut_block_ptr + haplosome2_mutindex)->scratch_ = PRESENT_HETEROZYGOUS; } } } } else { // intrinsically haploid case Haplosome *haplosome = individual->haplosomes_[species->FirstHaplosomeIndices()[chromosome_index]]; if (haplosome->IsNull()) continue; // haploid case const int32_t mutrun_count = haplosome->mutrun_count_; for (int run_index = 0; run_index < mutrun_count; ++run_index) { const MutationRun *mutrun = haplosome->mutruns_[run_index]; const MutationIndex *haplosome_iter = mutrun->begin_pointer_const(); const MutationIndex *haplosome_max = mutrun->end_pointer_const(); while (haplosome_iter != haplosome_max) { MutationIndex haplosome_mutation = *haplosome_iter++; Mutation *mutation = (mut_block_ptr + haplosome_mutation); mutation->scratch_ = PRESENT_HAPLOID; } } } } // then run through the mutations again and transfer counts (zygosity) to the result matrix int64_t *result_column_ptr = integer_result_data + focalMutations.size() * target_index; if ((hemizygousValue == 1) && (haploidValue == 1)) { // simple occurrence counts for (Mutation *mut : focalMutations) { int8_t scratch_value = mut->scratch_; // an occurrence count of 0 or 2 is unambiguous in the way it is recorded // other occurrence counts all translate to a zygosity of 1 if ((scratch_value == 0) || (scratch_value == 2)) *(result_column_ptr++) = scratch_value; else *(result_column_ptr++) = 1; } } else { // not simple occurrence counts for (Mutation *mut : focalMutations) { int8_t scratch_value = mut->scratch_; // an occurrence count of 0 or 2 is unambiguous in the way it is recorded if ((scratch_value == 0) || (scratch_value == 2)) { *(result_column_ptr++) = scratch_value; continue; } // other occurrence counts need to be translated to the correct result value if (scratch_value == PRESENT_HEMIZYGOUS) *(result_column_ptr++) = hemizygousValue; else if (scratch_value == PRESENT_HAPLOID) *(result_column_ptr++) = haploidValue; else *(result_column_ptr++) = 1; } } } // set the dimensionality of the result matrix const int64_t dim_buf[2] = {(int64_t)focalMutations.size(), target_size}; integer_result->SetDimensions(2, dim_buf); return EidosValue_SP(integer_result); } ================================================ FILE: core/individual.h ================================================ // // individual.h // SLiM // // Created by Ben Haller on 6/10/16. // Copyright (c) 2016-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class Individual is a simple placeholder for individual simulated organisms. It is not used by SLiM's core engine at all; it is provided solely for scripting convenience, as a bag containing the two haplosomes associated with an individual. This makes it easy to sample a subpopulation's individuals, rather than its haplosomes; to determine whether individuals have a given mutation on either of their haplosomes; and other similar tasks. Individuals are kept by Subpopulation, and have the same lifetime as the Subpopulation to which they belong. Since they do not actually contain any information specific to a particular individual – just an index in the Subpopulation's haplosomes vector – they do not get deallocated and reallocated between cycles; the same object continues to represent individual #17 of the subpopulation for as long as that subpopulation exists. This is safe because of the way that objects cannot live across code block boundaries in SLiM. The tag values of particular Individual objects will persist between cycles, even though the individual that is conceptually represented has changed, but that is fine since those values are officially undefined until set. */ #ifndef __SLiM__individual__ #define __SLiM__individual__ #include "haplosome.h" class Subpopulation; extern EidosClass *gSLiM_Individual_Class; // A global counter used to assign all Individual objects a unique ID. Note this is shared by all species. extern slim_pedigreeid_t gSLiM_next_pedigree_id; // use SLiM_GetNextPedigreeID() instead, for THREAD_SAFETY_IN_ACTIVE_PARALLEL() inline slim_pedigreeid_t SLiM_GetNextPedigreeID(void) { THREAD_SAFETY_IN_ACTIVE_PARALLEL("SLiM_GetNextPedigreeID(): gSLiM_next_pedigree_id change"); return gSLiM_next_pedigree_id++; } inline slim_pedigreeid_t SLiM_GetNextPedigreeID_Block(int p_block_size) { THREAD_SAFETY_IN_ACTIVE_PARALLEL("SLiM_GetNextPedigreeID_Block(): gSLiM_next_pedigree_id change"); slim_pedigreeid_t block_base = gSLiM_next_pedigree_id; gSLiM_next_pedigree_id += p_block_size; return block_base; } class Individual : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; #ifdef SLIMGUI public: #else private: #endif EidosValue_SP self_value_; // cached EidosValue object for speed #ifdef SLIMGUI // BCH 3/23/2025: color variables now only exist in SLiMgui, to save on memory footprint uint8_t color_set_; // set to true if the color for the individual has been set uint8_t colorR_, colorG_, colorB_; // cached color components from the color property #endif // Pedigree-tracking ivars. These are -1 if unknown, otherwise assigned sequentially from 0 counting upward. They // uniquely identify individuals within the simulation, so that relatedness of individuals can be assessed. They can // be accessed through the read-only pedigree properties. These are only maintained if sim->pedigrees_enabled_ is on. // If these are maintained, haplosome pedigree IDs are also maintained in parallel; see haplosome.h. float mean_parent_age_; // the mean age of this individual's parents; 0 if parentless, -1 in WF models slim_pedigreeid_t pedigree_id_; // the id of this individual slim_pedigreeid_t pedigree_p1_; // the id of parent 1 slim_pedigreeid_t pedigree_p2_; // the id of parent 2 slim_pedigreeid_t pedigree_g1_; // the id of grandparent 1 slim_pedigreeid_t pedigree_g2_; // the id of grandparent 2 slim_pedigreeid_t pedigree_g3_; // the id of grandparent 3 slim_pedigreeid_t pedigree_g4_; // the id of grandparent 4 int32_t reproductive_output_; // the number of offspring for which this individual has been a parent, so far // This holds the base tskit id used for haplosomes (nodes) belonging to this individual. If the haplosome is // in 1st position (the first for its chromosome), its node id is tsk_node_id_base_; if it is in 2nd position // (the second for its chromosome), its node id is tsk_node_id_base_ + 1. Since this is the same for all // haplosomes for a given individual, we store it here for space efficiency. This is set up by the method // SetCurrentNewIndividual(), which creates the two new entries in the shared node table that are used by all // haplosomes for the individual. tsk_id_t /* int32_t */ tsk_node_id_base_; public: // BCH 6 April 2017: making these ivars public; lots of other classes want to access them, but writing // accessors for them seems excessively complicated / slow, and friending the whole class is too invasive. // Basically I think of the Individual class as just being a struct-like bag in some aspects. uint8_t scratch_; // available for use by algorithms IndividualSex sex_; // must correspond to our position in the Subpopulation vector we live in unsigned int migrant_ : 1; // T if the individual has migrated in the current cycle, F otherwise unsigned int killed_ : 1; // T if the individual has been killed by killIndividuals(), F otherwise // note there are 4 bits free here for other logical flags unsigned tagL0_set_ : 1; // T if tagL0 has been set by the user unsigned tagL0_value_ : 1; // a user-defined tag value of logical type unsigned tagL1_set_ : 1; // T if tagL1 has been set by the user unsigned tagL1_value_ : 1; // a user-defined tag value of logical type unsigned tagL2_set_ : 1; // T if tagL2 has been set by the user unsigned tagL2_value_ : 1; // a user-defined tag value of logical type unsigned tagL3_set_ : 1; // T if tagL3 has been set by the user unsigned tagL3_value_ : 1; // a user-defined tag value of logical type unsigned tagL4_set_ : 1; // T if tagL4 has been set by the user unsigned tagL4_value_ : 1; // a user-defined tag value of logical type slim_usertag_t tag_value_; // a user-defined tag value of integer type double tagF_value_; // a user-defined tag value of float type double fitness_scaling_ = 1.0; // the fitnessScaling property value double cached_fitness_UNSAFE_; // the last calculated fitness value for this individual; NaN for new offspring, 1.0 for new subpops // this is marked UNSAFE because Subpopulation's individual_cached_fitness_OVERRIDE_ flag can override // this value in neutral models; that flag must be checked before using this cached value #ifdef SLIMGUI double cached_unscaled_fitness_; // the last calculated fitness value for this individual, WITHOUT subpop fitnessScaling; used only in // in SLiMgui, which wants to exclude that scaling because it usually represents density-dependence // that confuses interpretation; note that individual_cached_fitness_OVERRIDE_ is not relevant to this #endif Haplosome *hapbuffer_[2]; // *(hapbuffer_[2]), an internal buffer used to avoid allocation and increase memory nonlocality Haplosome **haplosomes_; // OWNED haplosomes; can point to hapbuffer_ or to an external malloced block slim_age_t age_; // nonWF only: the age of the individual, in cycles; -1 in WF models slim_popsize_t index_; // the individual index in that subpop (0-based, and not multiplied by 2) Subpopulation *subpopulation_; // the subpop to which we belong; cannot be a reference because it changes on migration! // Continuous space ivars. These are effectively free tag values of type float, unless they are used by interactions. double spatial_x_, spatial_y_, spatial_z_; // // This class should not be copied, in general, but the default copy constructor cannot be entirely // disabled, because we want to keep instances of this class inside STL containers. We therefore // override it to log whenever it is called, to reduce the risk of unintentional copying. // Individual(const Individual &p_original) = delete; Individual& operator= (const Individual &p_original) = delete; // no copy construction Individual(void) = delete; // no null construction Individual(Subpopulation *p_subpopulation, slim_popsize_t p_individual_index, IndividualSex p_sex, slim_age_t p_age, double p_fitness, float p_mean_parent_age); virtual ~Individual(void) override; inline __attribute__((always_inline)) void ClearColor(void) { #ifdef SLIMGUI // BCH 3/23/2025: color variables now only exist in SLiMgui, to save on memory footprint color_set_ = false; #endif } // This sets the receiver up as a new individual, with a newly assigned pedigree id, and gets // parental and grandparental information from the supplied parents. inline __attribute__((always_inline)) void TrackParentage_Biparental(slim_pedigreeid_t p_pedigree_id, Individual &p_parent1, Individual &p_parent2) { pedigree_id_ = p_pedigree_id; // haplosome_id_ for all haplosomes should be set to (p_pedigree_id * 2) or (p_pedigree_id * 2 + 1) // that used to be done here, but with multiple chromosomes we do it when the haplosomes are made pedigree_p1_ = p_parent1.pedigree_id_; pedigree_p2_ = p_parent2.pedigree_id_; pedigree_g1_ = p_parent1.pedigree_p1_; pedigree_g2_ = p_parent1.pedigree_p2_; pedigree_g3_ = p_parent2.pedigree_p1_; pedigree_g4_ = p_parent2.pedigree_p2_; #pragma omp critical (ReproductiveOutput) { p_parent1.reproductive_output_++; p_parent2.reproductive_output_++; } } inline __attribute__((always_inline)) void RevokeParentage_Biparental(Individual &p_parent1, Individual &p_parent2) { // note this does not need to be in #pragma omp critical (ReproductiveOutput) because it never gets hit when parallel // that is because it only happens when modifyChild() rejects a child, and that does not happen when parallel p_parent1.reproductive_output_--; p_parent2.reproductive_output_--; } inline __attribute__((always_inline)) void TrackParentage_Uniparental(slim_pedigreeid_t p_pedigree_id, Individual &p_parent) { pedigree_id_ = p_pedigree_id; // haplosome_id_ for all haplosomes should be set to (p_pedigree_id * 2) or (p_pedigree_id * 2 + 1) // that used to be done here, but with multiple chromosomes we do it when the haplosomes are made pedigree_p1_ = p_parent.pedigree_id_; pedigree_p2_ = p_parent.pedigree_id_; pedigree_g1_ = p_parent.pedigree_p1_; pedigree_g2_ = p_parent.pedigree_p2_; pedigree_g3_ = p_parent.pedigree_p1_; pedigree_g4_ = p_parent.pedigree_p2_; #pragma omp critical (ReproductiveOutput) { p_parent.reproductive_output_ += 2; } } inline __attribute__((always_inline)) void RevokeParentage_Uniparental(Individual &p_parent) { // note this does not need to be in #pragma omp critical (ReproductiveOutput) because it never gets hit when parallel // that is because it only happens when modifyChild() rejects a child, and that does not happen when parallel p_parent.reproductive_output_ -= 2; } // This alternative to TrackParentage() is used when the parents are not known, as in // addEmpty() and addRecombined(); the unset ivars are set to -1 by the Individual constructor inline __attribute__((always_inline)) void TrackParentage_Parentless(slim_pedigreeid_t p_pedigree_id) { pedigree_id_ = p_pedigree_id; // haplosome_id_ for all haplosomes should be set to (p_pedigree_id * 2) or (p_pedigree_id * 2 + 1) // that used to be done here, but with multiple chromosomes we do it when the haplosomes are made } inline __attribute__((always_inline)) void RevokeParentage_Parentless() { // just for parallel design, no parentage to revoke } // In the new multichromosome design, the individual is created with nullptr values for its haplosomes, // and then this method is used to add each new haplosome object after it is generated #if DEBUG void AddHaplosomeAtIndex(Haplosome *p_haplosome, int p_index); #else inline __attribute__((always_inline)) void AddHaplosomeAtIndex(Haplosome *p_haplosome, int p_index) { haplosomes_[p_index] = p_haplosome; } #endif // Fetch specific haplosomes; used by haplosomesForChromosomes() void AppendHaplosomesForChromosomes(EidosValue_Object *vec, std::vector &chromosome_indices, int64_t index, bool includeNulls); // Relatedness using pedigree data. Most clients will use RelatednessToIndividual() and SharedParentCountWithIndividual; // _Relatedness() and _SharedParentCount() are internal API made public for unit testing. double RelatednessToIndividual(Individual &p_ind, ChromosomeType p_chromosome_type); static double _Relatedness(slim_pedigreeid_t A, slim_pedigreeid_t A_P1, slim_pedigreeid_t A_P2, slim_pedigreeid_t A_G1, slim_pedigreeid_t A_G2, slim_pedigreeid_t A_G3, slim_pedigreeid_t A_G4, slim_pedigreeid_t B, slim_pedigreeid_t B_P1, slim_pedigreeid_t B_P2, slim_pedigreeid_t B_G1, slim_pedigreeid_t B_G2, slim_pedigreeid_t B_G3, slim_pedigreeid_t B_G4, IndividualSex A_sex, IndividualSex B_sex, ChromosomeType p_chromosome_type); int SharedParentCountWithIndividual(Individual &p_ind); static int _SharedParentCount(slim_pedigreeid_t X_P1, slim_pedigreeid_t X_P2, slim_pedigreeid_t Y_P1, slim_pedigreeid_t Y_P2); inline __attribute__((always_inline)) slim_pedigreeid_t PedigreeID() const { return pedigree_id_; } inline __attribute__((always_inline)) void SetPedigreeID(slim_pedigreeid_t p_new_id) { pedigree_id_ = p_new_id; } // should basically never be called inline __attribute__((always_inline)) slim_pedigreeid_t Parent1PedigreeID() const { return pedigree_p1_; } inline __attribute__((always_inline)) slim_pedigreeid_t Parent2PedigreeID() const { return pedigree_p2_; } inline __attribute__((always_inline)) void SetParentPedigreeID(slim_pedigreeid_t p1_new_id, slim_pedigreeid_t p2_new_id) { pedigree_p1_ = p1_new_id; pedigree_p2_ = p2_new_id; } // also? inline __attribute__((always_inline)) int32_t ReproductiveOutput() { return reproductive_output_; } // Each individual reserves two consecutive nodes in the node table; these get/set the base tskit id for that 2-node block inline __attribute__((always_inline)) tsk_id_t TskitNodeIdBase(void) const { return tsk_node_id_base_; } inline __attribute__((always_inline)) void SetTskitNodeIdBase(tsk_id_t p_id) { tsk_node_id_base_ = p_id; } // Spatial position inheritance from a parent; should be called in every code path that generates an offspring from a parent inline __attribute__((always_inline)) void InheritSpatialPosition(int p_dimensionality, Individual *p_parent) { if (p_dimensionality > 0) { switch (p_dimensionality) { case 1: spatial_x_ = p_parent->spatial_x_; break; case 2: spatial_x_ = p_parent->spatial_x_; spatial_y_ = p_parent->spatial_y_; break; case 3: spatial_x_ = p_parent->spatial_x_; spatial_y_ = p_parent->spatial_y_; spatial_z_ = p_parent->spatial_z_; break; } } } // Individual-level output methods; used by outputIndividuals() and outputIndividualsToVCF() static void PrintIndividuals_SLiM(std::ostream &p_out, const Individual **p_individuals, int64_t p_individuals_count, Species &p_species, bool p_output_spatial_positions, bool p_output_ages, bool p_output_ancestral_nucs, bool p_output_pedigree_ids, bool p_output_object_tags, bool p_output_substitutions, Chromosome *p_focal_chromosome); static void PrintIndividuals_VCF(std::ostream &p_out, const Individual **p_individuals, int64_t p_individuals_count, Species &p_species, bool p_output_multiallelics, bool p_simplify_nucs, bool p_output_nonnucs, Chromosome *p_focal_chromosome); // // Eidos support // void GenerateCachedEidosValue(void); inline __attribute__((always_inline)) EidosValue_SP CachedEidosValue(void) { if (!self_value_) GenerateCachedEidosValue(); return self_value_; }; virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_containsMutations(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); static EidosValue_SP ExecuteMethod_Accelerated_countOfMutationsOfType(EidosObject **p_values, size_t p_values_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_haplosomesForChromosomes(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_relatedness(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_sharedParentCount(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); static EidosValue_SP ExecuteMethod_Accelerated_sumOfMutationsOfType(EidosObject **p_values, size_t p_values_size, EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_uniqueMutationsOfType(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_mutationsFromHaplosomes(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); // Accelerated property access; see class EidosObject for comments on this mechanism static EidosValue *GetProperty_Accelerated_index(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_pedigreeID(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tagL0(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tagL1(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tagL2(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tagL3(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tagL4(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_age(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_reproductiveOutput(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tagF(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_migrant(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_fitnessScaling(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_x(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_y(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_z(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_spatialPosition(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_subpopulation(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_haploidGenome1(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_haploidGenome1NonNull(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_haploidGenome2(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_haploidGenome2NonNull(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_haplosomes(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_haplosomesNonNull(EidosObject **p_values, size_t p_values_size); // Accelerated property writing; see class EidosObject for comments on this mechanism static void SetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_tagF(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_tagL0(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_tagL1(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_tagL2(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_tagL3(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_tagL4(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static bool _SetFitnessScaling_1(double source_value, EidosObject **p_values, size_t p_values_size); static bool _SetFitnessScaling_N(const double *source_data, EidosObject **p_values, size_t p_values_size); static void SetProperty_Accelerated_fitnessScaling(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_x(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_y(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_z(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_age(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); static void SetProperty_Accelerated_color(EidosObject **p_values, size_t p_values_size, const EidosValue &p_source, size_t p_source_size); // These flags are used to minimize the work done by Subpopulation::SwapChildAndParentHaplosomes(); it only needs to // reset colors or dictionaries if they have ever been touched by the model. These flags are set and never cleared. // BCH 5/24/2022: Note that these globals are shared across species, so if one species uses a given facility, all // species will suffer the associated speed penalty for it. This is a bit unfortunate, but keeps the design simple. static bool s_any_individual_color_set_; static bool s_any_individual_dictionary_set_; static bool s_any_individual_tag_set_; static bool s_any_individual_tagF_set_; static bool s_any_individual_tagL_set_; static bool s_any_haplosome_tag_set_; static bool s_any_individual_fitness_scaling_set_; // for Subpopulation::ExecuteMethod_takeMigrants() friend Subpopulation; }; class Individual_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: Individual_Class(const Individual_Class &p_original) = delete; // no copy-construct Individual_Class& operator=(const Individual_Class&) = delete; // no copying inline Individual_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; virtual EidosValue_SP ExecuteClassMethod(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const override; EidosValue_SP ExecuteMethod_outputIndividuals(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_outputIndividualsToVCF(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_readIndividualsFromVCF(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_setSpatialPosition(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; EidosValue_SP ExecuteMethod_zygosityOfMutations(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; }; #endif /* defined(__SLiM__individual__) */ ================================================ FILE: core/interaction_type.cpp ================================================ // // interaction_type.cpp // SLiM // // Created by Ben Haller on 2/25/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "interaction_type.h" #include "eidos_call_signature.h" #include "eidos_property_signature.h" #include "slim_eidos_block.h" #include "subpopulation.h" #include "community.h" #include "species.h" #include "slim_globals.h" #include "sparse_vector.h" #include "eidos_simd.h" #include #include #include #include #pragma mark - #pragma mark InteractionType #pragma mark - // The SparseVector pool structure depends on whether we are built single-threaded or multi-threaded; see interaction_type.h #ifdef _OPENMP std::vector> InteractionType::s_freed_sparse_vectors_PERTHREAD; #if DEBUG std::vector InteractionType::s_sparse_vector_count_PERTHREAD; #endif #else std::vector InteractionType::s_freed_sparse_vectors_SINGLE; #if DEBUG int InteractionType::s_sparse_vector_count_SINGLE = 0; #endif #endif void InteractionType::_WarmUp(void) { // Called by InteractionType_Class::InteractionType_Class() when it is created during warmup static bool beenHere = false; if (!beenHere) { THREAD_SAFETY_IN_ANY_PARALLEL("InteractionType::_WarmUp(): not warmed up"); #ifdef _OPENMP // set up per-thread sparse vector pools to avoid lock contention s_freed_sparse_vectors_PERTHREAD.resize(gEidosMaxThreads); #if DEBUG s_sparse_vector_count_PERTHREAD.resize(gEidosMaxThreads, 0); #endif #endif beenHere = true; } } InteractionType::InteractionType(Community &p_community, slim_objectid_t p_interaction_type_id, std::string p_spatiality_string, bool p_reciprocal, double p_max_distance, IndividualSex p_receiver_sex, IndividualSex p_exerter_sex) : self_symbol_(EidosStringRegistry::GlobalStringIDForString(SLiMEidosScript::IDStringWithPrefix('i', p_interaction_type_id)), EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(this, gSLiM_InteractionType_Class))), spatiality_string_(std::move(p_spatiality_string)), reciprocal_(p_reciprocal), max_distance_(p_max_distance), max_distance_sq_(p_max_distance * p_max_distance), if_type_(SpatialKernelType::kFixed), if_param1_(1.0), if_param2_(0.0), community_(p_community), interaction_type_id_(p_interaction_type_id) { // self_symbol_ is always a constant, but can't be marked as such on construction self_symbol_.second->MarkAsConstant(); // Figure out our spatiality, which is the number of spatial dimensions we actively use for distances if (spatiality_string_ == "") { spatiality_ = 0; required_dimensionality_ = 0; } else if (spatiality_string_ == "x") { spatiality_ = 1; required_dimensionality_ = 1; } else if (spatiality_string_ == "y") { spatiality_ = 1; required_dimensionality_ = 2; } else if (spatiality_string_ == "z") { spatiality_ = 1; required_dimensionality_ = 3; } else if (spatiality_string_ == "xy") { spatiality_ = 2; required_dimensionality_ = 2; } else if (spatiality_string_ == "xz") { spatiality_ = 2; required_dimensionality_ = 3; } // NOLINT(*-branch-clone) : intentional branch clone else if (spatiality_string_ == "yz") { spatiality_ = 2; required_dimensionality_ = 3; } else if (spatiality_string_ == "xyz") { spatiality_ = 3; required_dimensionality_ = 3; } else EIDOS_TERMINATION << "ERROR (InteractionType::InteractionType): initializeInteractionType() spatiality '" << spatiality_string_ << "' must be '', 'x', 'y', 'z', 'xy', 'xz', 'yz', or 'xyz'." << EidosTerminate(); // In the single-species case, we want to do some checks up front for backward compatibility/reproducibility; // in the multispecies case these must be deferred to evaluate() time, since they are specific to one evaluated species Species *single_species = (community_.is_explicit_species_ ? nullptr : community_.AllSpecies()[0]); if (single_species) if (required_dimensionality_ > single_species->SpatialDimensionality()) EIDOS_TERMINATION << "ERROR (InteractionType::InteractionType): initializeInteractionType() spatiality cannot utilize spatial dimensions beyond those set in initializeSLiMOptions()." << EidosTerminate(); if (max_distance_ < 0.0) EIDOS_TERMINATION << "ERROR (InteractionType::InteractionType): initializeInteractionType() maxDistance must be >= 0.0." << EidosTerminate(); if ((required_dimensionality_ == 0) && (!std::isinf(max_distance_) || (max_distance_ < 0.0))) EIDOS_TERMINATION << "ERROR (InteractionType::InteractionType): initializeInteractionType() maxDistance must be INF for non-spatial interactions." << EidosTerminate(); // sex-segregation can be configured here, for historical reasons; see setConstraints() for all other constraint setting if ((p_receiver_sex != IndividualSex::kUnspecified) || (p_exerter_sex != IndividualSex::kUnspecified)) { if (single_species && !single_species->SexEnabled()) EIDOS_TERMINATION << "ERROR (InteractionType::InteractionType): initializeInteractionType() sexSegregation value other than '**' are unsupported in non-sexual simulations." << EidosTerminate(); if (p_receiver_sex != IndividualSex::kUnspecified) { receiver_constraints_.sex_ = p_receiver_sex; receiver_constraints_.has_constraints_ = true; } if (p_exerter_sex != IndividualSex::kUnspecified) { exerter_constraints_.sex_ = p_exerter_sex; exerter_constraints_.has_constraints_ = true; } } if ((required_dimensionality_ > 0) && std::isinf(max_distance_)) { if (!gEidosSuppressWarnings) { if (!community_.warned_no_max_distance_) { SLIM_ERRSTREAM << "#WARNING (Community::ExecuteContextFunction_initializeInteractionType): initializeInteractionType() called to configure a spatial interaction type with no maximum distance; this may result in very poor performance." << std::endl; community_.warned_no_max_distance_ = true; } } } } InteractionType::~InteractionType(void) { if (clipped_integral_) { free(clipped_integral_); clipped_integral_ = nullptr; } } void InteractionType::EvaluateSubpopulation(Subpopulation *p_subpop) { if (p_subpop->has_been_removed_) EIDOS_TERMINATION << "ERROR (InteractionType::EvaluateSubpopulation): you cannot evaluate an InteractionType for a subpopulation that has been removed." << EidosTerminate(); // We evaluate for receiver and exerter subpopulations, so that all interaction evaluation state (except for // interaction() callbacks) is frozen at the same time. Evaluate is necessary because the k-d trees are built // once and used to serve many queries, typically, and so they must be built based upon a fixed state snapshot. Species &species = p_subpop->species_; slim_objectid_t subpop_id = p_subpop->subpopulation_id_; slim_popsize_t subpop_size = p_subpop->parent_subpop_size_; Individual **subpop_individuals = p_subpop->parent_individuals_.data(); // Check that the subpopulation is compatible with the configuration of this interaction type // At this stage, we don't know whether it will be used as a receiver, exerter, or both CheckSpeciesCompatibility_Generic(p_subpop->species_); // Find/create a data object for this exerter auto data_iter = data_.find(subpop_id); InteractionsData *subpop_data; if (data_iter == data_.end()) { // No entry in our map table for this subpop_id, so we need to make a new entry subpop_data = &(data_.emplace(subpop_id, InteractionsData(subpop_size, p_subpop->parent_first_male_index_)).first->second); } else { // There is an existing entry, so we need to rehabilitate that entry by recycling its elements safely subpop_data = &(data_iter->second); subpop_data->individual_count_ = subpop_size; subpop_data->first_male_index_ = p_subpop->parent_first_male_index_; // Ensure that other parts of the subpop data block are correctly reset to the same state that Invalidate() // uses; normally this has already been done by Initialize(), but not necessarily. // FIXME we could keep the positions array allocated; we would then need a flag indicating whether it's valid. if (subpop_data->positions_) { free(subpop_data->positions_); subpop_data->positions_ = nullptr; } // Free both k-d trees, keeping in mind that the two might share their memory. FIXME we could keep the // k-d tree buffers around and reuse them; we would then need a flag indicating whether they're valid. if (subpop_data->kd_nodes_ALL_ == subpop_data->kd_nodes_EXERTERS_) subpop_data->kd_nodes_EXERTERS_ = nullptr; if (subpop_data->kd_nodes_ALL_) { free(subpop_data->kd_nodes_ALL_); subpop_data->kd_nodes_ALL_ = nullptr; } if (subpop_data->kd_nodes_EXERTERS_) { free(subpop_data->kd_nodes_EXERTERS_); subpop_data->kd_nodes_EXERTERS_ = nullptr; } subpop_data->kd_root_ALL_ = nullptr; subpop_data->kd_node_count_ALL_ = 0; subpop_data->kd_root_EXERTERS_ = nullptr; subpop_data->kd_node_count_EXERTERS_ = 0; // Free the interaction() callbacks that were cached subpop_data->evaluation_interaction_callbacks_.resize(0); } // At this point, positions_ is guaranteed to be nullptr, as are the k-d tree buffers. // Now we mark ourselves evaluated and fill in buffers as needed. subpop_data->evaluated_ = true; // At a minimum, fetch positional data from the subpopulation; this is guaranteed to be present (for spatiality > 0) if (spatiality_ > 0) { double *positions = (double *)malloc((size_t)subpop_size * SLIM_MAX_DIMENSIONALITY * sizeof(double)); if (!positions) EIDOS_TERMINATION << "ERROR (InteractionType::EvaluateSubpopulation): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); subpop_data->positions_ = positions; int ind_index = 0; Individual **individual = subpop_individuals; double *ind_positions = positions; // IMPORTANT: This is the only place in InteractionType's code where the spatial position of the individuals is // accessed. We cache all positions here, and then use the cache everywhere else. This means that except for // here, we can treat "x", "y", and "z" identically as 1D spatiality, "xy", "xz", and "yz" identically as 2D // spatiality, and of course "xyz" as 3D spatiality. We do that by storing the cached position information in // the same slots regardless of which original coordinates it represents. This also means that this is the // only place in the code where spatiality_string_ should be used (apart from the property accessor); everywhere // else, spatiality_ should suffice. Be careful to keep following this convention, and the different spatiality // values will just automatically work. bool out_of_bounds_seen = false; if (spatiality_string_ == "x") { species.SpatialPeriodicity(&subpop_data->periodic_x_, nullptr, nullptr); subpop_data->bounds_x1_ = p_subpop->bounds_x1_; if (!subpop_data->periodic_x_) { // fast loop for the non-periodic case while (ind_index < subpop_size) { ind_positions[0] = (*individual)->spatial_x_; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } else { // bounds-check individual coordinates when periodic double coord_bound = subpop_data->bounds_x1_; while (ind_index < subpop_size) { double coord = (*individual)->spatial_x_; if ((coord < 0.0) || (coord > coord_bound)) out_of_bounds_seen = true; ind_positions[0] = coord; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } } else if (spatiality_string_ == "y") { species.SpatialPeriodicity(nullptr, &subpop_data->periodic_x_, nullptr); subpop_data->bounds_x1_ = p_subpop->bounds_y1_; if (!subpop_data->periodic_x_) { // fast loop for the non-periodic case while (ind_index < subpop_size) { ind_positions[0] = (*individual)->spatial_y_; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } else { // bounds-check individual coordinates when periodic double coord_bound = subpop_data->bounds_x1_; while (ind_index < subpop_size) { double coord = (*individual)->spatial_y_; if ((coord < 0.0) || (coord > coord_bound)) out_of_bounds_seen = true; ind_positions[0] = coord; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } } else if (spatiality_string_ == "z") { species.SpatialPeriodicity(nullptr, nullptr, &subpop_data->periodic_x_); subpop_data->bounds_x1_ = p_subpop->bounds_z1_; if (!subpop_data->periodic_x_) { // fast loop for the non-periodic case while (ind_index < subpop_size) { ind_positions[0] = (*individual)->spatial_z_; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } else { // bounds-check individual coordinates when periodic double coord_bound = subpop_data->bounds_x1_; while (ind_index < subpop_size) { double coord = (*individual)->spatial_z_; if ((coord < 0.0) || (coord > coord_bound)) out_of_bounds_seen = true; ind_positions[0] = coord; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } } else if (spatiality_string_ == "xy") { species.SpatialPeriodicity(&subpop_data->periodic_x_, &subpop_data->periodic_y_, nullptr); subpop_data->bounds_x1_ = p_subpop->bounds_x1_; subpop_data->bounds_y1_ = p_subpop->bounds_y1_; if (!subpop_data->periodic_x_ && !subpop_data->periodic_y_) { // fast loop for the non-periodic case while (ind_index < subpop_size) { ind_positions[0] = (*individual)->spatial_x_; ind_positions[1] = (*individual)->spatial_y_; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } else { // bounds-check individual coordinates when periodic double coord1_bound = subpop_data->bounds_x1_; double coord2_bound = subpop_data->bounds_y1_; while (ind_index < subpop_size) { double coord1 = (*individual)->spatial_x_; double coord2 = (*individual)->spatial_y_; if ((subpop_data->periodic_x_ && ((coord1 < 0.0) || (coord1 > coord1_bound))) || (subpop_data->periodic_y_ && ((coord2 < 0.0) || (coord2 > coord2_bound)))) out_of_bounds_seen = true; ind_positions[0] = coord1; ind_positions[1] = coord2; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } } else if (spatiality_string_ == "xz") { species.SpatialPeriodicity(&subpop_data->periodic_x_, nullptr, &subpop_data->periodic_y_); subpop_data->bounds_x1_ = p_subpop->bounds_x1_; subpop_data->bounds_y1_ = p_subpop->bounds_z1_; if (!subpop_data->periodic_x_ && !subpop_data->periodic_y_) { // fast loop for the non-periodic case while (ind_index < subpop_size) { ind_positions[0] = (*individual)->spatial_x_; ind_positions[1] = (*individual)->spatial_z_; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } else { // bounds-check individual coordinates when periodic double coord1_bound = subpop_data->bounds_x1_; double coord2_bound = subpop_data->bounds_y1_; while (ind_index < subpop_size) { double coord1 = (*individual)->spatial_x_; double coord2 = (*individual)->spatial_z_; if ((subpop_data->periodic_x_ && ((coord1 < 0.0) || (coord1 > coord1_bound))) || (subpop_data->periodic_y_ && ((coord2 < 0.0) || (coord2 > coord2_bound)))) out_of_bounds_seen = true; ind_positions[0] = coord1; ind_positions[1] = coord2; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } } else if (spatiality_string_ == "yz") { species.SpatialPeriodicity(nullptr, &subpop_data->periodic_x_, &subpop_data->periodic_y_); subpop_data->bounds_x1_ = p_subpop->bounds_y1_; subpop_data->bounds_y1_ = p_subpop->bounds_z1_; if (!subpop_data->periodic_x_ && !subpop_data->periodic_y_) { // fast loop for the non-periodic case while (ind_index < subpop_size) { ind_positions[0] = (*individual)->spatial_y_; ind_positions[1] = (*individual)->spatial_z_; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } else { // bounds-check individual coordinates when periodic double coord1_bound = subpop_data->bounds_x1_; double coord2_bound = subpop_data->bounds_y1_; while (ind_index < subpop_size) { double coord1 = (*individual)->spatial_y_; double coord2 = (*individual)->spatial_z_; if ((subpop_data->periodic_x_ && ((coord1 < 0.0) || (coord1 > coord1_bound))) || (subpop_data->periodic_y_ && ((coord2 < 0.0) || (coord2 > coord2_bound)))) out_of_bounds_seen = true; ind_positions[0] = coord1; ind_positions[1] = coord2; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } } else if (spatiality_string_ == "xyz") { species.SpatialPeriodicity(&subpop_data->periodic_x_, &subpop_data->periodic_y_, &subpop_data->periodic_z_); subpop_data->bounds_x1_ = p_subpop->bounds_x1_; subpop_data->bounds_y1_ = p_subpop->bounds_y1_; subpop_data->bounds_z1_ = p_subpop->bounds_z1_; if (!subpop_data->periodic_x_ && !subpop_data->periodic_y_ && !subpop_data->periodic_z_) { // fast loop for the non-periodic case while (ind_index < subpop_size) { ind_positions[0] = (*individual)->spatial_x_; ind_positions[1] = (*individual)->spatial_y_; ind_positions[2] = (*individual)->spatial_z_; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } else { // bounds-check individual coordinates when periodic double coord1_bound = subpop_data->bounds_x1_; double coord2_bound = subpop_data->bounds_y1_; double coord3_bound = subpop_data->bounds_z1_; while (ind_index < subpop_size) { double coord1 = (*individual)->spatial_x_; double coord2 = (*individual)->spatial_y_; double coord3 = (*individual)->spatial_z_; if ((subpop_data->periodic_x_ && ((coord1 < 0.0) || (coord1 > coord1_bound))) || (subpop_data->periodic_y_ && ((coord2 < 0.0) || (coord2 > coord2_bound))) || (subpop_data->periodic_z_ && ((coord3 < 0.0) || (coord3 > coord3_bound)))) out_of_bounds_seen = true; ind_positions[0] = coord1; ind_positions[1] = coord2; ind_positions[2] = coord3; ++ind_index; ++individual; ind_positions += SLIM_MAX_DIMENSIONALITY; } } } else { EIDOS_TERMINATION << "ERROR (InteractionType::EvaluateSubpopulation): (internal error) illegal spatiality string value" << EidosTerminate(); } if (out_of_bounds_seen) EIDOS_TERMINATION << "ERROR (InteractionType::EvaluateSubpopulation): an individual position was seen that is out of bounds for a periodic spatial dimension; positions within periodic bounds are required by InteractionType since the underlying spatial engine's integrity depends upon them. The use of pointPeriodic() is recommended to enforce periodic boundaries." << EidosTerminate(); } // Check that our maximum interactions distance does not violate the assumptions of periodic boundaries; // an individual cannot interact with the same individual more than once, through wrapping around. if ((subpop_data->periodic_x_ && (subpop_data->bounds_x1_ <= max_distance_ * 2.0)) || (subpop_data->periodic_y_ && (subpop_data->bounds_y1_ <= max_distance_ * 2.0)) || (subpop_data->periodic_z_ && (subpop_data->bounds_z1_ <= max_distance_ * 2.0))) EIDOS_TERMINATION << "ERROR (InteractionType::EvaluateSubpopulation): maximum interaction distance is greater than or equal to half of the spatial extent of a periodic spatial dimension, which would allow an individual to participate in more than one interaction with a single individual. When periodic boundaries are used, the maximum interaction distance of interaction types involving periodic dimensions must be less than half of the spatial extent of those dimensions." << EidosTerminate(); // Cache the interaction() callbacks applicable at this moment, for the given subpopulation and this interaction type. // Note that interaction() callbacks are non-species-specific, so we fetch from the Community with species nullptr. // Callbacks used depend upon the exerter subpopulation, so this is snapping the callbacks for subpop as exerters; // the subpopulation of receivers does not influence the choice of which callbacks are used. subpop_data->evaluation_interaction_callbacks_ = community_.ScriptBlocksMatching(community_.Tick(), SLiMEidosBlockType::SLiMEidosInteractionCallback, -1, interaction_type_id_, subpop_id, -1, nullptr); // Note that we do not create the k-d tree here. Non-spatial models will never have a k-d tree; spatial models may or // may not need one, depending upon what methods are called by the client, which may vary cycle by cycle. // Also, receiver subpopulations need to be evaluated too, but (if used only for receivers) will not require a k-d tree. // Methods that need the k-d tree must therefore call EnsureKDTreePresent() prior to using it. // BCH 10/1/2023: For SLiM 4.1 this policy is now altered slightly. If non-sex exerter constraints are set, we need to cache // the EXERTER k-d tree nodes here, because those constraints need to be applied to the state of individuals at snapshot // time. We do not build the tree, just cache its nodes so it knows which individuals it contains. This is potentially // a little bit wasteful, if a subpopulation that is evaluated is used only for receivers, not for exerters; that is // a fairly uncommon usage pattern, and the overhead of caching the nodes is pretty minimal -- O(N) to cache, versus // O(N log N) to build the tree, and the constant factor for both operations is small. if ((spatiality_ > 0) && (exerter_constraints_.has_nonsex_constraints_)) { // There is one little hitch. CacheKDTreeNodes() will call CheckIndividualNonSexConstraints(), and that // method will raise if an exerter constraint exists for a tag/tagL value but a candidate individual // doesn't have that tag/tagL value defined. If the k-d tree is actually going to be used to find exerters, // then that raise is appropriate; an exerter has an unset tag/tagL and so the constraint cannot be applied. // BUT if the k-d tree is only going to be used to find receivers, or perhaps not at all, then the raise is // not appropriate and needs to be suppressed. SO, here we pre-test for it, and set a flag remembering that // "this subpop_data cannot be used to find exerters, because their state is non-compliant with the exerter // constraints". We check that flag in EnsureKDTreePresent_EXERTERS() and raise there, when we are certain // that the tree is actually being used. for (int i = 0; i < subpop_size; ++i) { Individual *ind = subpop_individuals[i]; if (!_PrecheckIndividualNonSexConstraints(ind, exerter_constraints_)) { // The k-d tree for this subpopulation will not get cached, because of an unset tag/tagL; if the // user tries to use this subpop as an exerter subpop, EnsureKDTreePresent_EXERTERS() will raise, // but if the user does not try to do that, there is no problem. subpop_data->kd_constraints_raise_EXERTERS_ = true; return; } } // OK, it's safe to proceed with caching the exerter k-d tree; nobody will raise. CacheKDTreeNodes(p_subpop, *subpop_data, /* p_apply_exerter_constraints */ true, &subpop_data->kd_nodes_EXERTERS_, &subpop_data->kd_root_EXERTERS_, &subpop_data->kd_node_count_EXERTERS_); } // Note that receiver constraints are evaluated at query time, not here. This means that they are applied to the // state of the receiver at query time, whereas exerter constraints are applied to the state of the exerter at // evaluate() time. This discrepancy is intentional and documented. The alternative would be to go through the // subpop here, at evaluate() time, and cache receiver eligibility for the whole subpopulation. That would be // made more complex by the fact that receivers might have undefined tag values that are needed to apply the // constraints, but - as above for exerters - the raise from that condition must be suppressed until the individual // is actually used as a receiver in a query. Doing that would be even more complex than for exerters, and the // performance penalty would be much larger than for exerters. } bool InteractionType::AnyEvaluated(void) { for (auto &data_iter : data_) { InteractionsData &data = data_iter.second; if (data.evaluated_) return true; } return false; } void InteractionType::_InvalidateData(InteractionsData &data) { data.evaluated_ = false; if (data.positions_) { free(data.positions_); data.positions_ = nullptr; } // keep in mind that the two k-d trees may share their memory if (data.kd_nodes_ALL_ == data.kd_nodes_EXERTERS_) data.kd_nodes_EXERTERS_ = nullptr; if (data.kd_nodes_ALL_) { free(data.kd_nodes_ALL_); data.kd_nodes_ALL_ = nullptr; } if (data.kd_nodes_EXERTERS_) { free(data.kd_nodes_EXERTERS_); data.kd_nodes_EXERTERS_ = nullptr; } data.kd_root_ALL_ = nullptr; data.kd_node_count_ALL_ = 0; data.kd_root_EXERTERS_ = nullptr; data.kd_node_count_EXERTERS_ = 0; data.evaluation_interaction_callbacks_.resize(0); } void InteractionType::Invalidate(void) { // Called by SLiM when the old generation goes away; should invalidate all evaluation. We avoid actually freeing the // big blocks if possible, though, since that can incur large overhead from madvise() – see header comments. We do free // the positional data and the k-d tree, though, in an attempt to make fatal errors occur if somebody doesn't manage // the buffers and evaluated state correctly. They should be smaller, and thus not trigger madvise(), anyway. for (auto &data_iter : data_) _InvalidateData(data_iter.second); } void InteractionType::InvalidateForSpecies(Species *p_invalid_species) { // This is like Invalidate(), but invalidates only data associated with a given species for (auto &data_iter : data_) { slim_objectid_t subpop_id = data_iter.first; Subpopulation *subpop = community_.SubpopulationWithID(subpop_id); if (subpop) { Species *species = &subpop->species_; if (species == p_invalid_species) _InvalidateData(data_iter.second); } } } void InteractionType::InvalidateForSubpopulation(Subpopulation *p_invalid_subpop) { // This is like Invalidate(), but invalidates only data associated with a given subpop for (auto &data_iter : data_) { slim_objectid_t subpop_id = data_iter.first; Subpopulation *subpop = community_.SubpopulationWithID(subpop_id); if (subpop == p_invalid_subpop) _InvalidateData(data_iter.second); } } void InteractionType::CheckSpeciesCompatibility_Generic(Species &species) { // This checks that a given subpop (unknown whether receiver or exerter) is compatible with this interaction type if (required_dimensionality_ > species.SpatialDimensionality()) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpeciesCompatibility_Generic): the exerter or receiver species has insufficient dimensionality to be used with this interaction type." << EidosTerminate(); // For this "generic" case we do not check sex constraints at all. This is useful partly when we don't know // whether the species will act as receiver or exerter, and partly when we specifically don't want to check // sex constraints, for queries like nearestNeighbors() that do not use constraints. } void InteractionType::CheckSpeciesCompatibility_Receiver(Species &species) { // This checks that a given receiver subpop is compatible with this interaction type if (required_dimensionality_ > species.SpatialDimensionality()) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpeciesCompatibility_Receiver): the receiver species has insufficient dimensionality to be used with this interaction type." << EidosTerminate(); // If there is a sex constraint for receivers, then the receiver species must be sexual if ((receiver_constraints_.sex_ != IndividualSex::kUnspecified) && !species.SexEnabled()) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpeciesCompatibility_Receiver): a sex constraint exists for receivers, but the receiver species is non-sexual." << EidosTerminate(); } void InteractionType::CheckSpeciesCompatibility_Exerter(Species &species) { // This checks that a given exerter subpop is compatible with this interaction type if (required_dimensionality_ > species.SpatialDimensionality()) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpeciesCompatibility_Exerter): the exerter species has insufficient dimensionality to be used with this interaction type." << EidosTerminate(); // If there is a sex constraint for receivers, then the receiver species must be sexual if ((exerter_constraints_.sex_ != IndividualSex::kUnspecified) && !species.SexEnabled()) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpeciesCompatibility_Exerter): a sex constraint exists for exerters, but the exerter species is non-sexual." << EidosTerminate(); } void InteractionType::CheckSpatialCompatibility(Subpopulation *receiver_subpop, Subpopulation *exerter_subpop) { // This checks that two subpops can legally interact with each other; it should always be guaranteed before a query is served if (receiver_subpop == exerter_subpop) return; // Check for identical dimensionality int dimensionality_ex = exerter_subpop->species_.SpatialDimensionality(); int dimensionality_re = receiver_subpop->species_.SpatialDimensionality(); if (dimensionality_ex != dimensionality_re) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpatialCompatibility): the exerter and receiver subpopulations have different dimensionalities." << EidosTerminate(); // Check for identical periodicity bool periodic_ex_x, periodic_ex_y, periodic_ex_z; bool periodic_re_x, periodic_re_y, periodic_re_z; exerter_subpop->species_.SpatialPeriodicity(&periodic_ex_x, &periodic_ex_y, &periodic_ex_z); receiver_subpop->species_.SpatialPeriodicity(&periodic_re_x, &periodic_re_y, &periodic_re_z); if ((periodic_ex_x != periodic_re_x) || (periodic_ex_y != periodic_re_y) || (periodic_ex_z != periodic_re_z)) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpatialCompatibility): the exerter and receiver subpopulations have different periodicities." << EidosTerminate(); // Check for identical bounds, required only when the dimension in question is periodic // For non-periodic dimensions it is assumed that the bounds, even if they don't match, exist in the same coordinate system if (periodic_ex_x && ((exerter_subpop->bounds_x0_ != receiver_subpop->bounds_x0_) || (exerter_subpop->bounds_x1_ != receiver_subpop->bounds_x1_))) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpatialCompatibility): the exerter and receiver subpopulations have different periodic x boundaries." << EidosTerminate(); if (periodic_ex_y && ((exerter_subpop->bounds_y0_ != receiver_subpop->bounds_y0_) || (exerter_subpop->bounds_y1_ != receiver_subpop->bounds_y1_))) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpatialCompatibility): the exerter and receiver subpopulations have different periodic y boundaries." << EidosTerminate(); if (periodic_ex_z && ((exerter_subpop->bounds_z0_ != receiver_subpop->bounds_z0_) || (exerter_subpop->bounds_z1_ != receiver_subpop->bounds_z1_))) EIDOS_TERMINATION << "ERROR (InteractionType::CheckSpatialCompatibility): the exerter and receiver subpopulations have different periodic z boundaries." << EidosTerminate(); } double InteractionType::CalculateDistance(double *p_position1, double *p_position2) { #ifndef __clang_analyzer__ if (spatiality_ == 1) { return fabs(p_position1[0] - p_position2[0]); } else if (spatiality_ == 2) { double distance_x = (p_position1[0] - p_position2[0]); double distance_y = (p_position1[1] - p_position2[1]); return sqrt(distance_x * distance_x + distance_y * distance_y); } else if (spatiality_ == 3) { double distance_x = (p_position1[0] - p_position2[0]); double distance_y = (p_position1[1] - p_position2[1]); double distance_z = (p_position1[2] - p_position2[2]); return sqrt(distance_x * distance_x + distance_y * distance_y + distance_z * distance_z); } else EIDOS_TERMINATION << "ERROR (InteractionType::CalculateDistance): (internal error) calculation of distances requires that the interaction be spatial." << EidosTerminate(); #else return 0.0; #endif } // Calculate a distance including effects of periodicity. This can always be called instead of // CalculateDistance(), it is just a little slower since it has to check the periodicity flags. double InteractionType::CalculateDistanceWithPeriodicity(double *p_position1, double *p_position2, InteractionsData &p_subpop_data) { if (spatiality_ == 1) { if (p_subpop_data.periodic_x_) { double x1 = p_position1[0], x2 = p_position2[0], d1, d2; if (x1 < x2) { d1 = x2 - x1; d2 = (x1 + p_subpop_data.bounds_x1_) - x2; } else { d1 = x1 - x2; d2 = (x2 + p_subpop_data.bounds_x1_) - x1; } return std::min(d1, d2); } else { return fabs(p_position1[0] - p_position2[0]); } } else if (spatiality_ == 2) { double distance_x, distance_y; if (p_subpop_data.periodic_x_) { double x1 = p_position1[0], x2 = p_position2[0], d1, d2; if (x1 < x2) { d1 = x2 - x1; d2 = (x1 + p_subpop_data.bounds_x1_) - x2; } else { d1 = x1 - x2; d2 = (x2 + p_subpop_data.bounds_x1_) - x1; } distance_x = std::min(d1, d2); } else { distance_x = p_position1[0] - p_position2[0]; } if (p_subpop_data.periodic_y_) { double y1 = p_position1[1], y2 = p_position2[1], d1, d2; if (y1 < y2) { d1 = y2 - y1; d2 = (y1 + p_subpop_data.bounds_y1_) - y2; } else { d1 = y1 - y2; d2 = (y2 + p_subpop_data.bounds_y1_) - y1; } distance_y = std::min(d1, d2); } else { distance_y = p_position1[1] - p_position2[1]; } return sqrt(distance_x * distance_x + distance_y * distance_y); } else if (spatiality_ == 3) { double distance_x, distance_y, distance_z; if (p_subpop_data.periodic_x_) { double x1 = p_position1[0], x2 = p_position2[0], d1, d2; if (x1 < x2) { d1 = x2 - x1; d2 = (x1 + p_subpop_data.bounds_x1_) - x2; } else { d1 = x1 - x2; d2 = (x2 + p_subpop_data.bounds_x1_) - x1; } distance_x = std::min(d1, d2); } else { distance_x = p_position1[0] - p_position2[0]; } if (p_subpop_data.periodic_y_) { double y1 = p_position1[1], y2 = p_position2[1], d1, d2; if (y1 < y2) { d1 = y2 - y1; d2 = (y1 + p_subpop_data.bounds_y1_) - y2; } else { d1 = y1 - y2; d2 = (y2 + p_subpop_data.bounds_y1_) - y1; } distance_y = std::min(d1, d2); } else { distance_y = p_position1[1] - p_position2[1]; } if (p_subpop_data.periodic_z_) { double z1 = p_position1[2], z2 = p_position2[2], d1, d2; if (z1 < z2) { d1 = z2 - z1; d2 = (z1 + p_subpop_data.bounds_z1_) - z2; } else { d1 = z1 - z2; d2 = (z2 + p_subpop_data.bounds_z1_) - z1; } distance_z = std::min(d1, d2); } else { distance_z = p_position1[2] - p_position2[2]; } return sqrt(distance_x * distance_x + distance_y * distance_y + distance_z * distance_z); } else EIDOS_TERMINATION << "ERROR (InteractionType::CalculateDistanceWithPeriodicity): (internal error) calculation of distances requires that the interaction be spatial." << EidosTerminate(); } double InteractionType::CalculateStrengthNoCallbacks(double p_distance) { // CAUTION: This method should only be called when p_distance <= max_distance_ (or is NAN). // It is the caller's responsibility to do that filtering, for performance reasons! // The caller is also responsible for guaranteeing that this is not a self-interaction, // and that it is not ruled out by sex-selectivity. // SEE ALSO: Kernel::DensityForDistance(), which is parallel to this switch (if_type_) { case SpatialKernelType::kFixed: return (if_param1_); // fmax case SpatialKernelType::kLinear: return (if_param1_ * (1.0 - p_distance / max_distance_)); // fmax * (1 − d/dmax) case SpatialKernelType::kExponential: return (if_param1_ * exp(-if_param2_ * p_distance)); // fmax * exp(−λd) case SpatialKernelType::kNormal: return (if_param1_ * exp(-(p_distance * p_distance) / n_2param2sq_)); // fmax * exp(−d^2/2σ^2) case SpatialKernelType::kCauchy: { double temp = p_distance / if_param2_; return (if_param1_ / (1.0 + temp * temp)); // fmax / (1+(d/λ)^2) } case SpatialKernelType::kStudentsT: return SpatialKernel::tdist(p_distance, if_param1_, if_param2_, if_param3_); // fmax / (1+(d/t)^2/n)^(−(ν+1)/2) } EIDOS_TERMINATION << "ERROR (InteractionType::CalculateStrengthNoCallbacks): (internal error) unexpected SpatialKernelType." << EidosTerminate(); } double InteractionType::CalculateStrengthWithCallbacks(double p_distance, Individual *p_receiver, Individual *p_exerter, std::vector &p_interaction_callbacks) { // CAUTION: This method should only be called when p_distance <= max_distance_ (or is NAN). // It is the caller's responsibility to do that filtering, for performance reasons! // The caller is also responsible for guaranteeing that this is not a self-interaction, // and that it is not ruled out by sex-selectivity. double strength = CalculateStrengthNoCallbacks(p_distance); strength = ApplyInteractionCallbacks(p_receiver, p_exerter, strength, p_distance, p_interaction_callbacks); return strength; } // the number of grid cells along one side of the 1D/2D/3D clipped_integral_ buffer; probably best to be a power of two // we want to make this big enough that we don't need to interpolate; picking the closest value is within ~0.25% for 1024 // at this size, clipped_integral_ takes 8 MB, which is quite acceptable, and the temp buffer takes about the same static const int64_t clipped_integral_size = 1024; void InteractionType::CacheClippedIntegral_1D(void) { if (clipped_integral_valid_ && clipped_integral_) return; if (clipped_integral_) { free(clipped_integral_); clipped_integral_ = nullptr; } if (!std::isfinite(max_distance_)) EIDOS_TERMINATION << "ERROR (InteractionType::CacheClippedIntegral_1D): clippedIntegral() requires that the maxDistance of the interaction be finite; integrals out to infinity cannot be computed numerically." << EidosTerminate(); // First, build a temporary buffer holding interaction function values for distances from a focal individual. // This is a 1D matrix of values, with the focal individual positioned at the very end of it, sitting on // the grid position at (0, 0). Distances used here are in [0, max_distance_], except that they are the // distance from the edge grid position (where the focal individual is) to the *center* of each cell (each value) // in the grid, so in fact the distances represent a slightly narrower range of values than [0, max_distance_]. int64_t dts_quadrant = clipped_integral_size - 1; // -1 because this is the count of cells between grid lines double *distance_to_strength = (double *)calloc(dts_quadrant, sizeof(double)); double dts_sum = 0.0; for (int64_t x = 0; x < dts_quadrant; ++x) { double cx = x + 0.5; // center of the interval starting at x double distance = (cx / dts_quadrant) * max_distance_; // x distance from the focal individual if (distance <= max_distance_) // if not, calloc() provides 0.0 { double strength = CalculateStrengthNoCallbacks(distance); distance_to_strength[x] = strength; dts_sum += strength; } } #if 0 // debug output of distance_to_strength std::cout << "distance_to_strength :" << std::endl; for (int64_t x = 0; x < dts_quadrant; ++x) printf("%.6f ", distance_to_strength[x]); std::cout << std::endl; #endif // Now we build clipped_integral_ itself. It is one larger than distance_to_strength in each dimension, // providing the integral for distances (dx) from the focal individual to the nearest edge in each // dimension. Each value in it is a sum of strengths from a linear subset of distance_to_strength: // the strengths that would be inside the spatial bounds, for the given (dx). The value at (0) // is exactly the sum of distance_to_strength, representing the integral for an individual that is // positioned at the edge of the space. At (clipped_integral_size - 1) is the value for an individual // exactly (max_distance_), or further, from the nearest edge; it is 2x the sum of the entirety of // distance_to_strength. Note that clipped_integral_ is dts_quadrant+1 values in length, because each // value of clipped_integral_ is conceptually positioned at the *grid position* between the intervals // of distance_to_strength; its values represent focal individual positions, which fall on the grid // positions of distance_to_strength. clipped_integral_ = (double *)calloc(clipped_integral_size, sizeof(double)); // fill the first row/column so we have previously computed values to work with below clipped_integral_[0] = dts_sum; for (int64_t x = 1; x < clipped_integral_size; ++x) { // start with a previously computed value double integral = clipped_integral_[x - 1]; // add the next value integral += distance_to_strength[x - 1]; clipped_integral_[x] = integral; } #if 0 // debug output of clipped_integral_ std::cout << "clipped_integral_ (point 1) :" << std::endl; for (int64_t x = 0; x < clipped_integral_size; ++x) printf("%.6f ", clipped_integral_[x]); std::cout << std::endl; #endif // rescale clipped_integral_ by the size of each grid cell: of the area covered by the grid // (max_distance_ x max_distance_), the subarea comprised by one cell (1/dts_quadrant^2) of that // we do this as a post-pass mostly for debugging purposes, so that the steps above can be // verified to be working correctly in themselves before complicating matters by rescaling int64_t grid_count = clipped_integral_size; double normalization = (1.0 / dts_quadrant) * max_distance_; for (int64_t index = 0; index < grid_count; ++index) clipped_integral_[index] *= normalization; #if 0 // debug output of clipped_integral_ std::cout << "clipped_integral_ (point 2) :" << std::endl; for (int64_t x = 0; x < clipped_integral_size; ++x) printf("%.6f ", clipped_integral_[x]); std::cout << std::endl; #endif free(distance_to_strength); clipped_integral_valid_ = true; } void InteractionType::CacheClippedIntegral_2D(void) { if (clipped_integral_valid_ && clipped_integral_) return; //double start_time = static_cast(std::clock()) / CLOCKS_PER_SEC, end_time; if (clipped_integral_) { free(clipped_integral_); clipped_integral_ = nullptr; } if (!std::isfinite(max_distance_)) EIDOS_TERMINATION << "ERROR (InteractionType::CacheClippedIntegral_2D): clippedIntegral() requires that the maxDistance of the interaction be finite; integrals out to infinity cannot be computed numerically." << EidosTerminate(); // First, build a temporary buffer holding interaction function values for distances from a focal individual. // This is a 2D matrix of values, with the focal individual positioned at the very corner of it, sitting on // the grid lines that form the outside corner around the value at (0, 0). Distances used here are in // [0, max_distance_], except that they are the distance from the corner grid intersection (where the focal // individual is) to the *center* of each cell (each value) in the grid, so in fact the distances represent // a slightly narrower range of values than [0, max_distance_]. int64_t dts_quadrant = clipped_integral_size - 1; // -1 because this is the count of cells between grid lines double *distance_to_strength = (double *)calloc(dts_quadrant * dts_quadrant, sizeof(double)); //std::cout << "distance_to_strength size == " << ((dts_quadrant * dts_quadrant * sizeof(double)) / (1024.0 * 1024.0)) << "MB" << std::endl << std::endl; for (int64_t x = 0; x < dts_quadrant; ++x) { for (int64_t y = x; y < dts_quadrant; ++y) { double cx = x + 0.5, cy = y + 0.5; // center of the grid cell (x, y) double dx = (cx / dts_quadrant) * max_distance_; // x distance from the focal individual double dy = (cy / dts_quadrant) * max_distance_; // y distance from the focal individual double distance = sqrt(dx * dx + dy * dy); // distance from the focal individual if (distance <= max_distance_) // if not, calloc() provides 0.0 { double strength = CalculateStrengthNoCallbacks(distance); distance_to_strength[x + y * dts_quadrant] = strength; distance_to_strength[y + x * dts_quadrant] = strength; } } } #if 0 // debug output of distance_to_strength std::cout << "distance_to_strength :" << std::endl; for (int64_t y = 0; y < dts_quadrant; ++y) { for (int64_t x = 0; x < dts_quadrant; ++x) { printf("%.6f ", distance_to_strength[x + y * dts_quadrant]); } std::cout << std::endl; } std::cout << std::endl; #endif // Now do preparatory summations to get a vector of cumulative column sums across distance_to_strength. // The first element of this vector is the sum of values from the first column of distance_to_strength. // The second element of this vector is that, *plus* the sum of the second column. And so forth, such // that the last element of this vector of the sum of the entirety of distance_to_strength. This will // allow us to work more efficiently below. Since building clipped_integral_ the brute force way is // an O(N^4) algorithm (NxN values, each a sum of NxN values from distance_to_strength), this kind of // work will be important if we try to build a large clipped_integral_ buffer. double *dts_cumsums = (double *)malloc(dts_quadrant * sizeof(double)); double *dts_colsums = (double *)malloc(dts_quadrant * sizeof(double)); double total = 0.0; for (int64_t x = 0; x < dts_quadrant; ++x) { double colsum = 0.0; for (int64_t y = 0; y < dts_quadrant; ++y) colsum += distance_to_strength[x + y * dts_quadrant]; dts_colsums[x] = colsum; total += colsum; dts_cumsums[x] = total; } #if 0 // debug output of dts_colsums std::cout << "dts_colsums :" << std::endl; for (int64_t x = 0; x < dts_quadrant; ++x) printf("%.6f ", dts_colsums[x]); std::cout << std::endl << std::endl; // debug output of dts_cumsums std::cout << "dts_cumsums :" << std::endl; for (int64_t x = 0; x < dts_quadrant; ++x) printf("%.6f ", dts_cumsums[x]); std::cout << std::endl << std::endl; #endif // Now we build clipped_integral_ itself. It is one larger than distance_to_strength in each dimension, // providing the integral for distances (dx, dy) from the focal individual to the nearest edge in each // dimension. Each value in it is a sum of strengths from a rectangular subset of distance_to_strength: // the strengths that would be inside the spatial bounds, for the given (dx, dy). The value at (0, 0) // is exactly the sum of distance_to_strength, representing the integral for an individual that is // positioned at the corner of the space. At (clipped_integral_size - 1, clipped_integral_size - 1) is // the value for an individual exactly (max_distance_, max_distance_), or further, from the nearest // corner; it is 4x the sum of the entirety of distance_to_strength. Each side of clipped_integral_ is // dts_quadrant+1 values in length, because each value of clipped_integral_ is conceptually positioned // at the *intersection of grid lines* between the cells of distance_to_strength; its values represent // focal individual positions, which fall on the grid lines of distance_to_strength. clipped_integral_ = (double *)calloc(clipped_integral_size * clipped_integral_size, sizeof(double)); //std::cout << "clipped_integral_ size == " << ((clipped_integral_size * clipped_integral_size * sizeof(double)) / (1024.0 * 1024.0)) << "MB" << std::endl << std::endl; // fill the first row/column so we have previously computed values to work with below for (int64_t x = 0; x < clipped_integral_size; ++x) { double integral = dts_cumsums[dts_quadrant - 1]; // full quadrant if (x > 0) integral += dts_cumsums[x - 1]; // additional columns clipped_integral_[x + 0 * clipped_integral_size] = integral; clipped_integral_[0 + x * clipped_integral_size] = integral; } for (int64_t y = 1; y < clipped_integral_size; ++y) { // start with a previously computed value double integral = clipped_integral_[y + (y - 1) * clipped_integral_size]; // add in previous values in the same row in this quadrant for (int64_t x = 1; x < y; ++x) integral += distance_to_strength[(x - 1) + (y - 1) * dts_quadrant]; // now fill new values in this row for (int64_t x = y; x < clipped_integral_size; ++x) { // add in the full row in the other quadrant integral += dts_colsums[x - 1]; // add in previous values in the same column in this quadrant; when x==y these were already in the previously computed value if (x > y) { for (int64_t yr = 1; yr < y; ++yr) integral += distance_to_strength[(x - 1) + (yr - 1) * dts_quadrant]; } // add in the one new value for this new column in this row integral += distance_to_strength[(x - 1) + (y - 1) * dts_quadrant]; clipped_integral_[x + y * clipped_integral_size] = integral; clipped_integral_[y + x * clipped_integral_size] = integral; } } #if 0 // debug output of clipped_integral_ std::cout << "clipped_integral_ (point 1) :" << std::endl; for (int64_t y = 0; y < clipped_integral_size; ++y) { for (int64_t x = 0; x < clipped_integral_size; ++x) { printf("%.6f ", clipped_integral_[x + y * clipped_integral_size]); } std::cout << std::endl; } std::cout << std::endl; #endif // rescale clipped_integral_ by the size of each grid cell: of the area covered by the grid // (max_distance_ x max_distance_), the subarea comprised by one cell (1/dts_quadrant^2) of that // we do this as a post-pass mostly for debugging purposes, so that the steps above can be // verified to be working correctly in themselves before complicating matters by rescaling int64_t grid_count = clipped_integral_size * clipped_integral_size; double normalization = (1.0 / (dts_quadrant * dts_quadrant)) * (max_distance_ * max_distance_); for (int64_t index = 0; index < grid_count; ++index) clipped_integral_[index] *= normalization; #if 0 // debug output of clipped_integral_ std::cout << "clipped_integral_ (point 2) :" << std::endl; for (int64_t y = 0; y < clipped_integral_size; ++y) { for (int64_t x = 0; x < clipped_integral_size; ++x) { printf("%.6f ", clipped_integral_[x + y * clipped_integral_size]); } std::cout << std::endl; } std::cout << std::endl; #endif free(distance_to_strength); free(dts_cumsums); free(dts_colsums); clipped_integral_valid_ = true; // for 1024x1024 this takes ~0.5 seconds, so ouch, but it generally only needs to be done once // note that the step commented "Now we build clipped_integral_ itself" is where 90% of the time here is spent //end_time = static_cast(std::clock()) / CLOCKS_PER_SEC; //std::cout << "InteractionType::CacheClippedIntegral_2D() time == " << (end_time - start_time) << std::endl; } double InteractionType::ClippedIntegral_1D(double indDistanceA1, double indDistanceA2, bool periodic_x) { if (periodic_x) { indDistanceA1 = max_distance_; indDistanceA2 = max_distance_; } if ((indDistanceA1 < max_distance_) && (indDistanceA2 < max_distance_)) EIDOS_TERMINATION << "ERROR (InteractionType::ClippedIntegral_1D): clippedIntegral() requires that the maximum interaction distance be less than half of the spatial bounds extent, for non-periodic boundaries, such that the interaction function cannot be clipped on both sides." << EidosTerminate(); double indDistanceA = std::min(std::min(indDistanceA1, indDistanceA2), max_distance_) / max_distance_; if (indDistanceA < 0.0) EIDOS_TERMINATION << "ERROR (InteractionType::ClippedIntegral_1D): clippedIntegral() requires that receivers lie within the spatial bounds of their subpopulation." << EidosTerminate(); int coordA = (int)round(indDistanceA * (clipped_integral_size - 1)); //std::cout << "indDistanceA == " << indDistanceA << " : coordA == " << coordA << " : " << clipped_integral_[coordA] << std::endl; return clipped_integral_[coordA]; } double InteractionType::ClippedIntegral_2D(double indDistanceA1, double indDistanceA2, double indDistanceB1, double indDistanceB2, bool periodic_x, bool periodic_y) { if (periodic_x) { indDistanceA1 = max_distance_; indDistanceA2 = max_distance_; } if (periodic_y) { indDistanceB1 = max_distance_; indDistanceB2 = max_distance_; } if (((indDistanceA1 < max_distance_) && (indDistanceA2 < max_distance_)) || ((indDistanceB1 < max_distance_) && (indDistanceB2 < max_distance_))) EIDOS_TERMINATION << "ERROR (InteractionType::ClippedIntegral_2D): clippedIntegral() requires that the maximum interaction distance be less than half of the spatial bounds extent, for non-periodic boundaries, such that the interaction function cannot be clipped on both sides." << EidosTerminate(); double indDistanceA = std::min(std::min(indDistanceA1, indDistanceA2), max_distance_) / max_distance_; double indDistanceB = std::min(std::min(indDistanceB1, indDistanceB2), max_distance_) / max_distance_; if ((indDistanceA < 0.0) || (indDistanceB < 0.0)) EIDOS_TERMINATION << "ERROR (InteractionType::ClippedIntegral_2D): clippedIntegral() requires that receivers lie within the spatial bounds of their subpopulation." << EidosTerminate(); int coordA = (int)round(indDistanceA * (clipped_integral_size - 1)); int coordB = (int)round(indDistanceB * (clipped_integral_size - 1)); //std::cout << "indDistanceA == " << indDistanceA << ", indDistanceB == " << indDistanceB << " : coordA == " << coordA << ", coordB == " << coordB << " : " << clipped_integral_[coordA + coordB * clipped_integral_size] << std::endl; return clipped_integral_[coordA + coordB * clipped_integral_size]; } double InteractionType::ApplyInteractionCallbacks(Individual *p_receiver, Individual *p_exerter, double p_strength, double p_distance, std::vector &p_interaction_callbacks) { // This uses THREAD_SAFETY_IN_ACTIVE_PARALLEL() instead of THREAD_SAFETY_IN_ANY_PARALLEL() because it does get // called from inactive (i.e., 1-thread) parallel regions, so that we don't have to special-case code paths THREAD_SAFETY_IN_ACTIVE_PARALLEL("InteractionType::ApplyInteractionCallbacks(): running Eidos callback"); #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_START(); #endif SLiMEidosBlockType old_executing_block_type = community_.executing_block_type_; community_.executing_block_type_ = SLiMEidosBlockType::SLiMEidosInteractionCallback; for (SLiMEidosBlock *interaction_callback : p_interaction_callbacks) { if (interaction_callback->block_active_) { #ifndef DEBUG_POINTS_ENABLED #error "DEBUG_POINTS_ENABLED is not defined; include eidos_globals.h" #endif #if DEBUG_POINTS_ENABLED // SLiMgui debugging point EidosDebugPointIndent indenter; { EidosInterpreterDebugPointsSet *debug_points = community_.DebugPoints(); EidosToken *decl_token = interaction_callback->root_node_->token_; if (debug_points && debug_points->set.size() && (decl_token->token_line_ != -1) && (debug_points->set.find(decl_token->token_line_) != debug_points->set.end())) { SLIM_ERRSTREAM << EidosDebugPointIndent::Indent() << "#DEBUG interaction(i" << interaction_callback->interaction_type_id_; if (interaction_callback->subpopulation_id_ != -1) SLIM_ERRSTREAM << ", p" << interaction_callback->subpopulation_id_; SLIM_ERRSTREAM << ")"; if (interaction_callback->block_id_ != -1) SLIM_ERRSTREAM << " s" << interaction_callback->block_id_; SLIM_ERRSTREAM << " (line " << (decl_token->token_line_ + 1) << community_.DebugPointInfo() << ")" << std::endl; indenter.indent(); } } #endif // The callback is active and matches our interaction type id, so we need to execute it const EidosASTNode *compound_statement_node = interaction_callback->compound_statement_node_; if (compound_statement_node->cached_return_value_) { // The script is a constant expression such as "{ return 1.1; }", so we can short-circuit it completely EidosValue *result = compound_statement_node->cached_return_value_.get(); if ((result->Type() != EidosValueType::kValueFloat) || (result->Count() != 1)) EIDOS_TERMINATION << "ERROR (InteractionType::ApplyInteractionCallbacks): interaction() callbacks must provide a float singleton return value." << EidosTerminate(interaction_callback->identifier_token_); #if DEBUG // this checks the value type at runtime p_strength = result->FloatData()[0]; #else // unsafe cast for speed p_strength = ((EidosValue_Float *)result)->data()[0]; #endif // the cached value is owned by the tree, so we do not dispose of it // there is also no script output to handle } else { // local variables for the callback parameters that we might need to allocate here, and thus need to free below EidosValue_Float local_distance(p_distance); EidosValue_Float local_strength(p_strength); // We need to actually execute the script; we start a block here to manage the lifetime of the symbol table { EidosSymbolTable callback_symbols(EidosSymbolTableType::kContextConstantsTable, &community_.SymbolTable()); EidosSymbolTable client_symbols(EidosSymbolTableType::kLocalVariablesTable, &callback_symbols); EidosFunctionMap &function_map = community_.FunctionMap(); EidosInterpreter interpreter(interaction_callback->compound_statement_node_, client_symbols, function_map, &community_, SLIM_OUTSTREAM, SLIM_ERRSTREAM #ifdef SLIMGUI , community_.check_infinite_loops_ #endif ); if (interaction_callback->contains_self_) callback_symbols.InitializeConstantSymbolEntry(interaction_callback->SelfSymbolTableEntry()); // define "self" // Set all of the callback's parameters; note we use InitializeConstantSymbolEntry() for speed. // We can use that method because we know the lifetime of the symbol table is shorter than that of // the value objects, and we know that the values we are setting here will not change (the objects // referred to by the values may change, but the values themselves will not change). // BCH 11/7/2025: note these symbols are now protected in SLiM_ConfigureContext() if (interaction_callback->contains_distance_) { local_distance.StackAllocated(); // prevent Eidos_intrusive_ptr from trying to delete this callback_symbols.InitializeConstantSymbolEntry(gID_distance, EidosValue_SP(&local_distance)); } if (interaction_callback->contains_strength_) { local_strength.StackAllocated(); // prevent Eidos_intrusive_ptr from trying to delete this callback_symbols.InitializeConstantSymbolEntry(gID_strength, EidosValue_SP(&local_strength)); } if (interaction_callback->contains_receiver_) callback_symbols.InitializeConstantSymbolEntry(gID_receiver, p_receiver->CachedEidosValue()); if (interaction_callback->contains_exerter_) callback_symbols.InitializeConstantSymbolEntry(gID_exerter, p_exerter->CachedEidosValue()); try { // Interpret the script; the result from the interpretation must be a singleton double used as a new fitness value EidosValue_SP result_SP = interpreter.EvaluateInternalBlock(interaction_callback->script_); EidosValue *result = result_SP.get(); if ((result->Type() != EidosValueType::kValueFloat) || (result->Count() != 1)) EIDOS_TERMINATION << "ERROR (InteractionType::ApplyInteractionCallbacks): interaction() callbacks must provide a float singleton return value." << EidosTerminate(interaction_callback->identifier_token_); p_strength = result->FloatData()[0]; if (std::isnan(p_strength) || std::isinf(p_strength) || (p_strength < 0.0)) EIDOS_TERMINATION << "ERROR (InteractionType::ApplyInteractionCallbacks): interaction() callbacks must return a finite value >= 0.0." << EidosTerminate(interaction_callback->identifier_token_); } catch (...) { throw; } } } } } community_.executing_block_type_ = old_executing_block_type; #if (SLIMPROFILING == 1) // PROFILING SLIM_PROFILE_BLOCK_END(community_.profile_callback_totals_[(int)(SLiMEidosBlockType::SLiMEidosInteractionCallback)]); #endif return p_strength; } size_t InteractionType::MemoryUsageForKDTrees(void) { size_t usage = 0; // this may be an underestimate, since we overallocate in some cases (exerter constraints) for (auto &iter : data_) { const InteractionsData &data = iter.second; usage += sizeof(SLiM_kdNode) * data.kd_node_count_ALL_; usage += sizeof(SLiM_kdNode) * data.kd_node_count_EXERTERS_; } return usage; } size_t InteractionType::MemoryUsageForPositions(void) { size_t usage = 0; for (auto &iter : data_) { const InteractionsData &data = iter.second; usage += sizeof(double) * data.individual_count_; } return usage; } size_t InteractionType::MemoryUsageForSparseVectorPool(void) { THREAD_SAFETY_IN_ACTIVE_PARALLEL("InteractionType::MemoryUsageForSparseVectorPool(): s_freed_sparse_vectors_"); size_t usage = 0; #ifdef _OPENMP // When running multithreaded, count all pools for (auto &pool : s_freed_sparse_vectors_PERTHREAD) { usage += sizeof(std::vector); usage += pool.size() * sizeof(SparseVector); for (SparseVector *free_sv : pool) usage += free_sv->MemoryUsage(); } #else usage = s_freed_sparse_vectors_SINGLE.size() * sizeof(SparseVector); for (SparseVector *free_sv : s_freed_sparse_vectors_SINGLE) usage += free_sv->MemoryUsage(); #endif return usage; } #pragma mark - #pragma mark k-d tree construction #pragma mark - // This k-d tree code is patterned after the C code at RosettaCode.org : https://rosettacode.org/wiki/K-d_tree#C // It uses a Quickselect-style algorithm to select medians to produce a balanced tree // Each spatiality case is coded separately, for maximum speed, but they are very parallel // Some of the code below is separated by phase. The k-d tree cycles through phase (x, y, z) as you descend, // and rather than passing phase as a parameter, the code has been factored into phase-specific functions // that are mutually recursive, for speed. It's not a huge win, but it does help a little. // BCH 14 August 2017: NOTE that I have found that the RosettaCode.org C example code was incorrect, which // is disappointing. It tried to check for duplicates of the median and terminate early, but its logic for // doing so was flawed and resulted in a bad tree that produced incorrect results. This code now follows // the logic of the pseudocode at Wikipedia (https://en.wikipedia.org/wiki/Quickselect), which seems correct. // Ironically, the incorrect logic of the RosettaCode version only produced incorrect results when there // were duplicated values in the coordinate vector. inline __attribute__((always_inline)) void swap(SLiM_kdNode *p_x, SLiM_kdNode *p_y) noexcept { std::swap(p_x->x, p_y->x); std::swap(p_x->individual_index_, p_y->individual_index_); } // find median for phase 0 (x) SLiM_kdNode *InteractionType::FindMedian_p0(SLiM_kdNode *start, SLiM_kdNode *end) { // BCH 12/11/2022: This used to use Quickselect, but we encounterested issues with this hitting // its O(n^2) worst case. Now we use the STL std::nth_element(), which seems to do better. SLiM_kdNode *mid = start + (end - start) / 2; std::nth_element(start, mid, end, [](const SLiM_kdNode &i1, const SLiM_kdNode &i2) { return i1.x[0] < i2.x[0]; }); return mid; } // find median for phase 1 (y) SLiM_kdNode *InteractionType::FindMedian_p1(SLiM_kdNode *start, SLiM_kdNode *end) { // BCH 12/11/2022: This used to use Quickselect, but we encounterested issues with this hitting // its O(n^2) worst case. Now we use the STL std::nth_element(), which seems to do better. SLiM_kdNode *mid = start + (end - start) / 2; std::nth_element(start, mid, end, [](const SLiM_kdNode &i1, const SLiM_kdNode &i2) { return i1.x[1] < i2.x[1]; }); return mid; } // find median for phase 2 (z) SLiM_kdNode *InteractionType::FindMedian_p2(SLiM_kdNode *start, SLiM_kdNode *end) { // BCH 12/11/2022: This used to use Quickselect, but we encounterested issues with this hitting // its O(n^2) worst case. Now we use the STL std::nth_element(), which seems to do better. SLiM_kdNode *mid = start + (end - start) / 2; std::nth_element(start, mid, end, [](const SLiM_kdNode &i1, const SLiM_kdNode &i2) { return i1.x[2] < i2.x[2]; }); return mid; } // make k-d tree recursively for the 1D case for phase 0 (x) SLiM_kdNode *InteractionType::MakeKDTree1_p0(SLiM_kdNode *t, int len) { SLiM_kdNode *n = ((len == 1) ? t : FindMedian_p0(t, t + len)); if (n) { int left_len = (int)(n - t); n->left = (left_len ? MakeKDTree1_p0(t, left_len) : 0); int right_len = (int)(t + len - (n + 1)); n->right = (right_len ? MakeKDTree1_p0(n + 1, right_len) : 0); } return n; } // make k-d tree recursively for the 2D case for phase 0 (x) SLiM_kdNode *InteractionType::MakeKDTree2_p0(SLiM_kdNode *t, int len) { SLiM_kdNode *n = ((len == 1) ? t : FindMedian_p0(t, t + len)); if (n) { int left_len = (int)(n - t); n->left = (left_len ? MakeKDTree2_p1(t, left_len) : 0); int right_len = (int)(t + len - (n + 1)); n->right = (right_len ? MakeKDTree2_p1(n + 1, right_len) : 0); } return n; } // make k-d tree recursively for the 2D case for phase 1 (y) SLiM_kdNode *InteractionType::MakeKDTree2_p1(SLiM_kdNode *t, int len) { SLiM_kdNode *n = ((len == 1) ? t : FindMedian_p1(t, t + len)); if (n) { int left_len = (int)(n - t); n->left = (left_len ? MakeKDTree2_p0(t, left_len) : 0); int right_len = (int)(t + len - (n + 1)); n->right = (right_len ? MakeKDTree2_p0(n + 1, right_len) : 0); } return n; } // make k-d tree recursively for the 3D case for phase 0 (x) SLiM_kdNode *InteractionType::MakeKDTree3_p0(SLiM_kdNode *t, int len) { SLiM_kdNode *n = ((len == 1) ? t : FindMedian_p0(t, t + len)); if (n) { int left_len = (int)(n - t); n->left = (left_len ? MakeKDTree3_p1(t, left_len) : 0); int right_len = (int)(t + len - (n + 1)); n->right = (right_len ? MakeKDTree3_p1(n + 1, right_len) : 0); } return n; } // make k-d tree recursively for the 3D case for phase 1 (y) SLiM_kdNode *InteractionType::MakeKDTree3_p1(SLiM_kdNode *t, int len) { SLiM_kdNode *n = ((len == 1) ? t : FindMedian_p1(t, t + len)); if (n) { int left_len = (int)(n - t); n->left = (left_len ? MakeKDTree3_p2(t, left_len) : 0); int right_len = (int)(t + len - (n + 1)); n->right = (right_len ? MakeKDTree3_p2(n + 1, right_len) : 0); } return n; } // make k-d tree recursively for the 3D case for phase 2 (z) SLiM_kdNode *InteractionType::MakeKDTree3_p2(SLiM_kdNode *t, int len) { SLiM_kdNode *n = ((len == 1) ? t : FindMedian_p2(t, t + len)); if (n) { int left_len = (int)(n - t); n->left = (left_len ? MakeKDTree3_p0(t, left_len) : 0); int right_len = (int)(t + len - (n + 1)); n->right = (right_len ? MakeKDTree3_p0(n + 1, right_len) : 0); } return n; } void InteractionType::CacheKDTreeNodes(Subpopulation *subpop, InteractionsData &p_subpop_data, bool p_apply_exerter_constraints, SLiM_kdNode **kd_nodes_ptr, SLiM_kdNode **kd_root_ptr, slim_popsize_t *kd_node_count_ptr) { Individual **subpop_individuals = subpop->parent_individuals_.data(); int individual_count = p_subpop_data.individual_count_; int first_individual_index, last_individual_index; // Calculate modified indices into the population, based on exerter sex-specificity. This lets us skip over // individuals that are disqualified by the exerter sex-specificity constraints without even looking at them. if (p_apply_exerter_constraints && (exerter_constraints_.sex_ == IndividualSex::kMale)) { first_individual_index = p_subpop_data.first_male_index_; last_individual_index = individual_count - 1; } else if (p_apply_exerter_constraints && (exerter_constraints_.sex_ == IndividualSex::kFemale)) { first_individual_index = 0; last_individual_index = p_subpop_data.first_male_index_ - 1; } else { first_individual_index = 0; last_individual_index = individual_count - 1; } // Allocate the chosen number of nodes int max_node_count = last_individual_index - first_individual_index + 1; SLiM_kdNode *nodes = (SLiM_kdNode *)calloc(max_node_count, sizeof(SLiM_kdNode)); if (!nodes) EIDOS_TERMINATION << "ERROR (InteractionType::CacheKDTreeNodes): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); // Fill the nodes with their initial data; start assuming the non-periodic base case, split into spatiality cases for speed int actual_node_count = 0; switch (spatiality_) { case 1: if (p_apply_exerter_constraints && exerter_constraints_.has_nonsex_constraints_) { for (int i = first_individual_index; i <= last_individual_index; ++i) { Individual *ind = subpop_individuals[i]; if (CheckIndividualNonSexConstraints(ind, exerter_constraints_)) // potentially raises { SLiM_kdNode *node = nodes + actual_node_count; double *position_data = p_subpop_data.positions_ + (size_t)i * SLIM_MAX_DIMENSIONALITY; node->x[0] = position_data[0]; node->individual_index_ = i; actual_node_count++; } } } else { for (int i = first_individual_index; i <= last_individual_index; ++i) { SLiM_kdNode *node = nodes + actual_node_count; double *position_data = p_subpop_data.positions_ + (size_t)i * SLIM_MAX_DIMENSIONALITY; node->x[0] = position_data[0]; node->individual_index_ = i; actual_node_count++; } } break; case 2: if (p_apply_exerter_constraints && exerter_constraints_.has_nonsex_constraints_) { for (int i = first_individual_index; i <= last_individual_index; ++i) { Individual *ind = subpop_individuals[i]; if (CheckIndividualNonSexConstraints(ind, exerter_constraints_)) // potentially raises { SLiM_kdNode *node = nodes + actual_node_count; double *position_data = p_subpop_data.positions_ + (size_t)i * SLIM_MAX_DIMENSIONALITY; node->x[0] = position_data[0]; node->x[1] = position_data[1]; node->individual_index_ = i; actual_node_count++; } } } else { for (int i = first_individual_index; i <= last_individual_index; ++i) { SLiM_kdNode *node = nodes + actual_node_count; double *position_data = p_subpop_data.positions_ + (size_t)i * SLIM_MAX_DIMENSIONALITY; node->x[0] = position_data[0]; node->x[1] = position_data[1]; node->individual_index_ = i; actual_node_count++; } } break; case 3: if (p_apply_exerter_constraints && exerter_constraints_.has_nonsex_constraints_) { for (int i = first_individual_index; i <= last_individual_index; ++i) { Individual *ind = subpop_individuals[i]; if (CheckIndividualNonSexConstraints(ind, exerter_constraints_)) // potentially raises { SLiM_kdNode *node = nodes + actual_node_count; double *position_data = p_subpop_data.positions_ + (size_t)i * SLIM_MAX_DIMENSIONALITY; node->x[0] = position_data[0]; node->x[1] = position_data[1]; node->x[2] = position_data[2]; node->individual_index_ = i; actual_node_count++; } } } else { for (int i = first_individual_index; i <= last_individual_index; ++i) { SLiM_kdNode *node = nodes + actual_node_count; double *position_data = p_subpop_data.positions_ + (size_t)i * SLIM_MAX_DIMENSIONALITY; node->x[0] = position_data[0]; node->x[1] = position_data[1]; node->x[2] = position_data[2]; node->individual_index_ = i; actual_node_count++; } } break; default: EIDOS_TERMINATION << "ERROR (InteractionType::CacheKDTreeNodes): (internal error) spatiality_ out of range." << EidosTerminate(nullptr); } // Note that replication of nodes for the periodic case is done in BuildKDTree(), // to save work when the k-d tree is not actually used for exerters // Write out the final constructed k-d tree to our parameters *kd_nodes_ptr = nodes; *kd_root_ptr = nullptr; *kd_node_count_ptr = actual_node_count; } void InteractionType::BuildKDTree(InteractionsData &p_subpop_data, SLiM_kdNode **kd_nodes_ptr, SLiM_kdNode **kd_root_ptr, slim_popsize_t *kd_node_count_ptr) { // If we have any periodic dimensions, we need to replicate our nodes spatially // Note that exerter constraints have already been applied int periodicity_multiplier = (p_subpop_data.periodic_x_ ? 3 : 1) * (p_subpop_data.periodic_y_ ? 3 : 1) * (p_subpop_data.periodic_z_ ? 3 : 1); if (periodicity_multiplier > 1) { SLiM_kdNode *nodes = *kd_nodes_ptr; int actual_node_count = *kd_node_count_ptr; int max_node_count = actual_node_count * periodicity_multiplier; nodes = (SLiM_kdNode *)realloc(nodes, max_node_count * sizeof(SLiM_kdNode)); // NOLINT(*-realloc-usage) : realloc failure is a fatal error anyway if (!nodes) EIDOS_TERMINATION << "ERROR (InteractionType::BuildKDTree): allocation failed; you may need to raise the memory limit for SLiM." << EidosTerminate(nullptr); // We want periodicity_multiplier replicates; 3 or 9 or 27. The central replicate is the base replicate, which we have // already created at replicate index 0 in the nodes buffer. So we want to make the remaining replicates at the remaining // indices. Each replicate gets offsets from the base position; to make that work easily, we calculate a modified index // that places the base replicate at the center of the buffer (even though it is really at position 0). int replicate_index_of_center = periodicity_multiplier / 2; // rounds down to nearest integer; 3 -> 1, 9 -> 4, 27 -> 13 for (int replicate = 1; replicate < periodicity_multiplier; ++replicate) { int replicate_quadrant_index = (replicate <= replicate_index_of_center) ? (replicate - 1) : replicate; SLiM_kdNode *replicate_nodes = nodes + (size_t)replicate * actual_node_count; double x_offset = 0, y_offset = 0, z_offset = 0; // Determine the correct offsets for this replicate of the individual position data; // maybe there is a smarter way to do this, but whatever int replication_dim_1 = (replicate_quadrant_index % 3) - 1; int replication_dim_2 = ((replicate_quadrant_index / 3) % 3) - 1; int replication_dim_3 = (replicate_quadrant_index / 9) - 1; if (p_subpop_data.periodic_x_) { x_offset = p_subpop_data.bounds_x1_ * replication_dim_1; if (p_subpop_data.periodic_y_) { y_offset = p_subpop_data.bounds_y1_ * replication_dim_2; if (p_subpop_data.periodic_z_) z_offset = p_subpop_data.bounds_z1_ * replication_dim_3; } else if (p_subpop_data.periodic_z_) { z_offset = p_subpop_data.bounds_z1_ * replication_dim_2; } } else if (p_subpop_data.periodic_y_) { y_offset = p_subpop_data.bounds_y1_ * replication_dim_1; if (p_subpop_data.periodic_z_) z_offset = p_subpop_data.bounds_z1_ * replication_dim_2; } else if (p_subpop_data.periodic_z_) { z_offset = p_subpop_data.bounds_z1_ * replication_dim_1; } // Now that we have our offsets, copy the data for the replicate switch (spatiality_) { case 1: for (int i = 0; i < actual_node_count; ++i) { SLiM_kdNode *original_node = nodes + i; SLiM_kdNode *replicate_node = replicate_nodes + i; replicate_node->x[0] = original_node->x[0] + x_offset; replicate_node->individual_index_ = original_node->individual_index_; } break; case 2: for (int i = 0; i < actual_node_count; ++i) { SLiM_kdNode *original_node = nodes + i; SLiM_kdNode *replicate_node = replicate_nodes + i; replicate_node->x[0] = original_node->x[0] + x_offset; replicate_node->x[1] = original_node->x[1] + y_offset; replicate_node->individual_index_ = original_node->individual_index_; } break; case 3: for (int i = 0; i < actual_node_count; ++i) { SLiM_kdNode *original_node = nodes + i; SLiM_kdNode *replicate_node = replicate_nodes + i; replicate_node->x[0] = original_node->x[0] + x_offset; replicate_node->x[1] = original_node->x[1] + y_offset; replicate_node->x[2] = original_node->x[2] + z_offset; replicate_node->individual_index_ = original_node->individual_index_; } break; default: EIDOS_TERMINATION << "ERROR (InteractionType::BuildKDTree): (internal error) spatiality_ out of range." << EidosTerminate(nullptr); } } actual_node_count *= periodicity_multiplier; // Write out the final constructed k-d tree to our parameters *kd_nodes_ptr = nodes; *kd_node_count_ptr = actual_node_count; } if (*kd_node_count_ptr == 0) { // Usually a root pointer of nullptr indicates that the tree hasn't been built, but it is // also used if the tree contains no nodes and thus has no root. *kd_root_ptr = nullptr; } else { // Now call out to recursively construct the tree switch (spatiality_) { case 1: *kd_root_ptr = MakeKDTree1_p0(*kd_nodes_ptr, *kd_node_count_ptr); break; case 2: *kd_root_ptr = MakeKDTree2_p0(*kd_nodes_ptr, *kd_node_count_ptr); break; case 3: *kd_root_ptr = MakeKDTree3_p0(*kd_nodes_ptr, *kd_node_count_ptr); break; default: EIDOS_TERMINATION << "ERROR (InteractionType::BuildKDTree): (internal error) spatiality_ out of range." << EidosTerminate(nullptr); } // Check the tree for correctness; for now I will leave this enabled in the DEBUG case, // because a bug was found in the k-d tree code in 2.4.1 that would have been caught by this. // Eventually, when it is clear that this code is robust, this check can be disabled. #if DEBUG int total_tree_count = 0; switch (spatiality_) { case 1: total_tree_count = CheckKDTree1_p0(*kd_root_ptr); break; case 2: total_tree_count = CheckKDTree2_p0(*kd_root_ptr); break; case 3: total_tree_count = CheckKDTree3_p0(*kd_root_ptr); break; default: EIDOS_TERMINATION << "ERROR (InteractionType::BuildKDTree): (internal error) spatiality_ out of range." << EidosTerminate(nullptr); } if (total_tree_count != *kd_node_count_ptr) EIDOS_TERMINATION << "ERROR (InteractionType::BuildKDTree): (internal error) the k-d tree count " << total_tree_count << " does not match the allocated node count" << *kd_node_count_ptr << "." << EidosTerminate(); #endif } } SLiM_kdNode *InteractionType::EnsureKDTreePresent_ALL(Subpopulation *subpop, InteractionsData &p_subpop_data) { if (!p_subpop_data.evaluated_) EIDOS_TERMINATION << "ERROR (InteractionType::EnsureKDTreePresent_ALL): (internal error) the interaction has not been evaluated." << EidosTerminate(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::EnsureKDTreePresent_ALL): (internal error) a k-d tree cannot be constructed for non-spatial interactions." << EidosTerminate(); if (!p_subpop_data.kd_nodes_ALL_) CacheKDTreeNodes(subpop, p_subpop_data, /* p_apply_exerter_constraints */ false, &p_subpop_data.kd_nodes_ALL_, &p_subpop_data.kd_root_ALL_, &p_subpop_data.kd_node_count_ALL_); if (!p_subpop_data.kd_root_ALL_ && (p_subpop_data.kd_node_count_ALL_ > 0)) BuildKDTree(p_subpop_data, &p_subpop_data.kd_nodes_ALL_, &p_subpop_data.kd_root_ALL_, &p_subpop_data.kd_node_count_ALL_); return p_subpop_data.kd_root_ALL_; // note that this will return nullptr if the k-d tree has zero entries! } SLiM_kdNode *InteractionType::EnsureKDTreePresent_EXERTERS(Subpopulation *subpop, InteractionsData &p_subpop_data) { if (!p_subpop_data.evaluated_) EIDOS_TERMINATION << "ERROR (InteractionType::EnsureKDTreePresent_EXERTERS): (internal error) the interaction has not been evaluated." << EidosTerminate(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::EnsureKDTreePresent_EXERTERS): (internal error) a k-d tree cannot be constructed for non-spatial interactions." << EidosTerminate(); if (!p_subpop_data.kd_nodes_EXERTERS_) { // If there are no exerter constraints, then the ALL tree should be the same as the EXERTERS tree; there's no reason to make both. // So at this point, if there are no exerter constraints, we first force the ALL tree to be constructed, and then we just leech on to it. if (!exerter_constraints_.has_constraints_) { EnsureKDTreePresent_ALL(subpop, p_subpop_data); p_subpop_data.kd_nodes_EXERTERS_ = p_subpop_data.kd_nodes_ALL_; p_subpop_data.kd_root_EXERTERS_ = p_subpop_data.kd_root_ALL_; p_subpop_data.kd_node_count_EXERTERS_ = p_subpop_data.kd_node_count_ALL_; return p_subpop_data.kd_root_EXERTERS_; } else { // If our flag is set that there was a constraint precondition violation earlier, then we cannot build an exerters // tree, and instead need to show a user-visible error. See EvaluateSubpopulation() for discussion. if (p_subpop_data.kd_constraints_raise_EXERTERS_) EIDOS_TERMINATION << "ERROR (InteractionType::EnsureKDTreePresent_EXERTERS): a tag, tagL0, tagL1, tagL2, tagL3, or tagL4 constraint is set for exerters, but the corresponding property is undefined (has not been set) for a candidate exerter being queried." << EidosTerminate(); // If there are non-sex exerter constraints, the k-d tree will be cached at evaluate() time. This code path is // therefore only hit when there are no non-sex exerter constraints (but there is an exerter sex constraint). // Let's check that assertion, to make sure we don't have a logic error anywhere, since it is important for us // to cache the exerter k-d tree at evaluate() time if non-sex constraints are present. if (exerter_constraints_.has_nonsex_constraints_) EIDOS_TERMINATION << "ERROR (InteractionType::EnsureKDTreePresent_EXERTERS): (internal error) an internal error in the exerter k-d tree caching logic has occurred; please report this error." << EidosTerminate(); CacheKDTreeNodes(subpop, p_subpop_data, /* p_apply_exerter_constraints */ true, &p_subpop_data.kd_nodes_EXERTERS_, &p_subpop_data.kd_root_EXERTERS_, &p_subpop_data.kd_node_count_EXERTERS_); } } if (!p_subpop_data.kd_root_EXERTERS_ && (p_subpop_data.kd_node_count_EXERTERS_ > 0)) BuildKDTree(p_subpop_data, &p_subpop_data.kd_nodes_EXERTERS_, &p_subpop_data.kd_root_EXERTERS_, &p_subpop_data.kd_node_count_EXERTERS_); return p_subpop_data.kd_root_EXERTERS_; // note that this will return nullptr if the k-d tree has zero entries! } #pragma mark - #pragma mark k-d tree consistency checking #pragma mark - // The general strategy is: the _pX() functions check that they are indeed a median node for all of the // nodes underneath the given node, for the coordinate of the given polarity. They do this by calling // the pX_r() method on their left and right subtree, with their own coordinate; it recurses over the // subtrees. The pX() method then makes a call on each subtree to have it check itself. Each pX() // method call returns the total number of nodes found in itself and its subtrees. int InteractionType::CheckKDTree1_p0(SLiM_kdNode *t) { double split = t->x[0]; if (t->left) CheckKDTree1_p0_r(t->left, split, true); if (t->right) CheckKDTree1_p0_r(t->right, split, false); int left_count = t->left ? CheckKDTree1_p0(t->left) : 0; int right_count = t->right ? CheckKDTree1_p0(t->right) : 0; return left_count + right_count + 1; } void InteractionType::CheckKDTree1_p0_r(SLiM_kdNode *t, double split, bool isLeftSubtree) { double x = t->x[0]; if (isLeftSubtree) { if (x > split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree1_p0_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } else { if (x < split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree1_p0_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } if (t->left) CheckKDTree1_p0_r(t->left, split, isLeftSubtree); if (t->right) CheckKDTree1_p0_r(t->right, split, isLeftSubtree); } int InteractionType::CheckKDTree2_p0(SLiM_kdNode *t) { double split = t->x[0]; if (t->left) CheckKDTree2_p0_r(t->left, split, true); if (t->right) CheckKDTree2_p0_r(t->right, split, false); int left_count = t->left ? CheckKDTree2_p1(t->left) : 0; int right_count = t->right ? CheckKDTree2_p1(t->right) : 0; return left_count + right_count + 1; } void InteractionType::CheckKDTree2_p0_r(SLiM_kdNode *t, double split, bool isLeftSubtree) { double x = t->x[0]; if (isLeftSubtree) { if (x > split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree2_p0_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } else { if (x < split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree2_p0_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } if (t->left) CheckKDTree2_p0_r(t->left, split, isLeftSubtree); if (t->right) CheckKDTree2_p0_r(t->right, split, isLeftSubtree); } int InteractionType::CheckKDTree2_p1(SLiM_kdNode *t) { double split = t->x[1]; if (t->left) CheckKDTree2_p1_r(t->left, split, true); if (t->right) CheckKDTree2_p1_r(t->right, split, false); int left_count = t->left ? CheckKDTree2_p0(t->left) : 0; int right_count = t->right ? CheckKDTree2_p0(t->right) : 0; return left_count + right_count + 1; } void InteractionType::CheckKDTree2_p1_r(SLiM_kdNode *t, double split, bool isLeftSubtree) { double x = t->x[1]; if (isLeftSubtree) { if (x > split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree2_p1_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } else { if (x < split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree2_p1_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } if (t->left) CheckKDTree2_p1_r(t->left, split, isLeftSubtree); if (t->right) CheckKDTree2_p1_r(t->right, split, isLeftSubtree); } int InteractionType::CheckKDTree3_p0(SLiM_kdNode *t) { double split = t->x[0]; if (t->left) CheckKDTree3_p0_r(t->left, split, true); if (t->right) CheckKDTree3_p0_r(t->right, split, false); int left_count = t->left ? CheckKDTree3_p1(t->left) : 0; int right_count = t->right ? CheckKDTree3_p1(t->right) : 0; return left_count + right_count + 1; } void InteractionType::CheckKDTree3_p0_r(SLiM_kdNode *t, double split, bool isLeftSubtree) { double x = t->x[0]; if (isLeftSubtree) { if (x > split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree3_p0_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } else { if (x < split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree3_p0_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } if (t->left) CheckKDTree3_p0_r(t->left, split, isLeftSubtree); if (t->right) CheckKDTree3_p0_r(t->right, split, isLeftSubtree); } int InteractionType::CheckKDTree3_p1(SLiM_kdNode *t) { double split = t->x[1]; if (t->left) CheckKDTree3_p1_r(t->left, split, true); if (t->right) CheckKDTree3_p1_r(t->right, split, false); int left_count = t->left ? CheckKDTree3_p2(t->left) : 0; int right_count = t->right ? CheckKDTree3_p2(t->right) : 0; return left_count + right_count + 1; } void InteractionType::CheckKDTree3_p1_r(SLiM_kdNode *t, double split, bool isLeftSubtree) { double x = t->x[1]; if (isLeftSubtree) { if (x > split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree3_p1_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } else { if (x < split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree3_p1_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } if (t->left) CheckKDTree3_p1_r(t->left, split, isLeftSubtree); if (t->right) CheckKDTree3_p1_r(t->right, split, isLeftSubtree); } int InteractionType::CheckKDTree3_p2(SLiM_kdNode *t) { double split = t->x[2]; if (t->left) CheckKDTree3_p2_r(t->left, split, true); if (t->right) CheckKDTree3_p2_r(t->right, split, false); int left_count = t->left ? CheckKDTree3_p0(t->left) : 0; int right_count = t->right ? CheckKDTree3_p0(t->right) : 0; return left_count + right_count + 1; } void InteractionType::CheckKDTree3_p2_r(SLiM_kdNode *t, double split, bool isLeftSubtree) { double x = t->x[2]; if (isLeftSubtree) { if (x > split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree3_p2_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } else { if (x < split) EIDOS_TERMINATION << "ERROR (InteractionType::CheckKDTree3_p2_r): (internal error) the k-d tree is not correctly sorted." << EidosTerminate(); } if (t->left) CheckKDTree3_p2_r(t->left, split, isLeftSubtree); if (t->right) CheckKDTree3_p2_r(t->right, split, isLeftSubtree); } #pragma mark - #pragma mark sparse vector building #pragma mark - inline __attribute__((always_inline)) double dist_sq1(SLiM_kdNode *a, double *b) { #ifndef __clang_analyzer__ double t = a->x[0] - b[0]; return t * t; #else return 0.0; #endif } inline __attribute__((always_inline)) double dist_sq2(SLiM_kdNode *a, double *b) { #ifndef __clang_analyzer__ double t, d; t = a->x[0] - b[0]; d = t * t; t = a->x[1] - b[1]; d += t * t; return d; #else return 0.0; #endif } inline __attribute__((always_inline)) double dist_sq3(SLiM_kdNode *a, double *b) { #ifndef __clang_analyzer__ double t, d; t = a->x[0] - b[0]; d = t * t; t = a->x[1] - b[1]; d += t * t; t = a->x[2] - b[2]; d += t * t; return d; #else return 0.0; #endif } // add neighbors to the sparse vector in 1D void InteractionType::BuildSV_Presences_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector) { double d = dist_sq1(root, nd); #ifndef __clang_analyzer__ double dx = root->x[0] - nd[0]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_sparse_vector->AddEntryPresence(root->individual_index_); if (dx > 0) { if (root->left) BuildSV_Presences_1(root->left, nd, p_focal_individual_index, p_sparse_vector); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Presences_1(root->right, nd, p_focal_individual_index, p_sparse_vector); } else { if (root->right) BuildSV_Presences_1(root->right, nd, p_focal_individual_index, p_sparse_vector); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Presences_1(root->left, nd, p_focal_individual_index, p_sparse_vector); } } // add neighbors to the sparse vector in 2D void InteractionType::BuildSV_Presences_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_sparse_vector->AddEntryPresence(root->individual_index_); if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Presences_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Presences_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Presences_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Presences_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbors to the sparse vector in 3D void InteractionType::BuildSV_Presences_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq3(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_sparse_vector->AddEntryPresence(root->individual_index_); if (++p_phase >= 3) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Presences_3(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Presences_3(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Presences_3(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Presences_3(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbors to the sparse vector in 1D void InteractionType::BuildSV_Distances_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector) { double d = dist_sq1(root, nd); #ifndef __clang_analyzer__ double dx = root->x[0] - nd[0]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_sparse_vector->AddEntryDistance(root->individual_index_, (sv_value_t)sqrt(d)); if (dx > 0) { if (root->left) BuildSV_Distances_1(root->left, nd, p_focal_individual_index, p_sparse_vector); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Distances_1(root->right, nd, p_focal_individual_index, p_sparse_vector); } else { if (root->right) BuildSV_Distances_1(root->right, nd, p_focal_individual_index, p_sparse_vector); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Distances_1(root->left, nd, p_focal_individual_index, p_sparse_vector); } } // add neighbors to the sparse vector in 2D void InteractionType::BuildSV_Distances_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_sparse_vector->AddEntryDistance(root->individual_index_, (sv_value_t)sqrt(d)); if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Distances_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Distances_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Distances_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Distances_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbors to the sparse vector in 3D void InteractionType::BuildSV_Distances_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq3(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_sparse_vector->AddEntryDistance(root->individual_index_, (sv_value_t)sqrt(d)); if (++p_phase >= 3) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Distances_3(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Distances_3(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Distances_3(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Distances_3(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbor strengths of type "f" (SpatialKernelType::kFixed : fixed) to the sparse vector in 2D void InteractionType::BuildSV_Strengths_f_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) { //d = sqrt(d); p_sparse_vector->AddEntryStrength(root->individual_index_, (sv_value_t)if_param1_); } if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Strengths_f_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Strengths_f_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Strengths_f_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Strengths_f_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbor strengths of type "l" (SpatialKernelType::kLinear : linear) to the sparse vector in 2D void InteractionType::BuildSV_Strengths_l_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) { d = sqrt(d); p_sparse_vector->AddEntryStrength(root->individual_index_, (sv_value_t)(if_param1_ * (1.0 - d / max_distance_))); } if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Strengths_l_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Strengths_l_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Strengths_l_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Strengths_l_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbor strengths of type "e" (SpatialKernelType::kExponential : exponential) to the sparse vector in 2D void InteractionType::BuildSV_Strengths_e_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) { d = sqrt(d); p_sparse_vector->AddEntryStrength(root->individual_index_, (sv_value_t)(if_param1_ * exp(-if_param2_ * d))); } if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Strengths_e_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Strengths_e_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Strengths_e_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Strengths_e_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbor strengths of type "n" (SpatialKernelType::kNormal : normal/Gaussian) to the sparse vector in 2D void InteractionType::BuildSV_Strengths_n_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) { //d = sqrt(d); p_sparse_vector->AddEntryStrength(root->individual_index_, (sv_value_t)(if_param1_ * exp(-d / n_2param2sq_))); } if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Strengths_n_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Strengths_n_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Strengths_n_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Strengths_n_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbor strengths of type "c" (SpatialKernelType::kCauchy : Cauchy) to the sparse vector in 2D void InteractionType::BuildSV_Strengths_c_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) { double temp = sqrt(d) / if_param2_; p_sparse_vector->AddEntryStrength(root->individual_index_, (sv_value_t)(if_param1_ / (1.0 + temp * temp))); } if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Strengths_c_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Strengths_c_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Strengths_c_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Strengths_c_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } // add neighbor strengths of type "t" (SpatialKernelType::kStudentsT : Student's t) to the sparse vector in 2D void InteractionType::BuildSV_Strengths_t_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) { d = sqrt(d); p_sparse_vector->AddEntryStrength(root->individual_index_, (sv_value_t)SpatialKernel::tdist(d, if_param1_, if_param2_, if_param3_)); } if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) BuildSV_Strengths_t_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) BuildSV_Strengths_t_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); } else { if (root->right) BuildSV_Strengths_t_2(root->right, nd, p_focal_individual_index, p_sparse_vector, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) BuildSV_Strengths_t_2(root->left, nd, p_focal_individual_index, p_sparse_vector, p_phase); } } bool InteractionType::_CheckIndividualNonSexConstraints(Individual *p_individual, InteractionConstraints &p_constraints) { // we do not check p_constraints.has_nonsex_constraints_; this should only be called when a constraint exists // BEWARE: this checks for tag/tagL values being defined, as needed, and raises if they aren't if (p_constraints.tag_ != SLIM_TAG_UNSET_VALUE) { slim_usertag_t tag_value = p_individual->tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (InteractionType::_CheckIndividualNonSexConstraints): a tag constraint is set for the interaction type, but the tag property is undefined (has not been set) for an individual being queried." << EidosTerminate(); if (p_constraints.tag_ != tag_value) return false; } if ((p_constraints.min_age_ != -1) && (p_constraints.min_age_ > p_individual->age_)) return false; if ((p_constraints.max_age_ != -1) && (p_constraints.max_age_ < p_individual->age_)) return false; if ((p_constraints.migrant_ != -1) && (p_constraints.migrant_ != p_individual->migrant_)) return false; if (p_constraints.has_tagL_constraints_) { if (p_constraints.tagL0_ != -1) { if (!p_individual->tagL0_set_) EIDOS_TERMINATION << "ERROR (InteractionType::_CheckIndividualNonSexConstraints): a tagL0 constraint is set for the interaction type, but the tagL0 property is undefined (has not been set) for an individual being queried." << EidosTerminate(); if (p_constraints.tagL0_ != p_individual->tagL0_value_) return false; } if (p_constraints.tagL1_ != -1) { if (!p_individual->tagL1_set_) EIDOS_TERMINATION << "ERROR (InteractionType::_CheckIndividualNonSexConstraints): a tagL1 constraint is set for the interaction type, but the tagL1 property is undefined (has not been set) for an individual being queried." << EidosTerminate(); if (p_constraints.tagL1_ != p_individual->tagL1_value_) return false; } if (p_constraints.tagL2_ != -1) { if (!p_individual->tagL2_set_) EIDOS_TERMINATION << "ERROR (InteractionType::_CheckIndividualNonSexConstraints): a tagL2 constraint is set for the interaction type, but the tagL2 property is undefined (has not been set) for an individual being queried." << EidosTerminate(); if (p_constraints.tagL2_ != p_individual->tagL2_value_) return false; } if (p_constraints.tagL3_ != -1) { if (!p_individual->tagL3_set_) EIDOS_TERMINATION << "ERROR (InteractionType::_CheckIndividualNonSexConstraints): a tagL3 constraint is set for the interaction type, but the tagL3 property is undefined (has not been set) for an individual being queried." << EidosTerminate(); if (p_constraints.tagL3_ != p_individual->tagL3_value_) return false; } if (p_constraints.tagL4_ != -1) { if (!p_individual->tagL4_set_) EIDOS_TERMINATION << "ERROR (InteractionType::_CheckIndividualNonSexConstraints): a tagL4 constraint is set for the interaction type, but the tagL4 property is undefined (has not been set) for an individual being queried." << EidosTerminate(); if (p_constraints.tagL4_ != p_individual->tagL4_value_) return false; } } return true; } bool InteractionType::_PrecheckIndividualNonSexConstraints(Individual *p_individual, InteractionConstraints &p_constraints) { // This is similar to _CheckIndividualNonSexConstraints(), but it does not actually check the constraints. // Instead, it checks that the constraints *can* be checked, without raising. If a tag/tagL value that is // needed to do the constraint check is missing, this method returns false; otherwise it returns true, // meaning "it is safe to check constraints". See EvaluateSubpopulation() for discussion. if ((p_constraints.tag_ != SLIM_TAG_UNSET_VALUE) && (p_individual->tag_value_ == SLIM_TAG_UNSET_VALUE)) return false; if (p_constraints.has_tagL_constraints_) { if ((p_constraints.tagL0_ != -1) && !p_individual->tagL0_set_) return false; if ((p_constraints.tagL1_ != -1) && !p_individual->tagL1_set_) return false; if ((p_constraints.tagL2_ != -1) && !p_individual->tagL2_set_) return false; if ((p_constraints.tagL3_ != -1) && !p_individual->tagL3_set_) return false; if ((p_constraints.tagL4_ != -1) && !p_individual->tagL4_set_) return false; } return true; } void InteractionType::FillSparseVectorForReceiverPresences(SparseVector *sv, Individual *receiver, double *receiver_position, Subpopulation *exerter_subpop, SLiM_kdNode *kd_root, __attribute__((__unused__)) bool constraints_active) { #if DEBUG // The caller should guarantee that the receiver and exerter species are compatible with the interaction if (constraints_active) { CheckSpeciesCompatibility_Receiver(receiver->subpopulation_->species_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); } else { CheckSpeciesCompatibility_Generic(receiver->subpopulation_->species_); CheckSpeciesCompatibility_Generic(exerter_subpop->species_); } // SparseVector relies on the k-d tree, so this is an error for now if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverPresences): (internal error) request for k-d tree information from a non-spatial interaction." << EidosTerminate(); // The caller should guarantee that the receiver and exerter subpops are compatible in spatial structure CheckSpatialCompatibility(receiver->subpopulation_, exerter_subpop); // The caller should ensure that this method is never called for a receiver that cannot receive interactions if (constraints_active && !CheckIndividualConstraints(receiver, receiver_constraints_)) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverPresences): (internal error) the receiver is disqualified by the current receiver constraints." << EidosTerminate(); // The caller should be handing us a sparse vector set up for distance data if (sv->DataType() != SparseVectorDataType::kPresences) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverPresences): (internal error) the sparse vector is not configured for presences." << EidosTerminate(); // The caller should guarantee that the receiver is not a new juvenile, because they need to have a saved position if (receiver->index_ < 0) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverPresences): (internal error) the receiver is a new juvenile." << EidosTerminate(); #endif // if the root is nullptr, the tree is empty and we have no results if (kd_root) { // Figure out what index in the exerter subpopulation, if any, needs to be excluded so self-interaction is zero slim_popsize_t excluded_index = (exerter_subpop == receiver->subpopulation_) ? receiver->index_ : -1; // Without a specified exerter sex, we can add each exerter with no sex test if (spatiality_ == 2) BuildSV_Presences_2(kd_root, receiver_position, excluded_index, sv, 0); else if (spatiality_ == 1) BuildSV_Presences_1(kd_root, receiver_position, excluded_index, sv); else if (spatiality_ == 3) BuildSV_Presences_3(kd_root, receiver_position, excluded_index, sv, 0); } // After building the sparse vector above, we mark it finished sv->Finished(); } void InteractionType::FillSparseVectorForReceiverDistances(SparseVector *sv, Individual *receiver, double *receiver_position, Subpopulation *exerter_subpop, SLiM_kdNode *kd_root, __attribute__((__unused__)) bool constraints_active) { #if DEBUG // The caller should guarantee that the receiver and exerter species are compatible with the interaction if (constraints_active) { CheckSpeciesCompatibility_Receiver(receiver->subpopulation_->species_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); } else { CheckSpeciesCompatibility_Generic(receiver->subpopulation_->species_); CheckSpeciesCompatibility_Generic(exerter_subpop->species_); } // Non-spatial interactions do not have a concept of distance, so this is an error if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverDistances): (internal error) request for distances from a non-spatial interaction." << EidosTerminate(); // The caller should guarantee that the receiver and exerter subpops are compatible in spatial structure CheckSpatialCompatibility(receiver->subpopulation_, exerter_subpop); // The caller should ensure that this method is never called for a receiver that cannot receive interactions if (constraints_active && !CheckIndividualConstraints(receiver, receiver_constraints_)) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverDistances): (internal error) the receiver is disqualified by the current receiver constraints." << EidosTerminate(); // The caller should be handing us a sparse vector set up for distance data if (sv->DataType() != SparseVectorDataType::kDistances) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverDistances): (internal error) the sparse vector is not configured for distances." << EidosTerminate(); // The caller should guarantee that the receiver is not a new juvenile, because they need to have a saved position if (receiver->index_ < 0) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverDistances): (internal error) the receiver is a new juvenile." << EidosTerminate(); #endif // if the root is nullptr, the tree is empty and we have no results if (kd_root) { // Figure out what index in the exerter subpopulation, if any, needs to be excluded so self-interaction is zero slim_popsize_t excluded_index = (exerter_subpop == receiver->subpopulation_) ? receiver->index_ : -1; if (spatiality_ == 2) BuildSV_Distances_2(kd_root, receiver_position, excluded_index, sv, 0); else if (spatiality_ == 1) BuildSV_Distances_1(kd_root, receiver_position, excluded_index, sv); else if (spatiality_ == 3) BuildSV_Distances_3(kd_root, receiver_position, excluded_index, sv, 0); } // After building the sparse vector above, we mark it finished sv->Finished(); } void InteractionType::FillSparseVectorForPointDistances(SparseVector *sv, double *position, __attribute__((__unused__)) Subpopulation *exerter_subpop, SLiM_kdNode *kd_root) { // This is a special version of FillSparseVectorForReceiverDistances() used for nearestNeighborsOfPoint(). // It searches for neighbors of a point, without using a receiver, just a point. #if DEBUG // The caller should guarantee that the exerter species is compatible with the interaction CheckSpeciesCompatibility_Generic(exerter_subpop->species_); // Non-spatial interactions do not have a concept of distance, so this is an error if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForPointDistances): (internal error) request for distances from a non-spatial interaction." << EidosTerminate(); // The caller should be handing us a sparse vector set up for distance data if (sv->DataType() != SparseVectorDataType::kDistances) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForPointDistances): (internal error) the sparse vector is not configured for distances." << EidosTerminate(); #endif // if the root is nullptr, the tree is empty and we have no results if (kd_root) { if (spatiality_ == 2) BuildSV_Distances_2(kd_root, position, -1, sv, 0); else if (spatiality_ == 1) BuildSV_Distances_1(kd_root, position, -1, sv); else if (spatiality_ == 3) BuildSV_Distances_3(kd_root, position, -1, sv, 0); } // After building the sparse vector above, we mark it finished sv->Finished(); } void InteractionType::FillSparseVectorForReceiverStrengths(SparseVector *sv, Individual *receiver, double *receiver_position, Subpopulation *exerter_subpop, SLiM_kdNode *kd_root, std::vector &interaction_callbacks) { #if DEBUG // The caller should guarantee that the receiver and exerter species are compatible with the interaction CheckSpeciesCompatibility_Receiver(receiver->subpopulation_->species_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); // Non-spatial interactions are not handled by this method (they must be handled separately by logic in the caller), so this is an error if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverStrengths): (internal error) request for strengths from a non-spatial interaction." << EidosTerminate(); // The caller should guarantee that the receiver and exerter subpops are compatible in spatial structure CheckSpatialCompatibility(receiver->subpopulation_, exerter_subpop); // The caller should ensure that this method is never called for a receiver that cannot receive interactions if (!CheckIndividualConstraints(receiver, receiver_constraints_)) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverStrengths): (internal error) the receiver is disqualified by the current receiver constraints." << EidosTerminate(); // The caller should be handing us a sparse vector set up for strength data if (sv->DataType() != SparseVectorDataType::kStrengths) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverStrengths): (internal error) the sparse vector is not configured for strengths." << EidosTerminate(); // The caller should guarantee that the receiver is not a new juvenile, because they need to have a saved position if (receiver->index_ < 0) EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverStrengths): (internal error) the receiver is a new juvenile." << EidosTerminate(); #endif // if the root is nullptr, the tree is empty and we have no results if (kd_root) { // Figure out what index in the exerter subpopulation, if any, needs to be excluded so self-interaction is zero slim_popsize_t excluded_index = (exerter_subpop == receiver->subpopulation_) ? receiver->index_ : -1; // We special-case Fixed kernel builds directly to strength values here, for efficiency, // with no callbacks and spatiality "xy". Other kernels use the two-pass path below // which enables SIMD optimizations for Exponential and Normal kernels. // ADK 12/16/2025: changed to only use special-case path for Fixed kernel if ((interaction_callbacks.size() == 0) && (spatiality_ == 2) && (if_type_ == SpatialKernelType::kFixed)) { sv->SetDataType(SparseVectorDataType::kStrengths); BuildSV_Strengths_f_2(kd_root, receiver_position, excluded_index, sv, 0); sv->Finished(); return; } // ADK 12/16/2025: The original switch below handled all kernel types in the special-case // single-pass path. Now only Fixed uses the special path above; other kernels fall // through to the two-pass distance-then-transform path, enabling SIMD optimizations. #if 0 if ((interaction_callbacks.size() == 0) && (spatiality_ == 2)) { sv->SetDataType(SparseVectorDataType::kStrengths); switch (if_type_) { case SpatialKernelType::kFixed: BuildSV_Strengths_f_2(kd_root, receiver_position, excluded_index, sv, 0); break; case SpatialKernelType::kLinear: BuildSV_Strengths_l_2(kd_root, receiver_position, excluded_index, sv, 0); break; case SpatialKernelType::kExponential: BuildSV_Strengths_e_2(kd_root, receiver_position, excluded_index, sv, 0); break; case SpatialKernelType::kNormal: BuildSV_Strengths_n_2(kd_root, receiver_position, excluded_index, sv, 0); break; case SpatialKernelType::kCauchy: BuildSV_Strengths_c_2(kd_root, receiver_position, excluded_index, sv, 0); break; case SpatialKernelType::kStudentsT: BuildSV_Strengths_t_2(kd_root, receiver_position, excluded_index, sv, 0); break; default: EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverStrengths): (internal error) unoptimized SpatialKernelType value." << EidosTerminate(); } sv->Finished(); return; } #endif // Set up to build distances first; this is an internal implementation detail, so we require the sparse vector set up for strengths above sv->SetDataType(SparseVectorDataType::kDistances); if (spatiality_ == 2) BuildSV_Distances_2(kd_root, receiver_position, excluded_index, sv, 0); else if (spatiality_ == 1) BuildSV_Distances_1(kd_root, receiver_position, excluded_index, sv); else if (spatiality_ == 3) BuildSV_Distances_3(kd_root, receiver_position, excluded_index, sv, 0); } // After building the sparse vector above, we mark it finished sv->Finished(); // Now we scan through the pre-existing sparse vector for interacting pairs, // and transform distances into interaction strength values calculated for each. uint32_t nnz, *columns; sv_value_t *values; sv->Distances(&nnz, &columns, &values); if (interaction_callbacks.size() == 0) { // No callbacks; strength calculations come from the interaction function only // CalculateStrengthNoCallbacks() is basically inlined here, moved outside the loop; see that function for comments switch (if_type_) { case SpatialKernelType::kFixed: { for (uint32_t col_iter = 0; col_iter < nnz; ++col_iter) values[col_iter] = (sv_value_t)if_param1_; break; } case SpatialKernelType::kLinear: { // Use SIMD-optimized kernel: fmax = if_param1_, max_distance = max_distance_ Eidos_SIMD::linear_kernel_float32(values, nnz, (float)if_param1_, (float)max_distance_); break; } case SpatialKernelType::kExponential: { // SIMD-accelerated exponential kernel: strength = fmax * exp(-lambda * distance) Eidos_SIMD::exp_kernel_float32(values, nnz, (float)if_param1_, (float)if_param2_); break; } case SpatialKernelType::kNormal: { // SIMD-accelerated Gaussian kernel: strength = fmax * exp(-d^2 / 2sigma^2) Eidos_SIMD::normal_kernel_float32(values, nnz, (float)if_param1_, (float)n_2param2sq_); break; } case SpatialKernelType::kCauchy: { // Use SIMD-optimized kernel: fmax = if_param1_, lambda = if_param2_ Eidos_SIMD::cauchy_kernel_float32(values, nnz, (float)if_param1_, (float)if_param2_); break; } case SpatialKernelType::kStudentsT: { // Use SIMD-optimized kernel: fmax = if_param1_, nu = if_param2_, tau = if_param3_ Eidos_SIMD::tdist_kernel_float32(values, nnz, (float)if_param1_, (float)if_param2_, (float)if_param3_); break; } default: { // should never be hit, but this is the base case for (uint32_t col_iter = 0; col_iter < nnz; ++col_iter) { sv_value_t distance = values[col_iter]; values[col_iter] = (sv_value_t)CalculateStrengthNoCallbacks(distance); } EIDOS_TERMINATION << "ERROR (InteractionType::FillSparseVectorForReceiverStrengths): (internal error) unimplemented SpatialKernelType case." << EidosTerminate(); } } } else { // Callbacks; strength calculations need to include callback effects // BEWARE: With callbacks, this method can raise arbitrarily, so the caller needs // to be prepared for that, particularly if they are running parallel! Individual **subpop_individuals = exerter_subpop->parent_individuals_.data(); for (uint32_t col_iter = 0; col_iter < nnz; ++col_iter) { uint32_t col = columns[col_iter]; sv_value_t distance = values[col_iter]; values[col_iter] = (sv_value_t)CalculateStrengthWithCallbacks(distance, receiver, subpop_individuals[col], interaction_callbacks); } } // We have transformed distances into strengths in the sparse vector's values_ buffer sv->SetDataType(SparseVectorDataType::kStrengths); } #pragma mark - #pragma mark k-d tree neighbor searches #pragma mark - // count neighbors in 1D int InteractionType::CountNeighbors_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index) { int neighborCount = 0; double d = dist_sq1(root, nd); #ifndef __clang_analyzer__ double dx = root->x[0] - nd[0]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) neighborCount++; if (dx > 0) { if (root->left) neighborCount += CountNeighbors_1(root->left, nd, p_focal_individual_index); if (dx2 > max_distance_sq_) return neighborCount; if (root->right) neighborCount += CountNeighbors_1(root->right, nd, p_focal_individual_index); } else { if (root->right) neighborCount += CountNeighbors_1(root->right, nd, p_focal_individual_index); if (dx2 > max_distance_sq_) return neighborCount; if (root->left) neighborCount += CountNeighbors_1(root->left, nd, p_focal_individual_index); } return neighborCount; } // count neighbors in 2D int InteractionType::CountNeighbors_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, int p_phase) { int neighborCount = 0; double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) neighborCount++; if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) neighborCount += CountNeighbors_2(root->left, nd, p_focal_individual_index, p_phase); if (dx2 > max_distance_sq_) return neighborCount; if (root->right) neighborCount += CountNeighbors_2(root->right, nd, p_focal_individual_index, p_phase); } else { if (root->right) neighborCount += CountNeighbors_2(root->right, nd, p_focal_individual_index, p_phase); if (dx2 > max_distance_sq_) return neighborCount; if (root->left) neighborCount += CountNeighbors_2(root->left, nd, p_focal_individual_index, p_phase); } return neighborCount; } // count neighbors in 3D int InteractionType::CountNeighbors_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, int p_phase) { int neighborCount = 0; double d = dist_sq3(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) neighborCount++; if (++p_phase >= 3) p_phase = 0; if (dx > 0) { if (root->left) neighborCount += CountNeighbors_3(root->left, nd, p_focal_individual_index, p_phase); if (dx2 > max_distance_sq_) return neighborCount; if (root->right) neighborCount += CountNeighbors_3(root->right, nd, p_focal_individual_index, p_phase); } else { if (root->right) neighborCount += CountNeighbors_3(root->right, nd, p_focal_individual_index, p_phase); if (dx2 > max_distance_sq_) return neighborCount; if (root->left) neighborCount += CountNeighbors_3(root->left, nd, p_focal_individual_index, p_phase); } return neighborCount; } // find the one best neighbor in 1D void InteractionType::FindNeighbors1_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SLiM_kdNode **best, double *best_dist) { double d = dist_sq1(root, nd); #ifndef __clang_analyzer__ double dx = root->x[0] - nd[0]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((!*best || d < *best_dist) && (root->individual_index_ != p_focal_individual_index)) { *best_dist = d; *best = root; } if (dx > 0) { if (root->left) FindNeighbors1_1(root->left, nd, p_focal_individual_index, best, best_dist); if (dx2 >= *best_dist) return; if (root->right) FindNeighbors1_1(root->right, nd, p_focal_individual_index, best, best_dist); } else { if (root->right) FindNeighbors1_1(root->right, nd, p_focal_individual_index, best, best_dist); if (dx2 >= *best_dist) return; if (root->left) FindNeighbors1_1(root->left, nd, p_focal_individual_index, best, best_dist); } } // find the one best neighbor in 2D void InteractionType::FindNeighbors1_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SLiM_kdNode **best, double *best_dist, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((!*best || d < *best_dist) && (root->individual_index_ != p_focal_individual_index)) { *best_dist = d; *best = root; } if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) FindNeighbors1_2(root->left, nd, p_focal_individual_index, best, best_dist, p_phase); if (dx2 >= *best_dist) return; if (root->right) FindNeighbors1_2(root->right, nd, p_focal_individual_index, best, best_dist, p_phase); } else { if (root->right) FindNeighbors1_2(root->right, nd, p_focal_individual_index, best, best_dist, p_phase); if (dx2 >= *best_dist) return; if (root->left) FindNeighbors1_2(root->left, nd, p_focal_individual_index, best, best_dist, p_phase); } } // find the one best neighbor in 3D void InteractionType::FindNeighbors1_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SLiM_kdNode **best, double *best_dist, int p_phase) { double d = dist_sq3(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((!*best || d < *best_dist) && (root->individual_index_ != p_focal_individual_index)) { *best_dist = d; *best = root; } if (++p_phase >= 3) p_phase = 0; if (dx > 0) { if (root->left) FindNeighbors1_3(root->left, nd, p_focal_individual_index, best, best_dist, p_phase); if (dx2 >= *best_dist) return; if (root->right) FindNeighbors1_3(root->right, nd, p_focal_individual_index, best, best_dist, p_phase); } else { if (root->right) FindNeighbors1_3(root->right, nd, p_focal_individual_index, best, best_dist, p_phase); if (dx2 >= *best_dist) return; if (root->left) FindNeighbors1_3(root->left, nd, p_focal_individual_index, best, best_dist, p_phase); } } // find all neighbors in 1D void InteractionType::FindNeighborsA_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, EidosValue_Object &p_result_vec, std::vector &p_individuals) { double d = dist_sq1(root, nd); #ifndef __clang_analyzer__ double dx = root->x[0] - nd[0]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_result_vec.push_object_element_capcheck_NORR(p_individuals[root->individual_index_]); if (dx > 0) { if (root->left) FindNeighborsA_1(root->left, nd, p_focal_individual_index, p_result_vec, p_individuals); if (dx2 > max_distance_sq_) return; if (root->right) FindNeighborsA_1(root->right, nd, p_focal_individual_index, p_result_vec, p_individuals); } else { if (root->right) FindNeighborsA_1(root->right, nd, p_focal_individual_index, p_result_vec, p_individuals); if (dx2 > max_distance_sq_) return; if (root->left) FindNeighborsA_1(root->left, nd, p_focal_individual_index, p_result_vec, p_individuals); } } // find all neighbors in 2D void InteractionType::FindNeighborsA_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, EidosValue_Object &p_result_vec, std::vector &p_individuals, int p_phase) { double d = dist_sq2(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_result_vec.push_object_element_capcheck_NORR(p_individuals[root->individual_index_]); if (++p_phase >= 2) p_phase = 0; if (dx > 0) { if (root->left) FindNeighborsA_2(root->left, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) FindNeighborsA_2(root->right, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); } else { if (root->right) FindNeighborsA_2(root->right, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) FindNeighborsA_2(root->left, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); } } // find all neighbors in 3D void InteractionType::FindNeighborsA_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, EidosValue_Object &p_result_vec, std::vector &p_individuals, int p_phase) { double d = dist_sq3(root, nd); #ifndef __clang_analyzer__ double dx = root->x[p_phase] - nd[p_phase]; #else double dx = 0.0; #endif double dx2 = dx * dx; if ((d <= max_distance_sq_) && (root->individual_index_ != p_focal_individual_index)) p_result_vec.push_object_element_capcheck_NORR(p_individuals[root->individual_index_]); if (++p_phase >= 3) p_phase = 0; if (dx > 0) { if (root->left) FindNeighborsA_3(root->left, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); if (dx2 > max_distance_sq_) return; if (root->right) FindNeighborsA_3(root->right, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); } else { if (root->right) FindNeighborsA_3(root->right, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); if (dx2 > max_distance_sq_) return; if (root->left) FindNeighborsA_3(root->left, nd, p_focal_individual_index, p_result_vec, p_individuals, p_phase); } } // BCH 5/24/2023: Here used to reside FindNeighborsN_1(), FindNeighborsN_2(), and FindNeighborsN_3(), // used for finding a particular number of neighbors (N), greater than 1 and less than all, in 1D / 2D / 3D. // They were not thread-safe, and were replaced by FillSparseVectorForReceiverDistances_ALL_NEIGHBORS(); // now (11/2/2023) that has turned into FillSparseVectorForReceiverDistances() using kd_root_ALL_, below. void InteractionType::FindNeighbors(Subpopulation *p_subpop, SLiM_kdNode *kd_root, slim_popsize_t kd_node_count, double *p_point, int p_count, EidosValue_Object &p_result_vec, Individual *p_excluded_individual, bool constraints_active) { // If this method is passed kd_root_ALL_, from EnsureKDTreePresent_ALL(), it finds all neighbors, regardless // of exerter constraints. If it is passed kd_root_EXERTERS_, from EnsureKDTreePresent_EXERTERS(), it finds // only neighbors that satisfy the exerter constraints. if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::FindNeighbors): (internal error) neighbors cannot be found for non-spatial interactions." << EidosTerminate(); // If zero neighbors are requested, or if the k-d tree root is nullptr (no nodes), return an empty result // BCH 11/2/2023: returning an empty result for !kd_root is a change in behavior; we used to throw an exception. if (!kd_root || (p_count == 0)) return; // Exclude the focal individual if and only if it is in the exerter subpopulation slim_popsize_t focal_individual_index; if (p_excluded_individual && (p_excluded_individual->subpopulation_ == p_subpop)) focal_individual_index = p_excluded_individual->index_; else focal_individual_index = -1; if (p_count == 1) { // Finding a single nearest neighbor is special-cased, and does not enforce the max distance; we do that after SLiM_kdNode *best = nullptr; double best_dist = 0.0; switch (spatiality_) { case 1: FindNeighbors1_1(kd_root, p_point, focal_individual_index, &best, &best_dist); break; case 2: FindNeighbors1_2(kd_root, p_point, focal_individual_index, &best, &best_dist, 0); break; case 3: FindNeighbors1_3(kd_root, p_point, focal_individual_index, &best, &best_dist, 0); break; default: EIDOS_TERMINATION << "ERROR (InteractionType::FindNeighbors): (internal error) spatiality_ out of range." << EidosTerminate(nullptr); } if (best && (best_dist <= max_distance_sq_)) { Individual *best_individual = p_subpop->parent_individuals_[best->individual_index_]; p_result_vec.push_object_element_NORR(best_individual); } } else if (p_count >= kd_node_count) // can't do (kd_node_count - 1), because the focal individual might not be among the nodes in the k-d tree { // Finding all neighbors within the interaction distance is special-cased switch (spatiality_) { case 1: FindNeighborsA_1(kd_root, p_point, focal_individual_index, p_result_vec, p_subpop->parent_individuals_); break; case 2: FindNeighborsA_2(kd_root, p_point, focal_individual_index, p_result_vec, p_subpop->parent_individuals_, 0); break; case 3: FindNeighborsA_3(kd_root, p_point, focal_individual_index, p_result_vec, p_subpop->parent_individuals_, 0); break; default: EIDOS_TERMINATION << "ERROR (InteractionType::FindNeighbors): (internal error) spatiality_ out of range." << EidosTerminate(nullptr); } } else { // Finding multiple neighbors is the slower general case; we use SparseVector to get all neighbors // (we would have to look at all of them anyway), and then sort them and return the top N // BCH 5/24/2023: This replaces the old algorithm using FindNeighborsN_X(), which was not thread-safe SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(p_subpop, SparseVectorDataType::kDistances); try { if (p_excluded_individual) FillSparseVectorForReceiverDistances(sv, p_excluded_individual, p_point, p_subpop, kd_root, constraints_active); else FillSparseVectorForPointDistances(sv, p_point, p_subpop, kd_root); uint32_t nnz; const uint32_t *columns; const sv_value_t *distances; distances = sv->Distances(&nnz, &columns); std::vector> neighbors; for (uint32_t col_index = 0; col_index < nnz; ++col_index) neighbors.emplace_back(col_index, distances[col_index]); std::sort(neighbors.begin(), neighbors.end(), [](const std::pair &l, const std::pair &r) { return l.second < r.second; }); std::vector &exerters = p_subpop->parent_individuals_; // the client requested p_count items, but we may have fewer if (p_count > (int)nnz) p_count = (int)nnz; for (int neighbor_index = 0; neighbor_index < p_count; ++neighbor_index) { Individual *exerter = exerters[columns[neighbors[neighbor_index].first]]; p_result_vec.push_object_element_capcheck_NORR(exerter); } } catch (...) { InteractionType::FreeSparseVector(sv); throw; } InteractionType::FreeSparseVector(sv); } } // // Eidos support // #pragma mark - #pragma mark Eidos support #pragma mark - const EidosClass *InteractionType::Class(void) const { return gSLiM_InteractionType_Class; } void InteractionType::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay() << ""; } EidosValue_SP InteractionType::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants case gID_id: // ACCELERATED { if (!cached_value_inttype_id_) cached_value_inttype_id_ = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(interaction_type_id_)); return cached_value_inttype_id_; } case gID_reciprocal: { return (reciprocal_ ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } case gID_sexSegregation: { std::string sex_segregation_string; switch (receiver_constraints_.sex_) { case IndividualSex::kFemale: sex_segregation_string += "F"; break; case IndividualSex::kMale: sex_segregation_string += "M"; break; default: sex_segregation_string += "*"; break; } switch (exerter_constraints_.sex_) { case IndividualSex::kFemale: sex_segregation_string += "F"; break; case IndividualSex::kMale: sex_segregation_string += "M"; break; default: sex_segregation_string += "*"; break; } return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(sex_segregation_string)); } case gID_spatiality: { return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(spatiality_string_)); } // variables case gID_maxDistance: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(max_distance_)); case gID_tag: // ACCELERATED { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (InteractionType::GetProperty): property tag accessed on interaction type before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } // all others, including gID_none default: return super::GetProperty(p_property_id); } } EidosValue *InteractionType::GetProperty_Accelerated_id(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { InteractionType *value = (InteractionType *)(p_values[value_index]); int_result->set_int_no_check(value->interaction_type_id_, value_index); } return int_result; } EidosValue *InteractionType::GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size) { EidosValue_Int *int_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(p_values_size); for (size_t value_index = 0; value_index < p_values_size; ++value_index) { InteractionType *value = (InteractionType *)(p_values[value_index]); slim_usertag_t tag_value = value->tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (InteractionType::GetProperty_Accelerated_tag): property tag accessed on interaction type before being set." << EidosTerminate(); int_result->set_int_no_check(tag_value, value_index); } return int_result; } void InteractionType::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { case gID_maxDistance: { if (AnyEvaluated()) EIDOS_TERMINATION << "ERROR (InteractionType::SetProperty): maxDistance cannot be changed while the interaction is being evaluated; call unevaluate() first, or set maxDistance prior to evaluation of the interaction." << EidosTerminate(); max_distance_ = p_value.FloatAtIndex_NOCAST(0, nullptr); max_distance_sq_ = max_distance_ * max_distance_; if (max_distance_ < 0.0) EIDOS_TERMINATION << "ERROR (InteractionType::SetProperty): the maximum interaction distance must be greater than or equal to zero." << EidosTerminate(); if ((if_type_ == SpatialKernelType::kLinear) && (std::isinf(max_distance_) || (max_distance_ <= 0.0))) EIDOS_TERMINATION << "ERROR (InteractionType::SetProperty): the maximum interaction distance must be finite and greater than zero when interaction type 'l' has been chosen." << EidosTerminate(); // tweak a flag to make SLiMgui update community_.interaction_types_changed_ = true; // changing max_distance_ invalidates the cached clipped_integral_ buffer; we don't deallocate it, just invalidate it clipped_integral_valid_ = false; return; } case gID_tag: { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; return; } default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP InteractionType::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { case gID_clippedIntegral: return ExecuteMethod_clippedIntegral(p_method_id, p_arguments, p_interpreter); case gID_distance: return ExecuteMethod_distance(p_method_id, p_arguments, p_interpreter); case gID_distanceFromPoint: return ExecuteMethod_distanceFromPoint(p_method_id, p_arguments, p_interpreter); case gID_drawByStrength: return ExecuteMethod_drawByStrength(p_method_id, p_arguments, p_interpreter); case gID_evaluate: return ExecuteMethod_evaluate(p_method_id, p_arguments, p_interpreter); case gID_interactingNeighborCount: return ExecuteMethod_interactingNeighborCount(p_method_id, p_arguments, p_interpreter); case gID_localPopulationDensity: return ExecuteMethod_localPopulationDensity(p_method_id, p_arguments, p_interpreter); case gID_interactionDistance: return ExecuteMethod_interactionDistance(p_method_id, p_arguments, p_interpreter); case gID_nearestInteractingNeighbors: return ExecuteMethod_nearestInteractingNeighbors(p_method_id, p_arguments, p_interpreter); case gID_nearestNeighbors: return ExecuteMethod_nearestNeighbors(p_method_id, p_arguments, p_interpreter); case gID_nearestNeighborsOfPoint: return ExecuteMethod_nearestNeighborsOfPoint(p_method_id, p_arguments, p_interpreter); case gID_neighborCount: return ExecuteMethod_neighborCount(p_method_id, p_arguments, p_interpreter); case gID_neighborCountOfPoint: return ExecuteMethod_neighborCountOfPoint(p_method_id, p_arguments, p_interpreter); case gID_setConstraints: return ExecuteMethod_setConstraints(p_method_id, p_arguments, p_interpreter); case gID_setInteractionFunction: return ExecuteMethod_setInteractionFunction(p_method_id, p_arguments, p_interpreter); case gID_strength: return ExecuteMethod_strength(p_method_id, p_arguments, p_interpreter); case gID_testConstraints: return ExecuteMethod_testConstraints(p_method_id, p_arguments, p_interpreter); case gID_totalOfNeighborStrengths: return ExecuteMethod_totalOfNeighborStrengths(p_method_id, p_arguments, p_interpreter); case gID_unevaluate: return ExecuteMethod_unevaluate(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } static inline __attribute__((always_inline)) InteractionsData &InteractionsDataForSubpop(std::map &data_, Subpopulation *subpop) { slim_objectid_t subpop_id = subpop->subpopulation_id_; auto subpop_data_iter = data_.find(subpop_id); if ((subpop_data_iter == data_.end()) || !subpop_data_iter->second.evaluated_) EIDOS_TERMINATION << "ERROR (InteractionsDataForSubpop): the interaction must be evaluated for the receiver and exerter subpopulations, by calling evaluate(), before any queries." << EidosTerminate(); return subpop_data_iter->second; } // // ********************* – (float)clippedIntegral(No receivers) EidosValue_SP InteractionType::ExecuteMethod_clippedIntegral(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *receivers_value = p_arguments[0].get(); int receivers_count = receivers_value->Count(); // BEWARE: ExecuteMethod_localPopulationDensity() assumes that its API matches that of ExecuteMethod_clippedIntegral()! // If any arguments are added here, its code will need to change because that assumption will then be violated! // In fact localPopulationDensity() has an additional argument now, but it does not need that argument to be handled // by clippedIntegral(), so this dependency still works. if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_clippedIntegral): clippedIntegral() has no meaning for non-spatial interactions." << EidosTerminate(); if (spatiality_ == 3) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_clippedIntegral): clippedIntegral() has not been implemented for the 'xyz' case yet. If you need this functionality, please file a GitHub issue." << EidosTerminate(); if (spatiality_ == 1) CacheClippedIntegral_1D(); else if (spatiality_ == 2) CacheClippedIntegral_2D(); else // (spatiality_ == 3) { ; // FIXME the big obstacle here is that a 1024x1024x1024 array of precalculated values is way too large, so interpolation is probably needed } // Note that clippedIntegral() ignores sex-specificity; every individual is in an "interaction field", which // clippedIntegral() measures, even if some individuals cannot actually feel any interactions exerted by others. // This policy means that clippedIntegral() never returns zero, avoiding divide-by-zero issues. // NULL means "what's the integral for a receiver that is not near any edge?" if (receivers_count == 0) { if (receivers_value->Type() == EidosValueType::kValueNULL) { double integral; if (spatiality_ == 1) integral = ClippedIntegral_1D(max_distance_, max_distance_, false); else if (spatiality_ == 2) integral = ClippedIntegral_2D(max_distance_, max_distance_, max_distance_, max_distance_, false, false); else // (spatiality_ == 3) integral = 0.0; // FIXME return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(integral)); } else { return gStaticEidosValue_Float_ZeroVec; } } // SPECIES CONSISTENCY CHECK Species *species = Community::SpeciesForIndividuals(receivers_value); if (!species) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_clippedIntegral): clippedIntegral() requires that all receivers belong to the same species." << EidosTerminate(); CheckSpeciesCompatibility_Generic(*species); bool periodic_x, periodic_y, periodic_z; species->SpatialPeriodicity(&periodic_x, &periodic_y, &periodic_z); // We have a singleton or vector of receivers; we'd like to treat them both in the same way, so we set up for that here // We do not try to create a singleton return value when passed a singleton receiver; too complicated to optimize for that here const Individual * const *receivers_data = (Individual * const *)receivers_value->ObjectData(); InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receivers_data[0]->subpopulation_); EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(receivers_count); // Now treat cases according to spatiality bool saw_error1 = false, saw_error2 = false; if (spatiality_ == 1) { if (spatiality_string_ == "x") { EIDOS_THREAD_COUNT(gEidos_OMP_threads_CLIPPEDINTEGRAL_1S); #pragma omp parallel for schedule(static) default(none) shared(receivers_count, receiver_subpop_data) firstprivate(receivers_data, float_result, periodic_x) reduction(||: saw_error1) reduction(||: saw_error2) if(receivers_count >= EIDOS_OMPMIN_CLIPPEDINTEGRAL_1S) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { const Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error1 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; Subpopulation *subpop = receiver->subpopulation_; double indA = receiver_position[0]; double integral; #ifdef _OPENMP try { integral = ClippedIntegral_1D(indA - subpop->bounds_x0_, subpop->bounds_x1_ - indA, periodic_x); } catch (...) { saw_error2 = true; continue; } #else integral = ClippedIntegral_1D(indA - subpop->bounds_x0_, subpop->bounds_x1_ - indA, periodic_x); #endif float_result->set_float_no_check(integral, receiver_index); } } else if (spatiality_string_ == "y") { EIDOS_THREAD_COUNT(gEidos_OMP_threads_CLIPPEDINTEGRAL_1S); #pragma omp parallel for schedule(static) default(none) shared(receivers_count, receiver_subpop_data) firstprivate(receivers_data, float_result, periodic_y) reduction(||: saw_error1) reduction(||: saw_error2) if(receivers_count >= EIDOS_OMPMIN_CLIPPEDINTEGRAL_1S) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { const Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error1 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; Subpopulation *subpop = receiver->subpopulation_; double indA = receiver_position[0]; double integral; #ifdef _OPENMP try { integral = ClippedIntegral_1D(indA - subpop->bounds_y0_, subpop->bounds_y1_ - indA, periodic_y); } catch (...) { saw_error2 = true; continue; } #else integral = ClippedIntegral_1D(indA - subpop->bounds_y0_, subpop->bounds_y1_ - indA, periodic_y); #endif float_result->set_float_no_check(integral, receiver_index); } } else // (spatiality_string_ == "z") { EIDOS_THREAD_COUNT(gEidos_OMP_threads_CLIPPEDINTEGRAL_1S); #pragma omp parallel for schedule(static) default(none) shared(receivers_count, receiver_subpop_data) firstprivate(receivers_data, float_result, periodic_z) reduction(||: saw_error1) reduction(||: saw_error2) if(receivers_count >= EIDOS_OMPMIN_CLIPPEDINTEGRAL_1S) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { const Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error1 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; Subpopulation *subpop = receiver->subpopulation_; double indA = receiver_position[0]; double integral; #ifdef _OPENMP try { integral = ClippedIntegral_1D(indA - subpop->bounds_z0_, subpop->bounds_z1_ - indA, periodic_z); } catch (...) { saw_error2 = true; continue; } #else integral = ClippedIntegral_1D(indA - subpop->bounds_z0_, subpop->bounds_z1_ - indA, periodic_z); #endif float_result->set_float_no_check(integral, receiver_index); } } } else if (spatiality_ == 2) { if (spatiality_string_ == "xy") { EIDOS_THREAD_COUNT(gEidos_OMP_threads_CLIPPEDINTEGRAL_2S); #pragma omp parallel for schedule(static) default(none) shared(receivers_count, receiver_subpop_data) firstprivate(receivers_data, float_result, periodic_x, periodic_y) reduction(||: saw_error1) reduction(||: saw_error2) if(receivers_count >= EIDOS_OMPMIN_CLIPPEDINTEGRAL_2S) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { const Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error1 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; Subpopulation *subpop = receiver->subpopulation_; double indA = receiver_position[0]; double indB = receiver_position[1]; double integral; #ifdef _OPENMP try { integral = ClippedIntegral_2D(indA - subpop->bounds_x0_, subpop->bounds_x1_ - indA, indB - subpop->bounds_y0_, subpop->bounds_y1_ - indB, periodic_x, periodic_y); } catch (...) { saw_error2 = true; continue; } #else integral = ClippedIntegral_2D(indA - subpop->bounds_x0_, subpop->bounds_x1_ - indA, indB - subpop->bounds_y0_, subpop->bounds_y1_ - indB, periodic_x, periodic_y); #endif float_result->set_float_no_check(integral, receiver_index); } } else if (spatiality_string_ == "xz") { EIDOS_THREAD_COUNT(gEidos_OMP_threads_CLIPPEDINTEGRAL_2S); #pragma omp parallel for schedule(static) default(none) shared(receivers_count, receiver_subpop_data) firstprivate(receivers_data, float_result, periodic_x, periodic_z) reduction(||: saw_error1) reduction(||: saw_error2) if(receivers_count >= EIDOS_OMPMIN_CLIPPEDINTEGRAL_2S) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { const Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error1 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; Subpopulation *subpop = receiver->subpopulation_; double indA = receiver_position[0]; double indB = receiver_position[1]; double integral; #ifdef _OPENMP try { integral = ClippedIntegral_2D(indA - subpop->bounds_x0_, subpop->bounds_x1_ - indA, indB - subpop->bounds_z0_, subpop->bounds_z1_ - indB, periodic_x, periodic_z); } catch (...) { saw_error2 = true; continue; } #else integral = ClippedIntegral_2D(indA - subpop->bounds_x0_, subpop->bounds_x1_ - indA, indB - subpop->bounds_z0_, subpop->bounds_z1_ - indB, periodic_x, periodic_z); #endif float_result->set_float_no_check(integral, receiver_index); } } else // (spatiality_string_ == "yz") { EIDOS_THREAD_COUNT(gEidos_OMP_threads_CLIPPEDINTEGRAL_2S); #pragma omp parallel for schedule(static) default(none) shared(receivers_count, receiver_subpop_data) firstprivate(receivers_data, float_result, periodic_y, periodic_z) reduction(||: saw_error1) reduction(||: saw_error2) if(receivers_count >= EIDOS_OMPMIN_CLIPPEDINTEGRAL_2S) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { const Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error1 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; Subpopulation *subpop = receiver->subpopulation_; double indA = receiver_position[0]; double indB = receiver_position[1]; double integral; #ifdef _OPENMP try { integral = ClippedIntegral_2D(indA - subpop->bounds_y0_, subpop->bounds_y1_ - indA, indB - subpop->bounds_z0_, subpop->bounds_z1_ - indB, periodic_y, periodic_z); } catch (...) { saw_error2 = true; continue; } #else integral = ClippedIntegral_2D(indA - subpop->bounds_y0_, subpop->bounds_y1_ - indA, indB - subpop->bounds_z0_, subpop->bounds_z1_ - indB, periodic_y, periodic_z); #endif float_result->set_float_no_check(integral, receiver_index); } } } else // (spatiality_ == 3) { // FIXME } // deferred raises, for OpenMP compatibility if (saw_error1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_clippedIntegral): clippedIntegral() requires receivers to be visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); if (saw_error2) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_clippedIntegral): an exception was caught inside a parallel region." << EidosTerminate(); return EidosValue_SP(float_result); } // // ********************* – (float)distance(object$ receiver, [No exerters = NULL]) EidosValue_SP InteractionType::ExecuteMethod_distance(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *receiver_value = p_arguments[0].get(); EidosValue *exerters_value = p_arguments[1].get(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distance): distance() requires that the interaction be spatial." << EidosTerminate(); // receiver_value is guaranteed to be singleton; let's get the info on it Individual *receiver = (Individual *)receiver_value->ObjectElementAtIndex_NOCAST(0, nullptr); slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distance): distance() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); Subpopulation *receiver_subpop = receiver->subpopulation_; Species &receiver_species = receiver_subpop->species_; CheckSpeciesCompatibility_Generic(receiver_species); InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; // figure out the exerter subpopulation and get info on it bool exerters_value_NULL = (exerters_value->Type() == EidosValueType::kValueNULL); int exerters_count = exerters_value->Count(); if ((exerters_count == 0) && !exerters_value_NULL) return gStaticEidosValue_Float_ZeroVec; Subpopulation *exerter_subpop = (exerters_value_NULL ? receiver_subpop : ((Individual *)exerters_value->ObjectElementAtIndex_NOCAST(0, nullptr))->subpopulation_); CheckSpeciesCompatibility_Generic(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); slim_popsize_t exerter_subpop_size = exerter_subpop->parent_subpop_size_; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); double *exerter_position_data = exerter_subpop_data.positions_; bool periodicity_enabled = (exerter_subpop_data.periodic_x_ || exerter_subpop_data.periodic_y_ || exerter_subpop_data.periodic_z_); if (exerters_value_NULL) { // NULL means return distances from receiver (which must be singleton) to all individuals in its subpopulation EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerter_subpop_size); if (periodicity_enabled) { for (int exerter_index = 0; exerter_index < exerter_subpop_size; ++exerter_index) { double distance = CalculateDistanceWithPeriodicity(receiver_position, exerter_position_data + (size_t)exerter_index * SLIM_MAX_DIMENSIONALITY, exerter_subpop_data); result_vec->set_float_no_check(distance, exerter_index); } } else { for (int exerter_index = 0; exerter_index < exerter_subpop_size; ++exerter_index) { double distance = CalculateDistance(receiver_position, exerter_position_data + (size_t)exerter_index * SLIM_MAX_DIMENSIONALITY); result_vec->set_float_no_check(distance, exerter_index); } } return EidosValue_SP(result_vec); } else { // Otherwise, individuals1 is singleton, and individuals2 is any length, so we loop over individuals2 EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); Individual * const *exerters_data = (Individual * const *)exerters_value->ObjectData(); for (int exerter_index = 0; exerter_index < exerters_count; ++exerter_index) { Individual *exerter = exerters_data[exerter_index]; // SPECIES CONSISTENCY CHECK if (exerter_subpop != exerter->subpopulation_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distance): distance() requires that all exerters be in the same subpopulation." << EidosTerminate(); slim_popsize_t exerter_index_in_subpop = exerter->index_; if (exerter_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distance): distance() requires that exerters are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); double distance; if (periodicity_enabled) distance = CalculateDistanceWithPeriodicity(receiver_position, exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY, exerter_subpop_data); else distance = CalculateDistance(receiver_position, exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY); result_vec->set_float_no_check(distance, exerter_index); } return EidosValue_SP(result_vec); } } // ********************* – (float)distanceFromPoint(float point, object exerters) // EidosValue_SP InteractionType::ExecuteMethod_distanceFromPoint(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *point_value = p_arguments[0].get(); EidosValue *exerters_value = p_arguments[1].get(); int exerters_count = exerters_value->Count(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distanceFromPoint): distanceFromPoint() requires that the interaction be spatial." << EidosTerminate(); if (point_value->Count() != spatiality_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distanceFromPoint): distanceFromPoint() requires that point is of length equal to the interaction spatiality." << EidosTerminate(); if (exerters_count == 0) return gStaticEidosValue_Float_ZeroVec; // Get the point's coordinates into a double[] double point_data[SLIM_MAX_DIMENSIONALITY]; #ifdef __clang_analyzer__ // The static analyzer does not understand some things, so we tell it here point_data[0] = 0; point_data[1] = 0; point_data[2] = 0; #endif for (int point_index = 0; point_index < spatiality_; ++point_index) point_data[point_index] = point_value->FloatAtIndex_NOCAST(point_index, nullptr); // exerters_value is guaranteed to be of length >= 1; let's get the info on it Individual * const *exerters_data = (Individual * const *)exerters_value->ObjectData(); Individual *exerter_first = (Individual *)exerters_data[0]; Subpopulation *exerter_subpop = exerter_first->subpopulation_; Species &exerter_species = exerter_subpop->species_; CheckSpeciesCompatibility_Generic(exerter_species); InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); double *exerter_position_data = exerter_subpop_data.positions_; bool periodicity_enabled = (exerter_subpop_data.periodic_x_ || exerter_subpop_data.periodic_y_ || exerter_subpop_data.periodic_z_); // If we're using periodic boundaries, the point supplied has to be within bounds in the periodic dimensions; points outside periodic bounds make no sense if (periodicity_enabled) { if ((exerter_subpop_data.periodic_x_ && ((point_data[0] < 0.0) || (point_data[0] > exerter_subpop_data.bounds_x1_))) || (exerter_subpop_data.periodic_y_ && ((point_data[1] < 0.0) || (point_data[1] > exerter_subpop_data.bounds_y1_))) || (exerter_subpop_data.periodic_z_ && ((point_data[2] < 0.0) || (point_data[2] > exerter_subpop_data.bounds_z1_)))) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distanceFromPoint): distanceFromPoint() requires that coordinates for periodic spatial dimensions fall inside spatial bounaries; use pointPeriodic() to ensure this if necessary." << EidosTerminate(); } EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); if (periodicity_enabled) { for (int exerter_index = 0; exerter_index < exerters_count; ++exerter_index) { Individual *exerter = exerters_data[exerter_index]; // SPECIES CONSISTENCY CHECK if (exerter_subpop != exerter->subpopulation_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distanceFromPoint): distanceFromPoint() requires that all exerters be in the same subpopulation." << EidosTerminate(); slim_popsize_t exerter_index_in_subpop = exerter->index_; if (exerter_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distanceFromPoint): distanceFromPoint() requires that exerters are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); double *ind_position = exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY; result_vec->set_float_no_check(CalculateDistanceWithPeriodicity(ind_position, point_data, exerter_subpop_data), exerter_index); } } else { for (int exerter_index = 0; exerter_index < exerters_count; ++exerter_index) { Individual *exerter = exerters_data[exerter_index]; // SPECIES CONSISTENCY CHECK if (exerter_subpop != exerter->subpopulation_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distanceFromPoint): distanceFromPoint() requires that all exerters be in the same subpopulation." << EidosTerminate(); slim_popsize_t exerter_index_in_subpop = exerter->index_; if (exerter_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_distanceFromPoint): distanceFromPoint() requires that exerters are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); double *ind_position = exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY; result_vec->set_float_no_check(CalculateDistance(ind_position, point_data), exerter_index); } } return EidosValue_SP(result_vec); } // a helper function for ExecuteMethod_drawByStrength() that does the draws using a vector of weights static void DrawByWeights(int draw_count, const double *weights, int n_weights, double weight_total, std::vector &draw_indices) { // Draw individuals; we do this using either the GSL or linear search, depending on the query size // This choice is somewhat problematic. I empirically determined at what query size the GSL started // to pay off despite the overhead of setup with gsl_ran_discrete_preproc(). However, I did that for // a particular subpopulation size; and the crossover point might also depend upon the distribution // of strength values in the subpopulation. I'm not going to worry about this too much, though; it // is not really answerable in general, and a crossover of 50 seems reasonable. For small counts, // linear search won't take that long anyway, and there must be a limit >= 1 where linear is faster // than the GSL; and for large counts the GSL is surely a win. Trying to figure out exactly where // the crossover is in all cases would be overkill; my testing indicates the performance difference // between the two methods is not really that large anyway. if (weight_total > 0.0) { if (draw_count > 50) // the empirically determined crossover point in performance { // Use gsl_ran_discrete() to do the drawing gsl_rng *rng_gsl = EIDOS_GSL_RNG(omp_get_thread_num()); gsl_ran_discrete_t *gsl_lookup = gsl_ran_discrete_preproc(n_weights, weights); for (int64_t draw_index = 0; draw_index < draw_count; ++draw_index) { int hit_index = (int)gsl_ran_discrete(rng_gsl, gsl_lookup); draw_indices.emplace_back(hit_index); } gsl_ran_discrete_free(gsl_lookup); } else { // Use linear search to do the drawing EidosRNG_64_bit &rng_64 = EIDOS_64BIT_RNG(omp_get_thread_num()); for (int64_t draw_index = 0; draw_index < draw_count; ++draw_index) { double the_rose_in_the_teeth = Eidos_rng_uniform_doubleCO(rng_64) * weight_total; double cumulative_weight = 0.0; int hit_index; for (hit_index = 0; hit_index < n_weights; ++hit_index) { double weight = weights[hit_index]; cumulative_weight += weight; if (the_rose_in_the_teeth <= cumulative_weight) break; } // We might overrun the end, due to roundoff error; if so, attribute it to the first non-zero weight entry if (hit_index >= n_weights) { for (hit_index = 0; hit_index < n_weights; ++hit_index) if (weights[hit_index] > 0.0) break; if (hit_index >= n_weights) hit_index = 0; } draw_indices.emplace_back(hit_index); } } } } // ********************* – (object)drawByStrength(object receiver, [integer$ count = 1], [No$ exerterSubpop = NULL], [logical$ returnDict = F]) // EidosValue_SP InteractionType::ExecuteMethod_drawByStrength(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *receiver_value = p_arguments[0].get(); EidosValue *count_value = p_arguments[1].get(); EidosValue *exerterSubpop_value = p_arguments[2].get(); EidosValue *returnDict_value = p_arguments[3].get(); eidos_logical_t returnDict = returnDict_value->LogicalAtIndex_NOCAST(0, nullptr); Subpopulation *receiver_subpop = nullptr; if (!returnDict) { // This is the single-threaded, single-receiver case; it returns a vector of Individual objects if (receiver_value->Count() != 1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): drawByStrength() requires that the receiver is singleton when returnDict is F; if you want to process multiple receivers in a single call, pass returnDict=T." << EidosTerminate(); Individual *receiver = (Individual *)receiver_value->ObjectElementAtIndex_NOCAST(0, nullptr); receiver_subpop = receiver->subpopulation_; } else { // This is the multi-threaded, multi-receiver case; it returns a Dictionary object vectors of Individual objects if (receiver_value->Count() == 0) { // With no receivers, return an empty Dictionary EidosDictionaryRetained *dictionary = new EidosDictionaryRetained(); EidosValue_SP result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(dictionary, gEidosDictionaryRetained_Class)); dictionary->ContentsChanged("InteractionType::ExecuteMethod_drawByStrength()"); // objectElement is now retained by result_SP, so we can release it dictionary->Release(); return result_SP; } receiver_subpop = ((Individual *)receiver_value->ObjectElementAtIndex_NOCAST(0, nullptr))->subpopulation_; } // shared logic for both cases Species &receiver_species = receiver_subpop->species_; CheckSpeciesCompatibility_Receiver(receiver_species); // the exerter subpopulation defaults to the same subpop as the receivers Subpopulation *exerter_subpop = ((exerterSubpop_value->Type() == EidosValueType::kValueNULL) ? receiver_subpop : (Subpopulation *)exerterSubpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); slim_popsize_t exerter_subpop_size = exerter_subpop->parent_subpop_size_; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); // Check the count; note that we do NOT clamp count to exerter_subpop_size, since draws are done with replacement! int64_t count = count_value->IntAtIndex_NOCAST(0, nullptr); if (count < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): drawByStrength() requires count >= 0." << EidosTerminate(); bool has_interaction_callbacks = (exerter_subpop_data.evaluation_interaction_callbacks_.size() != 0); bool optimize_fixed_interaction_strengths = (!has_interaction_callbacks && (if_type_ == SpatialKernelType::kFixed)); if (!returnDict) { // This is the single-threaded, single-receiver case; it returns a vector of Individual objects if (count == 0) return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); // receiver_value is guaranteed to be singleton; let's get the info on it Individual *receiver = (Individual *)receiver_value->ObjectElementAtIndex_NOCAST(0, nullptr); slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): drawByStrength() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); // Check constraints for the receiver; if the individual is disqualified, no draws can occur and the return is empty if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); if (spatiality_ == 0) { // Non-spatial case; no distances used. We have to worry about exerter constraints; they are not handled for us. // BCH 5/14/2023: We call ApplyInteractionCallbacks() below, so if this code ever goes parallel it // should stay single-threaded if/when any interaction() callbacks are present! slim_popsize_t receiver_index = ((exerter_subpop == receiver->subpopulation_) && (receiver->index_ >= 0) ? receiver->index_ : -1); std::vector &callbacks = exerter_subpop_data.evaluation_interaction_callbacks_; EidosValue_Object *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); double total_interaction_strength = 0.0; std::vector cached_strength; std::vector &exerters = exerter_subpop->parent_individuals_; for (slim_popsize_t exerter_index_in_subpop = 0; exerter_index_in_subpop < exerter_subpop_size; ++exerter_index_in_subpop) { Individual *exerter = exerters[exerter_index_in_subpop]; double strength = 0; if ((exerter_index_in_subpop != receiver_index) && CheckIndividualConstraints(exerter, exerter_constraints_)) // potentially raises strength = ApplyInteractionCallbacks(receiver, exerter, if_param1_, NAN, callbacks); // hard-coding interaction function "f" (SpatialKernelType::kFixed), which is required total_interaction_strength += strength; cached_strength.emplace_back(strength); } if (total_interaction_strength > 0.0) { std::vector strength_indices; result_vec->resize_no_initialize(count); DrawByWeights((int)count, cached_strength.data(), exerter_subpop_size, total_interaction_strength, strength_indices); for (size_t result_index = 0; result_index < strength_indices.size(); ++result_index) { int strength_index = strength_indices[result_index]; Individual *chosen_individual = exerters[strength_index]; result_vec->set_object_element_no_check_NORR(chosen_individual, result_index); } } return EidosValue_SP(result_vec); } else { // Spatial case; we use the k-d tree to get strengths for all neighbors. InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); EidosValue_Object *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); EidosValue_SP result_vec_SP(result_vec); // If there are no exerters satisfying constraints, short-circuit if (!kd_root_EXERTERS) return result_vec_SP; if (optimize_fixed_interaction_strengths) { // Optimized case: fixed interaction strength, no callbacks, so we can do uniform draws using presences only SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kPresences); try { FillSparseVectorForReceiverPresences(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, /* constraints_active */ true); uint32_t nnz; const uint32_t *columns; sv->Presences(&nnz, &columns); if (nnz > 0) { std::vector &exerters = exerter_subpop->parent_individuals_; EidosRNG_32_bit &rng_32 = EIDOS_32BIT_RNG(omp_get_thread_num()); result_vec->resize_no_initialize(count); for (int64_t result_index = 0; result_index < count; ++result_index) { int presence_index = Eidos_rng_interval_uint32(rng_32, nnz); // equal probability for each exerter uint32_t exerter_index = columns[presence_index]; Individual *chosen_individual = exerters[exerter_index]; result_vec->set_object_element_no_check_NORR(chosen_individual, result_index); } } } catch (...) { InteractionType::FreeSparseVector(sv); throw; } InteractionType::FreeSparseVector(sv); return result_vec_SP; } else { // General case, getting strengths and doing weighted draws // BCH 5/14/2023: The call to FillSparseVectorForReceiverStrengths() means we run interaction() callbacks, // so if this code is ever parallelized, it should stay single-threaded when callbacks are enabled. SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kStrengths); try { FillSparseVectorForReceiverStrengths(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.evaluation_interaction_callbacks_); uint32_t nnz; const uint32_t *columns; const sv_value_t *strengths; std::vector double_strengths; // needed by DrawByWeights() for gsl_ran_discrete_preproc() strengths = sv->Strengths(&nnz, &columns); // Total the interaction strengths, and gather a vector of strengths as doubles double total_interaction_strength = 0.0; for (uint32_t col_index = 0; col_index < nnz; ++col_index) { sv_value_t strength = strengths[col_index]; total_interaction_strength += strength; double_strengths.emplace_back((double)strength); } // Draw individuals if (total_interaction_strength > 0.0) { std::vector strength_indices; std::vector &exerters = exerter_subpop->parent_individuals_; result_vec->resize_no_initialize(count); DrawByWeights((int)count, double_strengths.data(), nnz, total_interaction_strength, strength_indices); for (size_t result_index = 0; result_index < strength_indices.size(); ++result_index) { int strength_index = strength_indices[result_index]; Individual *chosen_individual = exerters[columns[strength_index]]; result_vec->set_object_element_no_check_NORR(chosen_individual, result_index); } } } catch (...) { InteractionType::FreeSparseVector(sv); throw; } InteractionType::FreeSparseVector(sv); return result_vec_SP; } } } else { // This is the multi-threaded, multi-receiver case; it returns a Dictionary object vectors of Individual objects // We start by making a Dictionary with an empty Individual vector for each receiver if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): drawByStrength() supports returning a Dictionary of results, with returnDict=T, only in the spatial case." << EidosTerminate(); EidosDictionaryRetained *dictionary = new EidosDictionaryRetained(); EidosValue_SP result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(dictionary, gEidosDictionaryRetained_Class)); int receivers_count = receiver_value->Count(); EidosValue_Object **result_vectors = (EidosValue_Object **)malloc(receivers_count * sizeof(EidosValue_Object *)); for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { EidosValue_Object *empty_individuals_vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class); dictionary->SetKeyValue_IntegerKeys(receiver_index, EidosValue_SP(empty_individuals_vec)); result_vectors[receiver_index] = empty_individuals_vec; } dictionary->ContentsChanged("InteractionType::ExecuteMethod_drawByStrength()"); // objectElement is now retained by result_SP, so we can release it dictionary->Release(); if ((count > 0) && (exerter_subpop_size > 0)) // BCH 5/24/2023: if the exerter subpop is empty, no individuals are drawn; short-circuit { SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); // If there are no exerters satisfying constraints, short-circuit if (!kd_root_EXERTERS) { free(result_vectors); return result_SP; } bool saw_error_1 = false, saw_error_2 = false, saw_error_3 = false, saw_error_4 = false; InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); Individual * const *receiver_data = (Individual * const *)receiver_value->ObjectData(); EIDOS_THREAD_COUNT(gEidos_OMP_threads_DRAWBYSTRENGTH); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(gEidos_RNG_PERTHREAD, receivers_count, receiver_subpop, exerter_subpop, receiver_subpop_data, exerter_subpop_data, kd_root_EXERTERS, optimize_fixed_interaction_strengths) firstprivate(receiver_data, result_vectors, count, exerter_subpop_size) reduction(||: saw_error_1) reduction(||: saw_error_2) reduction(||: saw_error_3) reduction(||: saw_error_4) if(!has_interaction_callbacks && (receivers_count >= EIDOS_OMPMIN_DRAWBYSTRENGTH)) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { Individual *receiver = (Individual *)receiver_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error_1 = true; continue; } // SPECIES CONSISTENCY CHECK if (receiver_subpop != receiver->subpopulation_) { saw_error_2 = true; continue; } // Check constraints for the receiver; if the individual is disqualified, there are no candidates to draw from try { if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises; protected continue; } catch (...) { saw_error_4 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; EidosValue_Object *result_vec = result_vectors[receiver_index]; if (optimize_fixed_interaction_strengths) { // Optimized case: fixed interaction strength, no callbacks, so we can do uniform draws using presences only SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kPresences); try { FillSparseVectorForReceiverPresences(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, /* constraints_active */ true); uint32_t nnz; const uint32_t *columns; sv->Presences(&nnz, &columns); if (nnz > 0) { std::vector &exerters = exerter_subpop->parent_individuals_; EidosRNG_32_bit &rng_32 = EIDOS_32BIT_RNG(omp_get_thread_num()); result_vec->resize_no_initialize(count); for (int64_t result_index = 0; result_index < count; ++result_index) { int presence_index = Eidos_rng_interval_uint32(rng_32, nnz); // equal probability for each exerter uint32_t exerter_index = columns[presence_index]; Individual *chosen_individual = exerters[exerter_index]; result_vec->set_object_element_no_check_NORR(chosen_individual, result_index); } } } catch (...) { saw_error_3 = true; InteractionType::FreeSparseVector(sv); continue; } InteractionType::FreeSparseVector(sv); } else { // General case, getting strengths and doing weighted draws SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kStrengths); // Under OpenMP, raises can't go past the end of the parallel region; handle things the same way when not under OpenMP for simplicity try { FillSparseVectorForReceiverStrengths(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.evaluation_interaction_callbacks_); // protected from running interaction() callbacks in parallel, above } catch (...) { saw_error_3 = true; InteractionType::FreeSparseVector(sv); continue; } uint32_t nnz; const uint32_t *columns; const sv_value_t *strengths; std::vector double_strengths; // needed by DrawByWeights() for gsl_ran_discrete_preproc() strengths = sv->Strengths(&nnz, &columns); // Total the interaction strengths, and gather a vector of strengths as doubles double total_interaction_strength = 0.0; for (uint32_t col_index = 0; col_index < nnz; ++col_index) { sv_value_t strength = strengths[col_index]; total_interaction_strength += strength; double_strengths.emplace_back((double)strength); } // Draw individuals if (total_interaction_strength > 0.0) { std::vector strength_indices; std::vector &exerters = exerter_subpop->parent_individuals_; result_vec->resize_no_initialize(count); DrawByWeights((int)count, double_strengths.data(), nnz, total_interaction_strength, strength_indices); for (size_t result_index = 0; result_index < strength_indices.size(); ++result_index) { int strength_index = strength_indices[result_index]; Individual *chosen_individual = exerters[columns[strength_index]]; result_vec->set_object_element_no_check_NORR(chosen_individual, result_index); } } InteractionType::FreeSparseVector(sv); } } // deferred raises, for OpenMP compatibility if (saw_error_1) { free(result_vectors); EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): drawByStrength() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); } if (saw_error_2) { free(result_vectors); EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): drawByStrength() requires that all receivers be in the same subpopulation." << EidosTerminate(); } if (saw_error_3) { free(result_vectors); EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): an exception was caught inside a parallel region." << EidosTerminate(); } if (saw_error_4) { free(result_vectors); EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_drawByStrength): drawByStrength() tested a tag or tagL constraint, but a receiver's value for that property was not defined (had not been set)." << EidosTerminate(); } } free(result_vectors); return result_SP; } } // ********************* - (void)evaluate(io subpops) // EidosValue_SP InteractionType::ExecuteMethod_evaluate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *subpops_value = p_arguments[0].get(); // TIMING RESTRICTION if ((community_.CycleStage() == SLiMCycleStage::kWFStage2GenerateOffspring) || (community_.CycleStage() == SLiMCycleStage::kNonWFStage1GenerateOffspring) || (community_.CycleStage() == SLiMCycleStage::kNonWFStage4SurvivalSelection)) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_evaluate): evaluate() may not be called during the offspring generation or viability/survival cycle stages." << EidosTerminate(); // Get the requested subpops int requested_subpop_count = subpops_value->Count(); for (int requested_subpop_index = 0; requested_subpop_index < requested_subpop_count; ++requested_subpop_index) EvaluateSubpopulation(SLiM_ExtractSubpopulationFromEidosValue_io(subpops_value, requested_subpop_index, &community_, nullptr, "evaluate()")); return gStaticEidosValueVOID; } // ********************* – (integer)interactingNeighborCount(object receivers, [No$ exerterSubpop = NULL]) // EidosValue_SP InteractionType::ExecuteMethod_interactingNeighborCount(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // BCH 11/2/2023: Note that this method is now almost identical to ExecuteMethod_neighborCount() EidosValue *receivers_value = p_arguments[0].get(); EidosValue *exerterSubpop_value = p_arguments[1].get(); int receivers_count = receivers_value->Count(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactingNeighborCount): interactingNeighborCount() requires that the interaction be spatial." << EidosTerminate(); if (receivers_count == 0) return gStaticEidosValue_Integer_ZeroVec; // the exerter subpopulation defaults to the same subpop as the receivers Subpopulation *receiver_subpop = ((Individual *)receivers_value->ObjectElementAtIndex_NOCAST(0, nullptr))->subpopulation_; Subpopulation *exerter_subpop = ((exerterSubpop_value->Type() == EidosValueType::kValueNULL) ? receiver_subpop : (Subpopulation *)exerterSubpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); CheckSpeciesCompatibility_Receiver(receiver_subpop->species_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); // If there are no exerters satisfying constraints, short-circuit if (!kd_root_EXERTERS) { // If the exerter subpop is empty then all count values for the receivers are zero if (receivers_count == 1) { return gStaticEidosValue_Integer0; } else { EidosValue_Int *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(receivers_count); for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) result_vec->set_int_no_check(0, receiver_index); return EidosValue_SP(result_vec); } } InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); Individual * const *receivers_data = (Individual * const *)receivers_value->ObjectData(); if (receivers_count == 1) { // Just one value, so we can return a singleton and skip some work Individual *receiver = (Individual *)receivers_data[0]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactingNeighborCount): interactingNeighborCount() requires receivers to be visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); // Check constraints for the receiver; if the individual is disqualified, the count is zero if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises return gStaticEidosValue_Integer0; // Find the neighbors double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; slim_popsize_t focal_individual_index = (exerter_subpop == receiver_subpop) ? receiver_index_in_subpop : -1; int neighborCount; switch (spatiality_) { case 1: neighborCount = CountNeighbors_1(kd_root_EXERTERS, receiver_position, focal_individual_index); break; case 2: neighborCount = CountNeighbors_2(kd_root_EXERTERS, receiver_position, focal_individual_index, 0); break; case 3: neighborCount = CountNeighbors_3(kd_root_EXERTERS, receiver_position, focal_individual_index, 0); break; default: neighborCount = 0; break; // unsupported value } return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(neighborCount)); } else { EidosValue_Int *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(receivers_count); bool saw_error_1 = false, saw_error_2 = false, saw_error_3 = false; EIDOS_THREAD_COUNT(gEidos_OMP_threads_INTNEIGHCOUNT); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(receivers_count, receiver_subpop, exerter_subpop, receiver_subpop_data, exerter_subpop_data, kd_root_EXERTERS) firstprivate(receivers_data, result_vec) reduction(||: saw_error_1) reduction(||: saw_error_2) reduction(||: saw_error_3) if(receivers_count >= EIDOS_OMPMIN_INTNEIGHCOUNT) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error_1 = true; continue; } // SPECIES CONSISTENCY CHECK if (receiver_subpop != receiver->subpopulation_) { saw_error_2 = true; continue; } // Check constraints for the receiver; if the individual is disqualified, the count is zero // Under OpenMP, raises can't go past the end of the parallel region; handle things the same way when not under OpenMP for simplicity try { if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises; protected { result_vec->set_int_no_check(0, receiver_index); continue; } } catch (...) { saw_error_3 = true; continue; } // Find the neighbors double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; slim_popsize_t focal_individual_index = (exerter_subpop == receiver_subpop) ? receiver_index_in_subpop : -1; int neighborCount; switch (spatiality_) { case 1: neighborCount = CountNeighbors_1(kd_root_EXERTERS, receiver_position, focal_individual_index); break; case 2: neighborCount = CountNeighbors_2(kd_root_EXERTERS, receiver_position, focal_individual_index, 0); break; case 3: neighborCount = CountNeighbors_3(kd_root_EXERTERS, receiver_position, focal_individual_index, 0); break; default: neighborCount = 0; break; // unsupported value } result_vec->set_int_no_check(neighborCount, receiver_index); } // deferred raises, for OpenMP compatibility if (saw_error_1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactingNeighborCount): interactingNeighborCount() requires receivers to be visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); if (saw_error_2) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactingNeighborCount): interactingNeighborCount() requires that all receivers be in the same subpopulation." << EidosTerminate(); if (saw_error_3) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactingNeighborCount): interactingNeighborCount() tested a tag or tagL constraint, but a receiver's value for that property was not defined (had not been set)." << EidosTerminate(); return EidosValue_SP(result_vec); } return gStaticEidosValueVOID; } // ********************* – (float)localPopulationDensity(object receivers, [No$ exerterSubpop = NULL]) // EidosValue_SP InteractionType::ExecuteMethod_localPopulationDensity(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *receivers_value = p_arguments[0].get(); EidosValue *exerterSubpop_value = p_arguments[1].get(); int receivers_count = receivers_value->Count(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() requires that the interaction be spatial." << EidosTerminate(); if (spatiality_ == 3) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() does not support the 'xyz' case yet. If you need this functionality, please file a GitHub issue." << EidosTerminate(); if (receivers_count == 0) return gStaticEidosValue_Float_ZeroVec; // receivers_value is guaranteed to have at least one value Individual * const *receivers_data = (Individual * const *)receivers_value->ObjectData(); Individual *first_receiver = receivers_data[0]; Subpopulation *receiver_subpop = first_receiver->subpopulation_; // the exerter subpopulation defaults to the same subpop as the receivers Subpopulation *exerter_subpop = ((exerterSubpop_value->Type() == EidosValueType::kValueNULL) ? receiver_subpop : (Subpopulation *)exerterSubpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); CheckSpeciesCompatibility_Receiver(receiver_subpop->species_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); if (receiver_subpop != exerter_subpop) if ((receiver_subpop->bounds_x0_ != exerter_subpop->bounds_x0_) || (receiver_subpop->bounds_x1_ != exerter_subpop->bounds_x1_) || (receiver_subpop->bounds_y0_ != exerter_subpop->bounds_y0_) || (receiver_subpop->bounds_y1_ != exerter_subpop->bounds_y1_) || (receiver_subpop->bounds_z0_ != exerter_subpop->bounds_z0_) || (receiver_subpop->bounds_z1_ != exerter_subpop->bounds_z1_)) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() requires that the receiver and exerter subpopulations have identical bounds." << EidosTerminate(); InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); // If there are no exerters satisfying constraints, short-circuit if (!kd_root_EXERTERS) { // If the exerter subpop is empty then all density values for the receivers are zero (note that we // already handled the case of receivers_count == 0 above, so the receiver is not in the exerter subpop) if (receivers_count == 1) { return gStaticEidosValue_Float0; } else { EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(receivers_count); for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) result_vec->set_float_no_check(0, receiver_index); return EidosValue_SP(result_vec); } } InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); double strength_for_zero_distance = CalculateStrengthNoCallbacks(0.0); // probably always if_param1_, but let's not hard-code that... bool has_interaction_callbacks = (exerter_subpop_data.evaluation_interaction_callbacks_.size() != 0); if (has_interaction_callbacks) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() does not allow interaction() callbacks, since they cannot be integrated to compute density." << EidosTerminate(); // Subcontract to ExecuteMethod_clippedIntegral(); this handles all the spatiality crap for us // note that we pass our own parameters through to clippedIntegral()! So our APIs need to be the same! // Actually, we now have an extra parameter, exerterSubpop(), compared to clippedIntegral(); but we // do not need it to use that parameter (because we require identical bounds above), so it's OK. EidosValue_SP clipped_integrals_SP = ExecuteMethod_clippedIntegral(p_method_id, p_arguments, p_interpreter); EidosValue *clipped_integrals = clipped_integrals_SP.get(); const double *clipped_integrals_data = clipped_integrals->FloatData(); // Decide whether we can use our optimized case below bool optimize_fixed_interaction_strengths = (!has_interaction_callbacks && (if_type_ == SpatialKernelType::kFixed)); if (receivers_count == 1) { // Just one value, so we can return a singleton and skip some work slim_popsize_t receiver_index_in_subpop = first_receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() requires receivers to be visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); // Check constraints for the receiver; if the individual is disqualified, the local density of interacters is zero if (!CheckIndividualConstraints(first_receiver, receiver_constraints_)) // potentially raises return gStaticEidosValue_Float0; double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; double total_strength; if (optimize_fixed_interaction_strengths) { // Optimized case for fixed interaction strength and no callbacks SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kPresences); try { FillSparseVectorForReceiverPresences(sv, first_receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, /* constraints_active */ true); uint32_t nnz; sv->Presences(&nnz); total_strength = nnz * if_param1_; } catch (...) { InteractionType::FreeSparseVector(sv); throw; } FreeSparseVector(sv); } else { // General case, totalling strengths SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kStrengths); try { FillSparseVectorForReceiverStrengths(sv, first_receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.evaluation_interaction_callbacks_); // singleton case, not parallel // Get the sparse vector data uint32_t nnz; const sv_value_t *strengths; strengths = sv->Strengths(&nnz); // Total the interaction strengths total_strength = 0.0; for (uint32_t col_index = 0; col_index < nnz; ++col_index) total_strength += strengths[col_index]; } catch (...) { InteractionType::FreeSparseVector(sv); throw; } FreeSparseVector(sv); } // Add the interaction strength for the focal individual to the focal point, since it counts for density if (receiver_subpop == exerter_subpop) total_strength += strength_for_zero_distance; // Divide by the corresponding clipped integral to get density total_strength /= clipped_integrals->FloatAtIndex_NOCAST(0, nullptr); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(total_strength)); } else { // Loop over the requested individuals and get the totals EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(receivers_count); bool saw_error_1 = false, saw_error_2 = false, saw_error_3 = false, saw_error_4 = false; EIDOS_THREAD_COUNT(gEidos_OMP_threads_LOCALPOPDENSITY); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(receivers_count, receiver_subpop, exerter_subpop, receiver_subpop_data, exerter_subpop_data, kd_root_EXERTERS, strength_for_zero_distance, clipped_integrals_data, optimize_fixed_interaction_strengths) firstprivate(receivers_data, result_vec) reduction(||: saw_error_1) reduction(||: saw_error_2) reduction(||: saw_error_3) reduction(||: saw_error_4) if(!has_interaction_callbacks && (receivers_count >= EIDOS_OMPMIN_LOCALPOPDENSITY)) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error_1 = true; continue; } // SPECIES CONSISTENCY CHECK if (receiver_subpop != receiver->subpopulation_) { saw_error_2 = true; continue; } // Check constraints for the receiver; if the individual is disqualified, the local density of interacters is zero // Under OpenMP, raises can't go past the end of the parallel region; handle things the same way when not under OpenMP for simplicity try { if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises; protected { result_vec->set_float_no_check(0, receiver_index); continue; } } catch (...) { saw_error_3 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; double total_strength; SparseVector *sv; if (optimize_fixed_interaction_strengths) { // Optimized case for fixed interaction strength and no callbacks sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kPresences); try { FillSparseVectorForReceiverPresences(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, /* constraints_active */ true); uint32_t nnz; sv->Presences(&nnz); total_strength = nnz * if_param1_; } catch (...) { InteractionType::FreeSparseVector(sv); saw_error_4 = true; continue; } } else { // General case, totalling strengths sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kStrengths); try { FillSparseVectorForReceiverStrengths(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.evaluation_interaction_callbacks_); // we do not allow interaction() callbacks, so this should not raise // Get the sparse vector data uint32_t nnz; const sv_value_t *strengths; strengths = sv->Strengths(&nnz); // Total the interaction strengths total_strength = 0.0; for (uint32_t col_index = 0; col_index < nnz; ++col_index) total_strength += strengths[col_index]; } catch (...) { InteractionType::FreeSparseVector(sv); saw_error_4 = true; continue; } } // Add the interaction strength for the focal individual to the focal point, since it counts for density if (receiver_subpop == exerter_subpop) total_strength += strength_for_zero_distance; // Divide by the corresponding clipped integral to get density total_strength /= clipped_integrals_data[receiver_index]; result_vec->set_float_no_check(total_strength, receiver_index); FreeSparseVector(sv); } // deferred raises, for OpenMP compatibility if (saw_error_1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() requires receivers to be visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); if (saw_error_2) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() requires that all receivers be in the same subpopulation." << EidosTerminate(); if (saw_error_3) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): localPopulationDensity() tested a tag or tagL constraint, but a receiver's value for that property was not defined (had not been set)." << EidosTerminate(); if (saw_error_4) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_localPopulationDensity): an exception was caught inside a parallel region." << EidosTerminate(); return EidosValue_SP(result_vec); } } // // ********************* – (float)interactionDistance(object$ receiver, [No exerters = NULL]) EidosValue_SP InteractionType::ExecuteMethod_interactionDistance(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *receiver_value = p_arguments[0].get(); EidosValue *exerters_value = p_arguments[1].get(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactionDistance): interactionDistance() requires that the interaction be spatial." << EidosTerminate(); // receiver_value is guaranteed to be singleton; let's get the info on it Individual *receiver = (Individual *)receiver_value->ObjectElementAtIndex_NOCAST(0, nullptr); slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactionDistance): interactionDistance() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); Subpopulation *receiver_subpop = receiver->subpopulation_; Species &receiver_species = receiver_subpop->species_; CheckSpeciesCompatibility_Receiver(receiver_species); InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; // figure out the exerter subpopulation and get info on it bool exerters_value_NULL = (exerters_value->Type() == EidosValueType::kValueNULL); int exerters_count = exerters_value->Count(); if ((exerters_count == 0) && !exerters_value_NULL) return gStaticEidosValue_Float_ZeroVec; Subpopulation *exerter_subpop = (exerters_value_NULL ? receiver_subpop : ((Individual *)exerters_value->ObjectElementAtIndex_NOCAST(0, nullptr))->subpopulation_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); slim_popsize_t exerter_subpop_size = exerter_subpop->parent_subpop_size_; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); // set up our return value if (exerters_value_NULL) exerters_count = exerter_subpop_size; // Check constraints the receiver; if the individual is disqualified, the distance to each exerter is infinity if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises goto returnAllInfinity; if (exerters_value_NULL) { // NULL means return distances from individuals1 (which must be singleton) to all individuals in the subpopulation // We initialize the return vector to INFINITY, and fill in non-infinite values from the sparse vector SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); // If the k-d tree has no qualifying exerters, we return all infinity if (!kd_root_EXERTERS) goto returnAllInfinity; SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kDistances); uint32_t nnz; const uint32_t *columns; const sv_value_t *distances; try { FillSparseVectorForReceiverDistances(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, /* constraints_active */ true); distances = sv->Distances(&nnz, &columns); EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); EidosValue_SP result_SP(result_vec); double *result_ptr = result_vec->data_mutable(); for (int exerter_index = 0; exerter_index < exerter_subpop_size; ++exerter_index) *(result_ptr + exerter_index) = INFINITY; for (uint32_t col_index = 0; col_index < nnz; ++col_index) *(result_ptr + columns[col_index]) = distances[col_index]; InteractionType::FreeSparseVector(sv); return result_SP; } catch (...) { InteractionType::FreeSparseVector(sv); throw; } } else { // Otherwise, receiver is singleton, and exerters is any length, so we loop over exerters // To avoid searching through sparse vector results for each exerter, we calculate distances // ourselves for each receiver-exerter pair, which is a bit complicated - we need to worry // about constraints, self-interactions, max distance, callbacks, etc., which SparseVector // handles for most client code. Individual * const *exerters_data = (Individual * const *)exerters_value->ObjectData(); double *exerter_position_data = exerter_subpop_data.positions_; bool periodicity_enabled = (exerter_subpop_data.periodic_x_ || exerter_subpop_data.periodic_y_ || exerter_subpop_data.periodic_z_); EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); EidosValue_SP result_SP(result_vec); for (int exerter_index = 0; exerter_index < exerters_count; ++exerter_index) { Individual *exerter = exerters_data[exerter_index]; // SPECIES CONSISTENCY CHECK if (exerter_subpop != exerter->subpopulation_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactionDistance): interactionDistance() requires that all exerters be in the same subpopulation." << EidosTerminate(); slim_popsize_t exerter_index_in_subpop = exerter->index_; if (exerter_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_interactionDistance): interactionDistance() requires that exerters are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); if ((exerter == receiver) || !CheckIndividualConstraints(exerter, exerter_constraints_)) { // self-interactions and constraints result in an interaction distance of INF result_vec->set_float_no_check(INFINITY, exerter_index); } else { double distance; if (periodicity_enabled) distance = CalculateDistanceWithPeriodicity(receiver_position, exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY, exerter_subpop_data); else distance = CalculateDistance(receiver_position, exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY); if (distance > max_distance_) { // interactions beyond the maximum interaction distance also produce INF result_vec->set_float_no_check(INFINITY, exerter_index); } else { result_vec->set_float_no_check(distance, exerter_index); } } } return result_SP; } returnAllInfinity: { EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); EidosValue_SP result_SP(result_vec); double *result_ptr = result_vec->data_mutable(); for (int exerter_index = 0; exerter_index < exerter_subpop_size; ++exerter_index) *(result_ptr + exerter_index) = INFINITY; return result_SP; } } // ********************* – (object)nearestInteractingNeighbors(object receiver, [integer$ count = 1], [No$ exerterSubpop = NULL], [logical$ returnDict = F]) // EidosValue_SP InteractionType::ExecuteMethod_nearestInteractingNeighbors(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // BCH 11/2/2023: Note that this method is now almost identical to ExecuteMethod_nearestNeighbors() EidosValue *receiver_value = p_arguments[0].get(); EidosValue *count_value = p_arguments[1].get(); EidosValue *exerterSubpop_value = p_arguments[2].get(); EidosValue *returnDict_value = p_arguments[3].get(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestInteractingNeighbors): nearestInteractingNeighbors() requires that the interaction be spatial." << EidosTerminate(); eidos_logical_t returnDict = returnDict_value->LogicalAtIndex_NOCAST(0, nullptr); Subpopulation *receiver_subpop = nullptr; Individual * const *receiver_data = (Individual * const *)receiver_value->ObjectData(); if (!returnDict) { // This is the single-threaded, single-receiver case; it returns a vector of Individual objects if (receiver_value->Count() != 1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestInteractingNeighbors): nearestInteractingNeighbors() requires that the receiver is singleton when returnDict is F; if you want to process multiple receivers in a single call, pass returnDict=T." << EidosTerminate(); Individual *receiver = receiver_data[0]; receiver_subpop = receiver->subpopulation_; } else { // This is the multi-threaded, multi-receiver case; it returns a Dictionary object with vectors of Individual objects if (receiver_value->Count() == 0) { // With no receivers, return an empty Dictionary EidosDictionaryRetained *dictionary = new EidosDictionaryRetained(); EidosValue_SP result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(dictionary, gEidosDictionaryRetained_Class)); dictionary->ContentsChanged("InteractionType::ExecuteMethod_nearestInteractingNeighbors()"); // objectElement is now retained by result_SP, so we can release it dictionary->Release(); return result_SP; } receiver_subpop = receiver_data[0]->subpopulation_; } // shared logic for both cases Species &receiver_species = receiver_subpop->species_; CheckSpeciesCompatibility_Receiver(receiver_species); InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); // the exerter subpopulation defaults to the same subpop as the receivers Subpopulation *exerter_subpop = ((exerterSubpop_value->Type() == EidosValueType::kValueNULL) ? receiver_subpop : (Subpopulation *)exerterSubpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); // Check the count slim_popsize_t exerter_subpop_size = exerter_subpop->parent_subpop_size_; int64_t count = count_value->IntAtIndex_NOCAST(0, nullptr); if (count < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestInteractingNeighbors): nearestInteractingNeighbors() requires count >= 0." << EidosTerminate(); if (count > exerter_subpop_size) count = exerter_subpop_size; if (!returnDict) { // This is the single-threaded, single-receiver case; it returns a vector of Individual objects if (count == 0) return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); // receiver_value is guaranteed to be singleton; let's get the info on it Individual *receiver = receiver_data[0]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestInteractingNeighbors): nearestInteractingNeighbors() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); // Check constraints for the receiver; if the individual is disqualified, there are no interacting neighbors if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); // Find the neighbors double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); EidosValue_Object *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); if (count < exerter_subpop_size) // reserve only if we are finding fewer than every possible neighbor result_vec->reserve((int)count); FindNeighbors(exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.kd_node_count_EXERTERS_, receiver_position, (int)count, *result_vec, receiver, /* constraints_active */ true); return EidosValue_SP(result_vec); } else { // This is the multi-threaded, multi-receiver case; it returns a Dictionary object vectors of Individual objects // We start by making a Dictionary with an empty Individual vector for each receiver EidosDictionaryRetained *dictionary = new EidosDictionaryRetained(); EidosValue_SP result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(dictionary, gEidosDictionaryRetained_Class)); int receivers_count = receiver_value->Count(); EidosValue_Object **result_vectors = (EidosValue_Object **)malloc(receivers_count * sizeof(EidosValue_Object *)); for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { EidosValue_Object *empty_individuals_vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class); dictionary->SetKeyValue_IntegerKeys(receiver_index, EidosValue_SP(empty_individuals_vec)); result_vectors[receiver_index] = empty_individuals_vec; } dictionary->ContentsChanged("InteractionType::ExecuteMethod_nearestInteractingNeighbors()"); // objectElement is now retained by result_SP, so we can release it dictionary->Release(); if (count > 0) { bool saw_error_1 = false, saw_error_2 = false, saw_error_3 = false; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); EIDOS_THREAD_COUNT(gEidos_OMP_threads_NEARESTINTNEIGH); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(receivers_count, receiver_subpop, exerter_subpop, receiver_subpop_data, exerter_subpop_data, kd_root_EXERTERS) firstprivate(receiver_data, result_vectors, count, exerter_subpop_size) reduction(||: saw_error_1) reduction(||: saw_error_2) reduction(||: saw_error_3) if(receivers_count >= EIDOS_OMPMIN_NEARESTINTNEIGH) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { Individual *receiver = receiver_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error_1 = true; continue; } // SPECIES CONSISTENCY CHECK if (receiver_subpop != receiver->subpopulation_) { saw_error_2 = true; continue; } // Check constraints for the receiver; if the individual is disqualified, there are no interacting neighbors // Under OpenMP, raises can't go past the end of the parallel region; handle things the same way when not under OpenMP for simplicity try { if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises; protected continue; } catch (...) { saw_error_3 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; EidosValue_Object *result_vec = result_vectors[receiver_index]; if (count < exerter_subpop_size) // reserve only if we are finding fewer than every possible neighbor result_vec->reserve((int)count); FindNeighbors(exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.kd_node_count_EXERTERS_, receiver_position, (int)count, *result_vec, receiver, /* constraints_active */ true); } // deferred raises, for OpenMP compatibility if (saw_error_1) { free(result_vectors); EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestInteractingNeighbors): nearestInteractingNeighbors() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); } if (saw_error_2) { free(result_vectors); EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestInteractingNeighbors): nearestInteractingNeighbors() requires that all receivers be in the same subpopulation." << EidosTerminate(); } if (saw_error_3) { free(result_vectors); EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestInteractingNeighbors): nearestInteractingNeighbors() tested a tag or tagL constraint, but a receiver's value for that property was not defined (had not been set)." << EidosTerminate(); } } free(result_vectors); return result_SP; } } // ********************* – (object)nearestNeighbors(object receiver, [integer$ count = 1], [No$ exerterSubpop = NULL], [logical$ returnDict = F]) // EidosValue_SP InteractionType::ExecuteMethod_nearestNeighbors(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // BCH 11/2/2023: Note that this method is now almost identical to ExecuteMethod_nearestInteractingNeighbors() EidosValue *receiver_value = p_arguments[0].get(); EidosValue *count_value = p_arguments[1].get(); EidosValue *exerterSubpop_value = p_arguments[2].get(); EidosValue *returnDict_value = p_arguments[3].get(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighbors): nearestNeighbors() requires that the interaction be spatial." << EidosTerminate(); eidos_logical_t returnDict = returnDict_value->LogicalAtIndex_NOCAST(0, nullptr); Subpopulation *receiver_subpop = nullptr; Individual * const *receiver_data = (Individual * const *)receiver_value->ObjectData(); if (!returnDict) { // This is the single-threaded, single-receiver case; it returns a vector of Individual objects if (receiver_value->Count() != 1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighbors): nearestNeighbors() requires that the receiver is singleton when returnDict is F; if you want to process multiple receivers in a single call, pass returnDict=T." << EidosTerminate(); Individual *receiver = receiver_data[0]; receiver_subpop = receiver->subpopulation_; } else { // This is the multi-threaded, multi-receiver case; it returns a Dictionary object with vectors of Individual objects if (receiver_value->Count() == 0) { // With no receivers, return an empty Dictionary EidosDictionaryRetained *dictionary = new EidosDictionaryRetained(); EidosValue_SP result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(dictionary, gEidosDictionaryRetained_Class)); dictionary->ContentsChanged("InteractionType::ExecuteMethod_nearestNeighbors()"); // objectElement is now retained by result_SP, so we can release it dictionary->Release(); return result_SP; } receiver_subpop = receiver_data[0]->subpopulation_; } // shared logic for both cases Species &receiver_species = receiver_subpop->species_; CheckSpeciesCompatibility_Generic(receiver_species); InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); // the exerter subpopulation defaults to the same subpop as the receivers Subpopulation *exerter_subpop = ((exerterSubpop_value->Type() == EidosValueType::kValueNULL) ? receiver_subpop : (Subpopulation *)exerterSubpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); CheckSpeciesCompatibility_Generic(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); // Check the count slim_popsize_t exerter_subpop_size = exerter_subpop->parent_subpop_size_; int64_t count = count_value->IntAtIndex_NOCAST(0, nullptr); if (count < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighbors): nearestNeighbors() requires count >= 0." << EidosTerminate(); if (count > exerter_subpop_size) count = exerter_subpop_size; if (!returnDict) { // This is the single-threaded, single-receiver case; it returns a vector of Individual objects if (count == 0) return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); // receiver_value is guaranteed to be singleton; let's get the info on it Individual *receiver = receiver_data[0]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighbors): nearestNeighbors() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); // Find the neighbors double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_ALL = EnsureKDTreePresent_ALL(exerter_subpop, exerter_subpop_data); EidosValue_Object *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); if (count < exerter_subpop_size) // reserve only if we are finding fewer than every possible neighbor result_vec->reserve((int)count); FindNeighbors(exerter_subpop, kd_root_ALL, exerter_subpop_data.kd_node_count_ALL_, receiver_position, (int)count, *result_vec, receiver, /* constraints_active */ false); return EidosValue_SP(result_vec); } else { // This is the multi-threaded, multi-receiver case; it returns a Dictionary object vectors of Individual objects // We start by making a Dictionary with an empty Individual vector for each receiver EidosDictionaryRetained *dictionary = new EidosDictionaryRetained(); EidosValue_SP result_SP = EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(dictionary, gEidosDictionaryRetained_Class)); int receivers_count = receiver_value->Count(); EidosValue_Object **result_vectors = (EidosValue_Object **)malloc(receivers_count * sizeof(EidosValue_Object *)); for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { EidosValue_Object *empty_individuals_vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class); dictionary->SetKeyValue_IntegerKeys(receiver_index, EidosValue_SP(empty_individuals_vec)); result_vectors[receiver_index] = empty_individuals_vec; } dictionary->ContentsChanged("InteractionType::ExecuteMethod_nearestNeighbors()"); // objectElement is now retained by result_SP, so we can release it dictionary->Release(); if (count > 0) { bool saw_error_1 = false, saw_error_2 = false; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_ALL = EnsureKDTreePresent_ALL(exerter_subpop, exerter_subpop_data); EIDOS_THREAD_COUNT(gEidos_OMP_threads_NEARESTNEIGH); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(receivers_count, receiver_subpop, exerter_subpop, receiver_subpop_data, exerter_subpop_data, kd_root_ALL) firstprivate(receiver_data, result_vectors, count, exerter_subpop_size) reduction(||: saw_error_1) reduction(||: saw_error_2) if(receivers_count >= EIDOS_OMPMIN_NEARESTNEIGH) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { Individual *receiver = receiver_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error_1 = true; continue; } // SPECIES CONSISTENCY CHECK if (receiver_subpop != receiver->subpopulation_) { saw_error_2 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; EidosValue_Object *result_vec = result_vectors[receiver_index]; if (count < exerter_subpop_size) // reserve only if we are finding fewer than every possible neighbor result_vec->reserve((int)count); FindNeighbors(exerter_subpop, kd_root_ALL, exerter_subpop_data.kd_node_count_ALL_, receiver_position, (int)count, *result_vec, receiver, /* constraints_active */ false); } // deferred raises, for OpenMP compatibility if (saw_error_1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighbors): nearestNeighbors() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); if (saw_error_2) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighbors): nearestNeighbors() requires that all receivers be in the same subpopulation." << EidosTerminate(); } free(result_vectors); return result_SP; } } // ********************* – (object)nearestNeighborsOfPoint(float point, io$ exerterSubpop, [integer$ count = 1]) // EidosValue_SP InteractionType::ExecuteMethod_nearestNeighborsOfPoint(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *point_value = p_arguments[0].get(); EidosValue *exerterSubpop_value = p_arguments[1].get(); EidosValue *count_value = p_arguments[2].get(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighborsOfPoint): nearestNeighborsOfPoint() requires that the interaction be spatial." << EidosTerminate(); // Check the subpop Subpopulation *exerter_subpop = SLiM_ExtractSubpopulationFromEidosValue_io(exerterSubpop_value, 0, &community_, nullptr, "nearestNeighborsOfPoint()"); Species &exerter_species = exerter_subpop->species_; CheckSpeciesCompatibility_Generic(exerter_species); InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_ALL = EnsureKDTreePresent_ALL(exerter_subpop, exerter_subpop_data); // Check the point if (point_value->Count() != spatiality_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighborsOfPoint): nearestNeighborsOfPoint() requires that point is of length equal to the interaction spatiality." << EidosTerminate(); double point_array[SLIM_MAX_DIMENSIONALITY]; for (int point_index = 0; point_index < spatiality_; ++point_index) point_array[point_index] = point_value->FloatAtIndex_NOCAST(point_index, nullptr); // Check the count slim_popsize_t exerter_subpop_size = exerter_subpop->parent_subpop_size_; int64_t count = count_value->IntAtIndex_NOCAST(0, nullptr); if (count < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_nearestNeighborsOfPoint): nearestNeighborsOfPoint() requires count >= 0." << EidosTerminate(); if (count > exerter_subpop_data.kd_node_count_ALL_) count = exerter_subpop_data.kd_node_count_ALL_; if (count == 0) return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); // Find the neighbors EidosValue_Object *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); if (count < exerter_subpop_size) // reserve only if we are finding fewer than every possible neighbor result_vec->reserve((int)count); FindNeighbors(exerter_subpop, kd_root_ALL, exerter_subpop_data.kd_node_count_ALL_, point_array, (int)count, *result_vec, nullptr, /* constraints_active */ false); return EidosValue_SP(result_vec); } // ********************* – (integer)neighborCount(object receivers, [No$ exerterSubpop = NULL]) // EidosValue_SP InteractionType::ExecuteMethod_neighborCount(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) // BCH 11/2/2023: Note that this method is now almost identical to ExecuteMethod_interactingNeighborCount() EidosValue *receivers_value = p_arguments[0].get(); EidosValue *exerterSubpop_value = p_arguments[1].get(); int receivers_count = receivers_value->Count(); Individual * const *receivers_data = (Individual * const *)receivers_value->ObjectData(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_neighborCount): neighborCount() requires that the interaction be spatial." << EidosTerminate(); if (receivers_count == 0) return gStaticEidosValue_Integer_ZeroVec; // the exerter subpopulation defaults to the same subpop as the receivers Subpopulation *receiver_subpop = receivers_data[0]->subpopulation_; Subpopulation *exerter_subpop = ((exerterSubpop_value->Type() == EidosValueType::kValueNULL) ? receiver_subpop : (Subpopulation *)exerterSubpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); CheckSpeciesCompatibility_Generic(receiver_subpop->species_); CheckSpeciesCompatibility_Generic(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_ALL = EnsureKDTreePresent_ALL(exerter_subpop, exerter_subpop_data); // If there are no individuals in the tree, short-circuit if (!kd_root_ALL) { // If the exerter subpop is empty then all count values for the receivers are zero if (receivers_count == 1) { return gStaticEidosValue_Integer0; } else { EidosValue_Int *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(receivers_count); for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) result_vec->set_int_no_check(0, receiver_index); return EidosValue_SP(result_vec); } } InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); if (receivers_count == 1) { // Just one value, so we can return a singleton and skip some work Individual *receiver = receivers_data[0]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_neighborCount): neighborCount() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); // Find the neighbors double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; slim_popsize_t focal_individual_index = (exerter_subpop == receiver_subpop) ? receiver_index_in_subpop : -1; int neighborCount; switch (spatiality_) { case 1: neighborCount = CountNeighbors_1(kd_root_ALL, receiver_position, focal_individual_index); break; case 2: neighborCount = CountNeighbors_2(kd_root_ALL, receiver_position, focal_individual_index, 0); break; case 3: neighborCount = CountNeighbors_3(kd_root_ALL, receiver_position, focal_individual_index, 0); break; default: neighborCount = 0; break; // unsupported value } return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(neighborCount)); } else { EidosValue_Int *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Int())->resize_no_initialize(receivers_count); bool saw_error_1 = false, saw_error_2 = false; EIDOS_THREAD_COUNT(gEidos_OMP_threads_NEIGHCOUNT); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(receivers_count, receiver_subpop, exerter_subpop, receiver_subpop_data, exerter_subpop_data, kd_root_ALL) firstprivate(receivers_data, result_vec) reduction(||: saw_error_1) reduction(||: saw_error_2) if(receivers_count >= EIDOS_OMPMIN_NEIGHCOUNT) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error_1 = true; continue; } // SPECIES CONSISTENCY CHECK if (receiver_subpop != receiver->subpopulation_) { saw_error_2 = true; continue; } // Find the neighbors double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; slim_popsize_t focal_individual_index = (exerter_subpop == receiver_subpop) ? receiver_index_in_subpop : -1; int neighborCount; switch (spatiality_) { case 1: neighborCount = CountNeighbors_1(kd_root_ALL, receiver_position, focal_individual_index); break; case 2: neighborCount = CountNeighbors_2(kd_root_ALL, receiver_position, focal_individual_index, 0); break; case 3: neighborCount = CountNeighbors_3(kd_root_ALL, receiver_position, focal_individual_index, 0); break; default: neighborCount = 0; break; // unsupported value } result_vec->set_int_no_check(neighborCount, receiver_index); } // deferred raises, for OpenMP compatibility if (saw_error_1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_neighborCount): neighborCount() requires receivers to be visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); if (saw_error_2) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_neighborCount): neighborCount() requires that all receivers be in the same subpopulation." << EidosTerminate(); return EidosValue_SP(result_vec); } } // ********************* – (integer$)neighborCountOfPoint(float point, io$ exerterSubpop) // EidosValue_SP InteractionType::ExecuteMethod_neighborCountOfPoint(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *point_value = p_arguments[0].get(); EidosValue *exerterSubpop_value = p_arguments[1].get(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_neighborCountOfPoint): neighborCountOfPoint() requires that the interaction be spatial." << EidosTerminate(); // Check the subpop Subpopulation *exerter_subpop = SLiM_ExtractSubpopulationFromEidosValue_io(exerterSubpop_value, 0, &community_, nullptr, "nearestNeighborsOfPoint()"); Species &exerter_species = exerter_subpop->species_; CheckSpeciesCompatibility_Generic(exerter_species); InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_ALL = EnsureKDTreePresent_ALL(exerter_subpop, exerter_subpop_data); if (!kd_root_ALL) return gStaticEidosValue_Integer0; // Check the point if (point_value->Count() != spatiality_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_neighborCountOfPoint): neighborCountOfPoint() requires that point is of length equal to the interaction spatiality." << EidosTerminate(); double point_array[SLIM_MAX_DIMENSIONALITY]; for (int point_index = 0; point_index < spatiality_; ++point_index) point_array[point_index] = point_value->FloatAtIndex_NOCAST(point_index, nullptr); // Find the neighbors int neighborCount; switch (spatiality_) { case 1: neighborCount = CountNeighbors_1(kd_root_ALL, point_array, -1); break; case 2: neighborCount = CountNeighbors_2(kd_root_ALL, point_array, -1, 0); break; case 3: neighborCount = CountNeighbors_3(kd_root_ALL, point_array, -1, 0); break; default: EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_neighborCountOfPoint): (internal error) unsupported spatiality" << EidosTerminate(); } return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(neighborCount)); } // ********************* - (void)setConstraints(string$ who, [Ns$ sex = NULL], [Ni$ tag = NULL], [Ni$ minAge = NULL], [Ni$ maxAge = NULL], [Nl$ migrant = NULL], // [Nl$ tagL0 = NULL], [Nl$ tagL1 = NULL], [Nl$ tagL2 = NULL], [Nl$ tagL3 = NULL], [Nl$ tagL4 = NULL]) // EidosValue_SP InteractionType::ExecuteMethod_setConstraints(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (AnyEvaluated()) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() cannot be called while the interaction is being evaluated; call unevaluate() first, or call setConstraints() prior to evaluation of the interaction." << EidosTerminate(); EidosValue *who_value = p_arguments[0].get(); std::string who = who_value->StringAtIndex_NOCAST(0, nullptr); bool set_receiver_constraints = false; bool set_exerter_constraints = false; if (who == "receiver") { set_receiver_constraints = true; } else if (who == "exerter") { set_exerter_constraints = true; } else if (who == "both") { set_receiver_constraints = true; set_exerter_constraints = true; } else EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() requires the parameter who to be one of 'receiver', 'exerter', or 'both'." << EidosTerminate(); // do two passes, one for receiver constraints, one for exerter constraints for (int i = 0; i <= 1; ++i) { InteractionConstraints *constraints = nullptr; if (i == 0) { if (!set_receiver_constraints) continue; constraints = &receiver_constraints_; } if (i == 1) { if (!set_exerter_constraints) continue; constraints = &exerter_constraints_; } // first turn off all constraints constraints->has_constraints_ = false; constraints->sex_ = IndividualSex::kUnspecified; constraints->has_nonsex_constraints_ = false; constraints->tag_ = SLIM_TAG_UNSET_VALUE; constraints->min_age_ = -1; constraints->max_age_ = -1; constraints->migrant_ = -1; constraints->has_tagL_constraints_ = false; constraints->tagL0_ = -1; constraints->tagL1_ = -1; constraints->tagL2_ = -1; constraints->tagL3_ = -1; constraints->tagL4_ = -1; // then turn on constraints as requested EidosValue *sex_value = p_arguments[1].get(); if (sex_value->Type() != EidosValueType::kValueNULL) { std::string sex = sex_value->StringAtIndex_NOCAST(0, nullptr); if (sex == "M") { constraints->sex_ = IndividualSex::kMale; constraints->has_constraints_ = true; } else if (sex == "F") { constraints->sex_ = IndividualSex::kFemale; constraints->has_constraints_ = true; } else if (sex == "*") constraints->sex_ = IndividualSex::kUnspecified; else EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() requires the parameter sex to be 'M', 'F', or '*'." << EidosTerminate(); } EidosValue *tag_value = p_arguments[2].get(); if (tag_value->Type() != EidosValueType::kValueNULL) { slim_usertag_t tag = tag_value->IntAtIndex_NOCAST(0, nullptr); constraints->tag_ = tag; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; } EidosValue *minAge_value = p_arguments[3].get(); if (minAge_value->Type() != EidosValueType::kValueNULL) { if (community_.ModelType() == SLiMModelType::kModelTypeWF) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() cannot set a minAge constraint in a WF model (since the WF model is of non-overlapping generations)." << EidosTerminate(); slim_age_t minAge = SLiMCastToAgeTypeOrRaise(minAge_value->IntAtIndex_NOCAST(0, nullptr)); if ((minAge <= 0) || (minAge > 100000)) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() requires the parameter minAge to be >= 0 and <= 100000." << EidosTerminate(); constraints->min_age_ = minAge; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; } EidosValue *maxAge_value = p_arguments[4].get(); if (maxAge_value->Type() != EidosValueType::kValueNULL) { if (community_.ModelType() == SLiMModelType::kModelTypeWF) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() cannot set a maxAge constraint in a WF model (since the WF model is of non-overlapping generations)." << EidosTerminate(); slim_age_t maxAge = SLiMCastToAgeTypeOrRaise(maxAge_value->IntAtIndex_NOCAST(0, nullptr)); if ((maxAge <= 0) || (maxAge > 100000)) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() requires the parameter maxAge to be >= 0 and <= 100000." << EidosTerminate(); constraints->max_age_ = maxAge; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; } if ((constraints->min_age_ != -1) && (constraints->max_age_ != -1) && (constraints->min_age_ > constraints->max_age_)) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setConstraints): setConstraints() requires minAge <= maxAge." << EidosTerminate(); EidosValue *migrant_value = p_arguments[5].get(); if (migrant_value->Type() != EidosValueType::kValueNULL) { eidos_logical_t migrant = migrant_value->LogicalAtIndex_NOCAST(0, nullptr); constraints->migrant_ = migrant; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; } EidosValue *tagL0_value = p_arguments[6].get(); if (tagL0_value->Type() != EidosValueType::kValueNULL) { eidos_logical_t tagL0 = tagL0_value->LogicalAtIndex_NOCAST(0, nullptr); constraints->tagL0_ = tagL0; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; constraints->has_tagL_constraints_ = true; } EidosValue *tagL1_value = p_arguments[7].get(); if (tagL1_value->Type() != EidosValueType::kValueNULL) { eidos_logical_t tagL1 = tagL1_value->LogicalAtIndex_NOCAST(0, nullptr); constraints->tagL1_ = tagL1; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; constraints->has_tagL_constraints_ = true; } EidosValue *tagL2_value = p_arguments[8].get(); if (tagL2_value->Type() != EidosValueType::kValueNULL) { eidos_logical_t tagL2 = tagL2_value->LogicalAtIndex_NOCAST(0, nullptr); constraints->tagL2_ = tagL2; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; constraints->has_tagL_constraints_ = true; } EidosValue *tagL3_value = p_arguments[9].get(); if (tagL3_value->Type() != EidosValueType::kValueNULL) { eidos_logical_t tagL3 = tagL3_value->LogicalAtIndex_NOCAST(0, nullptr); constraints->tagL3_ = tagL3; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; constraints->has_tagL_constraints_ = true; } EidosValue *tagL4_value = p_arguments[10].get(); if (tagL4_value->Type() != EidosValueType::kValueNULL) { eidos_logical_t tagL4 = tagL4_value->LogicalAtIndex_NOCAST(0, nullptr); constraints->tagL4_ = tagL4; constraints->has_constraints_ = true; constraints->has_nonsex_constraints_ = true; constraints->has_tagL_constraints_ = true; } } // mark that interaction types changed, so they get redisplayed in SLiMgui community_.interaction_types_changed_ = true; return gStaticEidosValueVOID; } // ********************* - (void)setInteractionFunction(string$ functionType, ...) // EidosValue_SP InteractionType::ExecuteMethod_setInteractionFunction(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (AnyEvaluated()) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setInteractionFunction): setInteractionFunction() cannot be called while the interaction is being evaluated; call unevaluate() first, or call setInteractionFunction() prior to evaluation of the interaction." << EidosTerminate(); // SpatialKernel parses and bounds-checks our arguments for us SpatialKernelType k_type; int k_param_count; int kernel_count = SpatialKernel::PreprocessArguments(spatiality_, max_distance_, p_arguments, 0, /* p_expect_max_density */ true, &k_type, &k_param_count); if (kernel_count != 1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_setInteractionFunction): setInteractionFunction() requires a single kernel; all kernel definition arguments must be singletons." << EidosTerminate(); SpatialKernel kernel(spatiality_, max_distance_, p_arguments, 0, 0, /* p_expect_max_density */ true, k_type, k_param_count); // Everything seems to be in order, so replace our IF info with the new info // FIXME we could consider actually keeping an internal SpatialKernel instance permanently if_type_ = kernel.kernel_type_; if_param1_ = kernel.kernel_param1_; if_param2_ = kernel.kernel_param2_; if_param3_ = kernel.kernel_param3_; n_2param2sq_ = kernel.n_2param2sq_; // mark that interaction types changed, so they get redisplayed in SLiMgui community_.interaction_types_changed_ = true; // changing the interaction function invalidates the cached clipped_integral_ buffer; we don't deallocate it, just invalidate it clipped_integral_valid_ = false; return gStaticEidosValueVOID; } // ********************* – (float)strength(object$ receiver, [No exerters = NULL]) // EidosValue_SP InteractionType::ExecuteMethod_strength(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *receiver_value = p_arguments[0].get(); EidosValue *exerters_value = p_arguments[1].get(); // receiver_value is guaranteed to be singleton; let's get the info on it Individual *receiver = (Individual *)receiver_value->ObjectElementAtIndex_NOCAST(0, nullptr); slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_strength): strength() requires that the receiver is visible in a subpopulation (i.e., not a new juvenile)." << EidosTerminate(); Subpopulation *receiver_subpop = receiver->subpopulation_; Species &receiver_species = receiver_subpop->species_; CheckSpeciesCompatibility_Receiver(receiver_species); // figure out the exerter subpopulation and get info on it bool exerters_value_NULL = (exerters_value->Type() == EidosValueType::kValueNULL); int exerters_count = exerters_value->Count(); if ((exerters_count == 0) && !exerters_value_NULL) return gStaticEidosValue_Float_ZeroVec; Subpopulation *exerter_subpop = (exerters_value_NULL ? receiver_subpop : ((Individual *)exerters_value->ObjectElementAtIndex_NOCAST(0, nullptr))->subpopulation_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); slim_popsize_t exerter_subpop_size = exerter_subpop->parent_subpop_size_; InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); if (exerters_value_NULL) exerters_count = exerter_subpop_size; // Check constraints for the receiver; if the individual is disqualified, the strength to each exerter is zero if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises goto returnAllZero; if (spatiality_) { // Spatial case; we use the k-d tree to get strengths for all neighbors. // BCH 5/14/2023: The call to FillSparseVectorForReceiverStrengths() means we run interaction() callbacks, // so if this code is ever parallelized, it should stay single-threaded when callbacks are enabled. InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; std::vector &interaction_callbacks = exerter_subpop_data.evaluation_interaction_callbacks_; bool has_interaction_callbacks = (interaction_callbacks.size() > 0); if (exerters_value_NULL) { // NULL means return distances from individuals1 (which must be singleton) to all individuals in the subpopulation // We initialize the return vector to 0, and fill in non-zero values from the sparse vector SLiM_kdNode *kd_root_EXERTERS = (spatiality_ ? EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data) : nullptr); // If the k-d tree has no qualifying exerters, we return all zeros if (!kd_root_EXERTERS) goto returnAllZero; SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kStrengths); uint32_t nnz; const uint32_t *columns; const sv_value_t *strengths; try { FillSparseVectorForReceiverStrengths(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, interaction_callbacks); strengths = sv->Strengths(&nnz, &columns); EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); EidosValue_SP result_SP(result_vec); double *result_ptr = result_vec->data_mutable(); EIDOS_BZERO(result_ptr, exerter_subpop_size * sizeof(double)); for (uint32_t col_index = 0; col_index < nnz; ++col_index) *(result_ptr + columns[col_index]) = strengths[col_index]; InteractionType::FreeSparseVector(sv); return result_SP; } catch (...) { InteractionType::FreeSparseVector(sv); throw; } } else { // Otherwise, receiver is singleton, and exerters_value is any length, so we loop over exerters // To avoid searching through sparse vector results for each exerter, we calculate distances and // strengths ourselves for each receiver-exerter pair, which is a bit complicated - we need to // worry about constraints, self-interactions, max distance, callbacks, etc., which SparseVector // handles for most client code. Individual * const *exerters_data = (Individual * const *)exerters_value->ObjectData(); double *exerter_position_data = exerter_subpop_data.positions_; bool periodicity_enabled = (exerter_subpop_data.periodic_x_ || exerter_subpop_data.periodic_y_ || exerter_subpop_data.periodic_z_); EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); EidosValue_SP result_SP(result_vec); for (int exerter_index = 0; exerter_index < exerters_count; ++exerter_index) { Individual *exerter = exerters_data[exerter_index]; // SPECIES CONSISTENCY CHECK if (exerter_subpop != exerter->subpopulation_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_strength): strength() requires that all exerters be in the same subpopulation." << EidosTerminate(); slim_popsize_t exerter_index_in_subpop = exerter->index_; if (exerter_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_strength): strength() requires that exerters are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); if ((exerter == receiver) || !CheckIndividualConstraints(exerter, exerter_constraints_)) { // self-interactions and constraints result in an interaction strength of 0.0 result_vec->set_float_no_check(0.0, exerter_index); } else { double distance; if (periodicity_enabled) distance = CalculateDistanceWithPeriodicity(receiver_position, exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY, exerter_subpop_data); else distance = CalculateDistance(receiver_position, exerter_position_data + (size_t)exerter_index_in_subpop * SLIM_MAX_DIMENSIONALITY); if (distance > max_distance_) { // interactions beyond the maximum interaction distance also produce 0.0 result_vec->set_float_no_check(0.0, exerter_index); } else { double strength; if (has_interaction_callbacks) strength = CalculateStrengthWithCallbacks(distance, receiver, exerter, interaction_callbacks); else strength = CalculateStrengthNoCallbacks(distance); result_vec->set_float_no_check(strength, exerter_index); } } } return result_SP; } } else { // Non-spatial case; no distances used. We have to worry about exerter constraints; they are not handled for us. // BCH 5/14/2023: We call ApplyInteractionCallbacks() below, so if this code ever goes parallel it // should stay single-threaded if/when any interaction() callbacks are present! slim_popsize_t receiver_index = ((exerter_subpop == receiver->subpopulation_) && (receiver->index_ >= 0) ? receiver->index_ : -1); std::vector &callbacks = exerter_subpop_data.evaluation_interaction_callbacks_; if (exerters_value->Type() == EidosValueType::kValueNULL) { // NULL means return strengths to individuals1 (which must be singleton) from all individuals in the subpopulation EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerter_subpop_size); for (int exerter_index = 0; exerter_index < exerter_subpop_size; ++exerter_index) { double strength = 0; if (exerter_index != receiver_index) { Individual *exerter = exerter_subpop->parent_individuals_[exerter_index]; if (CheckIndividualConstraints(exerter, exerter_constraints_)) // potentially raises strength = ApplyInteractionCallbacks(receiver, exerter, if_param1_, NAN, callbacks); // hard-coding interaction function "f" (SpatialKernelType::kFixed), which is required } result_vec->set_float_no_check(strength, exerter_index); } return EidosValue_SP(result_vec); } else { // Otherwise, individuals1 is singleton, and exerters_value is any length, so we loop over exerters_value EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); Individual * const *exerters_data = (Individual * const *)exerters_value->ObjectData(); for (int exerter_index = 0; exerter_index < exerters_count; ++exerter_index) { Individual *exerter = exerters_data[exerter_index]; if (exerter_subpop != exerter->subpopulation_) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_strength): strength() requires that all individuals be in the same subpopulation." << EidosTerminate(); slim_popsize_t exerter_index_in_subpop = exerter->index_; if (exerter_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_strength): strength() requires that exerters are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); double strength = 0; if ((exerter_index_in_subpop != receiver_index) && CheckIndividualConstraints(exerter, exerter_constraints_)) // potentially raises strength = ApplyInteractionCallbacks(receiver, exerter, if_param1_, NAN, callbacks); // hard-coding interaction function "f" (SpatialKernelType::kFixed), which is required result_vec->set_float_no_check(strength, exerter_index); } return EidosValue_SP(result_vec); } } returnAllZero: { EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(exerters_count); EidosValue_SP result_SP(result_vec); EIDOS_BZERO(result_vec->data_mutable(), exerter_subpop_size * sizeof(double)); return result_SP; } } // ********************* – (lo)testConstraints(object individuals, string$ constraints, [logical$ returnIndividuals = F]) // EidosValue_SP InteractionType::ExecuteMethod_testConstraints(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *individuals_value = p_arguments[0].get(); EidosValue_String *constraints_value = (EidosValue_String *)p_arguments[1].get(); EidosValue *returnIndividuals_value = p_arguments[2].get(); int individuals_count = individuals_value->Count(); Individual * const *individuals_data = (Individual * const *)individuals_value->ObjectData(); const std::string &constraints_str = constraints_value->StringRefAtIndex_NOCAST(0, nullptr); InteractionConstraints *constraints; if (constraints_str == "receiver") constraints = &receiver_constraints_; else if (constraints_str == "exerter") constraints = &exerter_constraints_; else EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_testConstraints): testConstraints() requires that parameter constraints be 'receiver' or 'exerter'." << EidosTerminate(); bool returnIndividuals = returnIndividuals_value->LogicalAtIndex_NOCAST(0, nullptr); if (individuals_count == 1) { // singleton case Individual *ind = individuals_data[0]; if (CheckIndividualConstraints(ind, *constraints)) { if (returnIndividuals) return p_arguments[0]; // return the individual as it was passed in else return gStaticEidosValue_LogicalT; } else { if (returnIndividuals) return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); else return gStaticEidosValue_LogicalF; } } else { // non-singleton case if (returnIndividuals) { EidosValue_Object *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Object(gSLiM_Individual_Class)); for (int index = 0; index < individuals_count; ++index) { Individual *ind = individuals_data[index]; if (CheckIndividualConstraints(ind, *constraints)) result_vec->push_object_element_NORR(ind); } return EidosValue_SP(result_vec); } else { EidosValue_Logical *result_vec = new (gEidosValuePool->AllocateChunk()) EidosValue_Logical(); result_vec->resize_no_initialize(individuals_count); for (int index = 0; index < individuals_count; ++index) { Individual *ind = individuals_data[index]; bool satisfied = CheckIndividualConstraints(ind, *constraints); result_vec->set_logical_no_check(satisfied, index); } return EidosValue_Logical_SP(result_vec); } } } // ********************* – (float)totalOfNeighborStrengths(object receivers, [No$ exerterSubpop = NULL]) // EidosValue_SP InteractionType::ExecuteMethod_totalOfNeighborStrengths(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EidosValue *receivers_value = p_arguments[0].get(); EidosValue *exerterSubpop_value = p_arguments[1].get(); int receivers_count = receivers_value->Count(); if (spatiality_ == 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_totalOfNeighborStrengths): totalOfNeighborStrengths() requires that the interaction be spatial." << EidosTerminate(); if (receivers_count == 0) return gStaticEidosValue_Float_ZeroVec; // the exerter subpopulation defaults to the same subpop as the receivers Individual * const *receivers_data = (Individual * const *)receivers_value->ObjectData(); Subpopulation *receiver_subpop = receivers_data[0]->subpopulation_; Subpopulation *exerter_subpop = ((exerterSubpop_value->Type() == EidosValueType::kValueNULL) ? receiver_subpop : (Subpopulation *)exerterSubpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); CheckSpeciesCompatibility_Receiver(receiver_subpop->species_); CheckSpeciesCompatibility_Exerter(exerter_subpop->species_); CheckSpatialCompatibility(receiver_subpop, exerter_subpop); InteractionsData &exerter_subpop_data = InteractionsDataForSubpop(data_, exerter_subpop); SLiM_kdNode *kd_root_EXERTERS = EnsureKDTreePresent_EXERTERS(exerter_subpop, exerter_subpop_data); // If there are no exerters satisfying constraints, short-circuit if (!kd_root_EXERTERS) { // If the exerter subpop is empty then all strength totals for the receivers are zero if (receivers_count == 1) { return gStaticEidosValue_Float0; } else { EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(receivers_count); for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) result_vec->set_float_no_check(0, receiver_index); return EidosValue_SP(result_vec); } } InteractionsData &receiver_subpop_data = InteractionsDataForSubpop(data_, receiver_subpop); if (receivers_count == 1) { // Just one value, so we can return a singleton and skip some work Individual *receiver = receivers_data[0]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_totalOfNeighborStrengths): totalOfNeighborStrengths() requires that receivers are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); // Check sex-specificity for the receiver; if the individual is disqualified, the total is zero if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises return gStaticEidosValue_Float0; double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kStrengths); try { FillSparseVectorForReceiverStrengths(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.evaluation_interaction_callbacks_); // singleton case, not parallel } catch (...) { InteractionType::FreeSparseVector(sv); throw; } // Get the sparse vector data uint32_t nnz; const sv_value_t *strengths; strengths = sv->Strengths(&nnz); // Total the interaction strengths double total_strength = 0.0; for (uint32_t col_index = 0; col_index < nnz; ++col_index) total_strength += strengths[col_index]; InteractionType::FreeSparseVector(sv); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(total_strength)); } else { // Loop over the requested individuals and get the totals EidosValue_Float *result_vec = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(receivers_count); EidosValue_SP result_SP(result_vec); #ifdef _OPENMP bool has_interaction_callbacks = (exerter_subpop_data.evaluation_interaction_callbacks_.size() != 0); #endif bool saw_error_1 = false, saw_error_2 = false, saw_error_3 = false, saw_error_4 = false; EIDOS_THREAD_COUNT(gEidos_OMP_threads_TOTNEIGHSTRENGTH); #pragma omp parallel for schedule(dynamic, 16) default(none) shared(receivers_count, receiver_subpop, exerter_subpop, receiver_subpop_data, exerter_subpop_data, kd_root_EXERTERS) firstprivate(receivers_data, result_vec) reduction(||: saw_error_1) reduction(||: saw_error_2) reduction(||: saw_error_3) reduction(||: saw_error_4) if(!has_interaction_callbacks && (receivers_count >= EIDOS_OMPMIN_TOTNEIGHSTRENGTH)) num_threads(thread_count) for (int receiver_index = 0; receiver_index < receivers_count; ++receiver_index) { Individual *receiver = receivers_data[receiver_index]; slim_popsize_t receiver_index_in_subpop = receiver->index_; if (receiver_index_in_subpop < 0) { saw_error_1 = true; continue; } // SPECIES CONSISTENCY CHECK if (receiver_subpop != receiver->subpopulation_) { saw_error_2 = true; continue; } // Check constraints for the receiver; if the individual is disqualified, the total is zero // Under OpenMP, raises can't go past the end of the parallel region; handle things the same way when not under OpenMP for simplicity try { if (!CheckIndividualConstraints(receiver, receiver_constraints_)) // potentially raises; protected { result_vec->set_float_no_check(0, receiver_index); continue; } } catch (...) { saw_error_4 = true; continue; } double *receiver_position = receiver_subpop_data.positions_ + (size_t)receiver_index_in_subpop * SLIM_MAX_DIMENSIONALITY; SparseVector *sv = InteractionType::NewSparseVectorForExerterSubpop(exerter_subpop, SparseVectorDataType::kStrengths); // Under OpenMP, raises can't go past the end of the parallel region; handle things the same way when not under OpenMP for simplicity try { FillSparseVectorForReceiverStrengths(sv, receiver, receiver_position, exerter_subpop, kd_root_EXERTERS, exerter_subpop_data.evaluation_interaction_callbacks_); // protected from running interaction() callbacks in parallel, above } catch (...) { saw_error_3 = true; InteractionType::FreeSparseVector(sv); continue; } // Get the sparse vector data uint32_t nnz; const sv_value_t *strengths; strengths = sv->Strengths(&nnz); // Total the interaction strengths double total_strength = 0.0; for (uint32_t col_index = 0; col_index < nnz; ++col_index) total_strength += strengths[col_index]; result_vec->set_float_no_check(total_strength, receiver_index); InteractionType::FreeSparseVector(sv); } // deferred raises, for OpenMP compatibility if (saw_error_1) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_totalOfNeighborStrengths): totalOfNeighborStrengths() requires that receivers are visible in a subpopulation (i.e., not new juveniles)." << EidosTerminate(); if (saw_error_2) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_totalOfNeighborStrengths): totalOfNeighborStrengths() requires that all receivers be in the same subpopulation." << EidosTerminate(); if (saw_error_3) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_totalOfNeighborStrengths): an exception was caught inside a parallel region." << EidosTerminate(); if (saw_error_4) EIDOS_TERMINATION << "ERROR (InteractionType::ExecuteMethod_totalOfNeighborStrengths): totalOfNeighborStrengths() tested a tag or tagL constraint, but a receiver's value for that property was not defined (had not been set)." << EidosTerminate(); return result_SP; } } // ********************* – (void)unevaluate(void) // EidosValue_SP InteractionType::ExecuteMethod_unevaluate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) Invalidate(); return gStaticEidosValueVOID; } // // InteractionType_Class // #pragma mark - #pragma mark InteractionType_Class #pragma mark - EidosClass *gSLiM_InteractionType_Class = nullptr; const std::vector *InteractionType_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("InteractionType_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_id, true, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(InteractionType::GetProperty_Accelerated_id)); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_reciprocal, true, kEidosValueMaskLogical | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_sexSegregation, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_spatiality, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_maxDistance, false, kEidosValueMaskFloat | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))->DeclareAcceleratedGet(InteractionType::GetProperty_Accelerated_tag)); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *InteractionType_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("InteractionType_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_clippedIntegral, kEidosValueMaskFloat))->AddObject_N("receivers", gSLiM_Individual_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_distance, kEidosValueMaskFloat))->AddObject_S("receiver", gSLiM_Individual_Class)->AddObject_ON("exerters", gSLiM_Individual_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_distanceFromPoint, kEidosValueMaskFloat))->AddFloat("point")->AddObject("exerters", gSLiM_Individual_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_drawByStrength, kEidosValueMaskObject, nullptr))->AddObject("receiver", gSLiM_Individual_Class)->AddInt_OS("count", gStaticEidosValue_Integer1)->AddObject_OSN("exerterSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)->AddLogical_OS("returnDict", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_evaluate, kEidosValueMaskVOID))->AddIntObject("subpops", gSLiM_Subpopulation_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_interactingNeighborCount, kEidosValueMaskInt))->AddObject("receivers", gSLiM_Individual_Class)->AddObject_OSN("exerterSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_localPopulationDensity, kEidosValueMaskFloat))->AddObject("receivers", gSLiM_Individual_Class)->AddObject_OSN("exerterSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_interactionDistance, kEidosValueMaskFloat))->AddObject_S("receiver", gSLiM_Individual_Class)->AddObject_ON("exerters", gSLiM_Individual_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_nearestInteractingNeighbors, kEidosValueMaskObject, nullptr))->AddObject("receiver", gSLiM_Individual_Class)->AddInt_OS("count", gStaticEidosValue_Integer1)->AddObject_OSN("exerterSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)->AddLogical_OS("returnDict", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_nearestNeighbors, kEidosValueMaskObject, nullptr))->AddObject("receiver", gSLiM_Individual_Class)->AddInt_OS("count", gStaticEidosValue_Integer1)->AddObject_OSN("exerterSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)->AddLogical_OS("returnDict", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_nearestNeighborsOfPoint, kEidosValueMaskObject, gSLiM_Individual_Class))->AddFloat("point")->AddIntObject_S("exerterSubpop", gSLiM_Subpopulation_Class)->AddInt_OS("count", gStaticEidosValue_Integer1)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_neighborCount, kEidosValueMaskInt))->AddObject("receivers", gSLiM_Individual_Class)->AddObject_OSN("exerterSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_neighborCountOfPoint, kEidosValueMaskInt | kEidosValueMaskSingleton))->AddFloat("point")->AddIntObject_S("exerterSubpop", gSLiM_Subpopulation_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setConstraints, kEidosValueMaskVOID))->AddString_S("who")->AddString_OSN("sex", gStaticEidosValueNULL)->AddInt_OSN("tag", gStaticEidosValueNULL)->AddInt_OSN("minAge", gStaticEidosValueNULL)->AddInt_OSN("maxAge", gStaticEidosValueNULL)->AddLogical_OSN("migrant", gStaticEidosValueNULL)->AddLogical_OSN("tagL0", gStaticEidosValueNULL)->AddLogical_OSN("tagL1", gStaticEidosValueNULL)->AddLogical_OSN("tagL2", gStaticEidosValueNULL)->AddLogical_OSN("tagL3", gStaticEidosValueNULL)->AddLogical_OSN("tagL4", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setInteractionFunction, kEidosValueMaskVOID))->AddString_S("functionType")->AddEllipsis()); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_strength, kEidosValueMaskFloat))->AddObject_S("receiver", gSLiM_Individual_Class)->AddObject_ON("exerters", gSLiM_Individual_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_testConstraints, kEidosValueMaskLogical | kEidosValueMaskObject, gSLiM_Individual_Class))->AddObject("individuals", gSLiM_Individual_Class)->AddString_S("constraints")->AddLogical_OS("returnIndividuals", gStaticEidosValue_LogicalF)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_totalOfNeighborStrengths, kEidosValueMaskFloat))->AddObject("receivers", gSLiM_Individual_Class)->AddObject_OSN("exerterSubpop", gSLiM_Subpopulation_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_unevaluate, kEidosValueMaskVOID))); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } // // _InteractionsData // #pragma mark - #pragma mark _InteractionsData #pragma mark - _InteractionsData::_InteractionsData(_InteractionsData&& p_source) noexcept { evaluated_ = p_source.evaluated_; evaluation_interaction_callbacks_.swap(p_source.evaluation_interaction_callbacks_); individual_count_ = p_source.individual_count_; first_male_index_ = p_source.first_male_index_; periodic_x_ = p_source.periodic_x_; periodic_y_ = p_source.periodic_y_; periodic_z_ = p_source.periodic_z_; bounds_x1_ = p_source.bounds_x1_; bounds_y1_ = p_source.bounds_y1_; bounds_z1_ = p_source.bounds_z1_; positions_ = p_source.positions_; kd_nodes_ALL_ = p_source.kd_nodes_ALL_; kd_root_ALL_ = p_source.kd_root_ALL_; kd_node_count_ALL_ = p_source.kd_node_count_ALL_; kd_nodes_EXERTERS_ = p_source.kd_nodes_EXERTERS_; kd_root_EXERTERS_ = p_source.kd_root_EXERTERS_; kd_node_count_EXERTERS_ = p_source.kd_node_count_EXERTERS_; p_source.evaluated_ = false; p_source.evaluation_interaction_callbacks_.resize(0); p_source.individual_count_ = 0; p_source.first_male_index_ = 0; p_source.periodic_x_ = false; p_source.periodic_y_ = false; p_source.periodic_z_ = false; p_source.bounds_x1_ = 0.0; p_source.bounds_y1_ = 0.0; p_source.bounds_z1_ = 0.0; p_source.positions_ = nullptr; p_source.kd_nodes_ALL_ = nullptr; p_source.kd_root_ALL_ = nullptr; p_source.kd_node_count_ALL_ = 0; p_source.kd_nodes_EXERTERS_ = nullptr; p_source.kd_root_EXERTERS_ = nullptr; p_source.kd_node_count_EXERTERS_ = 0; } _InteractionsData& _InteractionsData::operator=(_InteractionsData&& p_source) noexcept { if (this != &p_source) { if (positions_) free(positions_); // keep in mind that the two k-d trees may share their memory if (kd_nodes_ALL_ == kd_nodes_EXERTERS_) kd_nodes_EXERTERS_ = nullptr; if (kd_nodes_ALL_) free(kd_nodes_ALL_); if (kd_nodes_EXERTERS_) free(kd_nodes_EXERTERS_); evaluated_ = p_source.evaluated_; evaluation_interaction_callbacks_.swap(p_source.evaluation_interaction_callbacks_); individual_count_ = p_source.individual_count_; first_male_index_ = p_source.first_male_index_; periodic_x_ = p_source.periodic_x_; periodic_y_ = p_source.periodic_y_; periodic_z_ = p_source.periodic_z_; bounds_x1_ = p_source.bounds_x1_; bounds_y1_ = p_source.bounds_y1_; bounds_z1_ = p_source.bounds_z1_; positions_ = p_source.positions_; kd_nodes_ALL_ = p_source.kd_nodes_ALL_; kd_root_ALL_ = p_source.kd_root_ALL_; kd_node_count_ALL_ = p_source.kd_node_count_ALL_; kd_nodes_EXERTERS_ = p_source.kd_nodes_EXERTERS_; kd_root_EXERTERS_ = p_source.kd_root_EXERTERS_; kd_node_count_EXERTERS_ = p_source.kd_node_count_EXERTERS_; p_source.evaluated_ = false; p_source.evaluation_interaction_callbacks_.resize(0); p_source.individual_count_ = 0; p_source.first_male_index_ = 0; p_source.periodic_x_ = false; p_source.periodic_y_ = false; p_source.periodic_z_ = false; p_source.bounds_x1_ = 0.0; p_source.bounds_y1_ = 0.0; p_source.bounds_z1_ = 0.0; p_source.positions_ = nullptr; p_source.kd_nodes_ALL_ = nullptr; p_source.kd_root_ALL_ = nullptr; p_source.kd_node_count_ALL_ = 0; p_source.kd_nodes_EXERTERS_ = nullptr; p_source.kd_root_EXERTERS_ = nullptr; p_source.kd_node_count_EXERTERS_ = 0; } return *this; } _InteractionsData::_InteractionsData(void) { } _InteractionsData::_InteractionsData(slim_popsize_t p_individual_count, slim_popsize_t p_first_male_index) : individual_count_(p_individual_count), first_male_index_(p_first_male_index) { } _InteractionsData::~_InteractionsData(void) { if (positions_) { free(positions_); positions_ = nullptr; } // keep in mind that the two k-d trees may share their memory if (kd_nodes_ALL_ == kd_nodes_EXERTERS_) kd_nodes_EXERTERS_ = nullptr; if (kd_nodes_ALL_) { free(kd_nodes_ALL_); kd_nodes_ALL_ = nullptr; } if (kd_nodes_EXERTERS_) { free(kd_nodes_EXERTERS_); kd_nodes_EXERTERS_ = nullptr; } kd_root_ALL_ = nullptr; kd_node_count_ALL_ = 0; kd_root_EXERTERS_ = nullptr; kd_node_count_EXERTERS_ = 0; // Unnecessary since it's about to be destroyed anyway //evaluation_interaction_callbacks_.resize(0); } ================================================ FILE: core/interaction_type.h ================================================ // // interaction_type.h // SLiM // // Created by Ben Haller on 2/25/17. // Copyright (c) 2017-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* The class InteractionType represents a type of interaction defined in the input file, such as spatial competition, phenotype-based resource competition, or spatial mating preference. A particular interaction is defined by a spatiality (x, xy, xyz, or none), a maximum distance threshold, and callbacks that map from distance to interaction strength (while also perhaps involving genetic and environmental factors that modify the interaction). */ #ifndef __SLiM__interaction_type__ #define __SLiM__interaction_type__ #include #include #include #include "eidos_value.h" #include "eidos_symbol_table.h" #include "slim_globals.h" #include "slim_eidos_block.h" #include "sparse_vector.h" #include "subpopulation.h" #include "spatial_kernel.h" class Species; class Subpopulation; class Individual; class InteractionType_Class; extern EidosClass *gSLiM_InteractionType_Class; // This class uses an internal implementation of kd-trees for fast nearest-neighbor finding. We use the same data structure to // save computed distances and interaction strengths. A value of NaN is used as a placeholder to indicate that a given value // has not yet been calculated, and we fill the data structure in lazily. We keep one such data structure per evaluated // subpopulation; if a subpopulation is not evaluated there is no overhead. #define SLIM_MAX_DIMENSIONALITY 3 struct _SLiM_kdNode { double x[SLIM_MAX_DIMENSIONALITY]; // the coordinates of the individual slim_popsize_t individual_index_; // the index of the individual in its subpopulation, and into positions_ struct _SLiM_kdNode *left; // the index of the KDNode for the left side struct _SLiM_kdNode *right; // the index of the KDNode for the right side }; typedef struct _SLiM_kdNode SLiM_kdNode; struct _InteractionsData { // This flag is true when the interaction has been evaluated. What that means in practice is that allocated blocks below // are in sync with the state of the population. Interactions automatically become un-evaluated at the end of each offspring // generation phase, when the parental generation that they are based upon expires. They then need to be re-evaluated, // which re-synchronizes their state with the state of the new parental generation. If an interaction is marked as unevaluated, // it may still have allocated blocks, and their size will be indicated by individual_count_; but individual_count_ may no // longer have anything to do with the subpopulation for this InteractionsData block. The intent of this design is to allow // us to avoid freeing and mallocing large blocks; we want to allocate them once and keep them for the lifetime of the model, // unless the subpopulation size changes (which forces a reallocation). This is important because these blocks can be so large // that they are considered "large blocks" by malloc, triggering some very slow processing such as madvise() that is to be // avoided at all costs. bool evaluated_ = false; std::vector evaluation_interaction_callbacks_; slim_popsize_t individual_count_ = 0; // the number of individuals managed; this will be equal to the size of the corresponding subpopulation slim_popsize_t first_male_index_ = 0; // from the subpopulation's value; needed for sex-segregation handling bool periodic_x_ = false; // true if this spatial coordinate is periodic, from the evaluated Species bool periodic_y_ = false; // these are in terms of the InteractionType's spatiality, not the simulation's dimensionality! bool periodic_z_ = false; double bounds_x1_ = 0.0, bounds_y1_ = 0.0, bounds_z1_ = 0.0; // copied from the Subpopulation; the zero-bound in each dimension is guaranteed to be zero *if* the dimension is periodic // individual_count_ * SLIM_MAX_DIMENSIONALITY entries, holding coordinate positions for all subpop individuals regardless of constraints double *positions_ = nullptr; // BCH 10/31/2023: We now have two separate k-d trees, one containing all individuals (ALL) and one containing only individuals // that satisfy the exerter constraints of the interaction type (EXERTERS). Each is constructed on demand, so probably most models // will only trigger the construction of one or the other; but models that exercise both facilities will now have ~2x the memory usage // for the k-d tree(s). That is not typically a ton of memory anyway. Note that receiver constraints are checked at query time, // whereas sex-specificity for exerters is checked at k-d tree construction time. Individuals that do not satisfy the exerter // constraints are omitted from the EXERTERS k-d tree, and are thus never found/returned. If no exerter constraints are present, // the EXERTERS k-d tree will share the same malloced blocks as the ALL k-d tree -- they will be the same tree. // // If a given kd_nodes_ pointer is nullptr, the tree has not yet been cached by CacheKDTreeNodes(). If that pointer is non-nullptr // but the kd_root_ pointer is nullptr, the tree has been cached, but it has not yet been built, OR the k-d tree has zero nodes // (i.e., is empty); the kd_node_count_ value can be used to distinguish these cases. If the kd_root_ pointer is also non-nullptr, // the tree is built and ready to use. This is all checked by the EnsureKDTreePresent_X() methods, which should always be called to // get the pointer to a k-d tree root; the pointers below should never be accessed directly by clients of the trees. // This k-d tree contains ALL subpop individuals regardless of constraints; it finds "neighbors", whether interacting or not SLiM_kdNode *kd_nodes_ALL_ = nullptr; // individual_count_ entries, holding the nodes of the k-d tree SLiM_kdNode *kd_root_ALL_ = nullptr; // the root of the k-d tree slim_popsize_t kd_node_count_ALL_ = 0; // the number of entries in the k-d tree; may be a multiple of individual_count_ due to periodicity // This k-d tree contains only individuals satisfying the EXERTERS constraints; it finds "exerters" or "interacting neighbors" SLiM_kdNode *kd_nodes_EXERTERS_ = nullptr; // up to individual_count_ entries, holding the nodes of the k-d tree SLiM_kdNode *kd_root_EXERTERS_ = nullptr; // the root of the k-d tree slim_popsize_t kd_node_count_EXERTERS_ = 0; // the number of entries in the k-d tree; may be greater than individual_count_ due to periodicity bool kd_constraints_raise_EXERTERS_ = false; // an exerter tree cannot be constructed due to constraints; see EvaluateSubpopulation() for discussion _InteractionsData(const _InteractionsData&) = delete; // no copying _InteractionsData& operator=(const _InteractionsData&) = delete; // no copying _InteractionsData(_InteractionsData&&) noexcept; // move constructor, for std::map compatibility _InteractionsData& operator=(_InteractionsData&&) noexcept; // move assignment, for std::map compatibility _InteractionsData(void); // null construction, for std::map compatibility _InteractionsData(slim_popsize_t p_individual_count, slim_popsize_t p_first_male_index); ~_InteractionsData(void); }; typedef struct _InteractionsData InteractionsData; // This structure expresses constraints present for exerters or receivers; see the // setConstraints() method for details. typedef struct _InteractionConstraints { bool has_constraints_ = false; // true if any constraints at all are present IndividualSex sex_ = IndividualSex::kUnspecified; // IndividualSex::kUnspecified if unspecified bool has_nonsex_constraints_ = false; // true if any non-sex constraints are present slim_usertag_t tag_ = SLIM_TAG_UNSET_VALUE; // SLIM_TAG_UNSET_VALUE if unspecified slim_age_t min_age_ = -1, max_age_ = -1; // -1 if unspecified; >= 0 otherwise int8_t migrant_ = -1; // -1 if unspecified; 0 or 1 otherwise bool has_tagL_constraints_ = false; // true if tagLX constraints are present int8_t tagL0_ = -1; // -1 if unspecified; 0 or 1 otherwise int8_t tagL1_ = -1; // -1 if unspecified; 0 or 1 otherwise int8_t tagL2_ = -1; // -1 if unspecified; 0 or 1 otherwise int8_t tagL3_ = -1; // -1 if unspecified; 0 or 1 otherwise int8_t tagL4_ = -1; // -1 if unspecified; 0 or 1 otherwise } InteractionConstraints; class InteractionType : public EidosDictionaryUnretained { // This class has its copy constructor and assignment operator disabled, to prevent accidental copying. private: typedef EidosDictionaryUnretained super; static void _WarmUp(void); // called internally at startup, do not call friend InteractionType_Class; // so it can call _WarmUp() for us #ifdef SLIMGUI public: #else private: #endif EidosSymbolTableEntry self_symbol_; // for fast setup of the symbol table std::string spatiality_string_; // can be "x", "y", "z", "xy", "xz", "yz", or "xyz"; this determines spatiality_ int required_dimensionality_; // the dimensionality required of all exerter/receiver subpops by our spatiality int spatiality_; // 0=none, 1=1D (x/y/z), 2=2D (xy/xz/yz), 3=3D (xyz) bool reciprocal_; // if true, interaction strengths A->B == B->A; NOW UNUSED double max_distance_; // the maximum distance, beyond which interaction strength is assumed to be zero double max_distance_sq_; // the maximum distance squared, cached for speed InteractionConstraints receiver_constraints_; // constraints on who can be a receiver InteractionConstraints exerter_constraints_; // constraints on who can be an exerter static bool _CheckIndividualNonSexConstraints(Individual *p_individual, InteractionConstraints &p_constraints); static bool _PrecheckIndividualNonSexConstraints(Individual *p_individual, InteractionConstraints &p_constraints); static inline __attribute__((always_inline)) bool CheckIndividualNonSexConstraints(Individual *p_individual, InteractionConstraints &p_constraints) { if (p_constraints.has_nonsex_constraints_) return _CheckIndividualNonSexConstraints(p_individual, p_constraints); return true; } static inline __attribute__((always_inline)) bool CheckIndividualConstraints(Individual *p_individual, InteractionConstraints &p_constraints) { if (p_constraints.has_constraints_) { if ((p_constraints.sex_ != IndividualSex::kUnspecified) && (p_constraints.sex_ != p_individual->sex_)) return false; if (p_constraints.has_nonsex_constraints_) return _CheckIndividualNonSexConstraints(p_individual, p_constraints); } return true; } slim_usertag_t tag_value_ = SLIM_TAG_UNSET_VALUE; // a user-defined tag value SpatialKernelType if_type_; // the interaction function (IF) to use double if_param1_, if_param2_, if_param3_; // the parameters for that IF (not all of which may be used) double n_2param2sq_; // for type "n", precalculated == 2.0 * if_param2_ * if_param2_ std::map data_; // cached data for the interaction, for each "exerter" subpopulation void _InvalidateData(InteractionsData &data); void CheckSpeciesCompatibility_Generic(Species &species); void CheckSpeciesCompatibility_Receiver(Species &species); void CheckSpeciesCompatibility_Exerter(Species &species); void CheckSpatialCompatibility(Subpopulation *receiver_subpop, Subpopulation *exerter_subpop); double CalculateDistance(double *p_position1, double *p_position2); double CalculateDistanceWithPeriodicity(double *p_position1, double *p_position2, InteractionsData &p_subpop_data); double CalculateStrengthNoCallbacks(double p_distance); double CalculateStrengthWithCallbacks(double p_distance, Individual *p_receiver, Individual *p_exerter, std::vector &p_interaction_callbacks); SLiM_kdNode *FindMedian_p0(SLiM_kdNode *start, SLiM_kdNode *end); SLiM_kdNode *FindMedian_p1(SLiM_kdNode *start, SLiM_kdNode *end); SLiM_kdNode *FindMedian_p2(SLiM_kdNode *start, SLiM_kdNode *end); SLiM_kdNode *MakeKDTree1_p0(SLiM_kdNode *t, int len); SLiM_kdNode *MakeKDTree2_p0(SLiM_kdNode *t, int len); SLiM_kdNode *MakeKDTree2_p1(SLiM_kdNode *t, int len); SLiM_kdNode *MakeKDTree3_p0(SLiM_kdNode *t, int len); SLiM_kdNode *MakeKDTree3_p1(SLiM_kdNode *t, int len); SLiM_kdNode *MakeKDTree3_p2(SLiM_kdNode *t, int len); // Setting up the k-d trees now proceeds in several steps. CacheKDTreeNodes() allocates the k-d tree buffers and copies positions and indices in, but does not // set up the left/right pointers -- it doesn't actually make the tree. It is called at evaluate() time to set up the EXERTERS tree if exerter constraints // are set up, so that those constraints get applied to the state of the model at snapshot time. For all other cases, it is called on demand when the tree // is needed. BuildKDTree() takes the structure set up by CacheKDTreeNodes() and actually builds the tree structure recursively; it is called on demand when // the tree is needed. EnsureKDTreePresent_ALL() and EnsureKDTreePresent_EXERTERS() are called when the corresponding k-d tree is actually needed, and it // triggers caching and building of the tree as needed. They return a pointer to the tree root, which is all that is needed to use the tree for queries. // BEWARE! Note that the EnsureKDTreePresent_X() methods will return nullptr if the requested tree contains zero nodes! This needs to be checked! void CacheKDTreeNodes(Subpopulation *subpop, InteractionsData &p_subpop_data, bool p_apply_exerter_constraints, SLiM_kdNode **kd_nodes_ptr, SLiM_kdNode **kd_root_ptr, slim_popsize_t *kd_node_count_ptr); void BuildKDTree(InteractionsData &p_subpop_data, SLiM_kdNode **kd_nodes_ptr, SLiM_kdNode **kd_root_ptr, slim_popsize_t *kd_node_count_ptr); SLiM_kdNode *EnsureKDTreePresent_ALL(Subpopulation *subpop, InteractionsData &p_subpop_data); SLiM_kdNode *EnsureKDTreePresent_EXERTERS(Subpopulation *subpop, InteractionsData &p_subpop_data); int CheckKDTree1_p0(SLiM_kdNode *t); void CheckKDTree1_p0_r(SLiM_kdNode *t, double split, bool isLeftSubtree); int CheckKDTree2_p0(SLiM_kdNode *t); void CheckKDTree2_p0_r(SLiM_kdNode *t, double split, bool isLeftSubtree); int CheckKDTree2_p1(SLiM_kdNode *t); void CheckKDTree2_p1_r(SLiM_kdNode *t, double split, bool isLeftSubtree); int CheckKDTree3_p0(SLiM_kdNode *t); void CheckKDTree3_p0_r(SLiM_kdNode *t, double split, bool isLeftSubtree); int CheckKDTree3_p1(SLiM_kdNode *t); void CheckKDTree3_p1_r(SLiM_kdNode *t, double split, bool isLeftSubtree); int CheckKDTree3_p2(SLiM_kdNode *t); void CheckKDTree3_p2_r(SLiM_kdNode *t, double split, bool isLeftSubtree); void BuildSV_Presences_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector); void BuildSV_Presences_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Presences_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Distances_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector); void BuildSV_Distances_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Distances_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Strengths_f_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Strengths_l_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Strengths_e_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Strengths_n_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Strengths_c_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); void BuildSV_Strengths_t_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SparseVector *p_sparse_vector, int p_phase); int CountNeighbors_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index); int CountNeighbors_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, int p_phase); int CountNeighbors_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, int p_phase); void FindNeighbors1_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SLiM_kdNode **best, double *best_dist); void FindNeighbors1_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SLiM_kdNode **best, double *best_dist, int p_phase); void FindNeighbors1_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, SLiM_kdNode **best, double *best_dist, int p_phase); void FindNeighborsA_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, EidosValue_Object &p_result_vec, std::vector &p_individuals); void FindNeighborsA_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, EidosValue_Object &p_result_vec, std::vector &p_individuals, int p_phase); void FindNeighborsA_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, EidosValue_Object &p_result_vec, std::vector &p_individuals, int p_phase); void FindNeighborsN_1(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, int p_count, SLiM_kdNode **best, double *best_dist); void FindNeighborsN_2(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, int p_count, SLiM_kdNode **best, double *best_dist, int p_phase); void FindNeighborsN_3(SLiM_kdNode *root, double *nd, slim_popsize_t p_focal_individual_index, int p_count, SLiM_kdNode **best, double *best_dist, int p_phase); void FindNeighbors(Subpopulation *p_subpop, SLiM_kdNode *kd_root, slim_popsize_t kd_node_count, double *p_point, int p_count, EidosValue_Object &p_result_vec, Individual *p_excluded_individual, bool constraints_active); // this is a malloced 1D/2D/3D buffer, depending on our spatiality, that contains clipped integral values // for distances, for a focal individual, from 0 to max_distance_ to the nearest edge in each dimension // it is a cache that can be invalidated, without being deallocated, by setting the cached flag to false double *clipped_integral_ = nullptr; bool clipped_integral_valid_ = false; // A pool of unused SparseVector objects so that, once equilibrated, there is no alloc/realloc activity. Note this is shared by all species. // When built multithreaded, we have per-thread sparse vector pools to avoid lock contention, but single-thread there is one pool. // At present we only use one SparseVector object per pool, but this design will allow new code to access multiple SparseVectors // simultaneously if that becomes useful for more complex functionality. The overhead of the pools should be quite small. #ifdef _OPENMP static std::vector> s_freed_sparse_vectors_PERTHREAD; #if DEBUG static std::vector s_sparse_vector_count_PERTHREAD; #endif #else static std::vector s_freed_sparse_vectors_SINGLE; #if DEBUG static int s_sparse_vector_count_SINGLE; #endif #endif static inline __attribute__((always_inline)) SparseVector *NewSparseVectorForExerterSubpop(Subpopulation *exerter_subpop, SparseVectorDataType data_type) { // Return a recycled SparseVector object, or create a new one if we have no recycled objects left. // Objects in the free list are not in a reuseable state yet, and must be reset; see FreeSparseVector() below. SparseVector *sv; #ifdef _OPENMP // When running multithreaded, look up the per-thread SparseVector pool to use, and then use the single-threaded variable names int threadnum = omp_get_thread_num(); std::vector &s_freed_sparse_vectors_SINGLE = s_freed_sparse_vectors_PERTHREAD[threadnum]; #if DEBUG int &s_sparse_vector_count_SINGLE = s_sparse_vector_count_PERTHREAD[threadnum]; #endif #endif if (s_freed_sparse_vectors_SINGLE.size()) { sv = s_freed_sparse_vectors_SINGLE.back(); s_freed_sparse_vectors_SINGLE.pop_back(); sv->Reset(exerter_subpop->parent_subpop_size_, data_type); } else { #if DEBUG if (++s_sparse_vector_count_SINGLE > 1) std::cout << "new SparseVector(), s_sparse_vector_count_ == " << s_sparse_vector_count_SINGLE << "..." << std::endl; #endif sv = new SparseVector(exerter_subpop->parent_subpop_size_); sv->SetDataType(data_type); } return sv; } static inline __attribute__((always_inline)) void FreeSparseVector(SparseVector *sv) { #ifdef _OPENMP // When running multithreaded, look up the per-thread SparseVector pool to use, and then use the single-threaded variable names int threadnum = omp_get_thread_num(); std::vector &s_freed_sparse_vectors_SINGLE = s_freed_sparse_vectors_PERTHREAD[threadnum]; #if DEBUG int &s_sparse_vector_count_SINGLE = s_sparse_vector_count_PERTHREAD[threadnum]; #endif #endif // We return mutation runs to the free list without resetting them, because we do not know the ncols // value for their next usage. They would hang on to their internal buffers for reuse. s_freed_sparse_vectors_SINGLE.emplace_back(sv); #if DEBUG s_sparse_vector_count_SINGLE--; #endif } void FillSparseVectorForReceiverPresences(SparseVector *sv, Individual *receiver, double *receiver_position, Subpopulation *exerter_subpop, SLiM_kdNode *kd_root, bool constraints_active); void FillSparseVectorForReceiverDistances(SparseVector *sv, Individual *receiver, double *receiver_position, Subpopulation *exerter_subpop, SLiM_kdNode *kd_root, bool constraints_active); void FillSparseVectorForPointDistances(SparseVector *sv, double *position, Subpopulation *exerter_subpop, SLiM_kdNode *kd_root); void FillSparseVectorForReceiverStrengths(SparseVector *sv, Individual *receiver, double *receiver_position, Subpopulation *exerter_subpop, SLiM_kdNode *kd_root, std::vector &interaction_callbacks); public: Community &community_; // we know the community, but we have no focal species slim_objectid_t interaction_type_id_; // the id by which this interaction type is indexed in the chromosome EidosValue_SP cached_value_inttype_id_; // a cached value for interaction_type_id_; reset() if that changes InteractionType(const InteractionType&) = delete; // no copying InteractionType& operator=(const InteractionType&) = delete; // no copying InteractionType(void) = delete; // no null construction InteractionType(Community &p_community, slim_objectid_t p_interaction_type_id, std::string p_spatiality_string, bool p_reciprocal, double p_max_distance, IndividualSex p_receiver_sex, IndividualSex p_exerter_sex); ~InteractionType(void); void EvaluateSubpopulation(Subpopulation *p_subpop); bool AnyEvaluated(void); void Invalidate(void); void InvalidateForSpecies(Species *p_invalid_species); void InvalidateForSubpopulation(Subpopulation *p_invalid_subpop); void CacheClippedIntegral_1D(void); void CacheClippedIntegral_2D(void); double ClippedIntegral_1D(double indDistanceA1, double indDistanceA2, bool periodic_x); double ClippedIntegral_2D(double indDistanceA1, double indDistanceA2, double indDistanceB1, double indDistanceB2, bool periodic_x, bool periodic_y); // apply interaction() callbacks to an interaction strength; the return value is the final interaction strength double ApplyInteractionCallbacks(Individual *p_receiver, Individual *p_exerter, double p_strength, double p_distance, std::vector &p_interaction_callbacks); // Memory usage tallying, for outputUsage() size_t MemoryUsageForKDTrees(void); size_t MemoryUsageForPositions(void); static size_t MemoryUsageForSparseVectorPool(void); static inline void DeleteSparseVectorFreeList(void) { // This is not normally used by SLiM, but it is used in the SLiM test code in order to prevent sparse vectors // that are allocated in one test from carrying over to later tests (which makes leak debugging a pain). THREAD_SAFETY_IN_ACTIVE_PARALLEL("InteractionType::DeleteSparseVectorFreeList(): s_freed_sparse_vectors_ change"); #ifdef _OPENMP // When running multithreaded, free all pools for (auto &pool : s_freed_sparse_vectors_PERTHREAD) { for (auto sv : pool) delete (sv); pool.resize(0); } #if DEBUG for (int &count : s_sparse_vector_count_PERTHREAD) count = 0; #endif #else for (auto sv : s_freed_sparse_vectors_SINGLE) delete (sv); s_freed_sparse_vectors_SINGLE.resize(0); #if DEBUG s_sparse_vector_count_SINGLE = 0; #endif #endif } // // Eidos support // inline EidosSymbolTableEntry &SymbolTableEntry(void) { return self_symbol_; } virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; EidosValue_SP ExecuteMethod_clippedIntegral(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_distance(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_distanceFromPoint(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_drawByStrength(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_evaluate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_interactingNeighborCount(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_localPopulationDensity(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_interactionDistance(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_nearestInteractingNeighbors(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_nearestNeighbors(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_nearestNeighborsOfPoint(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_neighborCount(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_neighborCountOfPoint(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setConstraints(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setInteractionFunction(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_strength(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_testConstraints(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_totalOfNeighborStrengths(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_unevaluate(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); // Accelerated property access; see class EidosObject for comments on this mechanism static EidosValue *GetProperty_Accelerated_id(EidosObject **p_values, size_t p_values_size); static EidosValue *GetProperty_Accelerated_tag(EidosObject **p_values, size_t p_values_size); }; class InteractionType_Class : public EidosDictionaryUnretained_Class { private: typedef EidosDictionaryUnretained_Class super; public: InteractionType_Class(const InteractionType_Class &p_original) = delete; // no copy-construct InteractionType_Class& operator=(const InteractionType_Class&) = delete; // no copying inline InteractionType_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { InteractionType::_WarmUp(); } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; }; #endif /* __SLiM__interaction_type__ */ ================================================ FILE: core/log_file.cpp ================================================ // // log_file.cpp // SLiM // // Created by Ben Haller on 11/2/20. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #include "log_file.h" #include #include #include #include #include "slim_globals.h" #include "community.h" #include "species.h" #include "subpopulation.h" // // LogFile // #pragma mark - #pragma mark LogFile #pragma mark - LogFile::LogFile(Community &p_community) : community_(p_community) { } LogFile::~LogFile(void) { } void LogFile::Raise_UsesStringKeys(void) const { EIDOS_TERMINATION << "ERROR (LogFile::Raise_UsesStringKeys): cannot use an integer key with the target LogFile object; LogFile always uses string keys." << EidosTerminate(nullptr); } void LogFile::ConfigureFile(const std::string &p_filePath, std::vector &p_initialContents, bool p_append, bool p_emitHeader, bool p_compress, const std::string &p_sep) { user_file_path_ = p_filePath; // correct the user-visible path to end in ".gz" if it doesn't already if (p_compress && !Eidos_string_hasSuffix(user_file_path_, ".gz")) user_file_path_.append(".gz"); // Resolve a ~ at the start of the path, and get a canonical absolute path resolved_file_path_ = Eidos_AbsolutePath(user_file_path_); compress_ = p_compress; sep_ = p_sep; emit_header_ = p_emitHeader; // We always open the file for writing (or appending) synchronously and write out the initial contents, if any Eidos_WriteToFile(resolved_file_path_, p_initialContents, p_append, p_compress, EidosFileFlush::kForceFlush); } void LogFile::SetLogInterval(bool p_autologging_enabled, int64_t p_logInterval) { if (p_autologging_enabled && (p_logInterval < 1)) EIDOS_TERMINATION << "ERROR (LogFile::SetLogInterval): the log interval must be >= 1 (or NULL, to disable automatic logging)." << EidosTerminate(); autologging_enabled_ = p_autologging_enabled; log_interval_ = p_autologging_enabled ? p_logInterval : 0; autolog_start_ = community_.Tick(); } void LogFile::SetFlushInterval(bool p_explicit_flushing, int64_t p_flushInterval) { if (p_explicit_flushing && (p_flushInterval < 1)) EIDOS_TERMINATION << "ERROR (LogFile::SetFlushInterval): the flush interval must be >= 1 (or NULL, to request the default flushing behavior)." << EidosTerminate(); explicit_flushing_ = p_explicit_flushing; flush_interval_ = p_flushInterval; } EidosValue_SP LogFile::_GeneratedValue_Cycle(const LogFileGeneratorInfo &p_generator_info) { const std::vector &all_species = community_.AllSpecies(); Species *species = all_species[p_generator_info.objectid_]; slim_tick_t cycle = species->Cycle(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(cycle)); } EidosValue_SP LogFile::_GeneratedValue_CycleStage(const LogFileGeneratorInfo &p_generator_info) { #pragma unused(p_generator_info) SLiMCycleStage cycle_stage = community_.CycleStage(); std::string stage_string = StringForSLiMCycleStage(cycle_stage); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(stage_string)); } EidosValue_SP LogFile::_GeneratedValue_PopulationSexRatio(const LogFileGeneratorInfo &p_generator_info) { const std::vector &all_species = community_.AllSpecies(); Species *species = all_species[p_generator_info.objectid_]; if (species->SexEnabled()) { slim_popsize_t total_individuals = 0, total_males = 0; for (auto &subpop_iter : species->population_.subpops_) { Subpopulation *subpop = subpop_iter.second; slim_popsize_t subpop_size = subpop->parent_subpop_size_; slim_popsize_t first_male_index = subpop->parent_first_male_index_; total_individuals += subpop_size; total_males += (subpop_size - first_male_index); } double sex_ratio = (total_individuals == 0) ? 0.0 : (total_males / (double)total_individuals); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(sex_ratio)); } else { // no dictionary entry, which will produce NULL return gStaticEidosValueNULL; } } EidosValue_SP LogFile::_GeneratedValue_PopulationSize(const LogFileGeneratorInfo &p_generator_info) { #pragma unused(p_generator_info) const std::vector &all_species = community_.AllSpecies(); Species *species = all_species[p_generator_info.objectid_]; slim_popsize_t total_individuals = 0; for (auto &subpop_iter : species->population_.subpops_) total_individuals += (subpop_iter.second)->parent_subpop_size_; return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(total_individuals)); } EidosValue_SP LogFile::_GeneratedValue_SubpopulationSexRatio(const LogFileGeneratorInfo &p_generator_info) { Subpopulation *subpop = community_.SubpopulationWithID(p_generator_info.objectid_); if (subpop && subpop->species_.SexEnabled()) { slim_popsize_t subpop_size = subpop->parent_subpop_size_; slim_popsize_t first_male_index = subpop->parent_first_male_index_; double sex_ratio = (subpop_size == 0) ? 0.0 : ((subpop_size - first_male_index) / (double)subpop_size); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Float(sex_ratio)); } else { // no dictionary entry, which will produce NULL return gStaticEidosValueNULL; } } EidosValue_SP LogFile::_GeneratedValue_SubpopulationSize(const LogFileGeneratorInfo &p_generator_info) { Subpopulation *subpop = community_.SubpopulationWithID(p_generator_info.objectid_); if (subpop) { slim_popsize_t subpop_size = subpop->parent_subpop_size_; return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(subpop_size)); } else { // no dictionary entry, which will produce NULL return gStaticEidosValueNULL; } } EidosValue_SP LogFile::_GeneratedValue_Tick(const LogFileGeneratorInfo &p_generator_info) { #pragma unused(p_generator_info) slim_tick_t tick = community_.Tick(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tick)); } EidosValue_SP LogFile::_GeneratedValue_CustomScript(const LogFileGeneratorInfo &p_generator_info) { // See, e.g., Subpopulation::ApplyFitnessEffectCallbacks() for comments on running scripts THREAD_SAFETY_IN_ACTIVE_PARALLEL("LogFile::_GeneratedValue_CustomScript(): running Eidos lambda"); EidosScript *generator_script = p_generator_info.script_; EidosErrorContext error_context_save = gEidosErrorContext; gEidosErrorContext = EidosErrorContext{{-1, -1, -1, -1}, generator_script}; EidosValue_SP result_SP; try { EidosSymbolTable callback_symbols(EidosSymbolTableType::kContextConstantsTable, &community_.SymbolTable()); EidosSymbolTable client_symbols(EidosSymbolTableType::kLocalVariablesTable, &callback_symbols); EidosFunctionMap &function_map = community_.FunctionMap(); EidosInterpreter interpreter(*generator_script, client_symbols, function_map, &community_, SLIM_OUTSTREAM, SLIM_ERRSTREAM #ifdef SLIMGUI , community_.check_infinite_loops_ #endif ); // BCH 11/7/2025: note this symbol is now protected in SLiM_ConfigureContext() callback_symbols.InitializeConstantSymbolEntry(gID_context, p_generator_info.context_); result_SP = interpreter.EvaluateInterpreterBlock(false, true); // do not print output, return the last statement value if (result_SP->Type() == EidosValueType::kValueObject) EIDOS_TERMINATION << "ERROR (LogFile::_GeneratedValue_CustomScript): a LogFile generator script for addCustomColumn() may not return type object." << EidosTerminate(nullptr); if ((result_SP->Type() != EidosValueType::kValueNULL) && (result_SP->Count() != 1)) EIDOS_TERMINATION << "ERROR (LogFile::_GeneratedValue_CustomScript): a LogFile generator script for addCustomColumn() must return a singleton value, or NULL." << EidosTerminate(nullptr); } catch (...) { if (gEidosTerminateThrows) { // In some cases, such as if the error occurred in a derived user-defined function, we can // actually get a user script error context at this point, and don't need to intervene. if (!gEidosErrorContext.currentScript || (gEidosErrorContext.currentScript->UserScriptUTF16Offset() == -1)) { gEidosErrorContext = error_context_save; TranslateErrorContextToUserScript("_GeneratedValue_CustomScript()"); } } throw; } gEidosErrorContext = error_context_save; return result_SP; } void LogFile::_GeneratedValues_CustomMeanAndSD(const LogFileGeneratorInfo &p_generator_info, EidosValue_SP *p_generated_value_1, EidosValue_SP *p_generated_value_2) { // See, e.g., Subpopulation::ApplyFitnessEffectCallbacks() for comments on running scripts THREAD_SAFETY_IN_ACTIVE_PARALLEL("LogFile::_GeneratedValues_CustomMeanAndSD(): running Eidos lambda"); EidosScript *generator_script = p_generator_info.script_; EidosErrorContext error_context_save = gEidosErrorContext; gEidosErrorContext = EidosErrorContext{{-1, -1, -1, -1}, generator_script}; EidosValue_SP result_SP; try { EidosSymbolTable callback_symbols(EidosSymbolTableType::kContextConstantsTable, &community_.SymbolTable()); EidosSymbolTable client_symbols(EidosSymbolTableType::kLocalVariablesTable, &callback_symbols); EidosFunctionMap &function_map = community_.FunctionMap(); EidosInterpreter interpreter(*generator_script, client_symbols, function_map, nullptr, SLIM_OUTSTREAM, SLIM_ERRSTREAM #ifdef SLIMGUI , community_.check_infinite_loops_ #endif ); // BCH 11/7/2025: note this symbol is now protected in SLiM_ConfigureContext() callback_symbols.InitializeConstantSymbolEntry(gID_context, p_generator_info.context_); result_SP = interpreter.EvaluateInterpreterBlock(false, true); // do not print output, return the last statement value if ((result_SP->Type() != EidosValueType::kValueInt) && (result_SP->Type() != EidosValueType::kValueFloat) && (result_SP->Type() != EidosValueType::kValueNULL)) EIDOS_TERMINATION << "ERROR (LogFile::_GeneratedValues_CustomMeanAndSD): a LogFile generator script for addMeanSDColumns() must return a vector of type integer or float, or NULL." << EidosTerminate(nullptr); if (result_SP->Count() == 0) { // A zero-length result vector, including NULL, will write out NA for mean and sd *p_generated_value_1 = gStaticEidosValueNULL; *p_generated_value_2 = gStaticEidosValueNULL; } else { // A non-zero result vector gets evaluated for its mean and sd (sd==NA if length 1) // We just use eidos_functions here, since it does exactly what we want anyway std::vector argument_vec; argument_vec.emplace_back(result_SP); if (result_SP->Count() == 1) { *p_generated_value_1 = Eidos_ExecuteFunction_mean(argument_vec, interpreter); *p_generated_value_2 = gStaticEidosValueNULL; } else { *p_generated_value_1 = Eidos_ExecuteFunction_mean(argument_vec, interpreter); *p_generated_value_2 = Eidos_ExecuteFunction_sd(argument_vec, interpreter); } } } catch (...) { if (gEidosTerminateThrows) { // In some cases, such as if the error occurred in a derived user-defined function, we can // actually get a user script error context at this point, and don't need to intervene. if (!gEidosErrorContext.currentScript || (gEidosErrorContext.currentScript->UserScriptUTF16Offset() == -1)) { gEidosErrorContext = error_context_save; TranslateErrorContextToUserScript("_GeneratedValues_CustomMeanAndSD()"); } } throw; } gEidosErrorContext = error_context_save; } void LogFile::_OutputValue(std::ostringstream &p_out, EidosValue *p_value) { EidosValueType type = p_value->Type(); if (type == EidosValueType::kValueNULL) { // NULL gets logged as NA; mixes paradigms a bit, but seems useful p_out << "NA"; } else { // Use EidosValue to write the value. However, we want to control the precision of float output. // Note that this is not thread-safe. int old_precision = gEidosFloatOutputPrecision; gEidosFloatOutputPrecision = float_precision_; p_out << *p_value; // FIXME this doesn't handle string quoting well at present gEidosFloatOutputPrecision = old_precision; } } void LogFile::AppendNewRow(void) { THREAD_SAFETY_IN_ACTIVE_PARALLEL("LogFile::AppendNewRow(): filesystem write"); // Guarantee that we are in the parent generation for all generators, so they don't need to worry const std::vector &all_species = community_.AllSpecies(); for (Species *species : all_species) if (species->population_.child_generation_valid_) EIDOS_TERMINATION << "ERROR (LogFile::AppendNewRow): (internal error) generating logfile entry with child generation active!" << EidosTerminate(); std::vector line_vec; std::string header_line; std::string row_line; // Gather all generators into our Dictionary RemoveAllKeys(); // Generate the header row if needed if (!header_logged_) { // skip emitting the header line if the user has requested that if (emit_header_) { std::ostringstream ss; bool first_column = true; #ifdef SLIMGUI std::vector gui_line; #endif for (const std::string &column_name : column_names_) { if (!first_column) ss << sep_; first_column = false; ss << column_name; #ifdef SLIMGUI std::ostringstream gui_ss; gui_ss << column_name; gui_line.emplace_back(gui_ss.str()); #endif } header_line = ss.str(); line_vec.emplace_back(&header_line); #ifdef SLIMGUI emitted_lines_.emplace_back(std::move(gui_line)); #endif } // Having emitted the header line, we lock ourselves to prevent inconsistencies in the emitted table header_logged_ = true; } // Generate the text of the row from the Dictionary entries { std::ostringstream ss; int column_index = 0; #ifdef SLIMGUI std::vector gui_line; #endif for (const LogFileGeneratorInfo &generator : generator_info_) { EidosValue_SP generated_value; switch (generator.type_) { case LogFileGeneratorType::kGenerator_Cycle: generated_value = _GeneratedValue_Cycle(generator); break; case LogFileGeneratorType::kGenerator_CycleStage: generated_value = _GeneratedValue_CycleStage(generator); break; case LogFileGeneratorType::kGenerator_PopulationSexRatio: generated_value = _GeneratedValue_PopulationSexRatio(generator); break; case LogFileGeneratorType::kGenerator_PopulationSize: generated_value = _GeneratedValue_PopulationSize(generator); break; case LogFileGeneratorType::kGenerator_SubpopulationSexRatio: generated_value = _GeneratedValue_SubpopulationSexRatio(generator); break; case LogFileGeneratorType::kGenerator_SubpopulationSize: generated_value = _GeneratedValue_SubpopulationSize(generator); break; case LogFileGeneratorType::kGenerator_Tick: generated_value = _GeneratedValue_Tick(generator); break; case LogFileGeneratorType::kGenerator_CustomScript: generated_value = _GeneratedValue_CustomScript(generator); break; case LogFileGeneratorType::kGenerator_CustomMeanAndSD: { // This requires special-casing because it generates two columns EidosValue_SP generated_value_1, generated_value_2; _GeneratedValues_CustomMeanAndSD(generator, &generated_value_1, &generated_value_2); // emit generated_value_1 if (column_index != 0) ss << sep_; _OutputValue(ss, generated_value_1.get()); #ifdef SLIMGUI std::ostringstream gui_ss; _OutputValue(gui_ss, generated_value_1.get()); gui_line.emplace_back(gui_ss.str()); #endif if (generated_value_1->Type() != EidosValueType::kValueNULL) SetKeyValue_StringKeys(column_names_[column_index], std::move(generated_value_1)); column_index++; // let the code below emit generated_value_2 generated_value = generated_value_2; break; } case LogFileGeneratorType::kGenerator_SuppliedColumn: generated_value = supplied_values_.GetValueForKey_StringKeys(column_names_[column_index]); break; } // Emit the generated value and add it to our Dictionary state if (column_index != 0) ss << sep_; _OutputValue(ss, generated_value.get()); #ifdef SLIMGUI std::ostringstream gui_ss; _OutputValue(gui_ss, generated_value.get()); gui_line.emplace_back(gui_ss.str()); #endif if (generated_value->Type() != EidosValueType::kValueNULL) SetKeyValue_StringKeys(column_names_[column_index], std::move(generated_value)); column_index++; } row_line = ss.str(); line_vec.emplace_back(&row_line); #ifdef SLIMGUI emitted_lines_.emplace_back(std::move(gui_line)); #endif } supplied_values_.RemoveAllKeys(); ContentsChanged("LogFile::AppendNewRow()"); // Write out the row EidosFileFlush flush = EidosFileFlush::kDefaultFlush; if (explicit_flushing_) { unflushed_row_count_++; if (unflushed_row_count_ >= flush_interval_) { flush = EidosFileFlush::kForceFlush; unflushed_row_count_ = 0; } else { flush = EidosFileFlush::kNoFlush; } } Eidos_WriteToFile(resolved_file_path_, line_vec, true, compress_, flush); } void LogFile::TickEndCallout(void) { if (autologging_enabled_) { slim_tick_t tick = community_.Tick(); if ((tick - autolog_start_) % log_interval_ == 0) AppendNewRow(); } } std::vector LogFile::SortedKeys_StringKeys(void) const { // We want to return the column names in order, so we have to override EidosDictionaryUnretained here // Our column_names_ vector should correspond to EidosDictionaryUnretained's state, just with a fixed order std::vector string_keys; if (header_logged_) { for (const std::string &column_name : column_names_) string_keys.push_back(column_name); } return string_keys; } const EidosClass *LogFile::Class(void) const { return gSLiM_LogFile_Class; } void LogFile::Print(std::ostream &p_ostream) const { p_ostream << Class()->ClassNameForDisplay() << "<" << user_file_path_ << ">"; } EidosValue_SP LogFile::GetProperty(EidosGlobalStringID p_property_id) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { // constants //case gEidosID_allKeys: // not technically overridden here, but we override AllKeys() to provide new behavior case gEidosID_filePath: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(user_file_path_)); case gID_logInterval: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(log_interval_)); // variables case gID_precision: return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(float_precision_)); case gID_tag: { slim_usertag_t tag_value = tag_value_; if (tag_value == SLIM_TAG_UNSET_VALUE) EIDOS_TERMINATION << "ERROR (LogFile::GetProperty): property tag accessed on simulation object before being set." << EidosTerminate(); return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_Int(tag_value)); } // all others, including gID_none default: return super::GetProperty(p_property_id); } } void LogFile::SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) { // All of our strings are in the global registry, so we can require a successful lookup switch (p_property_id) { case gID_precision: { int64_t value = p_value.IntAtIndex_NOCAST(0, nullptr); if ((value < 1) || (value > 22)) EIDOS_TERMINATION << "ERROR (LogFile::SetProperty): property precision must be in [1,22]." << EidosTerminate(); float_precision_ = (int)value; return; } case gID_tag: { slim_usertag_t value = SLiMCastToUsertagTypeOrRaise(p_value.IntAtIndex_NOCAST(0, nullptr)); tag_value_ = value; return; } default: { return super::SetProperty(p_property_id, p_value); } } } EidosValue_SP LogFile::ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { switch (p_method_id) { // our own methods case gID_addCustomColumn: return ExecuteMethod_addCustomColumn(p_method_id, p_arguments, p_interpreter); case gID_addCycle: return ExecuteMethod_addCycle(p_method_id, p_arguments, p_interpreter); case gID_addCycleStage: return ExecuteMethod_addCycleStage(p_method_id, p_arguments, p_interpreter); case gID_addMeanSDColumns: return ExecuteMethod_addMeanSDColumns(p_method_id, p_arguments, p_interpreter); case gID_addPopulationSexRatio: return ExecuteMethod_addPopulationSexRatio(p_method_id, p_arguments, p_interpreter); case gID_addPopulationSize: return ExecuteMethod_addPopulationSize(p_method_id, p_arguments, p_interpreter); case gID_addSubpopulationSexRatio: return ExecuteMethod_addSubpopulationSexRatio(p_method_id, p_arguments, p_interpreter); case gID_addSubpopulationSize: return ExecuteMethod_addSubpopulationSize(p_method_id, p_arguments, p_interpreter); case gID_addSuppliedColumn: return ExecuteMethod_addSuppliedColumn(p_method_id, p_arguments, p_interpreter); case gID_addTick: return ExecuteMethod_addTick(p_method_id, p_arguments, p_interpreter); case gID_flush: return ExecuteMethod_flush(p_method_id, p_arguments, p_interpreter); case gID_logRow: return ExecuteMethod_logRow(p_method_id, p_arguments, p_interpreter); case gID_setLogInterval: return ExecuteMethod_setLogInterval(p_method_id, p_arguments, p_interpreter); case gID_setFilePath: return ExecuteMethod_setFilePath(p_method_id, p_arguments, p_interpreter); case gID_setSuppliedValue: return ExecuteMethod_setSuppliedValue(p_method_id, p_arguments, p_interpreter); case gID_willAutolog: return ExecuteMethod_willAutolog(p_method_id, p_arguments, p_interpreter); // overrides from Dictionary case gEidosID_addKeysAndValuesFrom: return ExecuteMethod_addKeysAndValuesFrom(p_method_id, p_arguments, p_interpreter); case gEidosID_appendKeysAndValuesFrom: return ExecuteMethod_appendKeysAndValuesFrom(p_method_id, p_arguments, p_interpreter); case gEidosID_clearKeysAndValues: return ExecuteMethod_clearKeysAndValues(p_method_id, p_arguments, p_interpreter); case gEidosID_setValue: return ExecuteMethod_setValue(p_method_id, p_arguments, p_interpreter); default: return super::ExecuteInstanceMethod(p_method_id, p_arguments, p_interpreter); } } void LogFile::RaiseForLockedHeader(const std::string &p_caller_name) { EIDOS_TERMINATION << "ERROR (" << p_caller_name << "): this LogFile has already emitted its header line, so new data generators cannot be added." << EidosTerminate(nullptr); } // ********************* - (void)addCustomColumn(string$ columnName, string$ source, [* context = NULL]) EidosValue_SP LogFile::ExecuteMethod_addCustomColumn(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addCustomColumn"); EidosValue_String *columnName_value = (EidosValue_String *)p_arguments[0].get(); EidosValue_String *source_value = (EidosValue_String *)p_arguments[1].get(); EidosValue_SP context_value = p_arguments[2]; const std::string &column_name = columnName_value->StringRefAtIndex_NOCAST(0, nullptr); const std::string &source = source_value->StringRefAtIndex_NOCAST(0, nullptr); // See, e.g., Subpopulation::ApplyFitnessEffectCallbacks() for comments on parsing/running script blocks EidosErrorContext error_context_save = gEidosErrorContext; EidosScript *source_script = new EidosScript(source); gEidosErrorContext = EidosErrorContext{{-1, -1, -1, -1}, source_script}; try { source_script->Tokenize(); source_script->ParseInterpreterBlockToAST(false); } catch (...) { if (gEidosTerminateThrows) { gEidosErrorContext = error_context_save; TranslateErrorContextToUserScript("ExecuteMethod_addCustomColumn()"); } delete source_script; source_script = nullptr; EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_addCustomColumn): tokenize/parse error in script for addCustomColumn()." << EidosTerminate(); } gEidosErrorContext = error_context_save; // Check contextValue for validity and make a copy of it. Copying is needed to // ensure that the value is not changed underneath us externally, for example // by a for loop; see https://github.com/MesserLab/SLiM/issues/496. if (context_value->Type() == EidosValueType::kValueObject) { EidosValue_Object *context_object = (EidosValue_Object *)context_value.get(); if (!context_object->Class()->UsesRetainRelease()) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_addCustomColumn): the context parameter to addCustomColumn() cannot be an object of a class that is not under retain-release, since the lifetime of such objects cannot be guaranteed. See the documentation for addCustomColumn() for discussion of this limitation." << EidosTerminate(); } context_value = context_value->CopyValues(); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_CustomScript, source_script, -1, std::move(context_value)); column_names_.emplace_back(column_name); return gStaticEidosValueVOID; } // ********************* - (void)addCycle([No$ species]) EidosValue_SP LogFile::ExecuteMethod_addCycle(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addCycle"); // Figure out the species to log; if species is NULL, check for a singleton species to default to EidosValue *species_value = p_arguments[0].get(); Species *species = SLiM_ExtractSpeciesFromEidosValue_No(species_value, 0, &SLiM_GetCommunityFromInterpreter(p_interpreter), "addCycle()"); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_Cycle, nullptr, species->species_id_, EidosValue_SP()); // column name is "cycle" in single-species models; append the species name in multispecies models std::string col_name = "cycle"; if (community_.is_explicit_species_) { col_name.append("_"); col_name.append(species->name_); } column_names_.emplace_back(col_name); return gStaticEidosValueVOID; } // ********************* - (void)addCycleStage() EidosValue_SP LogFile::ExecuteMethod_addCycleStage(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addCycleStage"); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_CycleStage, nullptr, -1, EidosValue_SP()); column_names_.emplace_back("cycle_stage"); return gStaticEidosValueVOID; } // ********************* - (void)addMeanSDColumns(string$ columnName, string$ source, [* context = NULL]) EidosValue_SP LogFile::ExecuteMethod_addMeanSDColumns(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addMeanSDColumns"); EidosValue_String *columnName_value = (EidosValue_String *)p_arguments[0].get(); EidosValue_String *source_value = (EidosValue_String *)p_arguments[1].get(); EidosValue_SP context_value = p_arguments[2]; const std::string &column_name = columnName_value->StringRefAtIndex_NOCAST(0, nullptr); const std::string &source = source_value->StringRefAtIndex_NOCAST(0, nullptr); EidosErrorContext error_context_save = gEidosErrorContext; EidosScript *source_script = new EidosScript(source); gEidosErrorContext = EidosErrorContext{{-1, -1, -1, -1}, source_script}; try { source_script->Tokenize(); source_script->ParseInterpreterBlockToAST(false); } catch (...) { if (gEidosTerminateThrows) { gEidosErrorContext = error_context_save; TranslateErrorContextToUserScript("ExecuteMethod_addMeanSDColumns()"); } delete source_script; source_script = nullptr; EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_addMeanSDColumns): tokenize/parse error in script for addMeanSDColumns()." << EidosTerminate(); } gEidosErrorContext = error_context_save; // Check contextValue for validity and make a copy of it. Copying is needed to // ensure that the value is not changed underneath us externally, for example // by a for loop; see https://github.com/MesserLab/SLiM/issues/496. if (context_value->Type() == EidosValueType::kValueObject) { EidosValue_Object *context_object = (EidosValue_Object *)context_value.get(); if (!context_object->Class()->UsesRetainRelease()) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_addMeanSDColumns): the context parameter to addMeanSDColumns() cannot be an object of a class that is not under retain-release, since the lifetime of such objects cannot be guaranteed. See the documentation for addCustomColumn() for discussion of this limitation." << EidosTerminate(); } context_value = context_value->CopyValues(); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_CustomMeanAndSD, source_script, -1, std::move(context_value)); column_names_.emplace_back(column_name + "_mean"); column_names_.emplace_back(column_name + "_sd"); return gStaticEidosValueVOID; } // ********************* - (void)addPopulationSexRatio([No$ species]) EidosValue_SP LogFile::ExecuteMethod_addPopulationSexRatio(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addPopulationSexRatio"); // Figure out the species to log; if species is NULL, check for a singleton species to default to EidosValue *species_value = p_arguments[0].get(); Species *species = SLiM_ExtractSpeciesFromEidosValue_No(species_value, 0, &SLiM_GetCommunityFromInterpreter(p_interpreter), "addPopulationSexRatio()"); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_PopulationSexRatio, nullptr, species->species_id_, EidosValue_SP()); // column name is "sex_ratio" in single-species models; append the species name in multispecies models std::string col_name = "sex_ratio"; if (community_.is_explicit_species_) { col_name.append("_"); col_name.append(species->name_); } column_names_.emplace_back(col_name); return gStaticEidosValueVOID; } // ********************* - (void)addPopulationSize([No$ species]) EidosValue_SP LogFile::ExecuteMethod_addPopulationSize(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addPopulationSize"); // Figure out the species to log; if species is NULL, check for a singleton species to default to EidosValue *species_value = p_arguments[0].get(); Species *species = SLiM_ExtractSpeciesFromEidosValue_No(species_value, 0, &SLiM_GetCommunityFromInterpreter(p_interpreter), "addPopulationSize()"); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_PopulationSize, nullptr, species->species_id_, EidosValue_SP()); // column name is "num_individuals" in single-species models; append the species name in multispecies models std::string col_name = "num_individuals"; if (community_.is_explicit_species_) { col_name.append("_"); col_name.append(species->name_); } column_names_.emplace_back(col_name); return gStaticEidosValueVOID; } // ********************* - (void)addSubpopulationSexRatio(io$ subpop) EidosValue_SP LogFile::ExecuteMethod_addSubpopulationSexRatio(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addSubpopulationSexRatio"); // Extract the subpopulation id; we allow reference to nonexistent subpopulations, which is unusual, so there's no function to use EidosValue *subpop_value = p_arguments[0].get(); slim_objectid_t subpop_id; if (subpop_value->Type() == EidosValueType::kValueInt) { subpop_id = SLiMCastToObjectidTypeOrRaise(subpop_value->IntAtIndex_NOCAST(0, nullptr)); } else { Subpopulation *subpop = (Subpopulation *)(subpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); subpop_id = subpop->subpopulation_id_; } generator_info_.emplace_back(LogFileGeneratorType::kGenerator_SubpopulationSexRatio, nullptr, subpop_id, EidosValue_SP()); column_names_.emplace_back(SLiMEidosScript::IDStringWithPrefix('p', subpop_id) + "_sex_ratio"); return gStaticEidosValueVOID; } // ********************* - (void)addSubpopulationSize(io$ subpop) EidosValue_SP LogFile::ExecuteMethod_addSubpopulationSize(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addSubpopulationSize"); // Extract the subpopulation id; we allow reference to nonexistent subpopulations, which is unusual, so there's no function to use EidosValue *subpop_value = p_arguments[0].get(); slim_objectid_t subpop_id; if (subpop_value->Type() == EidosValueType::kValueInt) { subpop_id = SLiMCastToObjectidTypeOrRaise(subpop_value->IntAtIndex_NOCAST(0, nullptr)); } else { Subpopulation *subpop = (Subpopulation *)(subpop_value->ObjectElementAtIndex_NOCAST(0, nullptr)); subpop_id = subpop->subpopulation_id_; } generator_info_.emplace_back(LogFileGeneratorType::kGenerator_SubpopulationSize, nullptr, subpop_id, EidosValue_SP()); column_names_.emplace_back(SLiMEidosScript::IDStringWithPrefix('p', subpop_id) + "_num_individuals"); return gStaticEidosValueVOID; } // ********************* - (void)addSuppliedColumn(string$ columnName) EidosValue_SP LogFile::ExecuteMethod_addSuppliedColumn(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addSuppliedColumn"); EidosValue_String *columnName_value = (EidosValue_String *)p_arguments[0].get(); const std::string &column_name = columnName_value->StringRefAtIndex_NOCAST(0, nullptr); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_SuppliedColumn, nullptr, -1, EidosValue_SP()); column_names_.emplace_back(column_name); return gStaticEidosValueVOID; } // ********************* - (void)addTick() EidosValue_SP LogFile::ExecuteMethod_addTick(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) if (header_logged_) RaiseForLockedHeader("LogFile::ExecuteMethod_addTick"); generator_info_.emplace_back(LogFileGeneratorType::kGenerator_Tick, nullptr, -1, EidosValue_SP()); column_names_.emplace_back("tick"); return gStaticEidosValueVOID; } // ********************* - (void)flush(void) EidosValue_SP LogFile::ExecuteMethod_flush(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) Eidos_FlushFile(resolved_file_path_); unflushed_row_count_ = 0; return gStaticEidosValueVOID; } // ********************* - (void)logRow(void) EidosValue_SP LogFile::ExecuteMethod_logRow(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) AppendNewRow(); return gStaticEidosValueVOID; } // ********************* - (void)setLogInterval([Ni$ logInterval = NULL]) EidosValue_SP LogFile::ExecuteMethod_setLogInterval(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue *logInterval_value = p_arguments[0].get(); bool autologging = false; int64_t logInterval = 0; if (logInterval_value->Type() == EidosValueType::kValueNULL) { // NULL turns off autologging autologging = false; logInterval = 0; } else { autologging = true; logInterval = logInterval_value->IntAtIndex_NOCAST(0, nullptr); } SetLogInterval(autologging, logInterval); return gStaticEidosValueVOID; } // ********************* - (void)setFilePath(string$ filePath, [Ns initialContents = NULL], [logical$ append = F], [Nl$ compress = NULL], [Ns$ sep = NULL], [Nl$ header = NULL]) EidosValue_SP LogFile::ExecuteMethod_setFilePath(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue_String *filePath_value = (EidosValue_String *)p_arguments[0].get(); EidosValue *initialContents_value = p_arguments[1].get(); EidosValue *append_value = p_arguments[2].get(); EidosValue *compress_value = p_arguments[3].get(); EidosValue_String *sep_value = (EidosValue_String *)p_arguments[4].get(); EidosValue *header_value = p_arguments[5].get(); // BCH 5/23/2025: The documentation has always said "Any rows that have been buffered but not flushed // will be written to the previous file first, as if flush() had been called." It looks to me like // that was not happening. It seems best to just be explicit about it here, and maybe this fixes a bug. // Note that in the present design, flushing only affects compressed output; without compression, output // is always flushed immediately. See Eidos_WriteToFile(). Eidos_FlushFile(resolved_file_path_); unflushed_row_count_ = 0; // Note that the parameters and their interpretation is different from Community::ExecuteMethod_createLogFile(); // in particular, NULL here means "keep the existing value" const std::string &filePath = filePath_value->StringRefAtIndex_NOCAST(0, nullptr); std::vector initialContents; bool append = append_value->LogicalAtIndex_NOCAST(0, nullptr); bool do_compress = compress_; std::string sep = sep_; bool emit_header = emit_header_; if (initialContents_value->Type() != EidosValueType::kValueNULL) { EidosValue_String *ic_string_value = (EidosValue_String *)initialContents_value; int ic_count = initialContents_value->Count(); for (int ic_index = 0; ic_index < ic_count; ++ic_index) initialContents.emplace_back(&ic_string_value->StringRefAtIndex_NOCAST(ic_index, nullptr)); } if (compress_value->Type() != EidosValueType::kValueNULL) do_compress = compress_value->LogicalAtIndex_NOCAST(0, nullptr); if (sep_value->Type() != EidosValueType::kValueNULL) sep = sep_value->StringRefAtIndex_NOCAST(0, nullptr); if (header_value->Type() != EidosValueType::kValueNULL) emit_header = header_value->LogicalAtIndex_NOCAST(0, nullptr); ConfigureFile(filePath, initialContents, append, emit_header, do_compress, sep); // BCH 5/23/2025: I think there was probably a bug here before, that a new header line wouldn't be // emitted into the new target file when using this method. By setting header_logged_ back to false, // we not only cause a new header line to be emitted, but also again allow changes to the columns; // I think that's OK, since we're going to a new file, why not? header_logged_ = false; return gStaticEidosValueVOID; } // ********************* - (void)setSuppliedValue(string$ columnName, +$ value) EidosValue_SP LogFile::ExecuteMethod_setSuppliedValue(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_interpreter) EidosValue_String *columnName_value = (EidosValue_String *)p_arguments[0].get(); EidosValue_SP value = p_arguments[1]; const std::string &column_name = columnName_value->StringRefAtIndex_NOCAST(0, nullptr); // check that the column name exists and is a supplied column auto col_iter = std::find(column_names_.begin(), column_names_.end(), column_name); if (col_iter == column_names_.end()) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_setSuppliedValue): column name " << column_name << " is not a column in the LogFile." << EidosTerminate(); size_t col_index = std::distance(column_names_.begin(), col_iter); LogFileGeneratorInfo &generator = generator_info_[col_index]; if (generator.type_ != LogFileGeneratorType::kGenerator_SuppliedColumn) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_setSuppliedValue): column name " << column_name << " is not a supplied column; use addSuppliedColumn() to create a column whose value can be supplied to LogFile." << EidosTerminate(); // remember the supplied value supplied_values_.SetKeyValue_StringKeys(column_name, std::move(value)); return gStaticEidosValueVOID; } // ********************* - (logical$)willAutolog(void) EidosValue_SP LogFile::ExecuteMethod_willAutolog(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) bool will_autolog = false; if (autologging_enabled_) { slim_tick_t tick = community_.Tick(); if ((tick - autolog_start_) % log_interval_ == 0) will_autolog = true; } return (will_autolog ? gStaticEidosValue_LogicalT : gStaticEidosValue_LogicalF); } EidosValue_SP LogFile::ExecuteMethod_addKeysAndValuesFrom(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_addKeysAndValuesFrom): LogFile manages its dictionary entries; they cannot be modified by the user." << EidosTerminate(nullptr); } EidosValue_SP LogFile::ExecuteMethod_appendKeysAndValuesFrom(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_appendKeysAndValuesFrom): LogFile manages its dictionary entries; they cannot be modified by the user." << EidosTerminate(nullptr); } EidosValue_SP LogFile::ExecuteMethod_clearKeysAndValues(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_clearKeysAndValues): LogFile manages its dictionary entries; they cannot be modified by the user." << EidosTerminate(nullptr); } EidosValue_SP LogFile::ExecuteMethod_setValue(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) { #pragma unused (p_method_id, p_arguments, p_interpreter) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_setValue): LogFile manages its dictionary entries; they cannot be modified by the user." << EidosTerminate(nullptr); } // // LogFile_Class // #pragma mark - #pragma mark LogFile_Class #pragma mark - EidosClass *gSLiM_LogFile_Class = nullptr; const std::vector *LogFile_Class::Properties(void) const { static std::vector *properties = nullptr; if (!properties) { THREAD_SAFETY_IN_ANY_PARALLEL("LogFile_Class::Properties(): not warmed up"); properties = new std::vector(*super::Properties()); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gEidosStr_filePath, true, kEidosValueMaskString | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_logInterval, true, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_tag, false, kEidosValueMaskInt | kEidosValueMaskSingleton))); properties->emplace_back((EidosPropertySignature *)(new EidosPropertySignature(gStr_precision, false, kEidosValueMaskInt | kEidosValueMaskSingleton))); std::sort(properties->begin(), properties->end(), CompareEidosPropertySignatures); } return properties; } const std::vector *LogFile_Class::Methods(void) const { static std::vector *methods = nullptr; if (!methods) { THREAD_SAFETY_IN_ANY_PARALLEL("LogFile_Class::Methods(): not warmed up"); methods = new std::vector(*super::Methods()); // our own methods methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addCustomColumn, kEidosValueMaskVOID))->AddString_S("columnName")->AddString_S(gEidosStr_source)->AddAny_O("context", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addCycle, kEidosValueMaskVOID))->AddObject_OSN("species", gSLiM_Species_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addCycleStage, kEidosValueMaskVOID))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addMeanSDColumns, kEidosValueMaskVOID))->AddString_S("columnName")->AddString_S(gEidosStr_source)->AddAny_O("context", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addPopulationSexRatio, kEidosValueMaskVOID))->AddObject_OSN("species", gSLiM_Species_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addPopulationSize, kEidosValueMaskVOID))->AddObject_OSN("species", gSLiM_Species_Class, gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addSubpopulationSexRatio, kEidosValueMaskVOID))->AddIntObject_S(gStr_subpop, gSLiM_Subpopulation_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addSubpopulationSize, kEidosValueMaskVOID))->AddIntObject_S(gStr_subpop, gSLiM_Subpopulation_Class)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addSuppliedColumn, kEidosValueMaskVOID))->AddString_S("columnName")); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_addTick, kEidosValueMaskVOID))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_flush, kEidosValueMaskVOID))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_logRow, kEidosValueMaskVOID))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setLogInterval, kEidosValueMaskVOID))->AddInt_OSN("logInterval", gStaticEidosValueNULL)); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setFilePath, kEidosValueMaskVOID))->AddString_S(gEidosStr_filePath)->AddString_ON("initialContents", gStaticEidosValueNULL)->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddLogical_OSN("compress", gStaticEidosValueNULL)->AddString_OSN("sep", gStaticEidosValueNULL)->AddLogical_OSN("header", gStaticEidosValueNULL)); methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_setSuppliedValue, kEidosValueMaskVOID))->AddString_S("columnName")->AddAnyBase_S("value"))); methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gStr_willAutolog, kEidosValueMaskLogical | kEidosValueMaskSingleton))); // overrides of Dictionary methods; these should not be declared again, to avoid a duplicate in the methods table //methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gEidosStr_addKeysAndValuesFrom, kEidosValueMaskVOID))->AddObject_S(gEidosStr_source, gEidosDictionaryUnretained_Class)); //methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gEidosStr_appendKeysAndValuesFrom, kEidosValueMaskVOID))->AddObject(gEidosStr_source, gEidosDictionaryUnretained_Class)); //methods->emplace_back((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gEidosStr_clearKeysAndValues, kEidosValueMaskVOID))); //methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosInstanceMethodSignature(gEidosStr_setValue, kEidosValueMaskVOID))->AddArg(kEidosValueMaskInt | kEidosValueMaskString | kEidosValueMaskSingleton, "key", nullptr)->AddAny("value"))); //methods->emplace_back(((EidosInstanceMethodSignature *)(new EidosClassMethodSignature(gEidosStr_setValuesVectorized, kEidosValueMaskVOID))->AddArg(kEidosValueMaskInt | kEidosValueMaskString | kEidosValueMaskSingleton, "key", nullptr)->AddAny("value"))); std::sort(methods->begin(), methods->end(), CompareEidosCallSignatures); } return methods; } EidosValue_SP LogFile_Class::ExecuteClassMethod(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { switch (p_method_id) { case gEidosID_setValuesVectorized: return ExecuteMethod_setValuesVectorized(p_method_id, p_target, p_arguments, p_interpreter); default: return super::ExecuteClassMethod(p_method_id, p_target, p_arguments, p_interpreter); } } // ********************* + (void)setValuesVectorized(is$ key, * values) // EidosValue_SP LogFile_Class::ExecuteMethod_setValuesVectorized(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const { #pragma unused (p_method_id, p_target, p_arguments, p_interpreter) EIDOS_TERMINATION << "ERROR (LogFile::ExecuteMethod_setValuesVectorized): LogFile manages its dictionary entries; they cannot be modified by the user." << EidosTerminate(nullptr); } ================================================ FILE: core/log_file.h ================================================ // // log_file.h // SLiM // // Created by Ben Haller on 11/2/20. // Copyright (c) 2020-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . #ifndef log_file_h #define log_file_h #include "eidos_value.h" #include "slim_globals.h" #include #include class Community; extern EidosClass *gSLiM_LogFile_Class; // Built-in and custom generator types that are presently supported enum class LogFileGeneratorType { kGenerator_Cycle, kGenerator_CycleStage, kGenerator_PopulationSexRatio, kGenerator_PopulationSize, kGenerator_SubpopulationSexRatio, kGenerator_SubpopulationSize, kGenerator_Tick, kGenerator_CustomScript, kGenerator_CustomMeanAndSD, // results in two columns! kGenerator_SuppliedColumn }; struct LogFileGeneratorInfo { LogFileGeneratorType type_; // the generator's type, as above EidosScript *script_; // a script to execute to generate the data, or nullptr slim_objectid_t objectid_; // the identifier for whatever object type might be relevant, or -1 EidosValue_SP context_; // the context value for the generator, if any LogFileGeneratorInfo(LogFileGeneratorType p_type, EidosScript *p_script, slim_objectid_t p_objectid, EidosValue_SP p_context) : type_(p_type), script_(p_script), objectid_(p_objectid), context_(p_context) {}; }; class LogFile : public EidosDictionaryRetained { private: typedef EidosDictionaryRetained super; protected: virtual void Raise_UsesStringKeys() const override __attribute__((__noreturn__)) __attribute__((cold)) __attribute__((analyzer_noreturn)); #ifdef SLIMGUI public: #else private: #endif Community &community_; // UNOWNED POINTER: the community we're working with std::string user_file_path_; // the one given by the user to us std::string resolved_file_path_; // the path we use internally, which must be an absolute path bool emit_header_ = true; // true if we are supposed to emit a header line at the start bool header_logged_ = false; // true if the header has been written out (in which case our generators are locked) bool compress_; std::string sep_; // the separator string between values, such as "," or "\t" int float_precision_ = 6; // the precision of output of float values bool autologging_enabled_ = false; // an overall flag to enable/disable automatic logging int64_t log_interval_ = 0; // tick interval for automatic logging slim_tick_t autolog_start_ = 0; // the first tick in which autologging occurred bool explicit_flushing_ = false; // an overall flag to enable/disable flushing by number of rows int64_t flush_interval_ = 0; // the maximum number of logged rows before we flush int64_t unflushed_row_count_ = 0; // a running counter since the last flush slim_usertag_t tag_value_ = SLIM_TAG_UNSET_VALUE; // a user-defined tag value // Generators; these generate the data in the log file std::vector generator_info_; // Columns; note that one generator can generate more than one column! std::vector column_names_; // A dictionary of supplied values, for kGenerator_SuppliedColumn EidosDictionaryUnretained supplied_values_; #ifdef SLIMGUI // For SLiMgui, LogFile keeps a record of all of the output it generates, which SLiMgui pulls out of it std::vector> emitted_lines_; #endif void RaiseForLockedHeader(const std::string &p_caller_name); EidosValue_SP _GeneratedValue_Cycle(const LogFileGeneratorInfo &p_generator_info); EidosValue_SP _GeneratedValue_CycleStage(const LogFileGeneratorInfo &p_generator_info); EidosValue_SP _GeneratedValue_PopulationSexRatio(const LogFileGeneratorInfo &p_generator_info); EidosValue_SP _GeneratedValue_PopulationSize(const LogFileGeneratorInfo &p_generator_info); EidosValue_SP _GeneratedValue_SubpopulationSexRatio(const LogFileGeneratorInfo &p_generator_info); EidosValue_SP _GeneratedValue_SubpopulationSize(const LogFileGeneratorInfo &p_generator_info); EidosValue_SP _GeneratedValue_Tick(const LogFileGeneratorInfo &p_generator_info); EidosValue_SP _GeneratedValue_CustomScript(const LogFileGeneratorInfo &p_generator_info); void _GeneratedValues_CustomMeanAndSD(const LogFileGeneratorInfo &p_generator_info, EidosValue_SP *p_generated_value_1, EidosValue_SP *p_generated_value_2); void _OutputValue(std::ostringstream &ss, EidosValue *value); public: LogFile(const LogFile &p_original) = delete; // no copy-construct LogFile& operator=(const LogFile&) = delete; // no copying explicit LogFile(Community &p_community); virtual ~LogFile(void) override; void ConfigureFile(const std::string &p_filePath, std::vector &p_initialContents, bool p_append, bool p_emitHeader, bool p_compress, const std::string &p_sep); void SetLogInterval(bool p_autologging_enabled, int64_t p_logInterval); void SetFlushInterval(bool p_explicit_flushing, int64_t p_flushInterval); void AppendNewRow(void); void TickEndCallout(void); inline const std::string &UserFilePath(void) const { return user_file_path_; } inline const std::string &ResolvedFilePath(void) const { return resolved_file_path_; } virtual std::vector SortedKeys_StringKeys(void) const override; // provide keys in column order // // Eidos support // virtual const EidosClass *Class(void) const override; virtual void Print(std::ostream &p_ostream) const override; virtual EidosValue_SP GetProperty(EidosGlobalStringID p_property_id) override; virtual void SetProperty(EidosGlobalStringID p_property_id, const EidosValue &p_value) override; virtual EidosValue_SP ExecuteInstanceMethod(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter) override; // Our own methods EidosValue_SP ExecuteMethod_addCustomColumn(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addCycle(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addCycleStage(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addMeanSDColumns(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addPopulationSexRatio(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addPopulationSize(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addSubpopulationSexRatio(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addSubpopulationSize(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addSuppliedColumn(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_addTick(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_flush(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_logRow(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setLogInterval(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setFilePath(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setSuppliedValue(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_willAutolog(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); // Overrides of Dictionary methods, since we have a special Dictionary behavior EidosValue_SP ExecuteMethod_addKeysAndValuesFrom(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_appendKeysAndValuesFrom(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_clearKeysAndValues(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); EidosValue_SP ExecuteMethod_setValue(EidosGlobalStringID p_method_id, const std::vector &p_arguments, EidosInterpreter &p_interpreter); }; class LogFile_Class : public EidosDictionaryRetained_Class { private: typedef EidosDictionaryRetained_Class super; public: LogFile_Class(const LogFile_Class &p_original) = delete; // no copy-construct LogFile_Class& operator=(const LogFile_Class &) = delete; // no copying inline LogFile_Class(const std::string &p_class_name, EidosClass *p_superclass) : super(p_class_name, p_superclass) { } virtual const std::vector *Properties(void) const override; virtual const std::vector *Methods(void) const override; // Overrides of Dictionary methods, since we have a special Dictionary behavior virtual EidosValue_SP ExecuteClassMethod(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const override; EidosValue_SP ExecuteMethod_setValuesVectorized(EidosGlobalStringID p_method_id, EidosValue_Object *p_target, const std::vector &p_arguments, EidosInterpreter &p_interpreter) const; }; #endif /* log_file_h */ ================================================ FILE: core/main.cpp ================================================ // // main.cpp // SLiM // // Created by Ben Haller on 12/12/14. // Copyright (c) 2014-2025 Benjamin C. Haller. All rights reserved. // A product of the Messer Lab, http://messerlab.org/slim/ // // This file is part of SLiM. // // SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with SLiM. If not, see . /* This file defines main() and related functions that initiate and run a SLiM simulation. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "community.h" #include "species.h" #include "eidos_globals.h" #include "slim_globals.h" #include "eidos_test.h" #include "slim_test.h" #include "eidos_symbol_table.h" #include "interaction_type.h" #include "eidos_rng.h" // Get our Git commit SHA-1, as C string "g_GIT_SHA1" #include "../cmake/GitSHA1.h" static void PrintUsageAndDie(bool p_print_header, bool p_print_full_usage) { if (p_print_header) { SLIM_OUTSTREAM << "SLiM version " << SLIM_VERSION_STRING << ", built " << __DATE__ << " " __TIME__ << "." << std::endl; if (strcmp(g_GIT_SHA1, "GITDIR-NOTFOUND") == 0) SLIM_OUTSTREAM << "Git commit SHA-1: unknown (built from a non-Git source archive)" << std::endl; else SLIM_OUTSTREAM << "Git commit SHA-1: " << std::string(g_GIT_SHA1) << std::endl; #ifdef DEBUG SLIM_OUTSTREAM << "This is a DEBUG build of SLiM." << std::endl; #else SLIM_OUTSTREAM << "This is a RELEASE build of SLiM." << std::endl; #endif #ifdef _OPENMP SLIM_OUTSTREAM << "This is a PARALLEL (MULTI-THREADED) build of SLiM." << std::endl; #else SLIM_OUTSTREAM << "This is a NON-PARALLEL (SINGLE-THREADED) build of SLiM." << std::endl; #endif #if (SLIMPROFILING == 1) SLIM_OUTSTREAM << "This is a PROFILING build of SLiM." << std::endl; #endif SLIM_OUTSTREAM << std::endl; SLIM_OUTSTREAM << "SLiM is a product of the Messer Lab, http://messerlab.org/" << std::endl; SLIM_OUTSTREAM << "Copyright 2013-2026 Benjamin C. Haller. All rights reserved." << std::endl << std::endl; SLIM_OUTSTREAM << "By Benjamin C. Haller, http://benhaller.com/, and Philipp Messer." << std::endl << std::endl; SLIM_OUTSTREAM << "---------------------------------------------------------------------------------" << std::endl << std::endl; SLIM_OUTSTREAM << "SLiM home page: http://messerlab.org/slim/" << std::endl; SLIM_OUTSTREAM << "slim-announce mailing list: https://groups.google.com/d/forum/slim-announce" << std::endl; SLIM_OUTSTREAM << "slim-discuss mailing list: https://groups.google.com/d/forum/slim-discuss" << std::endl << std::endl; SLIM_OUTSTREAM << "---------------------------------------------------------------------------------" << std::endl << std::endl; SLIM_OUTSTREAM << "SLiM is free software: you can redistribute it and/or modify it under the terms" << std::endl; SLIM_OUTSTREAM << "of the GNU General Public License as published by the Free Software Foundation," << std::endl; SLIM_OUTSTREAM << "either version 3 of the License, or (at your option) any later version." << std::endl << std::endl; SLIM_OUTSTREAM << "SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;" << std::endl; SLIM_OUTSTREAM << "without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR" << std::endl; SLIM_OUTSTREAM << "PURPOSE. See the GNU General Public License for more details." << std::endl << std::endl; SLIM_OUTSTREAM << "You should have received a copy of the GNU General Public License along with" << std::endl; SLIM_OUTSTREAM << "SLiM. If not, see ." << std::endl << std::endl; SLIM_OUTSTREAM << "---------------------------------------------------------------------------------" << std::endl << std::endl; } SLIM_OUTSTREAM << "usage: slim -v[ersion] | -u[sage] | -h[elp] | -testEidos | -testSLiM |" << std::endl; SLIM_OUTSTREAM << " [-l[ong] []] [-s[eed] ] [-t[ime]] [-m[em]] [-M[emhist]] [-x]" << std::endl; SLIM_OUTSTREAM << " [-d[efine] ] [-c[heck]] [-p[rogress]] "; #ifdef _OPENMP // Some flags are visible only for a parallel build // FIXME: these might not fit on the same line as other things SLIM_OUTSTREAM << "[-maxThreads ] [-perTaskThreads \"x\"] "; #endif #if (SLIMPROFILING == 1) // Some flags are visible only for a profile build SLIM_OUTSTREAM << "[] "; #endif SLIM_OUTSTREAM << "[