Skip to content
Open
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
133 changes: 133 additions & 0 deletions Python/sample_timetravel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import sys
import os

# Adjust path to find pyecosystem module
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.join(current_dir, '..')
# Path based on typical structure seen in other sample files like Python/RandomTests/sanity_test.py
# Assumes 'root_dir' is the project root, and the script is in <project_root>/Python/
py_module_path = os.path.join(root_dir, 'simulation', 'build', 'python')

if not os.path.exists(py_module_path):
print(f"Error: Python module path does not exist: {py_module_path}")
print("Please ensure the simulation has been built (e.g., using CMake and make).")
sys.exit(1)

sys.path.insert(1, py_module_path)

try:
import pyecosystem as pyeco
except ImportError:
print(f"Error: Could not import 'pyecosystem' module from {py_module_path}.")
print("Make sure the module is correctly built and the path is correct.")
sys.exit(1)

def run_time_travel_test():
print("Initializing simulation with database recording enabled (gods_eye=True)...")
sim = pyeco.pyecosystem(True) # True enables gods_eye for database saving

# It's good practice to ensure the database is clean for a fresh test run.
# clean_slate() in C++ clears the DB if gods_eye is true.
# The pyecosystem wrapper for God::cleanSlate() is not explicitly defined in the provided snippets,
# but assuming it exists or that a new God instance with gods_eye=true starts fresh.
# Let's assume God's constructor or a new session ensures a clean relevant state for snapshots for this year,
# or that an explicit call to a wrapper for clean_slate() would be here.
# For now, we rely on the fact that `God::happy_new_year` saves data for the *current* year,
# and `God::load_snapshot` loads by year. A truly clean DB would require `db->clear_database()`.
# The `God` constructor does not clear the database. `God::cleanSlate()` does.
# Let's assume `pyecosystem` should have a `clean_slate` method.
# Based on `simulation/python/ecosystem.cpp`, `pyecosystem` does not have `clean_slate`.
# This is a potential issue for making the test repeatable if the DB isn't cleared.
# However, `God::load_snapshot` loads a specific year, and `God::happy_new_year` writes for current year.
# For this test, we'll proceed. If `clean_slate` is needed, it should be added to `pyecosystem`.
print("Note: Test assumes either a clean database or that snapshot years won't collide unexpectedly.")

# Define and Spawn Species
species_full_name = "animal/deer"
initial_population = 30 # Reduced for faster testing
initial_age = 10

# sim.reset_species(species_full_name) # This method was in the snippet, implies it's available.
# Let's check if reset_species is what we need. It calls God::reset_species which loads from json.
# For a simple spawn, we might not need reset_species if constants are already default.
# The `spawn_organism` in `pyecosystem` takes full_species_name, age, monitor.
# It does not take a count. We need to loop.

print(f"Spawning initial population of {initial_population} for {species_full_name}...")
for _ in range(initial_population):
sim.spawn_organism(species_full_name, initial_age, False, "") # name can be empty
print(f"Initial population for {species_full_name} spawned.")

# Run Simulation for an initial period (e.g., 10-20 years)
initial_run_years = 15 # Reduced for faster testing
print(f"Running simulation for {initial_run_years} years to create snapshots...")
for i in range(initial_run_years):
current_year_before_hny = sim.get_current_year()
sim.happy_new_year(False) # Log progress: False to reduce verbosity for now
print(f"Year {sim.get_current_year()} complete (was {current_year_before_hny}). Snapshots should be saved.")

current_sim_year = sim.get_current_year()
print(f"Initial simulation run complete. Current year: {current_sim_year}")

# List Available Snapshots
print("Fetching available snapshots...")
available_snapshots = sim.get_list_of_available_snapshots()

if not available_snapshots:
print("Error: No snapshots found! Please ensure:")
print("1. The `RAW_WORLD BLOB` column was manually added to the `ECOSYSTEM_MASTER` table in the database.")
print("2. Database saving is working correctly.")
print("3. The simulation ran for enough years to save snapshots.")
sys.exit(1)
print(f"Available snapshot years: {available_snapshots}")

# Load a Snapshot
load_success = False
year_to_load = -1

if available_snapshots:
# Try to load a year from the middle, but ensure it's not the most recent year
# to see a change.
if len(available_snapshots) > 1 and available_snapshots[-1] == current_sim_year:
# Prefer a year that is not the current one, if possible
target_index = max(0, len(available_snapshots) // 2 -1)
year_to_load = available_snapshots[target_index]
else:
year_to_load = available_snapshots[len(available_snapshots) // 2]

print(f"Attempting to load snapshot for year: {year_to_load}...")
load_success = sim.load_snapshot_from_year(year_to_load)

if load_success:
print(f"Successfully loaded snapshot for year {year_to_load}.")
loaded_year = sim.get_current_year()
print(f"Current simulation year after loading: {loaded_year}")
if loaded_year != year_to_load:
print(f"Error: Year after loading ({loaded_year}) does not match target year ({year_to_load})!")
sys.exit(1)
else:
print(f"Failed to load snapshot for year {year_to_load}.")
print("This could be due to issues with reading the snapshot or data integrity.")
sys.exit(1)
else:
print("Skipping snapshot load test as no snapshots were found (should have exited already).")


# Run Simulation for a few more years from the restored state
if load_success:
further_run_years = 5
print(f"Running simulation for {further_run_years} more years from restored state (year {sim.get_current_year()})...")
for i in range(further_run_years):
sim.happy_new_year(False) # Log progress: False
print(f"Year {sim.get_current_year()} complete after restore.")
print(f"Simulation complete. Final year: {sim.get_current_year()}")
expected_final_year = year_to_load + further_run_years
if sim.get_current_year() != expected_final_year:
print(f"Error: Final year ({sim.get_current_year()}) does not match expected year ({expected_final_year}) after running from snapshot!")
sys.exit(1)
print("Time travel test appears successful!")
else:
print("Skipping further simulation run as snapshot loading failed or was skipped.")

if __name__ == "__main__":
run_time_travel_test()
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@

<br>

### Time Travel (Snapshot Rollback)
The simulation now supports saving snapshots of the world state each year when database recording is enabled (via the `gods_eye` option). You can list available snapshots and revert the simulation to a previously saved year to explore different evolutionary paths or recover from unexpected simulation outcomes. See the `Python/sample_timetravel.py` script for an example of how to use this feature.

**Note:** This feature requires the `RAW_WORLD BLOB` column to be manually added to the `ECOSYSTEM_MASTER` table in `data/ecosystem_master.db`. Refer to project documentation or migration scripts if available for details on updating your database schema.

<br>

<div align="center">
<h2>Quick Navigation</h2>

Expand Down
2 changes: 2 additions & 0 deletions simulation/include/database_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ struct DatabaseManager

std::vector<std::vector<ByteArray>> read_all_rows();
void insert_rows(const std::vector<std::vector<FBufferView>> &);
std::vector<ByteArray> read_row_by_year(int year);
std::vector<int> get_available_years();

/******************************
* Miscellaneous operations *
Expand Down
1 change: 1 addition & 0 deletions simulation/include/god.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class God

void cleanSlate();
void happy_new_year(const bool &log = false);
bool load_snapshot(int year);

protected:
double killer_function(const double &, const double &) const;
Expand Down
29 changes: 28 additions & 1 deletion simulation/python/ecosystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,30 @@ struct pyecosystem
{
return guru_nanak->get_live_data();
}

bool load_snapshot_from_year(int year) {
if (guru_nanak) {
return guru_nanak->load_snapshot(year);
}
// Consider logging an error if guru_nanak is null
return false;
}

std::vector<int> get_list_of_available_snapshots() {
if (guru_nanak && guru_nanak->db) {
return guru_nanak->db->get_available_years();
}
// Consider logging an error if guru_nanak or db is null
return {};
}

int get_current_year() {
if (guru_nanak) {
return guru_nanak->year; // Accessing public member 'year' of God class
}
// Consider logging an error or throwing an exception if guru_nanak is null
return 0;
}
};

PYBIND11_MODULE(pyecosystem, m)
Expand All @@ -88,5 +112,8 @@ PYBIND11_MODULE(pyecosystem, m)
.def("happy_new_year", &pyecosystem::happy_new_year)
.def("remember_species", &pyecosystem::remember_species)
.def("get_annual_data", &pyecosystem::get_annual_data)
.def("get_live_data", &pyecosystem::get_live_data);
.def("get_live_data", &pyecosystem::get_live_data)
.def("load_snapshot_from_year", &pyecosystem::load_snapshot_from_year, "Loads a simulation snapshot from a specific year.")
.def("get_list_of_available_snapshots", &pyecosystem::get_list_of_available_snapshots, "Returns a list of years for which snapshots are available.")
.def("get_current_year", &pyecosystem::get_current_year, "Gets the current simulation year.");
}
163 changes: 143 additions & 20 deletions simulation/src/database_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <string>
#include <unordered_map>
#include <utility>
#include <algorithm> // Required for std::sort
#include <vector> // Required for std::vector

static std::vector<std::string> items;

Expand Down Expand Up @@ -38,29 +40,72 @@ void DatabaseManager::end_transaction() {

void DatabaseManager::insert_rows(
const std::vector<std::vector<FBufferView>> &rows) {
for (const auto &row : rows) {
auto &avg_world = row[0];
auto &population = row[1];
std::string sql_command = fmt::format(
"INSERT INTO ECOSYSTEM_MASTER VALUES ({}, ZEROBLOB({}), "
"ZEROBLOB({}))",
Ecosystem::GetWorld(avg_world.data)->year(), avg_world.size,
population.size);
sqlite3_exec(db, sql_command.c_str(), nullptr, 0, nullptr);
for (const auto &row_views : rows) { // Renamed 'row' to 'row_views' for clarity
if (row_views.empty()) continue;

const auto &avg_world_view = row_views[0];
const auto &population_view = row_views[1];

// Determine year from avg_world_view, assuming it's a valid World flatbuffer
// This part is a bit risky if avg_world_view.data is not a valid World buffer.
// However, Ecosystem::GetWorld is used elsewhere, so we follow the pattern.
// A more robust way might be to pass the year explicitly to insert_rows.
int year = Ecosystem::GetWorld(avg_world_view.data)->year();

std::string sql_command;
if (row_views.size() >= 3) {
const auto &raw_world_view = row_views[2];
sql_command = fmt::format(
"INSERT INTO ECOSYSTEM_MASTER (YEAR, AVG_WORLD, POPULATION_WORLD, RAW_WORLD) VALUES ({}, ZEROBLOB({}), ZEROBLOB({}), ZEROBLOB({}))",
year, avg_world_view.size, population_view.size, raw_world_view.size);
} else {
sql_command = fmt::format(
"INSERT INTO ECOSYSTEM_MASTER (YEAR, AVG_WORLD, POPULATION_WORLD) VALUES ({}, ZEROBLOB({}), ZEROBLOB({}))",
year, avg_world_view.size, population_view.size);
}

int rc = sqlite3_exec(db, sql_command.c_str(), nullptr, 0, nullptr);
if (rc != SQLITE_OK) {
std::cerr << "Failed to execute insert statement: " << sqlite3_errmsg(db) << std::endl;
// Consider how to handle this error; maybe throw an exception or return a status
continue;
}

auto last_insert_row = sqlite3_last_insert_rowid(db);

sqlite3_blob *avgBlob = 0;
sqlite3_blob_open(db, "main", "ECOSYSTEM_MASTER", "AVG_WORLD",
last_insert_row, 1, &avgBlob);
sqlite3_blob_write(avgBlob, avg_world.data, avg_world.size, 0);
sqlite3_blob_close(avgBlob);

sqlite3_blob *populationBlob = 0;
sqlite3_blob_open(db, "main", "ECOSYSTEM_MASTER", "POPULATION_WORLD",
last_insert_row, 1, &populationBlob);
sqlite3_blob_write(populationBlob, population.data, population.size, 0);
sqlite3_blob_close(populationBlob);
sqlite3_blob *blob = nullptr;

// Write AVG_WORLD
rc = sqlite3_blob_open(db, "main", "ECOSYSTEM_MASTER", "AVG_WORLD", last_insert_row, 1, &blob);
if (rc != SQLITE_OK) {
std::cerr << "Failed to open blob for AVG_WORLD: " << sqlite3_errmsg(db) << std::endl;
continue;
}
sqlite3_blob_write(blob, avg_world_view.data, avg_world_view.size, 0);
sqlite3_blob_close(blob);

// Write POPULATION_WORLD
rc = sqlite3_blob_open(db, "main", "ECOSYSTEM_MASTER", "POPULATION_WORLD", last_insert_row, 1, &blob);
if (rc != SQLITE_OK) {
std::cerr << "Failed to open blob for POPULATION_WORLD: " << sqlite3_errmsg(db) << std::endl;
continue;
}
sqlite3_blob_write(blob, population_view.data, population_view.size, 0);
sqlite3_blob_close(blob);

// Write RAW_WORLD if provided
if (row_views.size() >= 3) {
const auto &raw_world_view = row_views[2];
if (raw_world_view.size > 0) { // Only write if there's data
rc = sqlite3_blob_open(db, "main", "ECOSYSTEM_MASTER", "RAW_WORLD", last_insert_row, 1, &blob);
if (rc != SQLITE_OK) {
std::cerr << "Failed to open blob for RAW_WORLD: " << sqlite3_errmsg(db) << std::endl;
continue;
}
sqlite3_blob_write(blob, raw_world_view.data, raw_world_view.size, 0);
sqlite3_blob_close(blob);
}
}
}
}

Expand Down Expand Up @@ -105,3 +150,81 @@ void DatabaseManager::clear_database() {
std::string sql_command = "DELETE FROM ECOSYSTEM_MASTER;";
sqlite3_exec(db, sql_command.c_str(), nullptr, 0, nullptr);
}

std::vector<ByteArray> DatabaseManager::read_row_by_year(int year) {
std::vector<ByteArray> row_data_list; // Changed name for clarity
sqlite3_stmt *stmt;
// Select RAW_WORLD as the third column.
// Older rows might have NULL for RAW_WORLD if the column was added later.
std::string sql = "SELECT AVG_WORLD, POPULATION_WORLD, RAW_WORLD FROM ECOSYSTEM_MASTER WHERE YEAR = ?;";

if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
std::cerr << "Failed to prepare statement: " << sqlite3_errmsg(db) << std::endl;
return row_data_list; // Return empty vector on error
}

sqlite3_bind_int(stmt, 1, year);

int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
row_data_list.resize(3); // Expecting 3 blobs now

// Read AVG_WORLD blob (column 0)
const void *avg_blob_ptr = sqlite3_column_blob(stmt, 0);
int avg_blob_size = sqlite3_column_bytes(stmt, 0);
if (avg_blob_ptr && avg_blob_size > 0) {
row_data_list[0].resize(avg_blob_size);
memcpy(row_data_list[0].data(), avg_blob_ptr, avg_blob_size);
}

// Read POPULATION_WORLD blob (column 1)
const void *pop_blob_ptr = sqlite3_column_blob(stmt, 1);
int pop_blob_size = sqlite3_column_bytes(stmt, 1);
if (pop_blob_ptr && pop_blob_size > 0) {
row_data_list[1].resize(pop_blob_size);
memcpy(row_data_list[1].data(), pop_blob_ptr, pop_blob_size);
}

// Read RAW_WORLD blob (column 2)
// Check for SQLITE_NULL before trying to read blob data, as older rows might not have this column filled.
if (sqlite3_column_type(stmt, 2) != SQLITE_NULL) {
const void *raw_blob_ptr = sqlite3_column_blob(stmt, 2);
int raw_blob_size = sqlite3_column_bytes(stmt, 2);
if (raw_blob_ptr && raw_blob_size > 0) {
row_data_list[2].resize(raw_blob_size);
memcpy(row_data_list[2].data(), raw_blob_ptr, raw_blob_size);
}
} else {
// RAW_WORLD is NULL, row_data_list[2] will remain an empty ByteArray by default.
}

} else if (rc == SQLITE_DONE) {
// No data found for the year
std::cout << "No data found for year: " << year << std::endl;
// Return an empty vector, which God::load_snapshot should handle.
} else {
std::cerr << "Failed to execute statement: " << sqlite3_errmsg(db) << std::endl;
// Return an empty vector on error.
}

sqlite3_finalize(stmt);
return row_data_list;
}

std::vector<int> DatabaseManager::get_available_years() {
std::vector<int> years;
sqlite3_stmt *stmt;
std::string sql = "SELECT DISTINCT YEAR FROM ECOSYSTEM_MASTER ORDER BY YEAR;";

if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
std::cerr << "Failed to prepare statement: " << sqlite3_errmsg(db) << std::endl;
return years; // Return empty vector on error
}

while (sqlite3_step(stmt) == SQLITE_ROW) {
years.push_back(sqlite3_column_int(stmt, 0));
}

sqlite3_finalize(stmt);
return years;
}
Loading
Loading