Skip to content

Commit

Permalink
new test and coding improvement (#22)
Browse files Browse the repository at this point in the history
* update readme

* version update

* no default config generation

* add some checks to config reader

* add log

* add test
  • Loading branch information
aghaeifar authored Jan 3, 2025
1 parent 64d7a35 commit 4234315
Show file tree
Hide file tree
Showing 21 changed files with 454 additions and 320 deletions.
9 changes: 6 additions & 3 deletions .github/workflows/build_cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,13 @@ jobs:
run: chmod +x ${{github.workspace}}/spinwalk_test

- name: test phantom creation
run: ${{github.workspace}}/spinwalk_test --run_test=test_phantom_creation
run: ${{github.workspace}}/spinwalk_test --run_test=test_phantom_module

- name: test config creation
run: ${{github.workspace}}/spinwalk_test --run_test=test_config_creation
run: ${{github.workspace}}/spinwalk_test --run_test=test_config_module

- name: test kernel
run: ${{github.workspace}}/spinwalk_test --run_test=test_kernel
run: ${{github.workspace}}/spinwalk_test --run_test=test_kernel

- name: test sim
run: ${{github.workspace}}/spinwalk_test --run_test=test_sim_module
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,37 @@ T = permute(T, 4:-1:1);

Look at the provided demos for Python scripts.

## Alternative Projects

If you're exploring solutions in this space, here are some alternative projects you might find useful:

1. **[MC/DC Simulator](https://github.com/jonhrafe/MCDC_Simulator_public)**
A C++ open-source Diffusion-Weighted MRI Monte Carlo and Collision Simulator.

2. **[MCMRSimulator.jl](https://git.fmrib.ox.ac.uk/ndcn0236/mcmrsimulator.jl)**
A Monte Carlo simulator written in Julia to model the effect of microstructure on the MRI signal evolution

3. **[Disimpy](https://github.com/kerkelae/disimpy)**
Massively parallel Monte Carlo diffusion MR simulator written in Python.

While these tools are excellent in their own right, **SpinWalk** focuses on versatile sequence design and the inclusion of off-resonance effects, making it particularly suitable for modeling MR-signal formation in inhomogeneous tissue.

## Feature Requests

We welcome your ideas and suggestions for improving SpinWalk! If you have a feature in mind that you'd like to see implemented, please let us know. Here's how you can submit a feature request:

1. **Check Existing Issues**: Before submitting a request, take a look at the [Issues](https://github.com/aghaeifar/SpinWalk/issues) page to see if someone else has already suggested it. If so, feel free to add your thoughts or upvote the existing issue.

2. **Open a New Issue**: If your idea is new, create a [new issue](https://github.com/aghaeifar/SpinWalk/issues/new). Please include:
- A clear and concise description of the feature.
- Why you think this feature would be useful.
- Any additional context or examples, if applicable.

3. **Stay Involved**: We may ask follow-up questions or request feedback during implementation, so stay tuned for updates on your request.

Your contributions help make SpinWalk better—thank you for your support!


## Literature

There are many nice papers published about simulation of BOLD signal in vessels network. A few are listed here for reference:
Expand Down
5 changes: 0 additions & 5 deletions config/config_default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,3 @@ SCALE[46] = 22.9692
SCALE[47] = 27.0463
SCALE[48] = 31.8471
SCALE[49] = 37.5000





2 changes: 0 additions & 2 deletions config/se.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,3 @@ RF_T[1] = 10000
[SIMULATION_PARAMETERS]
; use 0 for random seed generation, otherwise use a positive integer to make a reproducible simulation
SEED = 10


200 changes: 106 additions & 94 deletions demo/spinwalk_bold.ipynb

Large diffs are not rendered by default.

122 changes: 61 additions & 61 deletions demo/spinwalk_dwi.ipynb

Large diffs are not rendered by default.

243 changes: 119 additions & 124 deletions src/config/config_generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,201 +7,196 @@
#include <boost/log/trivial.hpp>

#include "config_generator.h"
#include "ini.h"

namespace config
{

bool generate_default_config(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms, std::string output)
bool config_generator::generate_default_config(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms)
{
BOOST_LOG_TRIVIAL(info) << "Generating default config: " << output << " failed";
ini_parent.clear();
ini_parent["GENERAL"]["PARENT_CONFIG"] = "";
ini_parent["GENERAL"]["SEQ_NAME"] = "noname";

mINI::INIFile file(output);
mINI::INIStructure ini;

ini["GENERAL"]["PARENT_CONFIG"] = "";
ini["GENERAL"]["SEQ_NAME"] = "noname";

ini["FILES"]["OUTPUT_DIR"] = "./outputs";
ini_parent["FILES"]["OUTPUT_DIR"] = "./outputs";
// Off-resonance mapping and masking. The off-resonance map is in Tesla and computed for B0=1T. It will be internally adjusted based on the B0 parameters specified in the \"SIMULATION_PARAMETERS\" section."
for (size_t i = 0; i < phantoms.size(); i++)
ini["FILES"]["PHANTOM[" + std::to_string(i) + "]"] = phantoms[i];
ini_parent["FILES"]["PHANTOM[" + std::to_string(i) + "]"] = phantoms[i];
// optional
ini["FILES"]["XYZ0[0]"] = "";
ini["FILES"]["XYZ0[1]"] = "";
ini["FILES"]["M0[0]"] = "";
ini["FILES"]["M0[1]"] = "";
ini_parent["FILES"]["XYZ0[0]"] = "";
ini_parent["FILES"]["XYZ0[1]"] = "";
ini_parent["FILES"]["M0[0]"] = "";
ini_parent["FILES"]["M0[1]"] = "";

// m^2/s
ini["TISSUE_PARAMETERS"]["DIFFUSIVITY[0]"] = "1.0e-9";
ini["TISSUE_PARAMETERS"]["DIFFUSIVITY[1]"] = "1.0e-9";
ini_parent["TISSUE_PARAMETERS"]["DIFFUSIVITY[0]"] = "1.0e-9";
ini_parent["TISSUE_PARAMETERS"]["DIFFUSIVITY[1]"] = "1.0e-9";
// Probability to diffuse from tissue X to tissue Y (float). X and Y are taken from the values in the mask
ini["TISSUE_PARAMETERS"]["P_XY[0]"] = "1.0 0.0";
ini["TISSUE_PARAMETERS"]["P_XY[1]"] = "0.0 1.0";
ini_parent["TISSUE_PARAMETERS"]["P_XY[0]"] = "1.0 0.0";
ini_parent["TISSUE_PARAMETERS"]["P_XY[1]"] = "0.0 1.0";
// T1 and T2 in millisecond (float). Negative value to exclude it from the simulation
ini["TISSUE_PARAMETERS"]["T1[0]"] = "2200";
ini["TISSUE_PARAMETERS"]["T1[1]"] = "2200";
ini["TISSUE_PARAMETERS"]["T2[0]"] = "41";
ini["TISSUE_PARAMETERS"]["T2[1]"] = "41";
ini_parent["TISSUE_PARAMETERS"]["T1[0]"] = "2200";
ini_parent["TISSUE_PARAMETERS"]["T1[1]"] = "2200";
ini_parent["TISSUE_PARAMETERS"]["T2[0]"] = "41";
ini_parent["TISSUE_PARAMETERS"]["T2[1]"] = "41";

// repetition time in microsecond (integer)
ini["SCAN_PARAMETERS"]["TR"] = std::to_string(TE_us + timestep_us);
ini_parent["SCAN_PARAMETERS"]["TR"] = std::to_string(TE_us + timestep_us);
// echo time in microsecond (integer)
ini["SCAN_PARAMETERS"]["TE[0]"] = std::to_string(TE_us);
ini_parent["SCAN_PARAMETERS"]["TE[0]"] = std::to_string(TE_us);
// RF Flip angle in degree (float)
ini["SCAN_PARAMETERS"]["RF_FA[0]"] = "90.0";
ini_parent["SCAN_PARAMETERS"]["RF_FA[0]"] = "90.0";
// RF Phase in degree (float). Note PHASE_CYCLING will be added to the phase of the first RF
ini["SCAN_PARAMETERS"]["RF_PH[0]"] = "0.0";
ini_parent["SCAN_PARAMETERS"]["RF_PH[0]"] = "0.0";
// Time to apply RF in microsecond (integer). The first RF start time is always 0.0
ini["SCAN_PARAMETERS"]["RF_T[0]"] = "0";
ini_parent["SCAN_PARAMETERS"]["RF_T[0]"] = "0";

// Dephasing in degree (float). The initial spin in the population will experience a dephasing of 0.0 degrees. Dephasing will then progressively increase in a linear manner up to the final spin, which will undergo dephasing as specified by the given parameter
ini["SCAN_PARAMETERS"]["DEPHASING[0]"] = "";
ini_parent["SCAN_PARAMETERS"]["DEPHASING[0]"] = "";
// Time to apply dephasing in microsecond (integer).
ini["SCAN_PARAMETERS"]["DEPHASING_T[0]"] = "";
ini_parent["SCAN_PARAMETERS"]["DEPHASING_T[0]"] = "";
// Gradient in mT/m for each axis (float). Each sample is active for one TIME_STEP
ini["SCAN_PARAMETERS"]["GRADIENT_XYZ[0]"] = "";
ini_parent["SCAN_PARAMETERS"]["GRADIENT_XYZ[0]"] = "";
// Time to apply gradient in micro-second (integer).
ini["SCAN_PARAMETERS"]["GRADIENT_T[0]"] = "";
ini_parent["SCAN_PARAMETERS"]["GRADIENT_T[0]"] = "";
// time intervals per random-walk in micro-second (integer)
ini["SCAN_PARAMETERS"]["TIME_STEP"] = std::to_string(timestep_us);
ini_parent["SCAN_PARAMETERS"]["TIME_STEP"] = std::to_string(timestep_us);
// number of dummy scans to reach steady state. The first RF pulse (RF_FA[0]) is used for excitation in dummy scans. If negative, it will be set to 5T1/TR.
ini["SCAN_PARAMETERS"]["DUMMY_SCAN"] = "0";
ini_parent["SCAN_PARAMETERS"]["DUMMY_SCAN"] = "0";
// Phase cycling in degrees
ini["SCAN_PARAMETERS"]["PHASE_CYCLING"] = "0";
ini_parent["SCAN_PARAMETERS"]["PHASE_CYCLING"] = "0";

// static magnetic field in Tesla, set to 0 for no field.
ini["SIMULATION_PARAMETERS"]["B0"] = "9.4";
ini_parent["SIMULATION_PARAMETERS"]["B0"] = "9.4";
// use 0 for random seed generation, otherwise use a positive integer to make a reproducible simulation
ini["SIMULATION_PARAMETERS"]["SEED"] = "0";
ini["SIMULATION_PARAMETERS"]["NUMBER_OF_SPINS"] = "1e5";
ini_parent["SIMULATION_PARAMETERS"]["SEED"] = "0";
ini_parent["SIMULATION_PARAMETERS"]["NUMBER_OF_SPINS"] = "1e5";
// if 0, spins will not cross volume FoV. If 1, spins which cross FoV, will enter from the other side of the FoV
ini["SIMULATION_PARAMETERS"]["CROSS_FOV"] = "0";
ini_parent["SIMULATION_PARAMETERS"]["CROSS_FOV"] = "0";
// if 1, spins random-walk will be stored in XYZ1 file
ini["SIMULATION_PARAMETERS"]["RECORD_TRAJECTORY"] = "0";
ini_parent["SIMULATION_PARAMETERS"]["RECORD_TRAJECTORY"] = "0";
// maximum number of iterations that is allowed to generate random-walk. If spin can not move yet (e.g. because of restricted boundaries), it is considered lost and magnetization is set to zero
ini["SIMULATION_PARAMETERS"]["MAX_ITERATIONS"] = "1e4";
ini_parent["SIMULATION_PARAMETERS"]["MAX_ITERATIONS"] = "1e4";
// SCALE WHAT? 0: FOV, 1: GRADIENT
ini["SIMULATION_PARAMETERS"]["WHAT_TO_SCALE"] = "0";
ini_parent["SIMULATION_PARAMETERS"]["WHAT_TO_SCALE"] = "0";
// scale PHANTOM length to simulate different sample sizes
ini["SIMULATION_PARAMETERS"]["SCALE[0]"] = "1.0";
ini_parent["SIMULATION_PARAMETERS"]["SCALE[0]"] = "1.0";


auto output_file = std::filesystem::absolute(std::filesystem::path(output));
try {
std::filesystem::create_directories(output_file.parent_path());
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "Creating directory " << output_file.parent_path().string() << " failed. " << e.what();
return false;
}

if(file.generate(ini, true) == false)
{
BOOST_LOG_TRIVIAL(error) << "Failed to write config file: " << output;
return false;
}
return true;
}


bool generate_gre(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms, std::string output)
bool config_generator::generate_gre(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms, std::string output)
{
auto output_file = std::filesystem::path(output);
auto default_config_output = output_file.parent_path() / "default_config.ini";
if(generate_default_config(TE_us, timestep_us, phantoms, default_config_output.string()) == false)

if(generate_default_config(TE_us, timestep_us, phantoms) == false)
return false;

mINI::INIFile file(output);
mINI::INIStructure ini;

ini["GENERAL"]["PARENT_CONFIG"] = default_config_output.string();
ini["GENERAL"]["SEQ_NAME"] = "gre";
ini.clear();
add_param("GENERAL", "SEQ_NAME", "gre");

for (size_t i = 0; i < phantoms.size(); i++)
ini["FILES"]["PHANTOM[" + std::to_string(i) + "]"] = phantoms[i];
add_param("FILES", "PHANTOM[" + std::to_string(i) + "]", phantoms[i]);

ini["SCAN_PARAMETERS"]["TR"] = std::to_string(TE_us + timestep_us);
ini["SCAN_PARAMETERS"]["TE[0]"] = std::to_string(TE_us);
ini["SCAN_PARAMETERS"]["RF_FA[0]"] = "90.0";
ini["SCAN_PARAMETERS"]["RF_PH[0]"] = "0";
ini["SCAN_PARAMETERS"]["RF_T[0]"] = "0";
ini["SCAN_PARAMETERS"]["TIME_STEP"] = std::to_string(timestep_us);
add_param("SCAN_PARAMETERS", "TR", std::to_string(TE_us + timestep_us));
add_param("SCAN_PARAMETERS", "TE[0]", std::to_string(TE_us));
add_param("SCAN_PARAMETERS", "RF_FA[0]", "90.0");
add_param("SCAN_PARAMETERS", "RF_PH[0]", "0");
add_param("SCAN_PARAMETERS", "RF_T[0]", "0");
add_param("SCAN_PARAMETERS", "TIME_STEP", std::to_string(timestep_us));

if(output.empty() == false)
if(write_ini(output) == false)
return false;

if(file.generate(ini, true) == false)
{
BOOST_LOG_TRIVIAL(error) << "Failed to write config file: " << output;
return false;
}
return true;
}

bool generate_se(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms, std::string output)

bool config_generator::generate_se(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms, std::string output)
{
auto output_file = std::filesystem::path(output);
auto default_config_output = output_file.parent_path() / "default_config.ini";
if(generate_default_config(TE_us, timestep_us, phantoms, default_config_output.string()) == false)
if(generate_default_config(TE_us, timestep_us, phantoms) == false)
return false;

mINI::INIFile file(output);
mINI::INIStructure ini;

ini["GENERAL"]["PARENT_CONFIG"] = default_config_output.string();
ini["GENERAL"]["SEQ_NAME"] = "se";
ini.clear();
add_param("GENERAL", "SEQ_NAME", "se");

for (size_t i = 0; i < phantoms.size(); i++)
ini["FILES"]["PHANTOM[" + std::to_string(i) + "]"] = phantoms[i];

ini["SCAN_PARAMETERS"]["TR"] = std::to_string(TE_us + timestep_us);
ini["SCAN_PARAMETERS"]["TE[0]"] = std::to_string(TE_us);
ini["SCAN_PARAMETERS"]["RF_FA[0]"] = "90.0";
ini["SCAN_PARAMETERS"]["RF_FA[1]"] = "180.0";
ini["SCAN_PARAMETERS"]["RF_PH[0]"] = "0";
ini["SCAN_PARAMETERS"]["RF_PH[1]"] = "90";
ini["SCAN_PARAMETERS"]["RF_T[0]"] = "0";
ini["SCAN_PARAMETERS"]["RF_T[1]"] = std::to_string(TE_us/2);
ini["SCAN_PARAMETERS"]["TIME_STEP"] = std::to_string(timestep_us);
add_param("FILES", "PHANTOM[" + std::to_string(i) + "]", phantoms[i]);

add_param("SCAN_PARAMETERS", "TR", std::to_string(TE_us + timestep_us));
add_param("SCAN_PARAMETERS", "TE[0]", std::to_string(TE_us));
add_param("SCAN_PARAMETERS", "RF_FA[0]", "90.0");
add_param("SCAN_PARAMETERS", "RF_FA[1]", "180.0");
add_param("SCAN_PARAMETERS", "RF_PH[0]", "0");
add_param("SCAN_PARAMETERS", "RF_PH[1]", "90");
add_param("SCAN_PARAMETERS", "RF_T[0]", "0");
add_param("SCAN_PARAMETERS", "RF_T[1]", std::to_string(TE_us/2));
add_param("SCAN_PARAMETERS", "TIME_STEP", std::to_string(timestep_us));

if(output.empty() == false)
if(write_ini(output) == false)
return false;


if(file.generate(ini, true) == false)
{
BOOST_LOG_TRIVIAL(error) << "Failed to write config file: " << output;
return false;
}
return true;
}

bool generate_bssfp(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms, std::string output)

bool config_generator::generate_bssfp(uint32_t TE_us, uint32_t timestep_us, std::vector<std::string> phantoms, std::string output)
{
auto output_file = std::filesystem::path(output);
auto default_config_output = output_file.parent_path() / "default_config.ini";
if(generate_default_config(TE_us, timestep_us, phantoms, default_config_output.string()) == false)
if(generate_default_config(TE_us, timestep_us, phantoms) == false)
return false;

mINI::INIFile file(output);
mINI::INIStructure ini;

ini["GENERAL"]["PARENT_CONFIG"] = default_config_output.string();
ini["GENERAL"]["SEQ_NAME"] = "bssfp";
ini.clear();
add_param("GENERAL", "SEQ_NAME", "bssfp");

for (size_t i = 0; i < phantoms.size(); i++)
ini["FILES"]["PHANTOM[" + std::to_string(i) + "]"] = phantoms[i];
add_param("FILES", "PHANTOM[" + std::to_string(i) + "]", phantoms[i]);

add_param("SCAN_PARAMETERS", "TR", std::to_string(TE_us*2));
add_param("SCAN_PARAMETERS", "TE[0]", std::to_string(TE_us));
add_param("SCAN_PARAMETERS", "RF_FA[0]", "16.0");
add_param("SCAN_PARAMETERS", "RF_PH[0]", "0");
add_param("SCAN_PARAMETERS", "RF_T[0]", "0");
add_param("SCAN_PARAMETERS", "TIME_STEP", std::to_string(timestep_us));
add_param("SCAN_PARAMETERS", "DUMMY_SCAN", "-1");
add_param("SCAN_PARAMETERS", "PHASE_CYCLING", "180");

if(output.empty() == false)
if(write_ini(output) == false)
return false;

return true;
}

ini["SCAN_PARAMETERS"]["TR"] = std::to_string(TE_us*2);
ini["SCAN_PARAMETERS"]["TE[0]"] = std::to_string(TE_us);
ini["SCAN_PARAMETERS"]["RF_FA[0]"] = "16.0";
ini["SCAN_PARAMETERS"]["RF_PH[0]"] = "0";
ini["SCAN_PARAMETERS"]["RF_T[0]"] = "0";
ini["SCAN_PARAMETERS"]["TIME_STEP"] = std::to_string(timestep_us);
ini["SCAN_PARAMETERS"]["DUMMY_SCAN"] = "-1";
ini["SCAN_PARAMETERS"]["PHASE_CYCLING"] = "180";

bool config_generator::write_ini(std::string output){
auto output_file = std::filesystem::absolute(std::filesystem::path(output));
try {
std::filesystem::create_directories(output_file.parent_path());
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "Creating directory " << output_file.parent_path().string() << " failed. " << e.what();
return false;
}
auto default_config_output = output_file.parent_path() / "default_config.ini";
add_param("GENERAL", "PARENT_CONFIG", default_config_output.string().c_str());

if(file.generate(ini, true) == false)
{
mINI::INIFile file(output_file.string());
if(file.generate(ini, true) == false){
BOOST_LOG_TRIVIAL(error) << "Failed to write config file: " << output;
return false;
}

mINI::INIFile file_parent(default_config_output.string());
if(file_parent.generate(ini_parent, true) == false){
BOOST_LOG_TRIVIAL(error) << "Failed to write config file: " << default_config_output;
return false;
}

return true;
}

void config_generator::add_param(std::string section, std::string key, std::string value){
ini[section][key] = value;
}

} // namespace config
Loading

0 comments on commit 4234315

Please sign in to comment.