Skip to content

Commit a2019be

Browse files
committed
Initial commit
0 parents  commit a2019be

71 files changed

Lines changed: 28283 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Devon Callan
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.PHONY: setup dev-install clean test
2+
3+
# Setup development environment
4+
setup:
5+
@echo "🚀 Setting up RunKMC development environment..."
6+
python3 -m venv .venv || true
7+
.venv/bin/pip install --upgrade pip
8+
.venv/bin/pip install build wheel cmake
9+
.venv/bin/pip install -e .
10+
python3 build_binary.py
11+
@echo "✅ Setup complete! Activate with: source .venv/bin/activate"
12+
13+
# Install in development mode
14+
dev-install:
15+
pip install -e .
16+
17+
# Clean build artifacts
18+
clean:
19+
rm -rf build/
20+
rm -rf cpp/build/
21+
rm -rf dist/
22+
rm -rf *.egg-info/
23+
find . -name "__pycache__" -exec rm -rf {} + || true
24+
find . -name "*.pyc" -delete || true

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# RunKMC - A High-Performance Kinetic Monte Carlo Simulator for Polymerization
2+
3+
**RunKMC** is a kinetic Monte Carlo simulation engine written in C++ with a thin Python wrapper for accessibility. The core engine is also accessible through the command line.
4+
5+
[![PyPI](https://badge.fury.io/py/runkmc.svg)](https://badge.fury.io/py/runkmc) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17172027.svg)](https://doi.org/10.5281/zenodo.17172027) [![Python](https://img.shields.io/badge/Python-%3E%3D3.10-blue?logo=python&logoColor=yellow)](https://python.org)
6+
7+
## Quick install
8+
The easiest way to install **RunKMC** is via pip:
9+
10+
```shell
11+
pip3 install runkmc
12+
```
13+
14+
For local development, we recommend using cmake:
15+
16+
```shell
17+
pip3 install cmake
18+
make setup
19+
```
20+
21+
We recommend setting a python virtual environment before installing the package.
22+
```shell
23+
python3 -m venv .venv
24+
source .venv/bin/activate
25+
```
26+
27+
## Examples
28+
29+
Documentation for RunKMC concepts can be found [here](docs/). Example input files can be found in [examples](docs/examples/README.md). More relevant examples and integrations with [**SPaRKS**🔗](https://github.com/devoncallan/sparks) can be found at the supporting data for the manuscript below. This can be found at [this repository](https://github.com/devoncallan/ReversibleCopolymerizations): [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17172075.svg)](https://doi.org/10.5281/zenodo.17172075)
30+
31+
## How to Cite
32+
33+
**Citeable DOI for this version of RunKMC:** [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17172027.svg)](https://doi.org/10.5281/zenodo.17172027)
34+
35+
If you use RunKMC in your project, please cite
36+
* Callan, D. H., and Bates, C. M. Efficient Deterministic Modeling of Reversile Copolymerizations, *Macromolecules*, **2025**, [doi:10.1021/acs.macromol.5c01421](https://doi.org/10.1021/acs.macromol.5c01421)

build_binary.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env python3
2+
import subprocess
3+
import sys
4+
import os
5+
import glob
6+
import shutil
7+
8+
9+
def main():
10+
# Build with cmake
11+
subprocess.run(
12+
[
13+
"cmake",
14+
"-B",
15+
"build",
16+
"-S",
17+
"cpp",
18+
"-DRUNKMC_VERSION=0.1.0",
19+
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
20+
],
21+
check=True,
22+
)
23+
24+
subprocess.run(["cmake", "--build", "build"], check=True)
25+
26+
# Create destination directory
27+
os.makedirs("runkmc/build", exist_ok=True)
28+
29+
# Find the binary (Windows puts it in Debug/ subfolder)
30+
if sys.platform == "win32":
31+
pattern = "build/**/RunKMC.exe"
32+
else:
33+
pattern = "build/**/RunKMC"
34+
35+
matches = glob.glob(pattern, recursive=True)
36+
if not matches:
37+
raise FileNotFoundError(f"Could not find binary matching {pattern}")
38+
39+
src = matches[0]
40+
print(f"Found binary: {src}")
41+
42+
# Copy to destination
43+
shutil.copy2(src, "runkmc/build/")
44+
print(f"Copied to runkmc/build/")
45+
46+
47+
if __name__ == "__main__":
48+
main()

cpp/CMakeLists.txt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
cmake_minimum_required(VERSION 3.15)
2+
project(RunKMC)
3+
4+
set(CMAKE_CXX_STANDARD 17)
5+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
6+
7+
# Version configuration - can be overridden by build system
8+
if(NOT DEFINED RUNKMC_VERSION)
9+
set(RUNKMC_VERSION "0.1.0-dev")
10+
message(WARNING "RUNKMC_VERSION not provided, using dev version")
11+
endif()
12+
13+
# Parse version components
14+
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" _ ${RUNKMC_VERSION})
15+
set(RUNKMC_VERSION_MAJOR ${CMAKE_MATCH_1})
16+
set(RUNKMC_VERSION_MINOR ${CMAKE_MATCH_2})
17+
set(RUNKMC_VERSION_PATCH ${CMAKE_MATCH_3})
18+
19+
# Generate version header
20+
configure_file(
21+
"${CMAKE_SOURCE_DIR}/include/runkmc/version.h.in"
22+
"${CMAKE_BINARY_DIR}/include/runkmc/version.h"
23+
@ONLY
24+
)
25+
26+
# Disable tests and examples for dependencies
27+
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
28+
set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
29+
set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
30+
set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
31+
set(EIGEN_BUILD_TESTING OFF CACHE BOOL "" FORCE)
32+
set(EIGEN_BUILD_PKGCONFIG OFF CACHE BOOL "" FORCE)
33+
34+
# Fetch dependencies
35+
include(FetchContent)
36+
37+
# Fetch Eigen
38+
FetchContent_Declare(
39+
Eigen3
40+
GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
41+
GIT_TAG 3.4.0
42+
GIT_SHALLOW TRUE
43+
)
44+
45+
# Fetch yaml-cpp
46+
FetchContent_Declare(
47+
yaml-cpp
48+
GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git
49+
GIT_TAG 0.8.0 # Latest stable version
50+
GIT_SHALLOW TRUE
51+
)
52+
53+
FetchContent_MakeAvailable(Eigen3 yaml-cpp)
54+
55+
# Include directories
56+
include_directories(include/runkmc)
57+
include_directories(${CMAKE_BINARY_DIR}/include/runkmc) # For generated version.h
58+
59+
add_executable(RunKMC src/RunKMC.cpp)
60+
61+
# Link libraries
62+
target_link_libraries(RunKMC Eigen3::Eigen yaml-cpp::yaml-cpp)
63+
target_compile_options(RunKMC PRIVATE -O3)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#pragma once
2+
#include "common.h"
3+
#include "kmc/state.h"
4+
#include "analysis/utils.h"
5+
6+
namespace analysis
7+
{
8+
void analyzeChainLengthDist(Eigen::MatrixXd &sequenceStatsMatrix, const std::vector<double> &monomerFWs, AnalysisState &state)
9+
{
10+
if (sequenceStatsMatrix.rows() == 0 || sequenceStatsMatrix.cols() == 0)
11+
return;
12+
13+
// SequenceStatsMatrix: (numPolymers x (A Count, B Count, ..., A SeqCount, B SeqCount, ..., A SeqLen2, B SeqLen2, ...))
14+
// Extract monomer count distribution (shape: numPolymers x numMonomers)
15+
Eigen::MatrixXd monomerCountDist = sequenceStatsMatrix.leftCols(registry::NUM_MONOMERS);
16+
17+
// Chain length calculations (Get chain lengths by summing monomer counts)
18+
Eigen::VectorXd chainLengths = monomerCountDist.rowwise().sum();
19+
state.nAvgCL = chainLengths.mean();
20+
if (state.nAvgCL != 0.0)
21+
{
22+
state.wAvgCL = chainLengths.array().square().mean() / state.nAvgCL;
23+
state.dispCL = state.wAvgCL / state.nAvgCL;
24+
}
25+
26+
// If any monomer has FW of 0, skip molecular weight calculations
27+
// and set molecular weight averages to chain length averages
28+
Eigen::VectorXd FWs = Eigen::Map<const Eigen::VectorXd>(monomerFWs.data(), monomerFWs.size());
29+
if ((FWs.array() == 0.0).any())
30+
{
31+
state.nAvgMW = state.nAvgCL;
32+
state.wAvgMW = state.wAvgCL;
33+
state.dispMW = state.dispCL;
34+
return;
35+
}
36+
37+
// Molecular weight calculations
38+
Eigen::MatrixXd weightedMonomerCountDist = monomerCountDist.array().rowwise() * FWs.transpose().array();
39+
Eigen::VectorXd molecularWeights = weightedMonomerCountDist.rowwise().sum();
40+
state.nAvgMW = molecularWeights.mean();
41+
if (state.nAvgMW != 0.0)
42+
{
43+
state.wAvgMW = molecularWeights.array().square().mean() / state.nAvgMW;
44+
state.dispMW = state.wAvgMW / state.nAvgMW;
45+
}
46+
}
47+
48+
// SequenceStatsMatrix: (numPolymers x (A Count, B Count, ..., A SeqCount, B SeqCount, ..., A SeqLen2, B SeqLen2, ...))
49+
void analyzeSequenceLengthDist(Eigen::MatrixXd &sequenceStatsMatrix, AnalysisState &state)
50+
{
51+
52+
if (sequenceStatsMatrix.rows() == 0 || sequenceStatsMatrix.cols() < SequenceStats::SIZE())
53+
return;
54+
55+
// Sum stats over all polymers -> (1 x (Total A Count, B Count, ..., A SeqCount, B SeqCount, ..., A SeqLen2, B SeqLen2, ...))
56+
auto totalStats = sequenceStatsMatrix.colwise().sum();
57+
auto totalMonomerCounts = totalStats.leftCols(registry::NUM_MONOMERS).sum(); // Total chain length (i.e. total monomer count across all types)
58+
59+
for (size_t i = 0; i < registry::NUM_MONOMERS; ++i)
60+
{
61+
auto monomerCounts = totalStats(0 * registry::NUM_MONOMERS + i); // Total count of monomer type i across all polymers
62+
auto sequenceCounts = totalStats(1 * registry::NUM_MONOMERS + i); // Total number of sequences of monomer type i across all polymers
63+
auto sequenceLengths2 = totalStats(2 * registry::NUM_MONOMERS + i); // Sum of squared sequence lengths of monomer type i across all polymers
64+
65+
if (sequenceCounts > 0 && monomerCounts > 0)
66+
{
67+
state.nAvgComp[i] = monomerCounts / totalMonomerCounts; // Total count of monomer i / total count of all monomers
68+
state.nAvgSL[i] = monomerCounts / sequenceCounts; // Total count of monomer i / total number of sequences of monomer i
69+
state.wAvgSL[i] = sequenceLengths2 / monomerCounts; // Sum of squared sequence lengths of monomer i / total count of monomer i
70+
state.dispSL[i] = state.wAvgSL[i] / state.nAvgSL[i]; // Sequence length dispersity of monomer i
71+
}
72+
}
73+
}
74+
75+
void analyze(const SpeciesSet &speciesSet, SystemState &systemState)
76+
{
77+
auto sequenceData = speciesSet.getRawSequenceData();
78+
auto summary = analysis::calculateSequenceSummary(sequenceData);
79+
80+
SequenceState sequenceState = SequenceState{systemState.kmc, summary.positionalStats};
81+
82+
AnalysisState analysisState;
83+
analysis::analyzeChainLengthDist(summary.sequenceStatsMatrix, speciesSet.getMonomerFWs(), analysisState);
84+
analysis::analyzeSequenceLengthDist(summary.sequenceStatsMatrix, analysisState);
85+
86+
systemState.sequence = sequenceState;
87+
systemState.analysis = analysisState;
88+
}
89+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#pragma once
2+
#include <Eigen/Core>
3+
4+
#include "common.h"
5+
6+
namespace analysis
7+
{
8+
struct SequenceStats
9+
{
10+
std::vector<uint64_t> monCounts;
11+
std::vector<uint64_t> seqCounts;
12+
std::vector<uint64_t> seqLengths2;
13+
14+
const static size_t NUM_METRICS = 3;
15+
16+
SequenceStats()
17+
{
18+
monCounts.resize(registry::NUM_MONOMERS, 0);
19+
seqCounts.resize(registry::NUM_MONOMERS, 0);
20+
seqLengths2.resize(registry::NUM_MONOMERS, 0);
21+
}
22+
23+
static size_t SIZE() { return registry::NUM_MONOMERS * NUM_METRICS; }
24+
25+
SequenceStats &operator+=(const SequenceStats &other)
26+
{
27+
for (size_t i = 0; i < registry::NUM_MONOMERS; ++i)
28+
{
29+
monCounts[i] += other.monCounts[i];
30+
seqCounts[i] += other.seqCounts[i];
31+
seqLengths2[i] += other.seqLengths2[i];
32+
}
33+
return *this;
34+
}
35+
36+
/*
37+
MonCounts_A, MonCounts_B, ...,
38+
SeqCounts_A, SeqCounts_B, ...,
39+
SeqLengths2_A, SeqLengths2_B, ...
40+
*/
41+
Eigen::VectorXd toEigen() const
42+
{
43+
Eigen::VectorXd result(SIZE());
44+
for (size_t i = 0; i < registry::NUM_MONOMERS; ++i)
45+
result(0 * registry::NUM_MONOMERS + i) = static_cast<double>(monCounts[i]);
46+
for (size_t i = 0; i < registry::NUM_MONOMERS; ++i)
47+
result(1 * registry::NUM_MONOMERS + i) = static_cast<double>(seqCounts[i]);
48+
for (size_t i = 0; i < registry::NUM_MONOMERS; ++i)
49+
result(2 * registry::NUM_MONOMERS + i) = static_cast<double>(seqLengths2[i]);
50+
return result;
51+
}
52+
};
53+
54+
struct SequenceSummary
55+
{
56+
Eigen::MatrixXd sequenceStatsMatrix; // (polymers x (SequenceStats))
57+
std::vector<SequenceStats> positionalStats; // (buckets x (monomers*fields))
58+
};
59+
60+
struct RawSequenceData
61+
{
62+
std::vector<std::vector<SpeciesID>> sequences;
63+
std::vector<std::vector<SequenceStats>> precomputedStats;
64+
size_t length;
65+
66+
RawSequenceData(size_t n)
67+
{
68+
sequences.reserve(n);
69+
precomputedStats.reserve(n);
70+
length = n;
71+
}
72+
};
73+
}

0 commit comments

Comments
 (0)