From 3041b3abdd84f656f0839dc44f0eae9ce85e7b3c Mon Sep 17 00:00:00 2001
From: Thomas Purcell <purcell@fhi-berlin.mpg.de>
Date: Sat, 11 Jul 2020 18:23:16 +0200
Subject: [PATCH] postfix expressions added and replace actual expressions in
 selected features.txt

Using these expressions one can reconstruct features using the primary feature set
Capability to decompse features into the prevelance of primary features in them
---
 src/CMakeLists.txt                            |   2 +-
 src/descriptor_identifier/Model/Model.cpp     |  13 +-
 src/descriptor_identifier/SISSORegressor.cpp  |   4 +-
 .../feature_space/FeatureSpace.cpp            |   9 +-
 src/feature_creation/node/FeatureNode.cpp     |  15 +++
 src/feature_creation/node/FeatureNode.hpp     |  29 +++++
 src/feature_creation/node/ModelNode.cpp       |  45 ++++++-
 src/feature_creation/node/ModelNode.hpp       |  25 +++-
 src/feature_creation/node/Node.hpp            |  46 +++++++
 .../node/operator_nodes/OperatorNode.hpp      |  43 +++++++
 .../absolute_difference.hpp                   |   6 +
 .../allowed_operator_nodes/absolute_value.hpp |   6 +
 .../allowed_operator_nodes/add.hpp            |   6 +
 .../allowed_operator_nodes/cos.hpp            |   6 +
 .../allowed_operator_nodes/cube.hpp           |   6 +
 .../allowed_operator_nodes/cube_root.hpp      |   6 +
 .../allowed_operator_nodes/divide.hpp         |   6 +
 .../allowed_operator_nodes/exponential.hpp    |   6 +
 .../allowed_operator_nodes/inverse.hpp        |   6 +
 .../allowed_operator_nodes/log.hpp            |   6 +
 .../allowed_operator_nodes/multiply.hpp       |   6 +
 .../negative_exponential.hpp                  |   6 +
 .../allowed_operator_nodes/sin.hpp            |   6 +
 .../allowed_operator_nodes/sixth_power.hpp    |   6 +
 .../allowed_operator_nodes/square.hpp         |   6 +
 .../allowed_operator_nodes/square_root.hpp    |   6 +
 .../allowed_operator_nodes/subtract.hpp       |   6 +
 src/feature_creation/node/utils.cpp           | 112 ++++++++++++++++++
 src/feature_creation/node/utils.hpp           |  43 +++++++
 src/python/bindings_docstring_keyed.cpp       |   6 +-
 src/python/bindings_docstring_keyed.hpp       |   6 +
 src/python/conversion_utils.hpp               |  10 ++
 src/python/feature_creation/FeatureSpace.cpp  |   2 +-
 src/python/feature_creation/node_utils.cpp    |  11 ++
 src/python/feature_creation/node_utils.hpp    |  28 +++++
 test/sisso.json                               |   2 +-
 36 files changed, 533 insertions(+), 20 deletions(-)
 create mode 100644 src/feature_creation/node/utils.cpp
 create mode 100644 src/feature_creation/node/utils.hpp
 create mode 100644 src/python/feature_creation/node_utils.cpp
 create mode 100644 src/python/feature_creation/node_utils.hpp

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6489f0c9..d6a04a58 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -32,7 +32,7 @@ install(TARGETS sisso++ DESTINATION ${CMAKE_CURRENT_LIST_DIR}/../bin/)
 if(USE_PYTHON)
     include(${CMAKE_CURRENT_LIST_DIR}/../cmake/TransferDocStrings.cmake)
     file(GLOB_RECURSE SISSOLIB_SOURCES *.cpp)
-    list(REMOVE_ITEM SISSOLIB_SOURCES main.cpp)
+    list(REMOVE_ITEM SISSOLIB_SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
     list(REMOVE_ITEM SISSOLIB_SOURCES ${CMAKE_CURRENT_LIST_DIR}/python/bindings_docstring_keyed.cpp)
     list(REMOVE_ITEM SISSOLIB_SOURCES ${CMAKE_CURRENT_LIST_DIR}/python/bindings_docstring_keyed.hpp)
 
diff --git a/src/descriptor_identifier/Model/Model.cpp b/src/descriptor_identifier/Model/Model.cpp
index 744348a0..0b21669a 100644
--- a/src/descriptor_identifier/Model/Model.cpp
+++ b/src/descriptor_identifier/Model/Model.cpp
@@ -80,13 +80,14 @@ Model::Model(std::string train_file)
 
         int rung = std::stoi(split_str[0]);
         std::string unit_str = split_str[1];
-        std::string expr = split_str[2];
+        std::string postfix_expr = split_str[2];
+        std::string expr = split_str[3];
 
         std::vector<double> feat_val(_n_samp_train);
         std::vector<double> feat_test_val = {};
         std::copy_n(&_D_train[ff * _n_samp_train], _n_samp_train, feat_val.data());
 
-        model_node_ptr feat = std::make_shared<ModelNode>(ff, rung, expr, feat_val, feat_test_val, Unit(unit_str));
+        model_node_ptr feat = std::make_shared<ModelNode>(ff, rung, expr, postfix_expr, feat_val, feat_test_val, Unit(unit_str));
         _feats.push_back(feat);
     }
 
@@ -107,14 +108,16 @@ Model::Model(std::string train_file, std::string test_file)
 
         int rung = std::stoi(split_str[0]);
         std::string unit_str = split_str[1];
-        std::string expr = split_str[2];
+        std::string postfix_expr = split_str[2];
+        std::string expr = split_str[3];
+
         std::vector<double> feat_val(_n_samp_train);
         std::vector<double> feat_test_val(_n_samp_test);
 
         std::copy_n(&_D_train[ff * _n_samp_train], _n_samp_train, feat_val.data());
         std::copy_n(&_D_test[ff * _n_samp_test], _n_samp_test, feat_test_val.data());
 
-        _feats.push_back(std::make_shared<ModelNode>(ff, rung, expr, feat_val, feat_test_val, Unit(unit_str)));
+        _feats.push_back(std::make_shared<ModelNode>(ff, rung, expr, postfix_expr, feat_val, feat_test_val, Unit(unit_str)));
     }
 }
 
@@ -283,7 +286,7 @@ void Model::to_file(std::string filename, bool train, std::vector<int> test_inds
 
     out_file_stream << "# Feature Rung, Units, and Expressions" << std::endl;
     for(int ff = 0; ff < _feats.size(); ++ff)
-        out_file_stream << std::setw(6) << std::left << "# " + std::to_string(ff) + ", " << std::to_string(_feats[ff]->rung()) + ", " << std::setw(50) << _feats[ff]->unit().toString() + ", " << _feats[ff]->expr() << std::endl;
+        out_file_stream << std::setw(6) << std::left << "# " + std::to_string(ff) + ", " << std::to_string(_feats[ff]->rung()) + ", " << std::setw(50) << _feats[ff]->unit().toString() + ", " << _feats[ff]->postfix_expr() + "," << _feats[ff]->expr() << std::endl;
 
     out_file_stream << "# Number of Samples Per Task" << std::endl;
     if(train)
diff --git a/src/descriptor_identifier/SISSORegressor.cpp b/src/descriptor_identifier/SISSORegressor.cpp
index ae27fe0d..8542e30a 100644
--- a/src/descriptor_identifier/SISSORegressor.cpp
+++ b/src/descriptor_identifier/SISSORegressor.cpp
@@ -110,7 +110,7 @@ void SISSORegressor::fit()
     for(int rr = 0; rr < _n_residual; ++rr)
     {
         node_value_arrs::clear_temp_test_reg();
-        model_node_ptr model_feat = std::make_shared<ModelNode>(_feat_space->phi_selected()[rr]->arr_ind(), _feat_space->phi_selected()[rr]->rung(), _feat_space->phi_selected()[rr]->expr(), _feat_space->phi_selected()[rr]->value(), _feat_space->phi_selected()[rr]->test_value(), _feat_space->phi_selected()[rr]->unit());
+        model_node_ptr model_feat = std::make_shared<ModelNode>(_feat_space->phi_selected()[rr]->arr_ind(), _feat_space->phi_selected()[rr]->rung(), _feat_space->phi_selected()[rr]->expr(), _feat_space->phi_selected()[rr]->postfix_expr(), _feat_space->phi_selected()[rr]->value(), _feat_space->phi_selected()[rr]->test_value(), _feat_space->phi_selected()[rr]->unit());
         models.push_back(Model(_prop, _prop_test, {model_feat}, _task_sizes_train, _task_sizes_test));
         models.back().copy_error(&residual[rr * _n_samp]);
         if(_mpi_comm->rank() == 0)
@@ -210,7 +210,7 @@ void SISSORegressor::l0_norm(std::vector<double>& prop, int n_dim)
         for(int ii = 0; ii < n_dim; ++ii)
         {
             int index = all_inds_min[inds[rr] * n_dim + ii];
-            min_nodes[ii] = std::make_shared<ModelNode>(_feat_space->phi_selected()[index]->arr_ind(), _feat_space->phi_selected()[index]->rung(), _feat_space->phi_selected()[index]->expr(), _feat_space->phi_selected()[index]->value(), _feat_space->phi_selected()[index]->test_value(), _feat_space->phi_selected()[index]->unit());
+            min_nodes[ii] = std::make_shared<ModelNode>(_feat_space->phi_selected()[index]->arr_ind(), _feat_space->phi_selected()[index]->rung(), _feat_space->phi_selected()[index]->expr(), _feat_space->phi_selected()[index]->postfix_expr(), _feat_space->phi_selected()[index]->value(), _feat_space->phi_selected()[index]->test_value(), _feat_space->phi_selected()[index]->unit());
         }
         models.push_back(Model(_prop, _prop_test, min_nodes, _task_sizes_train, _task_sizes_test));
     }
diff --git a/src/feature_creation/feature_space/FeatureSpace.cpp b/src/feature_creation/feature_space/FeatureSpace.cpp
index 79a06815..b49a7ef0 100644
--- a/src/feature_creation/feature_space/FeatureSpace.cpp
+++ b/src/feature_creation/feature_space/FeatureSpace.cpp
@@ -66,7 +66,7 @@ void FeatureSpace::initialize_fs(std::vector<double> prop)
     {
         std::ofstream out_file_stream = std::ofstream();
         out_file_stream.open(_feature_space_file);
-        out_file_stream << std::setw(14) <<std::left << "# FEAT_ID" << std::setw(24) << std::left << "Score" << "Feature Expression" << std::endl;
+        out_file_stream << std::setw(14) <<std::left << "# FEAT_ID" << std::setw(24) << std::left << "Score" << "Feature Postfix Expression (RPN)" << std::endl;
         out_file_stream.close();
     }
     _project = project_funcs::project_r;
@@ -535,7 +535,6 @@ void FeatureSpace::sis(std::vector<double>& prop)
         if(is_valid)
         {
             scores_sel[cur_feat_local] = _scores[inds[ii]];
-            // phi_sel.push_back(std::make_shared<FeatureNode>(cur_feat + cur_feat_local, _phi[inds[ii]]->expr(), _phi[inds[ii]]->value(), _phi[inds[ii]]->test_value(), _phi[inds[ii]]->unit(), true));
             phi_sel.push_back(_phi[inds[ii]]);
             phi_sel.back()->set_selected(true);
             phi_sel.back()->set_d_mat_ind(cur_feat + cur_feat_local);
@@ -642,7 +641,7 @@ void FeatureSpace::sis(std::vector<double>& prop)
                 inds = util_funcs::argsort(sent_scores);
                 for(int ii = 0; ii < _n_sis_select; ++ii)
                 {
-                    out_file_stream << std::setw(14) <<std::left << cur_feat << std::setw(24) << std::setprecision(18) << std::left << -1 * sent_scores[inds[ii]] << sent_phi[inds[ii]]->expr() << std::endl;
+                    out_file_stream << std::setw(14) <<std::left << cur_feat << std::setw(24) << std::setprecision(18) << std::left << -1 * sent_scores[inds[ii]] << sent_phi[inds[ii]]->postfix_expr() << std::endl;
                     _phi_selected.push_back(sent_phi[inds[ii]]);
                     _phi_selected.back()->set_selected(true);
                     _phi_selected.back()->set_d_mat_ind(cur_feat);
@@ -665,7 +664,7 @@ void FeatureSpace::sis(std::vector<double>& prop)
                 {
                     if(valid_score_against_current(cur_feat_local, sent_phi[inds[ii]]->value().data(), sent_scores[inds[ii]], scores_sel, scores_comp))
                     {
-                        out_file_stream << std::setw(14) <<std::left << cur_feat << std::setw(24) << std::setprecision(18) << std::left << -1 * sent_scores[inds[ii]] << sent_phi[inds[ii]]->expr() << std::endl;
+                        out_file_stream << std::setw(14) <<std::left << cur_feat << std::setw(24) << std::setprecision(18) << std::left << -1 * sent_scores[inds[ii]] << sent_phi[inds[ii]]->postfix_expr() << std::endl;
 
                         _phi_selected.push_back(sent_phi[inds[ii]]);
 
@@ -711,7 +710,7 @@ void FeatureSpace::sis(std::vector<double>& prop)
         inds = util_funcs::argsort(scores_sel);
         for(auto& ind : inds)
         {
-            out_file_stream << std::setw(14) <<std::left << cur_feat << std::setw(24) << std::setprecision(18) << std::left << -1 * scores_sel[ind] << phi_sel[ind]->expr() << std::endl;
+            out_file_stream << std::setw(14) <<std::left << cur_feat << std::setw(24) << std::setprecision(18) << std::left << -1 * scores_sel[ind] << phi_sel[ind]->postfix_expr() << std::endl;
             _phi_selected.push_back(phi_sel[ind]);
             _phi_selected.back()->set_d_mat_ind(cur_feat);
             _phi_selected.back()->set_value();
diff --git a/src/feature_creation/node/FeatureNode.cpp b/src/feature_creation/node/FeatureNode.cpp
index 9b0031d4..1145a4f2 100644
--- a/src/feature_creation/node/FeatureNode.cpp
+++ b/src/feature_creation/node/FeatureNode.cpp
@@ -40,4 +40,19 @@ void FeatureNode::update_div_mult_leaves(std::map<std::string, double>& div_mult
     expected_abs_tot += std::abs(fact);
 }
 
+std::map<int, int> FeatureNode::primary_feature_decomp()
+{
+    std::map<int, int> pf_decomp;
+    pf_decomp[_arr_ind] = 1;
+    return pf_decomp;
+}
+
+void FeatureNode::update_primary_feature_decomp(std::map<int, int>& pf_decomp)
+{
+    if(pf_decomp.count(_arr_ind) > 0)
+        pf_decomp[_arr_ind] += 1;
+    else
+        pf_decomp[_arr_ind] = 1;
+}
+
 // BOOST_CLASS_EXPORT(FeatureNode)
diff --git a/src/feature_creation/node/FeatureNode.hpp b/src/feature_creation/node/FeatureNode.hpp
index c1d05d92..abf9086b 100644
--- a/src/feature_creation/node/FeatureNode.hpp
+++ b/src/feature_creation/node/FeatureNode.hpp
@@ -214,6 +214,35 @@ public:
      */
     inline int rung(int cur_rung = 0){return cur_rung;}
 
+    /**
+     * @brief Get the primary feature decomposition of a feature
+     * @return A map representing the primary feature comprising a feature
+     */
+    std::map<int, int> primary_feature_decomp();
+
+    /**
+     * @brief Update the primary feature decomposition of a feature
+     *
+     * @param pf_decomp The primary feature decomposition of the feature calling this function.
+     */
+    void update_primary_feature_decomp(std::map<int, int>& pf_decomp);
+
+    /**
+     * @brief Converts a feature into a postfix expression (reverse polish notation)
+     *
+     * @details Recursively creates a postfix representation of the string
+     *
+     * @param cur_expr The current expression
+     * @return The current postfix expression of the feature
+     */
+    inline void update_postfix(std::string& cur_expr){cur_expr = get_postfix_term() + "|" + cur_expr;};
+
+    /**
+     * @brief Get the three character representation of the operator
+     * @return the three character representation of the operator
+     */
+    inline std::string get_postfix_term(){return std::to_string(_feat_ind);}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/ModelNode.cpp b/src/feature_creation/node/ModelNode.cpp
index 13b96291..b6f4d214 100644
--- a/src/feature_creation/node/ModelNode.cpp
+++ b/src/feature_creation/node/ModelNode.cpp
@@ -3,8 +3,9 @@
 ModelNode::ModelNode()
 {}
 
-ModelNode::ModelNode(int feat_ind, int rung, std::string expr, std::vector<double> value, std::vector<double> test_value, Unit unit) :
+ModelNode::ModelNode(int feat_ind, int rung, std::string expr, std::string post_fix_expr, std::vector<double> value, std::vector<double> test_value, Unit unit) :
     FeatureNode(feat_ind, expr, value, test_value, unit, false),
+    _expr_postfix(post_fix_expr),
     _rung(rung)
  {}
 
@@ -30,3 +31,45 @@ void ModelNode::update_div_mult_leaves(std::map<std::string, double>& div_mult_l
 
     expected_abs_tot += std::abs(fact);
 }
+
+std::map<int, int> ModelNode::primary_feature_decomp()
+{
+    std::map<int, int> pf_decomp;
+    std::vector<std::string> split_postfix = str_utils::split_string_trim(_expr_postfix, "|");
+    for(auto& part : split_postfix)
+    {
+        try
+        {
+            if(pf_decomp.count(std::stoi(part)))
+                ++pf_decomp[std::stoi(part)];
+            else
+                pf_decomp[std::stoi(part)] = 1;
+        }
+        catch(const std::invalid_argument e)
+        {
+            // Do Nothing
+        }
+    }
+
+    return pf_decomp;
+}
+
+void ModelNode::update_primary_feature_decomp(std::map<int, int>& pf_decomp)
+{
+    pf_decomp.clear();
+    std::vector<std::string> split_postfix = str_utils::split_string_trim(_expr_postfix, "|");
+    for(auto& part : split_postfix)
+    {
+        try
+        {
+            if(pf_decomp.count(std::stoi(part)))
+                ++pf_decomp[std::stoi(part)];
+            else
+                pf_decomp[std::stoi(part)] = 1;
+        }
+        catch(const std::invalid_argument e)
+        {
+            // Do Nothing
+        }
+    }
+}
diff --git a/src/feature_creation/node/ModelNode.hpp b/src/feature_creation/node/ModelNode.hpp
index 2609c874..eabb2f7b 100644
--- a/src/feature_creation/node/ModelNode.hpp
+++ b/src/feature_creation/node/ModelNode.hpp
@@ -10,6 +10,7 @@
 #define MODEL_NODE
 
 #include <feature_creation/node/FeatureNode.hpp>
+#include <utils/string_utils.hpp>
 
 // DocString: cls_model_node
 /**
@@ -33,7 +34,7 @@ class ModelNode: public FeatureNode
 
 protected:
     int _rung;
-
+    std::string _expr_postfix;
 public:
     /**
      * @brief Base Constructor
@@ -51,7 +52,7 @@ public:
      * @param value Value of the feature for each test sample
      * @param unit Unit of the feature
      */
-    ModelNode(int feat_ind, int rung, std::string expr, std::vector<double> value, std::vector<double> test_value, Unit unit);
+    ModelNode(int feat_ind, int rung, std::string expr, std::string expr_postfix, std::vector<double> value, std::vector<double> test_value, Unit unit);
 
     /**
      * @brief Copy Constructor
@@ -132,7 +133,6 @@ public:
      */
     inline double* test_value_ptr(int offset = -1){return _test_value.data();}
 
-
     // DocString: model_node_rung
     /**
      * @brief return the rung of the feature
@@ -141,6 +141,25 @@ public:
      */
     inline int rung(int cur_rung = 0){return _rung;}
 
+    /**
+     * @brief Update the primary feature decomposition of a feature
+     *
+     * @param pf_decomp The primary feature decomposition of the feature calling this function.
+     */
+    void update_primary_feature_decomp(std::map<int, int>& pf_decomp);
+
+    /**
+     * @brief Get the primary feature decomposition of a feature
+     * @return A map representing the primary feature comprising a feature
+     */
+    std::map<int, int> primary_feature_decomp();
+
+    /**
+     * @brief Get the three character representation of the operator
+     * @return the three character representation of the operator
+     */
+    inline std::string get_postfix_term(){return _expr_postfix;}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/Node.hpp b/src/feature_creation/node/Node.hpp
index c15b32e9..b258b5e7 100644
--- a/src/feature_creation/node/Node.hpp
+++ b/src/feature_creation/node/Node.hpp
@@ -28,6 +28,7 @@
 #include <boost/serialization/unique_ptr.hpp>
 
 #ifdef PY_BINDINGS
+    namespace py = boost::python;
     namespace np = boost::python::numpy;
 #endif
 
@@ -262,6 +263,42 @@ public:
      */
     virtual int rung(int cur_rung = 0) = 0;
 
+    /**
+     * @brief Get the primary feature decomposition of a feature
+     * @return A map representing the primary feature comprising a feature
+     */
+    virtual std::map<int, int> primary_feature_decomp() = 0;
+
+    /**
+     * @brief Update the primary feature decomposition of a feature
+     *
+     * @param pf_decomp The primary feature decomposition of the feature calling this function.
+     */
+    virtual void update_primary_feature_decomp(std::map<int, int>& pf_decomp) = 0;
+
+    /**
+     * @brief Converts a feature into a postfix expression (reverse polish notation)
+     *
+     * @details Recursively creates a postfix representation of the string
+     *
+     * @param cur_expr The current expression
+     * @return The current postfix expression of the feature
+     */
+    virtual void update_postfix(std::string& cur_expr) = 0;
+
+    // DocString: node_postfix_expr
+    /**
+     * @brief Get the postfix expression for the feature
+     * @return The postfix string for the expression
+     */
+    inline std::string postfix_expr(){std::string cur_expr = ""; update_postfix(cur_expr); return cur_expr.substr(0, cur_expr.size() - 1);}
+
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    virtual std::string get_postfix_term() = 0;
+
     /**
      * @brief update the dictionary used to check if an Add/Sub/AbsDiff node is valid
      *
@@ -279,6 +316,8 @@ public:
      * @param expected_abs_tot The expected absolute sum of all values in div_mult_leaves
      */
     virtual void update_div_mult_leaves(std::map<std::string, double>& div_mult_leaves, double fact, double& expected_abs_tot) = 0;
+
+
     #ifdef PY_BINDINGS
 
         // DocString: node_value_py
@@ -294,6 +333,13 @@ public:
          * @return The test data as a numpy array
          */
         inline np::ndarray test_value_py(){return python_conv_utils::to_ndarray<double>(test_value());}
+
+        // DocString: node_primary_feature_decomp
+        /**
+         * @brief Get the primary feature decomposition of a feature
+         * @return A python dict representing the primary feature comprising a feature
+         */
+        inline py::dict primary_feature_decomp_py(){return python_conv_utils::to_dict<int, int>(primary_feature_decomp());}
     #endif
 };
 
diff --git a/src/feature_creation/node/operator_nodes/OperatorNode.hpp b/src/feature_creation/node/operator_nodes/OperatorNode.hpp
index 070e24fe..2fca6276 100644
--- a/src/feature_creation/node/operator_nodes/OperatorNode.hpp
+++ b/src/feature_creation/node/operator_nodes/OperatorNode.hpp
@@ -217,6 +217,49 @@ public:
      */
     virtual NODE_TYPE type() = 0;
 
+     /**
+     * @brief Get the primary feature decomposition of a feature
+     * @return A map representing the primary feature comprising a feature
+     */
+    std::map<int, int> primary_feature_decomp()
+    {
+        std::map<int, int> pf_decomp;
+        update_primary_feature_decomp(pf_decomp);
+        return pf_decomp;
+    }
+
+    /**
+     * @brief Update the primary feature decomposition of a feature
+     *
+     * @param pf_decomp The primary feature decomposition of the feature calling this function.
+     */
+    void update_primary_feature_decomp(std::map<int, int>& pf_decomp)
+    {
+        for(auto& feat : _feats)
+            feat->update_primary_feature_decomp(pf_decomp);
+    }
+
+    /**
+     * @brief Converts a feature into a postfix expression (reverse polish notation)
+     *
+     * @details Recursively creates a postfix representation of the string
+     *
+     * @param cur_expr The current expression
+     * @return The current postfix expression of the feature
+     */
+    inline void update_postfix(std::string& cur_expr)
+    {
+        cur_expr = get_postfix_term() + "|" + cur_expr;
+        for(int nn = N - 1; nn >= 0; --nn)
+            _feats[nn]->update_postfix(cur_expr);
+    };
+
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    virtual std::string get_postfix_term() = 0;
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_difference.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_difference.hpp
index faa4f563..1f4de2a1 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_difference.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_difference.hpp
@@ -113,6 +113,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::ABS_DIFF;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "abd";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_value.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_value.hpp
index 26c11078..e8e9734f 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_value.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/absolute_value.hpp
@@ -111,6 +111,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::ABS;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "abs";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/add.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/add.hpp
index 38a63e82..5bbb668c 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/add.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/add.hpp
@@ -112,6 +112,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::ADD;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "add";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cos.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cos.hpp
index b1689135..90475353 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cos.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cos.hpp
@@ -111,6 +111,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::COS;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "cos";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube.hpp
index 4730e179..6166868d 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube.hpp
@@ -111,6 +111,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::CB;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "cb";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube_root.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube_root.hpp
index 5054cdd3..41acf2cb 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube_root.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/cube_root.hpp
@@ -111,6 +111,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::CBRT;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "cbrt";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/divide.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/divide.hpp
index 3d5dc138..b7ba2d1e 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/divide.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/divide.hpp
@@ -112,6 +112,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::DIV;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "div";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/exponential.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/exponential.hpp
index b03ab0b1..8355ebc4 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/exponential.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/exponential.hpp
@@ -111,6 +111,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::EXP;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "exp";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/inverse.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/inverse.hpp
index 80735aa3..60109dc4 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/inverse.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/inverse.hpp
@@ -99,6 +99,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::INV;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "inv";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/log.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/log.hpp
index e59fd4df..a3c4542d 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/log.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/log.hpp
@@ -111,6 +111,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::LOG;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "log";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/multiply.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/multiply.hpp
index e58b0ce4..ba528c43 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/multiply.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/multiply.hpp
@@ -113,6 +113,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::MULT;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "mult";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/negative_exponential.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/negative_exponential.hpp
index 517f396f..66c17bf1 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/negative_exponential.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/negative_exponential.hpp
@@ -112,6 +112,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::NEG_EXP;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "nexp";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sin.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sin.hpp
index e06d55b6..69f5147d 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sin.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sin.hpp
@@ -112,6 +112,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::SIN;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "sin";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sixth_power.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sixth_power.hpp
index e2e6ad8c..dda10de5 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sixth_power.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/sixth_power.hpp
@@ -112,6 +112,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::SIX_POW;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "sp";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square.hpp
index cf9fa6df..b4044035 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square.hpp
@@ -111,6 +111,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::SQ;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "sq";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square_root.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square_root.hpp
index c1bcb880..cce30e4e 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square_root.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/square_root.hpp
@@ -112,6 +112,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::SQRT;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "sqrt";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/subtract.hpp b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/subtract.hpp
index 64b5e59b..246391a4 100644
--- a/src/feature_creation/node/operator_nodes/allowed_operator_nodes/subtract.hpp
+++ b/src/feature_creation/node/operator_nodes/allowed_operator_nodes/subtract.hpp
@@ -113,6 +113,12 @@ public:
      */
     inline NODE_TYPE type(){return NODE_TYPE::SUB;}
 
+    /**
+     * @brief Get the string character representation of the node for the postfix expression
+     * @return the string representation of the node for the postfix expression
+     */
+    inline std::string get_postfix_term(){return "sub";}
+
     /**
      * @brief update the dictionary used to check if an Add/Sub node is valid
      *
diff --git a/src/feature_creation/node/utils.cpp b/src/feature_creation/node/utils.cpp
new file mode 100644
index 00000000..8ce52607
--- /dev/null
+++ b/src/feature_creation/node/utils.cpp
@@ -0,0 +1,112 @@
+#include <feature_creation/node/utils.hpp>
+
+node_ptr str2node::postfix2node(std::string postfix_expr, const std::vector<node_ptr>& phi_0, int feat_ind)
+{
+    std::vector<node_ptr> stack;
+    std::vector<std::string> postfix_split = str_utils::split_string_trim(postfix_expr, "|");
+    feat_ind += postfix_split.size() - 1;
+    for(int ff = 0; ff < postfix_split.size(); ++ff)
+    {
+        std::string term = postfix_split[ff];
+        try
+        {
+            stack.push_back(phi_0[std::stoi(term)]);
+            --feat_ind;
+        }
+        catch(const std::invalid_argument e)
+        {
+            if(term == "add")
+            {
+                stack[stack.size() - 2] = std::make_shared<AddNode>(stack[stack.size() - 2], stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+                stack.pop_back();
+            }
+            else if(term == "sub")
+            {
+                stack[stack.size() - 2] = std::make_shared<SubNode>(stack[stack.size() - 2], stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+                stack.pop_back();
+            }
+            else if(term == "abd")
+            {
+                stack[stack.size() - 2] = std::make_shared<AbsDiffNode>(stack[stack.size() - 2], stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+                stack.pop_back();
+            }
+            else if(term == "mult")
+            {
+                stack[stack.size() - 2] = std::make_shared<MultNode>(stack[stack.size() - 2], stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+                stack.pop_back();
+            }
+            else if(term == "div")
+            {
+                stack[stack.size() - 2] = std::make_shared<DivNode>(stack[stack.size() - 2], stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+                stack.pop_back();
+            }
+            else if(term == "abs")
+                stack[stack.size() - 1] = std::make_shared<AbsNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "inv")
+                stack[stack.size() - 1] = std::make_shared<InvNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "exp")
+                stack[stack.size() - 1] = std::make_shared<ExpNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "nexp")
+                stack[stack.size() - 1] = std::make_shared<NegExpNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "log")
+                stack[stack.size() - 1] = std::make_shared<LogNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "sin")
+                stack[stack.size() - 1] = std::make_shared<SinNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "cos")
+                stack[stack.size() - 1] = std::make_shared<CosNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "sq")
+                stack[stack.size() - 1] = std::make_shared<SqNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "sqrt")
+                stack[stack.size() - 1] = std::make_shared<SqrtNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "cb")
+                stack[stack.size() - 1] = std::make_shared<CbNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "cbrt")
+                stack[stack.size() - 1] = std::make_shared<CbrtNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else if(term == "sp")
+                stack[stack.size() - 1] = std::make_shared<SixPowNode>(stack[stack.size() - 1], feat_ind, 1e-50, 1e50);
+            else
+                throw std::logic_error("Term in postfix expression does not represent a node");
+            --feat_ind;
+        }
+    }
+    if(stack.size() != 1)
+        throw std::logic_error("Went through postfix expression and still more than one node in the list. This must be an invalid expression: " + postfix_expr + ".");
+    return stack[0];
+}
+
+std::vector<node_ptr> str2node::phi_selected_from_file(std::string filename, std::vector<node_ptr> phi_0)
+{
+    node_value_arrs::resize_values_arr(0, phi_0.size(), true);
+    node_value_arrs::initialize_d_matrix_arr();
+
+    std::ifstream file_stream;
+    file_stream.open(filename, std::ios::in);
+
+    std::string line;
+    std::vector<std::string> split_line;
+
+    std::vector<node_ptr> phi_selected;
+    int feat_ind = phi_0.size();
+    int feat_sel = 0;
+    std::getline(file_stream, line);
+
+    while(std::getline(file_stream, line))
+    {
+        if(line[0] == '#')
+            continue;
+
+        node_value_arrs::resize_d_matrix_arr(1);
+        boost::algorithm::split(split_line, line, boost::algorithm::is_any_of("\t "), boost::token_compress_on);
+
+        node_ptr new_feat = postfix2node(split_line[2], phi_0, feat_ind);
+
+        new_feat->set_selected(true);
+        new_feat->set_d_mat_ind(feat_sel);
+        new_feat->set_value();
+        phi_selected.push_back(std::make_shared<ModelNode>(feat_ind, new_feat->rung(), new_feat->expr(), new_feat->postfix_expr(), new_feat->value(), new_feat->test_value(), new_feat->unit()));
+        ++feat_ind;
+        ++feat_sel;
+    }
+    file_stream.close();
+    return phi_selected;
+}
diff --git a/src/feature_creation/node/utils.hpp b/src/feature_creation/node/utils.hpp
new file mode 100644
index 00000000..e8d7d4a3
--- /dev/null
+++ b/src/feature_creation/node/utils.hpp
@@ -0,0 +1,43 @@
+/** @file feature_creation/node/utils.hpp
+ *  @brief utility functions to build node_ptrs from strings
+ *
+ *  @author Thomas A. R. Purcell (tpurcell)
+ *  @bug No known bugs.
+ */
+#ifndef NODE_UTILS
+#define NODE_UTILS
+
+#include <fstream>
+#include <feature_creation/node/FeatureNode.hpp>
+#include <feature_creation/node/ModelNode.hpp>
+#include <feature_creation/node/operator_nodes/allowed_ops.hpp>
+#include <feature_creation/node/value_storage/nodes_value_containers.hpp>
+
+#include <utils/string_utils.hpp>
+
+namespace str2node
+{
+    /**
+     * @brief Convert a postfix expression into a node_ptr
+     * @details Creates a stack to iteratively generate the feature represented by the expression
+     *
+     * @param postfix_expr The postfix expression of the feature node
+     * @param phi_0 The initial feature set
+     * @param feat_ind The desired feature index
+     * @return The feature node described by the postfix expression
+     */
+    node_ptr postfix2node(std::string postfix_expr, const std::vector<node_ptr>& phi_0, int feat_ind);
+
+    /**
+     * @brief Convert a feature_space/selected_features.txt into a phi_selected;
+     * @details Read in the file to get the postfix expressions and regenerate the selected features using phi_0
+     *
+     * @param filename The name of the feature_space/selected_features.txt file
+     * @param phi_0 The initial feature space
+     *
+     * @return The selected feature set from the file
+     */
+    std::vector<node_ptr> phi_selected_from_file(std::string filename, std::vector<node_ptr> phi_0);
+}
+
+#endif
\ No newline at end of file
diff --git a/src/python/bindings_docstring_keyed.cpp b/src/python/bindings_docstring_keyed.cpp
index aa507ff7..c4f609b3 100644
--- a/src/python/bindings_docstring_keyed.cpp
+++ b/src/python/bindings_docstring_keyed.cpp
@@ -30,6 +30,8 @@ void sisso::register_all()
     sisso::feature_creation::node::registerSqNode();
     sisso::feature_creation::node::registerSqrtNode();
     sisso::feature_creation::node::registerSixPowNode();
+
+    def("phi_selected_from_file", &str2node::phi_selected_from_file_py);
 }
 
 void sisso::feature_creation::registerFeatureSpace()
@@ -96,6 +98,8 @@ void sisso::feature_creation::node::registerNode()
         .add_property("d_mat_ind", &Node::d_mat_ind, &Node::set_d_mat_ind, "@DocString_node_set_d_mat_ind@")
         .add_property("value", &Node::value_py, "@DocString_node_value_py@")
         .add_property("test_value", &Node::test_value_py, "@DocString_node_test_value_py@")
+        .add_property("primary_feat_decomp", &Node::primary_feature_decomp_py, "@DocString_node_primary_feature_decomp@")
+        .add_property("postfix_expr", &Node::postfix_expr, "@DocString_node_postfix_expr@")
         .def("expr", pure_virtual(&Node::expr), "@DocString_node_expr@")
         .def("unit", pure_virtual(&Node::unit), "@DocString_node_unit@")
         .def("set_value", pure_virtual(&Node::set_value), "@DocString_node_set_value@")
@@ -131,7 +135,7 @@ void sisso::feature_creation::node::registerModelNode()
     std::string (ModelNode::*expr_const)() const = &ModelNode::expr;
 
     using namespace boost::python;
-    class_<ModelNode, bases<FeatureNode>>("ModelNode", init<int, int, std::string, std::vector<double>, std::vector<double>, Unit>())
+    class_<ModelNode, bases<FeatureNode>>("ModelNode", init<int, int, std::string, std::string, std::vector<double>, std::vector<double>, Unit>())
         .def("is_nan", &ModelNode::is_nan, "@DocString_model_node_is_nan@")
         .def("is_const", &ModelNode::is_const, "@DocString_model_node_is_const@")
         .def("set_value", &ModelNode::set_value, "@DocString_model_node_set_value@")
diff --git a/src/python/bindings_docstring_keyed.hpp b/src/python/bindings_docstring_keyed.hpp
index a68dc5e9..e11568b8 100644
--- a/src/python/bindings_docstring_keyed.hpp
+++ b/src/python/bindings_docstring_keyed.hpp
@@ -9,6 +9,7 @@
 
 #include <descriptor_identifier/SISSORegressor.hpp>
 #include <feature_creation/feature_space/FeatureSpace.hpp>
+#include <python/feature_creation/node_utils.hpp>
 
 namespace py = boost::python;
 namespace np = boost::python::numpy;
@@ -45,6 +46,10 @@ namespace sisso
                 inline bool is_const(){return this->get_override("is_const")();}
                 inline NODE_TYPE type(){return this->get_override("type")();}
                 inline int rung(int cur_rung = 0){return this->get_override("rung")();}
+                std::map<int, int> primary_feature_decomp(){return this->get_override("primary_feature_decomp")();}
+                void update_primary_feature_decomp(std::map<int, int>& pf_decomp){this->get_override("update_primary_feature_decomp")();}
+                void update_postfix(std::string& cur_expr){this->get_override("update_postfix")();}
+                std::string get_postfix_term(){return this->get_override("get_postfix_term")();}
                 inline void update_add_sub_leaves(std::map<std::string, int>& add_sub_leaves, int pl_mn, int& expected_abs_tot){this->get_override("update_add_sub_leaves");}
                 inline void update_div_mult_leaves(std::map<std::string, double>& div_mult_leaves, double fact, double& expected_abs_tot){this->get_override("update_div_mult_leaves");}
             };
@@ -60,6 +65,7 @@ namespace sisso
                 inline int rung(int cur_rung = 0){return this->get_override("rung")();}
                 inline std::string expr(){return this->get_override("expr")();}
                 inline Unit unit(){return this->get_override("unit")();}
+                std::string get_postfix_term(){return this->get_override("get_postfix_term")();}
                 inline void update_add_sub_leaves(std::map<std::string, int>& add_sub_leaves, int pl_mn, int& expected_abs_tot){this->get_override("update_add_sub_leaves")();}
                 inline void update_div_mult_leaves(std::map<std::string, double>& div_mult_leaves, double fact, double& expected_abs_tot){this->get_override("update_div_mult_leaves")();}
             };
diff --git a/src/python/conversion_utils.hpp b/src/python/conversion_utils.hpp
index 2549ec8c..cd82cf43 100644
--- a/src/python/conversion_utils.hpp
+++ b/src/python/conversion_utils.hpp
@@ -102,6 +102,16 @@ namespace python_conv_utils
         std::copy_n(vec.data(), vec.size(), reinterpret_cast<T*>(arr.get_data()));
         return arr;
     }
+
+    template<typename key, typename val>
+    py::dict to_dict(std::map<key, val> map)
+    {
+        py::dict dct;
+        for(auto& iter : map)
+            dct[iter.first] = iter.second;
+
+        return dct;
+    }
 }
 
 #endif
\ No newline at end of file
diff --git a/src/python/feature_creation/FeatureSpace.cpp b/src/python/feature_creation/FeatureSpace.cpp
index 589bb14a..73cbcd2a 100644
--- a/src/python/feature_creation/FeatureSpace.cpp
+++ b/src/python/feature_creation/FeatureSpace.cpp
@@ -76,6 +76,6 @@ py::list FeatureSpace::phi_selected_py()
 {
     py::list feat_lst;
     for(auto& feat : _phi_selected)
-        feat_lst.append<ModelNode>(ModelNode(feat->d_mat_ind(), feat->rung(), feat->expr(), feat->value(), feat->test_value(), feat->unit()));
+        feat_lst.append<ModelNode>(ModelNode(feat->d_mat_ind(), feat->rung(), feat->expr(), feat->postfix_expr(), feat->value(), feat->test_value(), feat->unit()));
     return feat_lst;
 }
diff --git a/src/python/feature_creation/node_utils.cpp b/src/python/feature_creation/node_utils.cpp
new file mode 100644
index 00000000..ec2b1c63
--- /dev/null
+++ b/src/python/feature_creation/node_utils.cpp
@@ -0,0 +1,11 @@
+#include <python/feature_creation/node_utils.hpp>
+
+py::list str2node::phi_selected_from_file_py(std::string filename, py::list phi_0)
+{
+    std::vector<node_ptr> phi_selected = phi_selected_from_file(filename, python_conv_utils::shared_ptr_vec_from_list<Node, FeatureNode>(phi_0));
+
+    py::list feat_lst;
+    for(auto& feat : phi_selected)
+        feat_lst.append<ModelNode>(ModelNode(feat->d_mat_ind(), feat->rung(), feat->expr(), feat->postfix_expr(), feat->value(), feat->test_value(), feat->unit()));
+    return feat_lst;
+}
\ No newline at end of file
diff --git a/src/python/feature_creation/node_utils.hpp b/src/python/feature_creation/node_utils.hpp
new file mode 100644
index 00000000..5d3d7e9a
--- /dev/null
+++ b/src/python/feature_creation/node_utils.hpp
@@ -0,0 +1,28 @@
+/** @file python/feature_creation/node_utils.hpp
+ *  @brief python binding functions for node utilities
+ *
+ *  @author Thomas A. R. Purcell (tpurcell)
+ *  @bug No known bugs.
+ */
+#ifndef NODE_UTILS_PY
+#define NODE_UTILS_PY
+
+#include <feature_creation/node/utils.hpp>
+
+namespace py = boost::python;
+
+namespace str2node
+{
+    /**
+     * @brief Convert a feature_space/selected_features.txt into a phi_selected;
+     * @details Read in the file to get the postfix expressions and regenerate the selected features using phi_0
+     *
+     * @param filename The name of the feature_space/selected_features.txt file
+     * @param phi_0 The initial feature space
+     *
+     * @return The selected feature set from the file as a python file
+     */
+    py::list phi_selected_from_file_py(std::string filename, py::list phi_0);
+}
+
+#endif
\ No newline at end of file
diff --git a/test/sisso.json b/test/sisso.json
index 1011b74c..51cc8128 100644
--- a/test/sisso.json
+++ b/test/sisso.json
@@ -7,7 +7,7 @@
     "max_abs_feat_val": 1e5,
     "data_file": "data.csv",
     "property_key": "energy_diff",
-    "leave_out_frac": 0.05,
+    "leave_out_frac": 0.0,
     "n_rung_generate": 0,
     "n_rung_store": 1,
     "leave_out_inds": [],
-- 
GitLab