Skip to content

Commit

Permalink
fix: Allow precomputed flow for state networks
Browse files Browse the repository at this point in the history
  • Loading branch information
danieledler committed Apr 25, 2023
1 parent b1b750e commit ab7c5b5
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 42 deletions.
4 changes: 4 additions & 0 deletions src/core/StateNetwork.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class StateNetwork {
double m_totalLinkWeightIgnored = 0.0;
std::map<unsigned int, double> m_outWeights;
bool m_haveNodeWeights = false;
bool m_haveStateNodeWeights = false;
bool m_haveFileInput = false;
// Attributes
std::map<unsigned int, std::string> m_names;
std::map<unsigned int, PhysNode> m_physNodes;
Expand Down Expand Up @@ -177,6 +179,8 @@ class StateNetwork {
std::map<unsigned int, std::string>& names() { return m_names; }
const std::map<unsigned int, std::string>& names() const { return m_names; }
bool haveNodeWeights() const { return m_haveNodeWeights; }
bool haveStateNodeWeights() const { return m_haveStateNodeWeights; }
bool haveFileInput() const { return m_haveFileInput; }

virtual const std::map<unsigned int, std::vector<int>>& metaData() const = 0;

Expand Down
2 changes: 1 addition & 1 deletion src/io/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ Config::Config(const std::string& flags, bool isCLI) : isCLI(isCLI)
} else if (flowModelArg == "precomputed") {
setFlowModel(FlowModel::precomputed);
} else if (!flowModelArg.empty()) {
throw std::runtime_error("Unrecognized flow model");
throw std::runtime_error(io::Str() << "Unrecognized flow model: '" << flowModelArg << "'");
}

if (regularized) {
Expand Down
7 changes: 6 additions & 1 deletion src/io/Network.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ void Network::parseNetwork(const std::string& filename)

void Network::parseNetwork(const std::string& filename, const InsensitiveStringSet& validHeadings, const InsensitiveStringSet& ignoreHeadings, const std::string& startHeading)
{
m_haveFileInput = true;
SafeInFile input(filename);

// Parse standard links by default until possible heading is reached
Expand Down Expand Up @@ -450,7 +451,11 @@ void Network::parseStateNode(const std::string& line, StateNetwork::StateNode& s
m_extractor.seekg(nameEnd + 1);
}
// Optional weight, default to 1.0
(m_extractor >> stateNode.weight) || (stateNode.weight = 1.0);
if ((m_extractor >> stateNode.weight)) {
m_haveStateNodeWeights = true;
if (stateNode.weight < 0)
throw std::runtime_error(io::Str() << "Negative state node weight (" << stateNode.weight << ") from line '" << line << "'");
}
}

void Network::parseLink(const std::string& line, unsigned int& n1, unsigned int& n2, double& weight)
Expand Down
12 changes: 9 additions & 3 deletions src/utils/FlowCalculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,15 @@ void FlowCalculator::usePrecomputedFlow(const StateNetwork& network, const Confi
Log() << "\n -> Using directed links with precomputed flow from input data.";
Log() << "\n -> Total link flow: " << sumLinkWeight << ".";

if (config.isCLI && !network.haveNodeWeights()) {
Log() << std::endl;
throw std::runtime_error("Missing node flow in input data. Should be passed as a third field under a *Vertices section.");
if (network.haveFileInput()) {
if (network.haveMemoryInput() && !network.haveStateNodeWeights()) {
Log() << std::endl;
throw std::runtime_error("Missing node flow in input data. Should be passed as a third field under a *States section.");
}
if (!network.haveMemoryInput() && !network.haveNodeWeights()) {
Log() << std::endl;
throw std::runtime_error("Missing node flow in input data. Should be passed as a third field under a *Vertices section.");
}
}

// Treat the link weights as flow
Expand Down
29 changes: 29 additions & 0 deletions test/data/multilayer.net
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# A multilayer network using explicit format
# Figure 3
*Vertices 5
# node_id name
1 "i"
2 "j"
3 "k"
4 "l"
5 "m"
*Multilayer
# layer_id node_id layer_id node_id weight
# intra
1 1 1 4 0.8
1 4 1 1 1
1 1 1 5 0.8
1 5 1 1 1
1 4 1 5 1
1 5 1 4 1
2 1 2 2 0.8
2 2 2 1 1
2 1 2 3 0.8
2 3 2 1 1
2 2 2 3 1
2 3 2 2 1
# inter
1 1 2 2 0.2
1 1 2 3 0.2
2 1 1 4 0.2
2 1 1 5 0.2
35 changes: 35 additions & 0 deletions test/data/states.net
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# A network in state format
# Figure 6
*Vertices 5
#node_id name
1 "i"
2 "j"
3 "k"
4 "l"
5 "m"
*States
#state_id node_id name
1 1 "α~_i"
2 2 "β~_j"
3 3 "γ~_k"
4 1 "δ~_i"
5 4 "ε~_l"
6 5 "ζ~_m"
*Links
#source target weight
1 2 0.8
1 3 0.8
1 5 0.2
1 6 0.2
2 1 1
2 3 1
3 1 1
3 2 1
4 5 0.8
4 6 0.8
4 2 0.2
4 3 0.2
5 4 1
5 6 1
6 4 1
6 5 1
35 changes: 35 additions & 0 deletions test/data/states_flow.net
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# A network in state format
# Figure 6
*Vertices 5
#node_id name
1 "i" 0.4
2 "j" 0.15
3 "k" 0.15
4 "l" 0.15
5 "m" 0.15
*States
#state_id node_id name
1 1 "α~_i" 0.1
2 2 "β~_j" 0.1
3 3 "γ~_k" 0.3
4 1 "δ~_i" 0.3
5 4 "ε~_l" 0.1
6 5 "ζ~_m" 0.1
*Links
#source target weight
1 2 0.08
1 3 0.08
1 5 0.02
1 6 0.02
2 1 0.1
2 3 0.1
3 1 0.1
3 2 0.1
4 5 0.08
4 6 0.08
4 2 0.02
4 3 0.02
5 4 0.1
5 6 0.1
6 4 0.1
6 5 0.1
29 changes: 29 additions & 0 deletions test/data/states_flow_output.net
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# v2.7.0
# ./Infomap --silent --flow-model precomputed
# State network as physical network
*Vertices
#id name flow
1 "1" 0.1
2 "2" 0.1
3 "3" 0.3
4 "4" 0.3
5 "5" 0.1
6 "6" 0.1
*Arcs
#source target flow
1 2 0.08
1 3 0.08
1 5 0.02
1 6 0.02
2 1 0.1
2 3 0.1
3 1 0.1
3 2 0.1
4 2 0.02
4 3 0.02
4 5 0.08
4 6 0.08
5 4 0.1
5 6 0.1
6 4 0.1
6 5 0.1
15 changes: 15 additions & 0 deletions test/data/twotriangles_flow.net
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*Vertices
1 "A" 0.1
2 "B" 0.2
3 "C" 0.3
4 "D" 0.2
5 "E" 0.1
6 "F" 0.1
*Edges
1 2 0.1
1 3 0.2
2 3 0.3
3 4 0.1
4 5 0.1
4 6 0.1
5 6 0.1
19 changes: 19 additions & 0 deletions test/data/twotriangles_flow_output.net
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# v2.7.0
# ./Infomap --silent --flow-model precomputed
*Vertices
#id name flow
1 "A" 0.1
2 "B" 0.2
3 "C" 0.3
4 "D" 0.2
5 "E" 0.1
6 "F" 0.1
*Arcs
#source target flow
1 2 0.1
1 3 0.2
2 3 0.3
3 4 0.1
4 5 0.1
4 6 0.1
5 6 0.1
39 changes: 39 additions & 0 deletions test/test_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from infomap import Infomap
import re
from pathlib import Path
import filecmp
import pytest


def test_precomputed_requirements():
im = Infomap(silent=True, flow_model="precomputed")
im.read_file("examples/networks/twotriangles.net")
with pytest.raises(RuntimeError, match=re.escape("Missing node flow in input data. Should be passed as a third field under a *Vertices section.")):
im.run()


def test_precomputed_states_requirements():
im = Infomap(silent=True, flow_model="precomputed")
im.read_file("examples/networks/states.net")
with pytest.raises(RuntimeError, match=re.escape("Missing node flow in input data. Should be passed as a third field under a *States section.")):
im.run()


def test_precomputed_from_file():
im = Infomap(silent=True, flow_model="precomputed")
im.read_file("test/data/twotriangles_flow.net")
im.run()
output_path = Path("test/data/output/twotriangles_flow_output.net")
output_path.parent.mkdir(exist_ok=True)
im.write_pajek(str(output_path), flow=True)
assert (filecmp.cmp(output_path, "test/data/twotriangles_flow_output.net"))


def test_precomputed_states_from_file():
im = Infomap(silent=True, flow_model="precomputed")
im.read_file("test/data/states_flow.net")
im.run()
output_path = Path("test/data/output/states_flow_output.net")
output_path.parent.mkdir(exist_ok=True)
im.write_pajek(str(output_path), flow=True)
assert (filecmp.cmp(output_path, "test/data/states_flow_output.net"))
13 changes: 8 additions & 5 deletions test/test_iterators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from infomap import Infomap
from operator import itemgetter
import pytest


def test_iter_physical_on_physical():
Expand All @@ -10,6 +11,7 @@ def test_iter_physical_on_physical():
modules = sorted([(node.node_id, node.module_id) for node in im.physical_nodes], key=itemgetter(0))
assert modules == [(1, 1), (2, 1), (3, 1), (4, 2), (5, 2), (6, 2)]


def test_iter_physical_on_states():
im = Infomap(num_trials=10, silent=True)
im.read_file("examples/networks/states.net")
Expand All @@ -20,30 +22,31 @@ def test_iter_physical_on_states():


def test_iter_physical_reliability():

for _ in range(100):
im = Infomap(num_trials=10, silent=True)
im.read_file("examples/networks/states.net")
im.run()

modules = [(node.node_id, node.module_id) for node in im.physical_nodes]
assert modules == [(1, 1), (2, 1), (3, 1), (1, 2), (4, 2), (5, 2)]


def test_multilevel_modules_on_states():
im = Infomap(silent=True)
im.read_file("examples/networks/states.net")
im.run()
modules = [(node, modules) for node, modules in im.get_multilevel_modules(states=True).items()]
assert modules == [(1, (1,)), (2, (1,)), (3, (1,)), (4, (2,)), (5, (2,)), (6, (2,))]


def test_multilevel_modules_on_physical():
im = Infomap(silent=True)
im.read_file("examples/networks/states.net")
im.run()
# todo: Use unit test function to assert exception is raised
# RuntimeError: Cannot get multilevel modules on higher-order network without states.
modules = im.get_multilevel_modules(states=False)

with pytest.raises(RuntimeError):
im.get_multilevel_modules(states=False)


if __name__ == "__main__":
Expand Down
Loading

0 comments on commit ab7c5b5

Please sign in to comment.