aegis_sim.recording.lineagerecorder

  1import logging
  2import pathlib
  3
  4import numpy as np
  5
  6from .recorder import Recorder
  7from aegis_sim import variables
  8from aegis_sim.parameterization import parametermanager
  9
 10
 11HEADER = "step,lineage_id,parent_lineage_id\n"
 12DEATHS_HEADER = "step,lineage_id,cause\n"
 13
 14
 15class LineageRecorder(Recorder):
 16    """Write a growing per-birth log to /lineage/births.csv.
 17
 18    One row per individual ever created: (step at which it was born,
 19    its unique lineage_id, its parent's lineage_id). Initial-population
 20    individuals are written at step 0 with parent_lineage_id=-1.
 21
 22    Triggered inline from bioreactor.reproduction() and from the run()
 23    entry point — NOT from the standard end-of-step recorder cycle, because
 24    it needs to see new births as they happen, not snapshots of the alive
 25    population.
 26
 27    LINEAGE_RATE controls flush cadence to disk (every Nth flush call);
 28    every birth is still recorded regardless of the rate.
 29    """
 30
 31    def __init__(self, odir: pathlib.Path):
 32        self.odir = odir / "lineage"
 33        self.init_odir()
 34        self._file_path = self.odir / "births.csv"
 35        self._deaths_path = self.odir / "deaths.csv"
 36        self._file = None
 37        self._deaths_file = None
 38        self._writes_since_flush = 0
 39
 40    def _ensure_open(self):
 41        if self._file is not None:
 42            return
 43        # Write mode the first time (fresh run); open in append mode otherwise.
 44        # We detect a fresh run by checking whether the header is already present.
 45        mode = "a" if self._file_path.exists() and self._file_path.stat().st_size > 0 else "w"
 46        self._file = open(self._file_path, mode)
 47        if mode == "w":
 48            self._file.write(HEADER)
 49
 50    def _ensure_deaths_open(self):
 51        if self._deaths_file is not None:
 52            return
 53        mode = "a" if self._deaths_path.exists() and self._deaths_path.stat().st_size > 0 else "w"
 54        self._deaths_file = open(self._deaths_path, mode)
 55        if mode == "w":
 56            self._deaths_file.write(DEATHS_HEADER)
 57
 58    def write_initial(self, lineage_ids):
 59        if parametermanager.parameters.LINEAGE_RATE <= 0:
 60            return
 61        if lineage_ids is None or len(lineage_ids) == 0:
 62            return
 63        self._ensure_open()
 64        for lid in lineage_ids:
 65            self._file.write(f"0,{int(lid)},-1\n")
 66        self._maybe_flush()
 67        logging.debug(f"lineage initial: wrote {len(lineage_ids)} rows at step 0.")
 68
 69    def write_births(self, parent_lineage_ids, child_lineage_ids, step):
 70        if parametermanager.parameters.LINEAGE_RATE <= 0:
 71            return
 72        if child_lineage_ids is None or len(child_lineage_ids) == 0:
 73            return
 74        assert len(parent_lineage_ids) == len(child_lineage_ids)
 75        self._ensure_open()
 76        parent_arr = np.asarray(parent_lineage_ids, dtype=np.int64)
 77        child_arr = np.asarray(child_lineage_ids, dtype=np.int64)
 78        for pid, cid in zip(parent_arr.tolist(), child_arr.tolist()):
 79            self._file.write(f"{step},{cid},{pid}\n")
 80        self._maybe_flush()
 81
 82    def write_deaths(self, lineage_ids, causeofdeath, step):
 83        if parametermanager.parameters.LINEAGE_RATE <= 0:
 84            return
 85        if lineage_ids is None or len(lineage_ids) == 0:
 86            return
 87        self._ensure_deaths_open()
 88        for lid in np.asarray(lineage_ids, dtype=np.int64).tolist():
 89            self._deaths_file.write(f"{step},{lid},{causeofdeath}\n")
 90        # Reuse the same flush counter as births.
 91        self._maybe_flush()
 92
 93    def _maybe_flush(self):
 94        self._writes_since_flush += 1
 95        rate = parametermanager.parameters.LINEAGE_RATE
 96        if rate > 0 and self._writes_since_flush >= rate:
 97            if self._file is not None:
 98                self._file.flush()
 99            if self._deaths_file is not None:
100                self._deaths_file.flush()
101            self._writes_since_flush = 0
102
103    def close(self):
104        if self._file is not None:
105            self._file.flush()
106            self._file.close()
107            self._file = None
108        if self._deaths_file is not None:
109            self._deaths_file.flush()
110            self._deaths_file.close()
111            self._deaths_file = None
112
113    # Compatibility no-op so the recorder can be safely added to the standard
114    # end-of-step recording cycle if anyone wires it in there (currently we
115    # don't — see module docstring).
116    def write(self, population):
117        return
DEATHS_HEADER = 'step,lineage_id,cause\n'
class LineageRecorder(aegis_sim.recording.recorder.Recorder):
 16class LineageRecorder(Recorder):
 17    """Write a growing per-birth log to /lineage/births.csv.
 18
 19    One row per individual ever created: (step at which it was born,
 20    its unique lineage_id, its parent's lineage_id). Initial-population
 21    individuals are written at step 0 with parent_lineage_id=-1.
 22
 23    Triggered inline from bioreactor.reproduction() and from the run()
 24    entry point — NOT from the standard end-of-step recorder cycle, because
 25    it needs to see new births as they happen, not snapshots of the alive
 26    population.
 27
 28    LINEAGE_RATE controls flush cadence to disk (every Nth flush call);
 29    every birth is still recorded regardless of the rate.
 30    """
 31
 32    def __init__(self, odir: pathlib.Path):
 33        self.odir = odir / "lineage"
 34        self.init_odir()
 35        self._file_path = self.odir / "births.csv"
 36        self._deaths_path = self.odir / "deaths.csv"
 37        self._file = None
 38        self._deaths_file = None
 39        self._writes_since_flush = 0
 40
 41    def _ensure_open(self):
 42        if self._file is not None:
 43            return
 44        # Write mode the first time (fresh run); open in append mode otherwise.
 45        # We detect a fresh run by checking whether the header is already present.
 46        mode = "a" if self._file_path.exists() and self._file_path.stat().st_size > 0 else "w"
 47        self._file = open(self._file_path, mode)
 48        if mode == "w":
 49            self._file.write(HEADER)
 50
 51    def _ensure_deaths_open(self):
 52        if self._deaths_file is not None:
 53            return
 54        mode = "a" if self._deaths_path.exists() and self._deaths_path.stat().st_size > 0 else "w"
 55        self._deaths_file = open(self._deaths_path, mode)
 56        if mode == "w":
 57            self._deaths_file.write(DEATHS_HEADER)
 58
 59    def write_initial(self, lineage_ids):
 60        if parametermanager.parameters.LINEAGE_RATE <= 0:
 61            return
 62        if lineage_ids is None or len(lineage_ids) == 0:
 63            return
 64        self._ensure_open()
 65        for lid in lineage_ids:
 66            self._file.write(f"0,{int(lid)},-1\n")
 67        self._maybe_flush()
 68        logging.debug(f"lineage initial: wrote {len(lineage_ids)} rows at step 0.")
 69
 70    def write_births(self, parent_lineage_ids, child_lineage_ids, step):
 71        if parametermanager.parameters.LINEAGE_RATE <= 0:
 72            return
 73        if child_lineage_ids is None or len(child_lineage_ids) == 0:
 74            return
 75        assert len(parent_lineage_ids) == len(child_lineage_ids)
 76        self._ensure_open()
 77        parent_arr = np.asarray(parent_lineage_ids, dtype=np.int64)
 78        child_arr = np.asarray(child_lineage_ids, dtype=np.int64)
 79        for pid, cid in zip(parent_arr.tolist(), child_arr.tolist()):
 80            self._file.write(f"{step},{cid},{pid}\n")
 81        self._maybe_flush()
 82
 83    def write_deaths(self, lineage_ids, causeofdeath, step):
 84        if parametermanager.parameters.LINEAGE_RATE <= 0:
 85            return
 86        if lineage_ids is None or len(lineage_ids) == 0:
 87            return
 88        self._ensure_deaths_open()
 89        for lid in np.asarray(lineage_ids, dtype=np.int64).tolist():
 90            self._deaths_file.write(f"{step},{lid},{causeofdeath}\n")
 91        # Reuse the same flush counter as births.
 92        self._maybe_flush()
 93
 94    def _maybe_flush(self):
 95        self._writes_since_flush += 1
 96        rate = parametermanager.parameters.LINEAGE_RATE
 97        if rate > 0 and self._writes_since_flush >= rate:
 98            if self._file is not None:
 99                self._file.flush()
100            if self._deaths_file is not None:
101                self._deaths_file.flush()
102            self._writes_since_flush = 0
103
104    def close(self):
105        if self._file is not None:
106            self._file.flush()
107            self._file.close()
108            self._file = None
109        if self._deaths_file is not None:
110            self._deaths_file.flush()
111            self._deaths_file.close()
112            self._deaths_file = None
113
114    # Compatibility no-op so the recorder can be safely added to the standard
115    # end-of-step recording cycle if anyone wires it in there (currently we
116    # don't — see module docstring).
117    def write(self, population):
118        return

Write a growing per-birth log to /lineage/births.csv.

One row per individual ever created: (step at which it was born, its unique lineage_id, its parent's lineage_id). Initial-population individuals are written at step 0 with parent_lineage_id=-1.

Triggered inline from bioreactor.reproduction() and from the run() entry point — NOT from the standard end-of-step recorder cycle, because it needs to see new births as they happen, not snapshots of the alive population.

LINEAGE_RATE controls flush cadence to disk (every Nth flush call); every birth is still recorded regardless of the rate.

LineageRecorder(odir: pathlib.Path)
32    def __init__(self, odir: pathlib.Path):
33        self.odir = odir / "lineage"
34        self.init_odir()
35        self._file_path = self.odir / "births.csv"
36        self._deaths_path = self.odir / "deaths.csv"
37        self._file = None
38        self._deaths_file = None
39        self._writes_since_flush = 0
odir
def write_initial(self, lineage_ids):
59    def write_initial(self, lineage_ids):
60        if parametermanager.parameters.LINEAGE_RATE <= 0:
61            return
62        if lineage_ids is None or len(lineage_ids) == 0:
63            return
64        self._ensure_open()
65        for lid in lineage_ids:
66            self._file.write(f"0,{int(lid)},-1\n")
67        self._maybe_flush()
68        logging.debug(f"lineage initial: wrote {len(lineage_ids)} rows at step 0.")
def write_births(self, parent_lineage_ids, child_lineage_ids, step):
70    def write_births(self, parent_lineage_ids, child_lineage_ids, step):
71        if parametermanager.parameters.LINEAGE_RATE <= 0:
72            return
73        if child_lineage_ids is None or len(child_lineage_ids) == 0:
74            return
75        assert len(parent_lineage_ids) == len(child_lineage_ids)
76        self._ensure_open()
77        parent_arr = np.asarray(parent_lineage_ids, dtype=np.int64)
78        child_arr = np.asarray(child_lineage_ids, dtype=np.int64)
79        for pid, cid in zip(parent_arr.tolist(), child_arr.tolist()):
80            self._file.write(f"{step},{cid},{pid}\n")
81        self._maybe_flush()
def write_deaths(self, lineage_ids, causeofdeath, step):
83    def write_deaths(self, lineage_ids, causeofdeath, step):
84        if parametermanager.parameters.LINEAGE_RATE <= 0:
85            return
86        if lineage_ids is None or len(lineage_ids) == 0:
87            return
88        self._ensure_deaths_open()
89        for lid in np.asarray(lineage_ids, dtype=np.int64).tolist():
90            self._deaths_file.write(f"{step},{lid},{causeofdeath}\n")
91        # Reuse the same flush counter as births.
92        self._maybe_flush()
def close(self):
104    def close(self):
105        if self._file is not None:
106            self._file.flush()
107            self._file.close()
108            self._file = None
109        if self._deaths_file is not None:
110            self._deaths_file.flush()
111            self._deaths_file.close()
112            self._deaths_file = None
def write(self, population):
117    def write(self, population):
118        return