Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ authors:
repository-code: 'https://github.com/Grufoony/DynamicalSystemFramework'
url: 'https://grufoony.github.io/DynamicalSystemFramework/'
license: AGPL-3.0-only
version: 6.5.6
version: 6.5.7
date-released: '2026-07-20'
7 changes: 7 additions & 0 deletions src/dsf/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,13 @@ PYBIND11_MODULE(dsf_cpp, m) {
pybind11::arg("fileName"),
R"doc(Import traffic light configurations from a file.

Args:
fileName (str): Path to the configuration file.)doc")
.def("exportTrafficLights",
&dsf::mobility::RoadNetwork::exportTrafficLights,
pybind11::arg("fileName"),
R"doc(Export traffic light configurations to a file.

Args:
fileName (str): Path to the configuration file.)doc")
.def(
Expand Down
2 changes: 1 addition & 1 deletion src/dsf/dsf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

static constexpr uint8_t DSF_VERSION_MAJOR = 6;
static constexpr uint8_t DSF_VERSION_MINOR = 5;
static constexpr uint8_t DSF_VERSION_PATCH = 6;
static constexpr uint8_t DSF_VERSION_PATCH = 7;

static auto const DSF_VERSION =
std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH);
Expand Down
42 changes: 42 additions & 0 deletions src/dsf/mobility/RoadNetwork.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,48 @@ namespace dsf::mobility {
}
}

void RoadNetwork::exportTrafficLights(std::string_view const fileName) const {
std::ofstream file{std::string(fileName)};
if (!file.is_open()) {
throw std::runtime_error("Error opening file \"" + std::string(fileName) +
"\" for writing.");
}

file << "id;sourceId;cycleTime;greenTime\n";

for (auto const& [nodeId, pNode] : m_nodes) {
if (!pNode->isTrafficLight()) {
continue;
}
auto const& tl = static_cast<TrafficLight const&>(*pNode);

auto const cycleTime = tl.cycleTime();
if (cycleTime == 0) {
spdlog::warn("exportTrafficLights: {} has no phases — skipping.", tl);
continue;
}

std::unordered_map<Id, Delay> greenTimePerStreet;
for (auto const& phase : tl.phases()) {
for (auto const& [streetId, dirs] : phase.greenSet()) {
greenTimePerStreet[streetId] += phase.duration();
}
}
std::vector<std::pair<Id, Delay>> sortedRows(greenTimePerStreet.begin(),
greenTimePerStreet.end());
std::sort(sortedRows.begin(), sortedRows.end(), [](auto const& a, auto const& b) {
return a.first < b.first;
});

for (auto const& [streetId, greenTime] : sortedRows) {
file << std::format(
"{};{};{};{}\n", nodeId, edge(streetId).source(), cycleTime, greenTime);
}
}

spdlog::debug("exportTrafficLights: wrote traffic light data to \"{}\".", fileName);
}

TrafficLight& RoadNetwork::makeTrafficLight(Id const nodeId) {
auto& pNode = m_nodes.at(nodeId);
pNode = std::make_unique<TrafficLight>(*pNode);
Expand Down
2 changes: 2 additions & 0 deletions src/dsf/mobility/RoadNetwork.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ namespace dsf::mobility {

PathCollection allEdgePathsTo(Id const targetEdgeId) const final;

void exportTrafficLights(std::string_view const fileName) const;

/// @brief Export the graph's edges and nodes to two CSV files in the specified folder
/// @param folder The folder to export the files to
void exportCSV(std::string_view const folder) const;
Expand Down
133 changes: 133 additions & 0 deletions test/mobility/Test_graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,139 @@ TEST_CASE("RoadNetwork") {
std::filesystem::remove(csvPath);
}
}
SUBCASE("exportTrafficLights") {
GIVEN("A graph object with a two-phase traffic light") {
RoadNetwork graph;
graph.addNDefaultNodes(4);
graph.addStreets(Street{100, std::make_pair(0, 1), 50., 13.8888888889},
Street{101, std::make_pair(2, 1), 50., 13.8888888889},
Street{102, std::make_pair(3, 1), 50., 13.8888888889});

graph.makeTrafficLight(1);
auto& tl = graph.node<TrafficLight>(1);

TrafficLightPhase phase0{30};
phase0.addGreen(100); // Direction::ANY
TrafficLightPhase phase1{60};
phase1.addGreen(101); // Direction::ANY

tl.setPhases({phase0, phase1});

auto const csvPath =
std::filesystem::temp_directory_path() / "dsf_export_traffic_lights.csv";

WHEN("We export the traffic light definition") {
graph.exportTrafficLights(csvPath.string());

THEN(
"The CSV contains one row per green street with the right cycle/green "
"times") {
std::ifstream in{csvPath};
REQUIRE(in.is_open());

std::string header;
std::getline(in, header);
CHECK_EQ(header, "id;sourceId;cycleTime;greenTime");

std::unordered_map<Id, std::pair<Delay, Delay>>
rows; // sourceId -> {cycleTime, greenTime}
std::string line;
std::size_t nRows{0};
while (std::getline(in, line)) {
if (line.empty())
continue;
++nRows;
std::istringstream iss{line};
std::string strId, strSource, strCycle, strGreen;
std::getline(iss, strId, ';');
std::getline(iss, strSource, ';');
std::getline(iss, strCycle, ';');
std::getline(iss, strGreen, '\n');

CHECK_EQ(static_cast<Id>(std::stoul(strId)), 1);
auto const sourceId = static_cast<Id>(std::stoul(strSource));
rows[sourceId] = {static_cast<Delay>(std::stoul(strCycle)),
static_cast<Delay>(std::stoul(strGreen))};
}

CHECK_EQ(nRows, 2);
REQUIRE(rows.contains(0));
REQUIRE(rows.contains(2));
CHECK_EQ(rows.at(0).first, 90); // cycleTime
CHECK_EQ(rows.at(0).second, 30); // greenTime (phase0)
CHECK_EQ(rows.at(2).first, 90);
CHECK_EQ(rows.at(2).second, 60); // greenTime (phase1)

CHECK_FALSE(rows.contains(3)); // street 102 not green in any phase
}

THEN(
"The CSV contains one row per green street with the right cycle/green "
"times") {
std::ifstream in{csvPath};
REQUIRE(in.is_open());

std::string header;
std::getline(in, header);
CHECK_EQ(header, "id;sourceId;cycleTime;greenTime");

std::unordered_map<Id, std::pair<Delay, Delay>> rows;
std::string line;
std::size_t nRows{0};
while (std::getline(in, line)) {
if (line.empty())
continue;
++nRows;
std::istringstream iss{line};
std::string strId, strSource, strCycle, strGreen;
std::getline(iss, strId, ';');
std::getline(iss, strSource, ';');
std::getline(iss, strCycle, ';');
std::getline(iss, strGreen, '\n');

CHECK_EQ(static_cast<Id>(std::stoul(strId)), 1);
auto const sourceId = static_cast<Id>(std::stoul(strSource));
rows[sourceId] = {static_cast<Delay>(std::stoul(strCycle)),
static_cast<Delay>(std::stoul(strGreen))};
}

CHECK_EQ(nRows, 2);
REQUIRE(rows.contains(0));
REQUIRE(rows.contains(2));
CHECK_EQ(rows.at(0).first, 90);
CHECK_EQ(rows.at(0).second, 30);
CHECK_EQ(rows.at(2).first, 90);
CHECK_EQ(rows.at(2).second, 60);

CHECK_FALSE(rows.contains(3));
}

THEN("Re-importing the exported CSV reconstructs an equivalent traffic light") {
RoadNetwork roundTrip;
roundTrip.addNDefaultNodes(4);
roundTrip.addStreets(Street{100, std::make_pair(0, 1), 50., 13.8888888889},
Street{101, std::make_pair(2, 1), 50., 13.8888888889},
Street{102, std::make_pair(3, 1), 50., 13.8888888889});

roundTrip.importTrafficLights(csvPath.string());
auto& tl2 = roundTrip.node<TrafficLight>(1);

CHECK_EQ(tl2.phases().size(), 2);
CHECK_EQ(tl2.cycleTime(), tl.cycleTime());

// Only phase 0 is active by default — check its contents directly
// rather than assuming both streets are simultaneously green.
CHECK(tl2.phases()[0].containsGreen(100, dsf::Direction::ANY));
CHECK(tl2.phases()[1].containsGreen(101, dsf::Direction::ANY));
CHECK(tl2.isGreen(100, dsf::Direction::ANY)); // active phase
CHECK_FALSE(tl2.isGreen(101, dsf::Direction::ANY)); // not active yet
CHECK_FALSE(tl2.isGreen(102, dsf::Direction::RIGHT));
}
}

std::filesystem::remove(csvPath);
}
}

SUBCASE("importEdges and importNodeProperties") {
GIVEN("A graph object") {
Expand Down
Loading