aegis_sim

  1import logging
  2import pathlib
  3
  4from aegis_sim.dataclasses.population import Population
  5from aegis_sim.bioreactor import Bioreactor
  6from aegis_sim import variables, submodels, parameterization
  7from aegis_sim.parameterization import parametermanager
  8from aegis_sim.recording import recordingmanager
  9
 10
 11def run(custom_config_path, pickle_path, overwrite, custom_input_params, resume_path=None, extend_steps=None):
 12    if resume_path is not None:
 13        odir = pathlib.Path(resume_path)
 14        checkpoint_file = odir / "checkpoint"
 15
 16        if not odir.exists():
 17            # No output dir yet → fresh run
 18            logging.info(f"No output directory at {odir}, starting fresh run.")
 19            init(custom_config_path, overwrite=False, pickle_path=pickle_path, custom_input_params=custom_input_params)
 20            population = (
 21                Population.initialize(
 22                    n=parametermanager.parameters.INITIAL_POPULATION_SIZE,
 23                    AGE_LIMIT=parametermanager.parameters.AGE_LIMIT,
 24                )
 25                if pickle_path is None
 26                else Population.load_pickle_from(pickle_path)
 27            )
 28            eggs = None
 29        elif not checkpoint_file.exists():
 30            # Output dir exists but no checkpoint → error
 31            raise FileNotFoundError(
 32                f"Output directory {odir} exists but contains no checkpoint file. "
 33                f"Cannot resume. Use -o to overwrite, or delete the directory."
 34            )
 35        else:
 36            # Output dir + checkpoint → resume
 37            population, eggs = init_resume(resume_path, extend_steps=extend_steps)
 38    else:
 39        init(custom_config_path, overwrite, pickle_path, custom_input_params)
 40        population = (
 41            Population.initialize(
 42                n=parametermanager.parameters.INITIAL_POPULATION_SIZE,
 43                AGE_LIMIT=parametermanager.parameters.AGE_LIMIT,
 44            )
 45            if pickle_path is None
 46            else Population.load_pickle_from(pickle_path)
 47        )
 48        eggs = None
 49        population = _seed_introgression(population)
 50
 51        if parametermanager.parameters.LINEAGE_TRACING and population.lineage_id is not None:
 52            recordingmanager.lineagerecorder.write_initial(population.lineage_id)
 53
 54    bioreactor = Bioreactor(population)
 55    bioreactor.eggs = eggs
 56    sim(bioreactor=bioreactor)
 57
 58
 59def init(custom_config_path, overwrite=False, pickle_path=None, custom_input_params={}):
 60    """
 61    When testing aegis, initialize all modules using this function, e.g.
 62
 63    import aegis_sim
 64    aegis_sim.init("_.yml")
 65
 66    And then you can safely import any module.
 67    """
 68
 69    custom_config_path = pathlib.Path(custom_config_path)
 70
 71    parametermanager.init(
 72        custom_config_path=custom_config_path,
 73        custom_input_params=custom_input_params,
 74    )
 75    variables.init(
 76        variables,
 77        custom_config_path=custom_config_path,
 78        pickle_path=pickle_path,
 79        RANDOM_SEED=parametermanager.parameters.RANDOM_SEED,
 80    )
 81    parameterization.init_traits(parameterization)
 82    submodels.init(submodels, parametermanager=parametermanager)
 83
 84    recordingmanager.init(custom_config_path, overwrite)
 85    recordingmanager.initialize_recorders(TICKER_RATE=parametermanager.parameters.TICKER_RATE)
 86
 87    if (
 88        parametermanager.parameters.LINEAGE_TRACING
 89        and parametermanager.parameters.REPRODUCTION_MODE != "asexual"
 90    ):
 91        logging.warning(
 92            "LINEAGE_TRACING is True but REPRODUCTION_MODE is %r; lineage IDs will be "
 93            "assigned to the initial population only and NOT propagated to offspring. "
 94            "Sexual lineage tracing is not yet implemented.",
 95            parametermanager.parameters.REPRODUCTION_MODE,
 96        )
 97
 98
 99def init_resume(resume_path, extend_steps=None):
100    """Initialize all modules from the latest checkpoint in the given output directory.
101
102    Args:
103        resume_path: Path to the output directory containing the checkpoint.
104        extend_steps: If set, override STEPS_PER_SIMULATION to extend the run.
105    """
106    from aegis_sim.checkpoint import Checkpoint
107
108    odir = pathlib.Path(resume_path)
109    checkpoint_path = Checkpoint.find_latest(odir)
110    checkpoint = Checkpoint.load(checkpoint_path)
111
112    # Restore parameters from checkpoint config
113    parametermanager.init_from_config(checkpoint.final_config, checkpoint.custom_config_path)
114
115    # Apply --extend override if provided
116    if extend_steps is not None:
117        if extend_steps <= checkpoint.step:
118            raise ValueError(
119                f"--extend {extend_steps} must be greater than checkpoint step {checkpoint.step}"
120            )
121        parametermanager.parameters.STEPS_PER_SIMULATION = extend_steps
122        parametermanager.final_config["STEPS_PER_SIMULATION"] = extend_steps
123        logging.info(f"Extending simulation to {extend_steps} steps (was {checkpoint.final_config['STEPS_PER_SIMULATION']}).")
124
125    # Restore variables (step, RNG state)
126    variables.restore_from_checkpoint(variables, checkpoint)
127
128    # Re-init traits and submodels
129    parameterization.init_traits(parameterization)
130    submodels.init(submodels, parametermanager=parametermanager)
131
132    # Restore envdrift map if it was active
133    if checkpoint.envdrift_map is not None:
134        submodels.architect.envdrift.map = checkpoint.envdrift_map
135
136    # Restore predator population size
137    submodels.predation.N = checkpoint.predator_population_size
138
139    # Restore resource capacity
140    from aegis_sim.submodels.resources.resources import resources
141    resources.capacity = checkpoint.resource_capacity
142
143    # Init recording in append mode (don't overwrite, don't write headers)
144    recordingmanager.init_for_resume(checkpoint.custom_config_path)
145    recordingmanager.initialize_recorders(
146        TICKER_RATE=parametermanager.parameters.TICKER_RATE,
147        resuming=True,
148    )
149
150    # Truncate output files to remove data recorded after the checkpoint step
151    recordingmanager.truncate_for_resume(checkpoint.step)
152
153    # Update TE recorder's file counter after truncation may have deleted files
154    te_dir = recordingmanager.odir / "te"
155    if te_dir.exists():
156        remaining = list(te_dir.glob("*.csv"))
157        recordingmanager.terecorder.TE_number = len(remaining)
158
159    return checkpoint.population, checkpoint.eggs
160
161
162def _seed_introgression(population: Population) -> Population:
163    """If INTROGRESSION_SOURCE and INTROGRESSION_SEEDS are set, load pop A from pickle,
164    sample the requested number of individuals, mark their ancestry as True (introgressed),
165    set pop B ancestry to False (native), and merge into one population.
166    Returns population unchanged if introgression is not configured.
167    """
168    import numpy as np
169
170    n_seeds = parametermanager.parameters.INTROGRESSION_SEEDS
171    source_path = parametermanager.parameters.INTROGRESSION_SOURCE
172
173    if n_seeds == 0 or source_path is None:
174        return population
175
176    source_path = pathlib.Path(source_path)
177    pop_a = Population.load_pickle_from(source_path)
178
179    if n_seeds > len(pop_a):
180        logging.warning(
181            f"INTROGRESSION_SEEDS={n_seeds} exceeds source population size {len(pop_a)}; "
182            f"clamping to {len(pop_a)}"
183        )
184        n_seeds = len(pop_a)
185
186    # Sample n_seeds individuals from pop A
187    indices = variables.rng.choice(len(pop_a), size=n_seeds, replace=False)
188    pop_a *= indices
189
190    genome_shape = pop_a.genomes.array.shape  # (n_seeds, ploidy, n_loci, bpl)
191
192    # Mark all pop A seeds as introgressed
193    pop_a.ancestry = np.ones(genome_shape, dtype=np.bool_)
194
195    # Mark all pop B individuals as native
196    pop_b_shape = population.genomes.array.shape
197    population.ancestry = np.zeros(pop_b_shape, dtype=np.bool_)
198
199    logging.info(
200        f"Introgression: seeding {n_seeds} individuals from {source_path} into population of {len(population)}"
201    )
202
203    population += pop_a
204    return population
205
206
207def sim(bioreactor):
208    # presim
209    recordingmanager.configrecorder.write_final_config_file(parametermanager.final_config)
210    recordingmanager.ticker.start_process()
211    ticker_pid = recordingmanager.ticker.pid
212    assert ticker_pid is not None
213    recordingmanager.summaryrecorder.write_input_summary(ticker_pid=recordingmanager.ticker.pid)
214    # TODO hacky solution of decrementing and incrementing steps
215    variables.steps -= 1
216    recordingmanager.featherrecorder.write(bioreactor.population)
217    variables.steps += 1
218
219    # sim
220    recordingmanager.phenomaprecorder.write()
221
222    # Write initial checkpoint before the loop so there's always something to resume from
223    if parametermanager.parameters.CHECKPOINT_RATE > 0:
224        from aegis_sim.checkpoint import Checkpoint
225        initial_cp = Checkpoint.capture(bioreactor.population, bioreactor.eggs, variables, submodels, parametermanager)
226        initial_cp.save(recordingmanager.checkpointrecorder.checkpoint_path)
227        logging.debug("Initial checkpoint saved before sim loop.")
228
229    while (variables.steps <= parametermanager.parameters.STEPS_PER_SIMULATION) and not recordingmanager.is_extinct():
230        recordingmanager.progressrecorder.write(len(bioreactor.population))
231        recordingmanager.simpleprogressrecorder.write()
232        bioreactor.run_step()
233        variables.steps += 1
234
235    # postsim
236    recordingmanager.popsizerecorder.flush_all()
237    recordingmanager.resourcerecorder.flush_all()
238    recordingmanager.summaryrecorder.write_output_summary()
239    logging.info("Simulation finished.")
240    recordingmanager.ticker.stop_process()
def run( custom_config_path, pickle_path, overwrite, custom_input_params, resume_path=None, extend_steps=None):
12def run(custom_config_path, pickle_path, overwrite, custom_input_params, resume_path=None, extend_steps=None):
13    if resume_path is not None:
14        odir = pathlib.Path(resume_path)
15        checkpoint_file = odir / "checkpoint"
16
17        if not odir.exists():
18            # No output dir yet → fresh run
19            logging.info(f"No output directory at {odir}, starting fresh run.")
20            init(custom_config_path, overwrite=False, pickle_path=pickle_path, custom_input_params=custom_input_params)
21            population = (
22                Population.initialize(
23                    n=parametermanager.parameters.INITIAL_POPULATION_SIZE,
24                    AGE_LIMIT=parametermanager.parameters.AGE_LIMIT,
25                )
26                if pickle_path is None
27                else Population.load_pickle_from(pickle_path)
28            )
29            eggs = None
30        elif not checkpoint_file.exists():
31            # Output dir exists but no checkpoint → error
32            raise FileNotFoundError(
33                f"Output directory {odir} exists but contains no checkpoint file. "
34                f"Cannot resume. Use -o to overwrite, or delete the directory."
35            )
36        else:
37            # Output dir + checkpoint → resume
38            population, eggs = init_resume(resume_path, extend_steps=extend_steps)
39    else:
40        init(custom_config_path, overwrite, pickle_path, custom_input_params)
41        population = (
42            Population.initialize(
43                n=parametermanager.parameters.INITIAL_POPULATION_SIZE,
44                AGE_LIMIT=parametermanager.parameters.AGE_LIMIT,
45            )
46            if pickle_path is None
47            else Population.load_pickle_from(pickle_path)
48        )
49        eggs = None
50        population = _seed_introgression(population)
51
52        if parametermanager.parameters.LINEAGE_TRACING and population.lineage_id is not None:
53            recordingmanager.lineagerecorder.write_initial(population.lineage_id)
54
55    bioreactor = Bioreactor(population)
56    bioreactor.eggs = eggs
57    sim(bioreactor=bioreactor)
def init( custom_config_path, overwrite=False, pickle_path=None, custom_input_params={}):
60def init(custom_config_path, overwrite=False, pickle_path=None, custom_input_params={}):
61    """
62    When testing aegis, initialize all modules using this function, e.g.
63
64    import aegis_sim
65    aegis_sim.init("_.yml")
66
67    And then you can safely import any module.
68    """
69
70    custom_config_path = pathlib.Path(custom_config_path)
71
72    parametermanager.init(
73        custom_config_path=custom_config_path,
74        custom_input_params=custom_input_params,
75    )
76    variables.init(
77        variables,
78        custom_config_path=custom_config_path,
79        pickle_path=pickle_path,
80        RANDOM_SEED=parametermanager.parameters.RANDOM_SEED,
81    )
82    parameterization.init_traits(parameterization)
83    submodels.init(submodels, parametermanager=parametermanager)
84
85    recordingmanager.init(custom_config_path, overwrite)
86    recordingmanager.initialize_recorders(TICKER_RATE=parametermanager.parameters.TICKER_RATE)
87
88    if (
89        parametermanager.parameters.LINEAGE_TRACING
90        and parametermanager.parameters.REPRODUCTION_MODE != "asexual"
91    ):
92        logging.warning(
93            "LINEAGE_TRACING is True but REPRODUCTION_MODE is %r; lineage IDs will be "
94            "assigned to the initial population only and NOT propagated to offspring. "
95            "Sexual lineage tracing is not yet implemented.",
96            parametermanager.parameters.REPRODUCTION_MODE,
97        )

When testing aegis, initialize all modules using this function, e.g.

import aegis_sim aegis_sim.init("_.yml")

And then you can safely import any module.

def init_resume(resume_path, extend_steps=None):
100def init_resume(resume_path, extend_steps=None):
101    """Initialize all modules from the latest checkpoint in the given output directory.
102
103    Args:
104        resume_path: Path to the output directory containing the checkpoint.
105        extend_steps: If set, override STEPS_PER_SIMULATION to extend the run.
106    """
107    from aegis_sim.checkpoint import Checkpoint
108
109    odir = pathlib.Path(resume_path)
110    checkpoint_path = Checkpoint.find_latest(odir)
111    checkpoint = Checkpoint.load(checkpoint_path)
112
113    # Restore parameters from checkpoint config
114    parametermanager.init_from_config(checkpoint.final_config, checkpoint.custom_config_path)
115
116    # Apply --extend override if provided
117    if extend_steps is not None:
118        if extend_steps <= checkpoint.step:
119            raise ValueError(
120                f"--extend {extend_steps} must be greater than checkpoint step {checkpoint.step}"
121            )
122        parametermanager.parameters.STEPS_PER_SIMULATION = extend_steps
123        parametermanager.final_config["STEPS_PER_SIMULATION"] = extend_steps
124        logging.info(f"Extending simulation to {extend_steps} steps (was {checkpoint.final_config['STEPS_PER_SIMULATION']}).")
125
126    # Restore variables (step, RNG state)
127    variables.restore_from_checkpoint(variables, checkpoint)
128
129    # Re-init traits and submodels
130    parameterization.init_traits(parameterization)
131    submodels.init(submodels, parametermanager=parametermanager)
132
133    # Restore envdrift map if it was active
134    if checkpoint.envdrift_map is not None:
135        submodels.architect.envdrift.map = checkpoint.envdrift_map
136
137    # Restore predator population size
138    submodels.predation.N = checkpoint.predator_population_size
139
140    # Restore resource capacity
141    from aegis_sim.submodels.resources.resources import resources
142    resources.capacity = checkpoint.resource_capacity
143
144    # Init recording in append mode (don't overwrite, don't write headers)
145    recordingmanager.init_for_resume(checkpoint.custom_config_path)
146    recordingmanager.initialize_recorders(
147        TICKER_RATE=parametermanager.parameters.TICKER_RATE,
148        resuming=True,
149    )
150
151    # Truncate output files to remove data recorded after the checkpoint step
152    recordingmanager.truncate_for_resume(checkpoint.step)
153
154    # Update TE recorder's file counter after truncation may have deleted files
155    te_dir = recordingmanager.odir / "te"
156    if te_dir.exists():
157        remaining = list(te_dir.glob("*.csv"))
158        recordingmanager.terecorder.TE_number = len(remaining)
159
160    return checkpoint.population, checkpoint.eggs

Initialize all modules from the latest checkpoint in the given output directory.

Args: resume_path: Path to the output directory containing the checkpoint. extend_steps: If set, override STEPS_PER_SIMULATION to extend the run.

def sim(bioreactor):
208def sim(bioreactor):
209    # presim
210    recordingmanager.configrecorder.write_final_config_file(parametermanager.final_config)
211    recordingmanager.ticker.start_process()
212    ticker_pid = recordingmanager.ticker.pid
213    assert ticker_pid is not None
214    recordingmanager.summaryrecorder.write_input_summary(ticker_pid=recordingmanager.ticker.pid)
215    # TODO hacky solution of decrementing and incrementing steps
216    variables.steps -= 1
217    recordingmanager.featherrecorder.write(bioreactor.population)
218    variables.steps += 1
219
220    # sim
221    recordingmanager.phenomaprecorder.write()
222
223    # Write initial checkpoint before the loop so there's always something to resume from
224    if parametermanager.parameters.CHECKPOINT_RATE > 0:
225        from aegis_sim.checkpoint import Checkpoint
226        initial_cp = Checkpoint.capture(bioreactor.population, bioreactor.eggs, variables, submodels, parametermanager)
227        initial_cp.save(recordingmanager.checkpointrecorder.checkpoint_path)
228        logging.debug("Initial checkpoint saved before sim loop.")
229
230    while (variables.steps <= parametermanager.parameters.STEPS_PER_SIMULATION) and not recordingmanager.is_extinct():
231        recordingmanager.progressrecorder.write(len(bioreactor.population))
232        recordingmanager.simpleprogressrecorder.write()
233        bioreactor.run_step()
234        variables.steps += 1
235
236    # postsim
237    recordingmanager.popsizerecorder.flush_all()
238    recordingmanager.resourcerecorder.flush_all()
239    recordingmanager.summaryrecorder.write_output_summary()
240    logging.info("Simulation finished.")
241    recordingmanager.ticker.stop_process()