From 8f4049f58dc7d4bde5fabfcc7ae812a47fb3ce16 Mon Sep 17 00:00:00 2001
From: Kyle <kyle.c.klenk@gmail.com>
Date: Fri, 30 Jun 2023 11:29:19 -0600
Subject: [PATCH] Add Actor to find s2

---
 .gitignore                        |   1 +
 environment_setup/Dockerfile      |   1 +
 find_s1.py                        |  49 ++++-----
 find_s2.py                        |   9 +-
 legacy_scripts/find_s1_example.py | 101 +++++++++++++++++
 main.cpp                          | 174 +++++++++++++++++++++++++++---
 6 files changed, 287 insertions(+), 48 deletions(-)
 create mode 100644 legacy_scripts/find_s1_example.py

diff --git a/.gitignore b/.gitignore
index 8b90690..36d6dd3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ meshes/
 output8.txt
 .vscode/*
 simulations/logs/*
+.DS_Store
diff --git a/environment_setup/Dockerfile b/environment_setup/Dockerfile
index 1727d45..c2edf64 100755
--- a/environment_setup/Dockerfile
+++ b/environment_setup/Dockerfile
@@ -70,6 +70,7 @@ RUN cmake --build _build
 WORKDIR /opt/openCARP/external/carputils
 RUN python3 -m pip install --user -r requirements.txt
 ENV PATH=$PATH:/opt/openCARP/external/carputils/bin
+ENV PATH=$PATH:/opt/openCARP/_build/bin
 ENV PYTHONPATH=$PYTHONPATH:/opt/openCARP/external/carputils/
 
 # Create a settings.yaml - add your own settings.yaml
diff --git a/find_s1.py b/find_s1.py
index 7aed094..77f5104 100644
--- a/find_s1.py
+++ b/find_s1.py
@@ -1,8 +1,7 @@
 #!/usr/bin/env python
 
 import os
-import sys
-NAME = 'Find_S1'
+NAME = 'Find_S2'
 AUTHORS = 'Joyce Reimer, Kyle Klenk, Raymond Spiteri'
 GUIinclude = True
 
@@ -14,31 +13,32 @@ from carputils.carpio import txt
 import numpy as np
 from numpy import array as nplist
 
-def parser_for_s1():
+def parser_for_s2():
     parser = tools.standard_parser()
-    group  = parser.add_argument_group('Experiment for finding S1 threshold')
+    group  = parser.add_argument_group('experiment specific options')
     group.add_argument('--duration',
                         type = float,
                         default = 20.,
                         help = 'Duration of simulation in [ms] (default: 20.)')
     group.add_argument('--S1-strength',
-                        type = float,
-                        default = 20.,
-                        help = 'pick transmembrane current stimulus strength in [uA/cm^2] = [pA/pF] considering fixed Cm (default: 20.)')
+                       type = float,
+                       default = 20.,
+                       help = 'pick S1 transmembrane current stimulus strength in [uA/cm^2] = [pA/pF] considering fixed Cm (default: 20.)')
     group.add_argument('--S1-dur',
                         type = float,
                         default = 2.,
                         help = 'pick transmembrane current stimulus duration in [ms] (default: 2.)')
     return parser
 
+
 def jobID(args):
     """
     Generate name of top level output directory.
     """
     today = date.today()
-    return 'simulations/output/s1_attempt_{}'.format(int(args.S1_strength))
+    return 'simulations/output/s1_{}_attempt_{}'.format(int(args.duration), int(args.S1_strength))
 
-@tools.carpexample(parser_for_s1, jobID)
+@tools.carpexample(parser_for_s2, jobID)
 def run(args, job):
         #===========================================
     # 1: Define the mesh: Units are mm
@@ -74,23 +74,20 @@ def run(args, job):
     # Define the geometry of the stimulus at one end of the block
     # Units are um
     stim = ['-num_stim',             1,
-            '-stimulus[0].name',    'S1',
-            '-stimulus[0].stimtype', 0,
-            '-stimulus[0].strength', args.S1_strength,
-            '-stimulus[0].duration', args.S1_dur ]
-    
-    electrode = mesh.block_boundary_condition(geom, 'stimulus', 0, 'x', True)
+            '-stimulus[0].name',    'S1', #index 0 because only one type of electrode
+            '-stimulus[0].stimtype', 0, #0 means intracellular stim
+            '-stimulus[0].strength', args.S1_strength, #uA/Cm^2
+            '-stimulus[0].duration', args.S1_dur]
 
+    #electrode = mesh.block_boundary_condition(geom, 'stimulus', 0, 'x', True) #True refers to a BC between tissue and bath
+    E1_lower_bound = nplist([ 0.0, 0.0, 0.0])
+    E1_upper_bound = nplist([ 2.0, 0.3, 0.2])
+    electrode = mesh.block_region(geom, 'stimulus', 0, E1_lower_bound, E1_upper_bound, False) # introudce a stimulus in the region defined by the lower and upper bounds
 
-    # #electrode = mesh.block_boundary_condition(geom, 'stimulus', 0, 'x', True) #True refers to a BC between tissue and bath
-    # E1_lower_bound = nplist([ 0.0, 0.0, 0.0])
-    # E1_upper_bound = nplist([ 2.0, 0.3, 0.2])
-    # electrode = mesh.block_region(geom, 'stimulus', 0, E1_lower_bound, E1_upper_bound, False) # introudce a stimulus in the region defined by the lower and upper bounds
-
-    # #electrode = mesh.block_boundary_condition(geom, 'stimulus', 0, 'x', True) #True refers to a BC between tissue and bath
-    # E1_lower_bound = nplist([ 0.0, 0.0, 0.0])
-    # E1_upper_bound = nplist([ 2.0, 0.3, 0.2])
-    # electrode = mesh.block_region(geom, 'stimulus', 0, E1_lower_bound, E1_upper_bound, False) # introudce a stimulus in the region defined by the lower and upper bounds
+    #electrode = mesh.block_boundary_condition(geom, 'stimulus', 0, 'x', True) #True refers to a BC between tissue and bath
+    E1_lower_bound = nplist([ 0.0, 0.0, 0.0])
+    E1_upper_bound = nplist([ 2.0, 0.3, 0.2])
+    electrode = mesh.block_region(geom, 'stimulus', 0, E1_lower_bound, E1_upper_bound, False) # introudce a stimulus in the region defined by the lower and upper bounds
 
     #===========================================
     # 4: Defining simulator options:
@@ -109,6 +106,4 @@ def run(args, job):
     # Run simulation 
     job.carp(cmd) #cmd includes the params
 
-
-
-run()
+run()
\ No newline at end of file
diff --git a/find_s2.py b/find_s2.py
index c522555..5d28d79 100644
--- a/find_s2.py
+++ b/find_s2.py
@@ -13,7 +13,7 @@ from carputils.carpio import txt
 import numpy as np
 from numpy import array as nplist
 
-def parser_for_s2(s1_strength):
+def parser_for_s2():
     parser = tools.standard_parser()
     group  = parser.add_argument_group('experiment specific options')
     group.add_argument('--duration',
@@ -22,7 +22,7 @@ def parser_for_s2(s1_strength):
                         help = 'Duration of simulation in [ms] (default: 20.)')
     group.add_argument('--S1-strength',
                        type = float,
-                       default = s1_strength,
+                       default = 172,
                        help = 'pick S1 transmembrane current stimulus strength in [uA/cm^2] = [pA/pF] considering fixed Cm (default: 20.)')
     group.add_argument('--S1-dur',
                         type = float,
@@ -52,11 +52,12 @@ def jobID(args):
     Generate name of top level output directory.
     """
     today = date.today()
-    return 'output/s2_{}_basic_{}'.format(today.isoformat(), args.S2_strength)
+    return 'simulations/output/s2_{}_attempt_{}'.format(int(args.S2_start), int(args.S2_strength))
+
 
 @tools.carpexample(parser_for_s2, jobID)
 def run(args, job):
-        #===========================================
+    #===========================================
     # 1: Define the mesh: Units are mm
     #===========================================
     
diff --git a/legacy_scripts/find_s1_example.py b/legacy_scripts/find_s1_example.py
new file mode 100644
index 0000000..7594bd5
--- /dev/null
+++ b/legacy_scripts/find_s1_example.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+
+import os
+import sys
+NAME = 'Find_S1'
+AUTHORS = 'Joyce Reimer, Kyle Klenk, Raymond Spiteri'
+GUIinclude = True
+
+from datetime import date
+from carputils import settings
+from carputils import tools
+from carputils import mesh
+from carputils.carpio import txt
+import numpy as np
+from numpy import array as nplist
+
+def parser_for_s1():
+    parser = tools.standard_parser()
+    group  = parser.add_argument_group('Experiment for finding S1 threshold')
+    group.add_argument('--duration',
+                        type = float,
+                        default = 20.,
+                        help = 'Duration of simulation in [ms] (default: 20.)')
+    group.add_argument('--S1-strength',
+                        type = float,
+                        default = 20.,
+                        help = 'pick transmembrane current stimulus strength in [uA/cm^2] = [pA/pF] considering fixed Cm (default: 20.)')
+    group.add_argument('--S1-dur',
+                        type = float,
+                        default = 2.,
+                        help = 'pick transmembrane current stimulus duration in [ms] (default: 2.)')
+    return parser
+
+def jobID(args):
+    """
+    Generate name of top level output directory.
+    """
+    today = date.today()
+    return 'simulations/output/s1_attempt_{}'.format(int(args.S1_strength))
+
+@tools.carpexample(parser_for_s1, jobID)
+def run(args, job):
+        #===========================================
+    # 1: Define the mesh: Units are mm
+    #===========================================
+    
+    # Create a block with a thin z dimension
+    x = 10.0 #(mm)
+    y = 1.0  #(mm)
+    z = 0.2  #(mm)
+    geom = mesh.Block(size=(x,y,z))
+
+    # Set fibre angle to 0, sheet angle to 0
+    geom.set_fibres(0, 0, 90, 90) #endo, epi, sheet endo, sheet epicard
+
+    # Finilaize the creation of the mesh
+    meshname = mesh.generate(geom)
+
+    #===========================================
+    # 2: Define the ionic models & conductivities
+    #===========================================
+
+    # Query for element labels
+    _, etags, _ = txt.read(meshname + '.elem')
+    etags = np.unique(etags)
+
+    IntraTags = etags[etags != 0].tolist()
+    ExtraTags = etags.tolist().copy()
+
+    #===========================================
+    # 3: Configure the simulation
+    #===========================================
+
+    # Define the geometry of the stimulus at one end of the block
+    # Units are um
+    stim = ['-num_stim',             1,
+            '-stimulus[0].name',    'S1',
+            '-stimulus[0].stimtype', 0,
+            '-stimulus[0].strength', args.S1_strength,
+            '-stimulus[0].duration', args.S1_dur ]
+    
+    electrode = mesh.block_boundary_condition(geom, 'stimulus', 0, 'x', True)
+
+    #===========================================
+    # 4: Defining simulator options:
+    #===========================================
+
+    # Get basic command line, including solver options
+    cmd = tools.carp_cmd(os.path.join('simulations/input/basic.par')) #cmd is a carp command
+
+    cmd += ['-simID',    job.ID,
+            '-meshname', meshname,
+            '-dt',       25,
+            '-tend',     args.duration]
+    cmd += tools.gen_physics_opts(ExtraTags=ExtraTags, IntraTags=IntraTags)
+    cmd += stim + electrode
+
+    # Run simulation 
+    job.carp(cmd) #cmd includes the params
+
+run()
diff --git a/main.cpp b/main.cpp
index 50d264b..6a3e784 100644
--- a/main.cpp
+++ b/main.cpp
@@ -17,10 +17,15 @@ using namespace caf;
 class config : public actor_system_config {
     public:
         int duration = 20;
-        int s1_min;
-        int s1_max;
+        int s1_min = 0;
+        int s1_max = 100;
         int tol = 1;
         int s1_duration = 1;
+        int s2_min = 153000;
+        int s2_max = 157000;
+        int s2_interval_min = 220;
+        int s2_interval_max = 220;
+        int s2_interval_increment = 5;
 
         config() {
             opt_group{custom_options_, "global"}
@@ -28,7 +33,12 @@ class config : public actor_system_config {
                 .add(s1_min, "S1-min", "minimum S1 strength for bisection [uA/cm^2]")
                 .add(s1_max, "S1-max", "maximum S1 strength for bisection [uA/cm^2]")
                 .add(s1_duration, "S1-dur", "Duration of S1 stimulus in [ms]")
-                .add(tol, "tol", "Tolerance of Bisection Method");
+                .add(tol, "tol", "Tolerance of Bisection Method")
+                .add(s2_min, "S2-min", "minimum S2 strength for bisection [uA/cm^2]")
+                .add(s2_max, "S2-max", "maximum S2 strength for bisection [uA/cm^2]")
+                .add(s2_interval_min, "S2-interval-min", "minimum S2 interval for bisection [ms]")
+                .add(s2_interval_max, "S2-interval-max", "maximum S2 interval for bisection [ms]")
+                .add(s2_interval_increment, "S2-interval-inc", "increment of S2 interval for bisection [ms]");                
         }
 };
 
@@ -54,7 +64,7 @@ std::string execute_command(std::stringstream &command) {
 
 
 
-bool verify_ap(int s1_strength) {
+bool verify_ap(int s1_strength, int start_index, bool find_s2=false) {
     int num_nodes = 0;
     bool ap_occured = false;
     std::stringstream results_file_path, igbhead_command, igbextract_command, 
@@ -62,8 +72,17 @@ bool verify_ap(int s1_strength) {
     std::string command_output;
     std::smatch match; // for regex matchin
 
+    // Set File Names
+    if (find_s2) {
+        results_file_path << "simulations/output/s2_" << start_index << "_attempt_" << s1_strength << "/vm.igb";
+        tmp_output_file_path << "simulations/output/s2_" << start_index << "_attempt_" << s1_strength << "/tmp/";
+    } else {
+        results_file_path << "simulations/output/s1_attempt_" << s1_strength << "/vm.igb";
+        tmp_output_file_path << "simulations/output/s1_attempt_" << s1_strength << "/tmp/";
+    }
+
+
     // Verify the file exists
-    results_file_path << "simulations/output/s1_attempt_" << s1_strength << "/vm.igb";
     std::ifstream file(results_file_path.str().c_str());
     if (!file.good()) throw std::runtime_error("Failed to find the results file");
 
@@ -78,7 +97,6 @@ bool verify_ap(int s1_strength) {
         throw std::runtime_error("Failed to find the number of nodes in the mesh");
     }
 
-    tmp_output_file_path << "simulations/output/s1_attempt_" << s1_strength << "/tmp/";
     if (!std::filesystem::create_directories(tmp_output_file_path.str().c_str()))
         throw std::runtime_error("Failed to create temp output directory");
 
@@ -100,7 +118,7 @@ bool verify_ap(int s1_strength) {
         tmp_output_file_in.close();
 
         // Check if AP occured (if there is any value within the array larger than 0.0)
-        if (std::any_of(node_data.begin(), node_data.end(), [](float i){return i > 0.0;})) {
+        if (std::any_of(node_data.begin() + start_index, node_data.end(), [](float i){return i > 0.0;})) {
             ap_occured = true;
         } else {
             ap_occured = false;
@@ -123,6 +141,117 @@ int bisection(int s1_min, int s1_max) {
     return s1_mid;
 }
 
+behavior s2_actor(event_based_actor* self) {
+    return {
+        [=](int s1_strenth, int s2_min, int s2_max, int interval_min, 
+                int interval_max, int interval_increment, int tol) {
+            
+            int interval = interval_max;
+            int duration = 300;
+            int current_guess;
+            int s2_min_orginal = s2_min;
+            int s2_max_orginal = s2_max;
+            std::stringstream python_command, log_file;
+            std::string command_output = "";
+
+            
+            // Verify S2_min does not cause an AP
+            aout(self) << "Verifying S2_min does not cause an AP\n";
+
+            python_command << "python3 /code/find_s2.py" 
+               << " --S1-strength " << s1_strenth 
+               << " --S2-strength " << s2_min 
+               << " --S2-start " << interval
+               << " --duration " << duration;
+
+            log_file << "simulations/logs/output_s2_" << s2_min << ".txt";
+
+            command_output = execute_command(python_command);
+            if (verify_ap(s2_min, interval, true))
+                throw std::runtime_error("AP occured at S1_min - Select a new S1_min");
+
+            python_command.str("");
+            log_file.str("");
+
+            // Verify S2_max causes an AP
+            aout(self) << "Verifying S2_max causes an AP\n";
+
+            python_command << "python3 /code/find_s2.py" 
+               << " --S1-strength " << s1_strenth 
+               << " --S2-strength " << s2_max 
+               << " --S2-start " << interval
+               << " --duration " << duration;
+            
+            log_file << "simulations/logs/output_s2_" << s2_max << ".txt";
+
+            command_output = execute_command(python_command);
+            if (!verify_ap(s2_max, interval, true))
+                throw std::runtime_error("AP did not occur at S1_max - Select a new S1_max");
+            
+            python_command.str("");
+            log_file.str("");
+
+            aout(self) << "Moving to bisection\n";
+
+
+
+            while((interval - interval_min) >= 0) {
+
+                current_guess = bisection(s2_min, s2_max);
+                
+
+
+                // Inner threshold bisection loop
+                while(s2_max - s2_min > tol) {
+                    aout(self) << "Current guess: " << current_guess << "\n";
+                    aout(self) << "S2_min: " << s2_min << "\n";
+                    aout(self) << "S2_max: " << s2_max << "\n";
+                    aout(self) << "Interval: " << interval << "\n";
+
+                    // Verify the current guess does not cause an AP
+                    aout(self) << "Verifying current guess does not cause an AP\n";
+
+                    python_command << "python3 /code/find_s2.py" 
+                    << " --S1-strength " << s1_strenth 
+                    << " --S2-strength " << current_guess 
+                    << " --S2-start " << interval
+                    << " --duration " << duration;
+                
+                    log_file << "simulations/logs/output_s2_" << current_guess << ".txt";
+
+                    command_output = execute_command(python_command);
+                    if (verify_ap(current_guess, interval, true)) {
+                        s2_max = current_guess;
+                    } else {
+                        s2_min = current_guess;
+                    }
+
+                    python_command.str("");
+                    log_file.str("");
+
+                    current_guess = bisection(s2_min, s2_max);
+                }
+
+                aout(self) << "S2 threshold found: " << current_guess << "\n";
+
+                aout(self) << "Adjusting interval\n";
+
+                interval = interval - interval_increment;
+
+                aout(self) << "New interval: " << interval << "\n";
+
+                // Reset the S2_min and S2_max values
+                s2_min = s2_min_orginal;
+                s2_max = s2_max_orginal;
+            
+            
+            }
+        
+        
+        }
+    };
+}
+
 
 /**
  * Actor that finds the S1 threshold using bisection
@@ -153,7 +282,7 @@ behavior s1_actor(event_based_actor* self) {
             out_stream << command_output;
             out_stream.close();
 
-            if (verify_ap(s1_strength_min))
+            if (verify_ap(s1_strength_min, 0))
                 throw std::runtime_error("AP occured at S1_min - Select a new S1_min");
             
             python_command.str("");
@@ -174,7 +303,7 @@ behavior s1_actor(event_based_actor* self) {
             out_stream << command_output;
             out_stream.close();
 
-            if (!verify_ap(s1_strength_max))
+            if (!verify_ap(s1_strength_max, 0))
                 throw std::runtime_error("AP did not occur at S1_max - Select a new S1_max");
         
             python_command.str("");
@@ -186,7 +315,7 @@ behavior s1_actor(event_based_actor* self) {
             current_guess = bisection(s1_strength_min, s1_strength_max);
 
             while(s1_strength_max - s1_strength_min > tol) {
-                aout(self) << "Current guess: " << current_guess << "\n";
+                aout(self) << "\nNew guess: " << current_guess << "\n";
 
                 python_command << "python3 /code/find_s1.py" 
                    << " --duration " << duration 
@@ -200,11 +329,11 @@ behavior s1_actor(event_based_actor* self) {
                 out_stream << command_output;
                 out_stream.close();
 
-                if (verify_ap(current_guess)) {
-                    aout(self) << "AP occured at current guess\n";
+                if (verify_ap(current_guess, 0)) {
+                    aout(self) << "AP occured at: " << current_guess << "\n";
                     s1_strength_max = current_guess;
                 } else {
-                    aout(self) << "AP did not occur at current guess\n";
+                    aout(self) << "AP did not occur at: " << current_guess << "\n";
                     s1_strength_min = current_guess;
                 }
 
@@ -214,7 +343,8 @@ behavior s1_actor(event_based_actor* self) {
                 command_output = "";
             }
 
-            aout(self) << "Final S1_strength: " << s1_strength_max << "\n";
+            // aout(self) << "Final S1_strength: " << s1_strength_max << "\n";
+            return s1_strength_max;
 
         }
     };
@@ -232,13 +362,23 @@ void caf_main(actor_system& system, const config& cfg) {
     aout(self) << "S1 min: " << cfg.s1_min << "\n";
     aout(self) << "S1 max: " << cfg.s1_max << "\n";
 
-
     // Cleanup the output directory
-    for (auto& p: std::filesystem::directory_iterator("simulations/output/")) {
+    for (auto& p: std::filesystem::directory_iterator("simulations/output/"))
         std::filesystem::remove_all(p);
-    }
+    
 
     auto s1_solver = system.spawn(s1_actor);
+    self->request(s1_solver, infinite, cfg.duration, cfg.s1_min, cfg.s1_max, cfg.tol).receive(
+        [&](int s1_strength) {
+            aout(self) << "S1 strength: " << s1_strength << "\n";
+            auto s2_solver = system.spawn(s2_actor);
+            self->send(s2_solver, s1_strength, cfg.s2_min, cfg.s2_max, cfg.s2_interval_min, 
+                    cfg.s2_interval_max, cfg.s2_interval_increment, cfg.tol);
+        },
+        [&](error& err) {
+            aout(self) << "Error: " << err << "\n";
+        }
+    );
     self->send(s1_solver, cfg.duration, cfg.s1_min, cfg.s1_max, cfg.tol);
 
 }
-- 
GitLab