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 @@
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
- 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@
-
-
-
+
+
-
+
-