Cycles rotation builder class

cycles.CyclesRotationBuilder(simulation, executable, crops, control_dict, *, build_yield_matrix=True) dataclass

Run Cycles iteratively and append economically optimal operations.

Executes Cycles simulations for candidate crop-operation combinations, selects the highest-return option at each break-point, and iteratively builds a multi-year rotation. Integrates economic scoring with agronomic constraints (e.g., minimum planting interval, crop group penalties).

Attributes:
  • simulation (str) –

    Base simulation name for generated input/output directories.

  • executable (str) –

    Absolute path to the Cycles executable binary.

  • crops (list[Crop]) –

    List of Crop objects with available operations for rotation.

  • control_dict (dict) –

    Base control file parameters (simulation years, options, etc.).

  • fertilizers (dict[str, Fertilizer]) –

    Dictionary mapping fertilizer names to Fertilizer objects.

  • yield_matrix (dict[str, DataFrame]) –

    Dictionary mapping crop names to yield prediction DataFrames.

  • build_yield_matrix (bool) –

    If True, run simulations to build yield matrix; else read from disk.

  • crop_price_data (DataFrame | None) –

    DataFrame of crop prices indexed by calendar year.

  • fertilizer_price_data (DataFrame | None) –

    DataFrame of fertilizer prices indexed by year, or None.

  • production_cost_data (DataFrame | None) –

    DataFrame of production costs indexed by year, or None.

  • rotation_frequency (dict[str, tuple[float, float]] | None) –

    Dictionary mapping crop names to (min, max) frequency tuples.

  • _times_planted (dict[str, int]) –

    Internal tracker of how many times each crop has been planted.

run(*, crop_price, fertilizer_price=None, production_cost=None, rotation_frequency=None)

Run the dynamic rotation-building loop.

Parameters:
  • crop_price (str | Path) –

    CSV file of crop prices indexed by year.

  • fertilizer_price (str | Path | None, default: None ) –

    Optional fertilizer price table indexed by year.

  • production_cost (str | Path | None, default: None ) –

    Optional crop production cost table indexed by year.

  • rotation_frequency (dict[str, tuple[float, float]] | None, default: None ) –

    Optional min/max frequency constraints per crop symbol.

Source code in cycles/rotation_builder.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def run(self, *, crop_price: str | Path, fertilizer_price: str | Path | None=None, production_cost: str | Path | None=None, rotation_frequency: dict[str, tuple[float, float]] | None=None) -> None:
    """Run the dynamic rotation-building loop.

    Args:
        crop_price: CSV file of crop prices indexed by year.
        fertilizer_price: Optional fertilizer price table indexed by year.
        production_cost: Optional crop production cost table indexed by year.
        rotation_frequency: Optional min/max frequency constraints per crop symbol.
    """
    self.crop_price_data = _optional_csv(crop_price)
    self.fertilizer_price_data = _optional_csv(fertilizer_price)
    self.production_cost_data = _optional_csv(production_cost)
    self.rotation_frequency = rotation_frequency

    if self.crop_price_data is None:
        raise ValueError('Crop price data is required to run the rotation builder.')

    operations: list[Operation] = []
    self.control_dict['rotation_size'] = self.control_dict['simulation_end_year'] - self.control_dict['simulation_start_year'] + 1
    self.control_dict['operation_file'] = f'{self.simulation}.operation'

    generate_control_file(f'./input/{self.simulation}.ctrl', self.control_dict)
    _write_operation_file(Path('./input') / f'{self.simulation}.operation', operations)

    cycles = Cycles(path='.', simulation=self.simulation, executable=self.executable)
    options = '-b'

    while True:
        status, screen_output = cycles.run(options=options, silence=False)
        if status != BREAK_POINT_REACHED:
            break

        options = '-rb'
        year, doy = _find_break_doy(screen_output)
        economic_parameters = EconomicParameters.from_builder(self, year)

        last_crop = _find_crop(self.crops, _last_planting(operations).crop) if operations else None

        result = self._find_best_rotation(year, doy, last_crop, economic_parameters)
        self._append_operations(result, year, doy, operations)
        self._times_planted[result.crop.symbol] += 1

        _write_operation_file(Path('./input') / f'{self.simulation}.operation', operations)