diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..20cd5b3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,18 @@ +name: Unit/integration tests +on: [ push, pull_request, workflow_dispatch ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 25 + server-id: github + server-username: GITHUB_ACTOR + server-password: GITHUB_TOKEN + - run: mvn test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c55bec8..bebee7c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ /.idea /morph-models.iml /out + +# Maven build output +target/ + +# Package ZIPs +*.v*.zip diff --git a/README.md b/README.md index 017ee2a..38d6d4e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,102 @@ morph-models ============ -Models for dealing with standard discrete morphological data. +Models for discrete morphological character data in [BEAST 3](https://github.com/CompEvol/beast3). -Aiming at MK and MKv models from Lewis, 2001 (http://sysbio.oxfordjournals.org/content/50/6/913.short) +Implements the Lewis MK and MKv substitution models (Lewis, 2001), along with ordinal and nested ordinal variants for ordered character data. -A tutorial is available [here](http://www.beast2.org/morphological-models/) +## Modules + +- **beast-morph-models** — core substitution models and alignment classes (depends on `beast-base`) +- **beast-morph-models-fx** — BEAUti integration (depends on `beast-fx`) + +## Substitution models + +| Class | Description | +|-------|-------------| +| `LewisMK` | Equal or user-specified frequency Mk model | +| `Ordinal` | Tridiagonal rate matrix for ordered characters | +| `NestedOrdinal` | Nested ordinal rate matrix (state 0 transitions to all others) | + +## Building + +BEAST 3 dependencies are resolved from [GitHub Packages](https://github.com/CompEvol/beast3/packages) (or Maven Central, if published there). For GitHub Packages, add a [personal access token](https://github.com/settings/tokens) (classic) with `read:packages` scope to `~/.m2/settings.xml`: + +```xml + + + + github + YOUR_GITHUB_USERNAME + YOUR_GITHUB_PAT + + + +``` + +Then build morph-models: + +```bash +cd ~/Git/morph-models +mvn compile +mvn test -pl beast-morph-models +``` + +Alternatively, you can install BEAST 3 from source (no GitHub auth needed): + +```bash +cd ~/Git/beast3 +mvn install -DskipTests +``` + +## Running + +```bash +# Validate an XML +mvn -pl beast-morph-models exec:exec -Dbeast.args="-validate examples/M3982.xml" + +# Run an analysis +mvn -pl beast-morph-models exec:exec -Dbeast.args="-overwrite examples/M3982.xml" +``` + +## Examples + +- `examples/M3982.xml` — Anolis lizard morphological analysis using BEAST 3 spec classes +- `examples/legacy-2.7/` — original BEAST 2.7 XML files (some require external packages) + +## Releasing + +### ZIP / CBAN + +The existing `release.sh` script builds a BEAST package ZIP and optionally +creates a GitHub release for submission to [CBAN](https://github.com/CompEvol/CBAN): + +```bash +./release.sh # build ZIP only +./release.sh --release # build ZIP + create GitHub release +``` + +### Maven Central + +```bash +mvn clean deploy -Prelease +``` + +This builds the JARs (with `version.xml` embedded for service discovery), generates +sources and javadoc JARs, signs everything with GPG, and uploads to Maven Central. + +BEAST 3 users can then install with: + +``` +Package Manager > Install from Maven > io.github.compevol:beast-morph-models:1.3.0 +``` + +Or from the command line: + +```bash +packagemanager -maven io.github.compevol:beast-morph-models:1.3.0 +``` + +## References + +Lewis, P. O. (2001). A likelihood approach to estimating phylogeny from discrete morphological character data. *Systematic Biology*, 50(6), 913–925. diff --git a/beast-morph-models-fx/pom.xml b/beast-morph-models-fx/pom.xml new file mode 100644 index 0000000..fb43515 --- /dev/null +++ b/beast-morph-models-fx/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + + io.github.compevol + morph-models-parent + 1.3.0-SNAPSHOT + + + beast-morph-models-fx + BEAST Morph Models FX + + + + + io.github.compevol + beast-morph-models + + + io.github.compevol + beast-base + + + io.github.compevol + beast-fx + + + io.github.compevol + beast-pkgmgmt + + + + + org.openjfx + javafx-controls + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + java + ${maven.multiModuleProjectDirectory} + --module-path %classpath -m beast.fx/${beast.main} ${beast.args} + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + src/assembly/beast-package.xml + + ${beast.pkg.name}.v${beast.pkg.version} + false + + + + beast-package + package + single + + + + + + diff --git a/beast-morph-models-fx/src/assembly/beast-package.xml b/beast-morph-models-fx/src/assembly/beast-package.xml new file mode 100644 index 0000000..e4d1ace --- /dev/null +++ b/beast-morph-models-fx/src/assembly/beast-package.xml @@ -0,0 +1,46 @@ + + beast-package + + zip + + false + + + + + ${project.parent.basedir}/version.xml + / + + + + + + + /lib + true + runtime + + io.github.compevol:beast-morph-models + io.github.compevol:beast-morph-models-fx + + + + + + + + ${project.basedir}/src/main/resources/beast.morph.models.fx/fxtemplates + /fxtemplates + + + + ${project.parent.basedir}/examples + /examples + + legacy*/** + + + + diff --git a/beast-morph-models-fx/src/main/java/module-info.java b/beast-morph-models-fx/src/main/java/module-info.java new file mode 100644 index 0000000..55d4a35 --- /dev/null +++ b/beast-morph-models-fx/src/main/java/module-info.java @@ -0,0 +1,12 @@ +open module beast.morph.models.fx { + requires beast.morph.models; + requires beast.base; + requires beast.pkgmgmt; + requires beast.fx; + requires javafx.controls; + + exports morphmodels.app.beauti; + + provides beast.base.core.BEASTInterface with + morphmodels.app.beauti.BeautiMorphModelAlignmentProvider; +} diff --git a/src/morphmodels/app/beauti/BeautiMorphModelAlignmentProvider.java b/beast-morph-models-fx/src/main/java/morphmodels/app/beauti/BeautiMorphModelAlignmentProvider.java similarity index 99% rename from src/morphmodels/app/beauti/BeautiMorphModelAlignmentProvider.java rename to beast-morph-models-fx/src/main/java/morphmodels/app/beauti/BeautiMorphModelAlignmentProvider.java index 39fe28d..00acb39 100644 --- a/src/morphmodels/app/beauti/BeautiMorphModelAlignmentProvider.java +++ b/beast-morph-models-fx/src/main/java/morphmodels/app/beauti/BeautiMorphModelAlignmentProvider.java @@ -11,7 +11,7 @@ import beastfx.app.inputeditor.BeautiAlignmentProvider; import beastfx.app.inputeditor.BeautiDoc; import beastfx.app.util.Alert; -import beastfx.app.util.ExtensionFileFilter; + import beastfx.app.util.FXUtils; import javafx.scene.control.ButtonType; import beast.base.core.BEASTInterface; diff --git a/fxtemplates/morph-models.xml b/beast-morph-models-fx/src/main/resources/beast.morph.models.fx/fxtemplates/morph-models.xml similarity index 68% rename from fxtemplates/morph-models.xml rename to beast-morph-models-fx/src/main/resources/beast.morph.models.fx/fxtemplates/morph-models.xml index c5eb999..674b074 100644 --- a/fxtemplates/morph-models.xml +++ b/beast-morph-models-fx/src/main/resources/beast.morph.models.fx/fxtemplates/morph-models.xml @@ -1,5 +1,5 @@ + namespace='morphmodels.evolution.substitutionmodel:morphmodels.evolution.alignment:morphmodels.app.beauti:beastfx.app.beauti:beastfx.app.inputeditor:beast.pkgmgmt:beast.base.core:beast.base.inference:beast.base.evolution.branchratemodel:beast.base.evolution.speciation:beast.base.evolution.tree.coalescent:beast.base.util:beast.base.math:beast.evolution.nuc:beast.base.evolution.operator:beast.base.inference.operator:beast.base.evolution.operator.kernel:beast.base.spec.evolution.sitemodel:beast.base.evolution.substitutionmodel:beast.base.evolution.likelihood:beast.evolution:beast.base.inference.distribution'> @@ -16,7 +16,6 @@ - @@ -27,64 +26,65 @@ - - + - + - - - + + + - + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + - + - - - + + - + ]]> @@ -116,7 +116,7 @@ Estimates tip dates for tree t:$(n) - Scales all internal nodes for tree t:$(n) + Scale internal nodes for tree t:$(n) Scales root node for tree t:$(n) Draws new internal node heights uniformally for tree t:$(n) Performs subtree slide rearrangement of tree t:$(n) @@ -155,4 +155,3 @@ - diff --git a/beast-morph-models/pom.xml b/beast-morph-models/pom.xml new file mode 100644 index 0000000..f1383d3 --- /dev/null +++ b/beast-morph-models/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + io.github.compevol + morph-models-parent + 1.3.0-SNAPSHOT + + + beast-morph-models + BEAST Morph Models + + + + + io.github.compevol + beast-base + + + io.github.compevol + beast-pkgmgmt + + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${project.build.testOutputDirectory} + + --add-reads beast.morph.models=ALL-UNNAMED + --add-reads beast.base=ALL-UNNAMED + --add-reads beast.pkgmgmt=ALL-UNNAMED + + + ${project.build.outputDirectory} + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-version-xml + generate-resources + copy-resources + + ${project.build.directory} + + + ${maven.multiModuleProjectDirectory} + + version.xml + + + + + + + embed-version-xml-in-jar + generate-resources + copy-resources + + ${project.build.outputDirectory} + + + ${maven.multiModuleProjectDirectory} + + version.xml + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + java + ${maven.multiModuleProjectDirectory} + --module-path %classpath -DBEAST_PACKAGE_PATH=${project.build.outputDirectory} -m beast.base/beast.base.minimal.BeastMain ${beast.args} + + + + + diff --git a/beast-morph-models/src/main/java/module-info.java b/beast-morph-models/src/main/java/module-info.java new file mode 100644 index 0000000..91fb9ed --- /dev/null +++ b/beast-morph-models/src/main/java/module-info.java @@ -0,0 +1,14 @@ +open module beast.morph.models { + requires beast.base; + requires beast.pkgmgmt; + + exports morphmodels.evolution.substitutionmodel; + exports morphmodels.evolution.alignment; + + provides beast.base.core.BEASTInterface with + morphmodels.evolution.alignment.AscertainedForParsimonyUninformativeAlignment, + morphmodels.evolution.alignment.AscertainedForParsimonyUninformativeFilteredAlignment, + morphmodels.evolution.substitutionmodel.LewisMK, + morphmodels.evolution.substitutionmodel.Ordinal, + morphmodels.evolution.substitutionmodel.NestedOrdinal; +} diff --git a/src/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeAlignment.java b/beast-morph-models/src/main/java/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeAlignment.java similarity index 100% rename from src/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeAlignment.java rename to beast-morph-models/src/main/java/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeAlignment.java diff --git a/src/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeFilteredAlignment.java b/beast-morph-models/src/main/java/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeFilteredAlignment.java similarity index 100% rename from src/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeFilteredAlignment.java rename to beast-morph-models/src/main/java/morphmodels/evolution/alignment/AscertainedForParsimonyUninformativeFilteredAlignment.java diff --git a/src/morphmodels/evolution/substitutionmodel/LewisMK.java b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/LewisMK.java similarity index 68% rename from src/morphmodels/evolution/substitutionmodel/LewisMK.java rename to beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/LewisMK.java index d0cd909..cf3edb3 100644 --- a/src/morphmodels/evolution/substitutionmodel/LewisMK.java +++ b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/LewisMK.java @@ -8,8 +8,10 @@ import beast.base.evolution.datatype.Binary; import beast.base.evolution.datatype.DataType; import beast.base.evolution.datatype.StandardData; +import beast.base.evolution.substitutionmodel.EigenDecomposition; import beast.base.evolution.tree.Node; -import beast.base.evolution.substitutionmodel.*; +import beast.base.spec.evolution.substitutionmodel.Base; +import beast.base.spec.evolution.substitutionmodel.Frequencies; import java.util.Arrays; @@ -22,33 +24,22 @@ "A likelihood approach to estimating phylogeny from discrete morphological character data. \n" + "Systematic biology, 50(6), 913-925.", year = 2001, firstAuthorSurname = "Lewis", DOI="10.1080/106351501753462876") -public class LewisMK extends SubstitutionModel.Base { +public class LewisMK extends Base { public Input nrOfStatesInput = new Input("stateNumber", "the number of character states"); public Input dataTypeInput = new Input("datatype", "datatype, used to determine the number of states", Validate.XOR, nrOfStatesInput); - //public Input isMKvInput = new Input<>("isMkv", "whether to use MKv model or just MK", false); boolean hasFreqs; private boolean updateFreqs; public LewisMK() { // this is added to avoid a parsing error inherited from superclass because frequencies are not provided. frequenciesInput.setRule(Validate.OPTIONAL); - try { - // this call will be made twice when constructed from XML - // but this ensures that the object is validly constructed for testing purposes. - // RRB: for testing you should call initAndValidate() independently - // At this stage, the inputs are not valid, hence initAndValidate fails. - // initAndValidate(); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("initAndValidate() call failed when constructing LewisMK()"); - } } double totalSubRate; - double[] frequencies; + double[] freqValues; EigenDecomposition eigenDecomposition; private void setFrequencies() { @@ -57,14 +48,14 @@ private void setFrequencies() { if( frequencies1.getFreqs().length != nrOfStates ) { throw new RuntimeException("number of stationary frequencies does not match number of states."); } - System.arraycopy(frequencies1.getFreqs(), 0, frequencies, 0, nrOfStates); + System.arraycopy(frequencies1.getFreqs(), 0, freqValues, 0, nrOfStates); totalSubRate = 1; for(int k = 0; k < nrOfStates; ++k) { - totalSubRate -= frequencies[k]*frequencies[k]; + totalSubRate -= freqValues[k]*freqValues[k]; } hasFreqs = true; } else { - Arrays.fill(frequencies, (1.0 / nrOfStates)); + Arrays.fill(freqValues, (1.0 / nrOfStates)); hasFreqs = false; } updateFreqs = false; @@ -78,26 +69,19 @@ public void initAndValidate() { nrOfStates = dataTypeInput.get().getStateCount(); } - if (nrOfStates <= 1) { - // this goes here so that nrOfStates/(nrOfStates-1) in getTransitionProbabilities() - // does not throw a division by zero exception + if (nrOfStates <= 1) { throw new IllegalArgumentException("The number of states should be at least 2 but is" + nrOfStates + ".\n" + "This may be due to a site in the alignment having only 1 state, which can be fixed by " + "removing the site from the alignment."); } -// double[] eval = new double[]{0.0, -1.3333333333333333, -1.3333333333333333, -1.3333333333333333}; -// double[] evec = new double[]{1.0, 2.0, 0.0, 0.5, 1.0, -2.0, 0.5, 0.0, 1.0, 2.0, 0.0, -0.5, 1.0, -2.0, -0.5, 0.0}; -// double[] ivec = new double[]{0.25, 0.25, 0.25, 0.25, 0.125, -0.125, 0.125, -0.125, 0.0, 1.0, 0.0, -1.0, 1.0, 0.0, -1.0, 0.0}; -// -// eigenDecomposition = new EigenDecomposition(evec, ivec, eval); - frequencies = new double[nrOfStates]; + freqValues = new double[nrOfStates]; setFrequencies(); } @Override public double[] getFrequencies() { - return frequencies; + return freqValues; } @Override @@ -113,7 +97,7 @@ public void getTransitionProbabilities(Node node, double fStartTime, double fEnd for( int i = 0; i < nrOfStates; ++i ) { final int r = i * nrOfStates; for( int j = 0; j < nrOfStates; ++j ) { - matrix[r + j] = frequencies[j] * e2; + matrix[r + j] = freqValues[j] * e2; } matrix[r + i] += e1; } @@ -130,6 +114,39 @@ public void getTransitionProbabilities(Node node, double fStartTime, double fEnd } } + @Override + public double[] getRateMatrix(Node node) { + if (updateFreqs) { + setFrequencies(); + } + + double[] matrix = new double[nrOfStates * nrOfStates]; + if (hasFreqs) { + for (int i = 0; i < nrOfStates; i++) { + double rowSum = 0; + for (int j = 0; j < nrOfStates; j++) { + if (i != j) { + matrix[i * nrOfStates + j] = freqValues[j] / totalSubRate; + rowSum += matrix[i * nrOfStates + j]; + } + } + matrix[i * nrOfStates + i] = -rowSum; + } + } else { + double offDiag = 1.0 / (nrOfStates - 1); + for (int i = 0; i < nrOfStates; i++) { + for (int j = 0; j < nrOfStates; j++) { + if (i == j) { + matrix[i * nrOfStates + j] = -1.0; + } else { + matrix[i * nrOfStates + j] = offDiag; + } + } + } + } + return matrix; + } + @Override public EigenDecomposition getEigenDecomposition(Node node) { return eigenDecomposition; @@ -137,11 +154,7 @@ public EigenDecomposition getEigenDecomposition(Node node) { @Override public boolean canHandleDataType(DataType dataType) { - if (dataType instanceof StandardData || dataType instanceof Binary) { - return true; - } - return false; - //throw new Exception("Can only handle StandardData and binary data"); + return dataType instanceof StandardData || dataType instanceof Binary; } protected boolean requiresRecalculation() { diff --git a/src/morphmodels/evolution/substitutionmodel/NStatesNoRatesSubstitutionModel.java b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/NStatesNoRatesSubstitutionModel.java similarity index 64% rename from src/morphmodels/evolution/substitutionmodel/NStatesNoRatesSubstitutionModel.java rename to beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/NStatesNoRatesSubstitutionModel.java index 13b6c1d..047992f 100644 --- a/src/morphmodels/evolution/substitutionmodel/NStatesNoRatesSubstitutionModel.java +++ b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/NStatesNoRatesSubstitutionModel.java @@ -3,27 +3,18 @@ import beast.base.core.Description; import beast.base.core.Input; import beast.base.core.Input.Validate; -import beast.base.evolution.substitutionmodel.*; +import beast.base.spec.evolution.substitutionmodel.BasicGeneralSubstitutionModel; /** * @author Luke Maurits */ -@Description("A simple subclass of GeneralSubstitutionModel which does not require a rates input and does require a number of states input.") -public class NStatesNoRatesSubstitutionModel extends GeneralSubstitutionModel { +@Description("A simple subclass of BasicGeneralSubstitutionModel which does not require a rates input and does require a number of states input.") +public abstract class NStatesNoRatesSubstitutionModel extends BasicGeneralSubstitutionModel { // Number of states input is required public Input nrOfStatesInput = new Input("stateNumber", "the number of character states", Validate.REQUIRED); - public NStatesNoRatesSubstitutionModel() { - // Rates input is *not* required - ratesInput.setRule(Validate.OPTIONAL); - } - @Override - // Minimally changed from parent implementation: - // Derive nrOfStates from the appropriate input, not freqs. - // Ensure frequencies has correct length. - // Do not check dimension of rates parameter. public void initAndValidate() { nrOfStates = nrOfStatesInput.get(); frequencies = frequenciesInput.get(); diff --git a/src/morphmodels/evolution/substitutionmodel/NestedOrdinal.java b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/NestedOrdinal.java similarity index 82% rename from src/morphmodels/evolution/substitutionmodel/NestedOrdinal.java rename to beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/NestedOrdinal.java index 186324f..bb58ec5 100644 --- a/src/morphmodels/evolution/substitutionmodel/NestedOrdinal.java +++ b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/NestedOrdinal.java @@ -1,18 +1,7 @@ package morphmodels.evolution.substitutionmodel; -import beast.base.core.Citation; import beast.base.core.Description; -import beast.base.core.Input; -import beast.base.core.Input.Validate; -import beast.base.inference.parameter.RealParameter; -import beast.base.evolution.datatype.Binary; -import beast.base.evolution.datatype.DataType; -import beast.base.evolution.datatype.StandardData; -import beast.base.evolution.tree.Node; - -import java.util.Arrays; -import beast.base.evolution.substitutionmodel.*; /** * @author Luke Maurits diff --git a/src/morphmodels/evolution/substitutionmodel/Ordinal.java b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/Ordinal.java similarity index 77% rename from src/morphmodels/evolution/substitutionmodel/Ordinal.java rename to beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/Ordinal.java index 91bb586..6d3da92 100644 --- a/src/morphmodels/evolution/substitutionmodel/Ordinal.java +++ b/beast-morph-models/src/main/java/morphmodels/evolution/substitutionmodel/Ordinal.java @@ -1,16 +1,6 @@ package morphmodels.evolution.substitutionmodel; -import beast.base.core.Citation; import beast.base.core.Description; -import beast.base.core.Input; -import beast.base.core.Input.Validate; -import beast.base.evolution.datatype.Binary; -import beast.base.evolution.datatype.DataType; -import beast.base.evolution.datatype.StandardData; -import beast.base.evolution.tree.Node; - -import java.util.Arrays; -import beast.base.evolution.substitutionmodel.*; /** * @author Luke Maurits diff --git a/beast-morph-models/src/test/java/morphmodels/evolution/substitutionmodel/LewisMKTest.java b/beast-morph-models/src/test/java/morphmodels/evolution/substitutionmodel/LewisMKTest.java new file mode 100644 index 0000000..11f3220 --- /dev/null +++ b/beast-morph-models/src/test/java/morphmodels/evolution/substitutionmodel/LewisMKTest.java @@ -0,0 +1,180 @@ +package morphmodels.evolution.substitutionmodel; + +import beast.base.evolution.tree.Node; +import beast.base.spec.evolution.substitutionmodel.Frequencies; +import beast.base.spec.inference.parameter.SimplexParam; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class LewisMKTest { + + @Test + void testLewisMKInitWithStateNumber() { + LewisMK model = new LewisMK(); + model.initByName("stateNumber", 4); + + assertEquals(4, model.getStateCount()); + + double[] freqs = model.getFrequencies(); + assertEquals(4, freqs.length); + for (double f : freqs) { + assertEquals(0.25, f, 1e-10); + } + } + + @Test + void testTransitionProbabilitiesRowsSumToOne() { + LewisMK model = new LewisMK(); + model.initByName("stateNumber", 4); + + double[] matrix = new double[16]; + Node node = new Node(); + model.getTransitionProbabilities(node, 1.0, 0.0, 1.0, matrix); + + for (int i = 0; i < 4; i++) { + double rowSum = 0; + for (int j = 0; j < 4; j++) { + rowSum += matrix[i * 4 + j]; + assertTrue(matrix[i * 4 + j] >= 0, "Transition probability should be non-negative"); + } + assertEquals(1.0, rowSum, 1e-10, "Row " + i + " should sum to 1.0"); + } + } + + @Test + void testTransitionProbabilitiesAtZeroTime() { + LewisMK model = new LewisMK(); + model.initByName("stateNumber", 3); + + double[] matrix = new double[9]; + Node node = new Node(); + // zero branch length => identity matrix + model.getTransitionProbabilities(node, 1.0, 1.0, 1.0, matrix); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (i == j) { + assertEquals(1.0, matrix[i * 3 + j], 1e-10); + } else { + assertEquals(0.0, matrix[i * 3 + j], 1e-10); + } + } + } + } + + @Test + void testWithProvidedFrequencies() { + LewisMK model = new LewisMK(); + Frequencies freqs = new Frequencies(); + freqs.initByName("frequencies", new SimplexParam(new double[]{0.1, 0.2, 0.3, 0.4})); + + model.initByName("stateNumber", 4, "frequencies", freqs); + + double[] modelFreqs = model.getFrequencies(); + assertEquals(0.1, modelFreqs[0], 1e-10); + assertEquals(0.2, modelFreqs[1], 1e-10); + assertEquals(0.3, modelFreqs[2], 1e-10); + assertEquals(0.4, modelFreqs[3], 1e-10); + + // transition probabilities should still sum to 1.0 + double[] matrix = new double[16]; + Node node = new Node(); + model.getTransitionProbabilities(node, 1.0, 0.0, 1.0, matrix); + + for (int i = 0; i < 4; i++) { + double rowSum = 0; + for (int j = 0; j < 4; j++) { + rowSum += matrix[i * 4 + j]; + } + assertEquals(1.0, rowSum, 1e-10, "Row " + i + " should sum to 1.0"); + } + } + + @Test + void testOrdinalRateMatrixIsTridiagonal() { + Ordinal model = new Ordinal(); + Frequencies freqs = new Frequencies(); + freqs.initByName("frequencies", new SimplexParam(new double[]{0.2, 0.2, 0.2, 0.2, 0.2})); + model.initByName("stateNumber", 5, "frequencies", freqs); + + // trigger rate setup + double[] matrix = new double[25]; + Node node = new Node(); + model.getTransitionProbabilities(node, 1.0, 0.0, 1.0, matrix); + + // verify transition probabilities sum to 1.0 + for (int i = 0; i < 5; i++) { + double rowSum = 0; + for (int j = 0; j < 5; j++) { + rowSum += matrix[i * 5 + j]; + } + assertEquals(1.0, rowSum, 1e-10, "Row " + i + " should sum to 1.0"); + } + + // check relative rates are tridiagonal + double[] rates = model.getRelativeRates(); + // relativeRates[0] = rate(0->1) = 1.0 + assertEquals(1.0, rates[0], 1e-10); + // relativeRates[nrOfStates*(nrOfStates-1)-1] = rate(4->3) = 1.0 + assertEquals(1.0, rates[19], 1e-10); + } + + @Test + void testNestedOrdinalRateMatrix() { + NestedOrdinal model = new NestedOrdinal(); + Frequencies freqs = new Frequencies(); + freqs.initByName("frequencies", new SimplexParam(new double[]{0.2, 0.2, 0.2, 0.2, 0.2})); + model.initByName("stateNumber", 5, "frequencies", freqs); + + // trigger rate setup + double[] matrix = new double[25]; + Node node = new Node(); + model.getTransitionProbabilities(node, 1.0, 0.0, 1.0, matrix); + + // verify transition probabilities sum to 1.0 + for (int i = 0; i < 5; i++) { + double rowSum = 0; + for (int j = 0; j < 5; j++) { + rowSum += matrix[i * 5 + j]; + } + assertEquals(1.0, rowSum, 1e-10, "Row " + i + " should sum to 1.0"); + } + + // check relative rates: first row should have all 1s (state 0 can go to any state) + double[] rates = model.getRelativeRates(); + for (int i = 0; i < 4; i++) { + assertEquals(1.0, rates[i], 1e-10, "Rate from state 0 to state " + (i+1) + " should be 1.0"); + } + } + + @Test + void testRateMatrixEqualFrequencies() { + LewisMK model = new LewisMK(); + model.initByName("stateNumber", 4); + + Node node = new Node(); + double[] rateMatrix = model.getRateMatrix(node); + assertEquals(16, rateMatrix.length); + + // all off-diagonal should be 1/(n-1) = 1/3 + // all diagonal should be -1.0 + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (i == j) { + assertEquals(-1.0, rateMatrix[i * 4 + j], 1e-10); + } else { + assertEquals(1.0 / 3.0, rateMatrix[i * 4 + j], 1e-10); + } + } + } + } + + @Test + void testRejectsOneState() { + LewisMK model = new LewisMK(); + assertThrows(RuntimeException.class, () -> + model.initByName("stateNumber", 1) + ); + } +} diff --git a/beast-morph-models/src/test/resources/examples/M3982.xml b/beast-morph-models/src/test/resources/examples/M3982.xml new file mode 100644 index 0000000..0b47006 --- /dev/null +++ b/beast-morph-models/src/test/resources/examples/M3982.xml @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.xml b/build.xml deleted file mode 100644 index 8de1abc..0000000 --- a/build.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - Build MM. - Also used by Hudson MM project. - JUnit test is available for this build. - $Id: build_MM.xml $ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** Required file version.xml does not exist. ** - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/M3982.xml b/examples/M3982.xml index 99f9c1e..0b47006 100644 --- a/examples/M3982.xml +++ b/examples/M3982.xml @@ -1,4 +1,9 @@ - + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - -beast.base.inference.distribution.Uniform -beast.base.inference.distribution.Exponential -beast.base.inference.distribution.LogNormalDistributionModel -beast.base.inference.distribution.Normal -beast.base.inference.distribution.Beta -beast.base.inference.distribution.Gamma -beast.base.inference.distribution.LaplaceDistribution -beast.base.inference.distribution.Prior -beast.base.inference.distribution.InverseGamma -beast.base.inference.distribution.OneOnX - - - + + - 1.0 + - - - 1.0 + + + - - - - - - + + + + - - - - 1.0 - 1.0 - 0.0 - + + + + + - - 1.0 - + - - - + + + + - - 1.0 - 1.0 - 0.0 - + + - - - + + + + - - 1.0 - 1.0 - 0.0 - + + - - - + + + + - - 1.0 - 1.0 - 0.0 - + + - - - + + + + - - 1.0 - 1.0 - 0.0 - + + - - - + + + + - - 1.0 - 1.0 - 0.0 - + + - - - + + + + - - 1.0 - 1.0 - 0.0 - + + + - + - + - + - + - + @@ -247,12 +216,12 @@ spec="FilteredAlignment"> - + - + @@ -262,9 +231,13 @@ spec="FilteredAlignment"> + + + + - + diff --git a/examples/legacy-2.7/M3982.xml b/examples/legacy-2.7/M3982.xml new file mode 100644 index 0000000..99f9c1e --- /dev/null +++ b/examples/legacy-2.7/M3982.xml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +beast.base.inference.distribution.Uniform +beast.base.inference.distribution.Exponential +beast.base.inference.distribution.LogNormalDistributionModel +beast.base.inference.distribution.Normal +beast.base.inference.distribution.Beta +beast.base.inference.distribution.Gamma +beast.base.inference.distribution.LaplaceDistribution +beast.base.inference.distribution.Prior +beast.base.inference.distribution.InverseGamma +beast.base.inference.distribution.OneOnX + + + + + + + + + + + + + 1.0 + + + + + 1.0 + + + + + + + + + + + + + + 1.0 + 1.0 + 0.0 + + + + 1.0 + + + + + + + + 1.0 + 1.0 + 0.0 + + + + + + + + + 1.0 + 1.0 + 0.0 + + + + + + + + + 1.0 + 1.0 + 0.0 + + + + + + + + + 1.0 + 1.0 + 0.0 + + + + + + + + + 1.0 + 1.0 + 0.0 + + + + + + + + + 1.0 + 1.0 + 0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/nonEqualFreqs.xml b/examples/legacy-2.7/nonEqualFreqs.xml similarity index 100% rename from examples/nonEqualFreqs.xml rename to examples/legacy-2.7/nonEqualFreqs.xml diff --git a/examples/penguins.xml b/examples/legacy-2.7/penguins.xml similarity index 100% rename from examples/penguins.xml rename to examples/legacy-2.7/penguins.xml diff --git a/examples/penguins_Mkv.xml b/examples/legacy-2.7/penguins_Mkv.xml similarity index 100% rename from examples/penguins_Mkv.xml rename to examples/legacy-2.7/penguins_Mkv.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..21dbec9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,182 @@ + + + 4.0.0 + + io.github.compevol + morph-models-parent + 1.3.0-SNAPSHOT + pom + + Morph Models + Lewis MK/MKv substitution models for discrete morphological data + + https://github.com/CompEvol/morph-models + + + + GNU Lesser General Public License v3.0 + https://www.gnu.org/licenses/lgpl-3.0.html + + + + + + Alexei Drummond + https://github.com/alexeid + + + + + scm:git:git://github.com/CompEvol/morph-models.git + scm:git:ssh://github.com:CompEvol/morph-models.git + https://github.com/CompEvol/morph-models + + + + beast-morph-models + beast-morph-models-fx + + + + UTF-8 + UTF-8 + 25 + + 2.8.0-SNAPSHOT + 25.0.2 + 5.8.2 + beastfx.app.beauti.Beauti + + MM + 1.3.0 + + + + + + + io.github.compevol + beast-pkgmgmt + ${beast.version} + + + io.github.compevol + beast-base + ${beast.version} + + + io.github.compevol + beast-fx + ${beast.version} + + + + + io.github.compevol + beast-morph-models + ${project.version} + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + github + https://maven.pkg.github.com/CompEvol/beast3 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + 25 + UTF-8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + jar-no-fork + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + none + + + + attach-javadocs + jar + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + sign-artifacts + verify + sign + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.6.0 + true + + central + + + + + + + diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..606044d --- /dev/null +++ b/release.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# +# Build and release a BEAST 2.8 package. +# +# Runs `mvn package` to produce the BEAST package ZIP (via maven-assembly-plugin), +# then optionally creates a GitHub release with the ZIP attached. +# +# Usage: +# ./release.sh # build + create package ZIP +# ./release.sh --release # also create a GitHub release with the ZIP attached +# +# The ZIP can then be submitted to CBAN (CompEvol/CBAN) by adding an entry +# to packages2.8.xml via pull request. See README.md for details. +# +set -euo pipefail + +# --- Extract metadata from version.xml --- + +if [[ ! -f version.xml ]]; then + echo "ERROR: version.xml not found in $(pwd)" >&2 + exit 1 +fi + +# Parse only the element (not etc.) +PKG_LINE=$(grep '&2 + exit 1 +fi + +ZIP_NAME="${PKG_NAME}.v${VERSION}.zip" +GITHUB_REPO=$(git remote get-url origin 2>/dev/null \ + | sed 's|.*github.com[:/]\(.*\)\.git$|\1|; s|.*github.com[:/]\(.*\)$|\1|') + +echo "=== ${PKG_NAME} v${VERSION} ===" +echo "" + +# --- Step 1: Maven build + assemble package ZIP --- + +echo "--- Building with Maven ---" +mvn clean package -DskipTests +echo "" + +# Locate the ZIP produced by maven-assembly-plugin (in the -fx module for multi-module) +ZIP_PATH="beast-morph-models-fx/target/${ZIP_NAME}" +if [[ ! -f "$ZIP_PATH" ]]; then + echo "ERROR: expected ZIP not found at ${ZIP_PATH}" >&2 + exit 1 +fi + +# Copy to project root for convenience +cp "$ZIP_PATH" "$ZIP_NAME" + +echo "=== Package: ${ZIP_NAME} ===" +unzip -l "$ZIP_NAME" + +# --- Step 2: Optionally create GitHub release --- + +if [[ "${1:-}" == "--release" ]]; then + if [[ -z "$GITHUB_REPO" ]]; then + echo "ERROR: could not determine GitHub repo from git remote" >&2 + exit 1 + fi + + echo "" + echo "--- Creating GitHub release v${VERSION} on ${GITHUB_REPO} ---" + gh release create "v${VERSION}" "$ZIP_NAME" \ + --repo "$GITHUB_REPO" \ + --title "${PKG_NAME} v${VERSION}" \ + --generate-notes + + DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/${ZIP_NAME}" + echo "" + echo "=== Release created ===" + echo "URL: https://github.com/${GITHUB_REPO}/releases/tag/v${VERSION}" + echo "" + echo "--- Next step: submit to CBAN ---" + echo "Add this entry to packages2.8.xml in https://github.com/CompEvol/CBAN via pull request:" + echo "" + cat < + + +XMLEOF +fi diff --git a/version.xml b/version.xml index 4c2f56e..06984e3 100644 --- a/version.xml +++ b/version.xml @@ -1,14 +1,12 @@ - - - + + - + -