Skip to content

API Reference

aquimodpy.Model

Model

Manages simulation configuration, components, and execution.

Attributes:

Name Type Description
model_name str

Name of the simulation.

executable_path str

Absolute path to the AquiMod 2 binary.

working_directory str

Absolute path to the simulation working directory.

exec_prefix List[str]

Command prefix for running the binary.

soil_zone SoilZone

Soil zone component instance.

unsat_zone UnsatZone

Unsaturated zone component instance.

sat_zone SatZone

Saturated zone component instance.

observations Observations

Observations component instance.

runner Runner

Runner instance (e.g., CalibrationRunner, EvaluationRunner).

simulation_mode str

'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA).

spinup_time int

Number of days for simulation spin-up.

obj_func List[Any]

Objective function ID and parameters.

output_switches List[bool]

Which component output files to write [soil, unsat, sat].

mc_params List[Any]

Parameters for Monte Carlo simulation.

sce_params List[Any]

Parameters for SCE-UA simulation.

eval_params List[Any]

Parameters for evaluation simulation.

Source code in src/aquimodpy/Model.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
class Model:
    """Manages simulation configuration, components, and execution.

    Attributes:
        model_name (str): Name of the simulation.
        executable_path (str): Absolute path to the AquiMod 2 binary.
        working_directory (str): Absolute path to the simulation working directory.
        exec_prefix (List[str]): Command prefix for running the binary.
        soil_zone (SoilZone, optional): Soil zone component instance.
        unsat_zone (UnsatZone, optional): Unsaturated zone component instance.
        sat_zone (SatZone, optional): Saturated zone component instance.
        observations (Observations, optional): Observations component instance.
        runner (Runner, optional): Runner instance (e.g., CalibrationRunner, EvaluationRunner).
        simulation_mode (str): 'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA).
        spinup_time (int): Number of days for simulation spin-up.
        obj_func (List[Any]): Objective function ID and parameters.
        output_switches (List[bool]): Which component output files to write [soil, unsat, sat].
        mc_params (List[Any]): Parameters for Monte Carlo simulation.
        sce_params (List[Any]): Parameters for SCE-UA simulation.
        eval_params (List[Any]): Parameters for evaluation simulation.
    """

    def __init__(
        self,
        model_name: str,
        executable_path: str,
        working_directory: str,
        exec_prefix: Optional[List[str]] = None,
        *,
        simulation_mode: str = "e",
        spinup_time: int = 365,
        obj_func: Optional[List[Any]] = None,
        output_switches: Optional[List[bool]] = None,
    ) -> None:
        """Initializes the model.

        Args:
            model_name (str): Name of the simulation.
            executable_path (str): Path to the AquiMod 2 binary.
            working_directory (str): Path to the working directory.
            exec_prefix (List[str], optional): Command prefix for running the binary (e.g., ["wine"]). Defaults to None.
            simulation_mode (str): 'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA). Defaults to 'e'.
            spinup_time (int): Number of days for simulation spin-up. Defaults to 365.
            obj_func (List[Any], optional): Objective function ID and parameters. Defaults to [1].
            output_switches (List[bool], optional): Which component output files to write [soil, unsat, sat]. Defaults to [True, True, True].
        """

        self.model_name: str = model_name
        self.executable_path: str = os.path.abspath(os.path.expanduser(executable_path))
        self.working_directory: str = os.path.abspath(
            os.path.expanduser(working_directory)
        )
        self.exec_prefix: List[str] = exec_prefix if exec_prefix is not None else []

        self.soil_zone: Optional["SoilZone"] = None
        self.unsat_zone: Optional["UnsatZone"] = None
        self.sat_zone: Optional["SatZone"] = None

        self.observations: Optional["Observations"] = None
        self.runner: Optional["Runner"] = None

        # Configuration parameters
        self.simulation_mode: str = simulation_mode
        self.spinup_time: int = spinup_time
        self.obj_func: List[Any] = obj_func if obj_func is not None else [1]
        self.output_switches: List[bool] = (
            output_switches if output_switches is not None else [True, True, True]
        )

        # Simulation specific parameters (defaults)
        self.mc_params: List[Any] = [10000, 0.5, 100, "g"]
        self.sce_params: List[Any] = [100, 50, -1, -1, "g"]
        self.eval_params: List[Any] = [1, "g"]

    def set_runner(self, runner: "Runner") -> None:
        """Sets the runner for the model simulation.

        Args:
            runner: A runner instance (e.g., EvaluationRunner, CalibrationRunner).
        """
        self.runner = runner

    def add_component(self, component: "Component") -> None:
        """Categorizes and stores the component.

        Args:
            component (Component): An instance of a model component (SoilZone, UnsatZone, or SatZone).

        Raises:
            ValueError: If the component type is unknown.
        """

        if isinstance(component, SoilZone):
            if self.soil_zone is not None:
                warnings.warn(
                    f"Replacing existing SoilZone component: {type(self.soil_zone).__name__} with {type(component).__name__}"
                )
            self.soil_zone = component
        elif isinstance(component, UnsatZone):
            if self.unsat_zone is not None:
                warnings.warn(
                    f"Replacing existing UnsatZone component: {type(self.unsat_zone).__name__} with {type(component).__name__}"
                )
            self.unsat_zone = component
        elif isinstance(component, SatZone):
            if self.sat_zone is not None:
                warnings.warn(
                    f"Replacing existing SatZone component: {type(self.sat_zone).__name__} with {type(component).__name__}"
                )
            self.sat_zone = component
        else:
            raise ValueError(f"Unknown component type: {type(component)}")

    def set_simulation_mode(self, mode: str, **kwargs: Any) -> None:
        """Configures simulation mode and its parameters.

        Args:
            mode (str): 'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA).
            **kwargs: Parameters for the chosen mode (e.g., n_runs, threshold, variable).

        Raises:
            ValueError: If an invalid mode is provided.
        """
        if mode not in ["e", "m", "s"]:
            raise ValueError(
                "Mode must be 'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA)"
            )
        self.simulation_mode = mode
        if mode == "m":
            self.mc_params = [
                kwargs.get("n_runs", self.mc_params[0]),
                kwargs.get("threshold", self.mc_params[1]),
                kwargs.get("n_max", self.mc_params[2]),
                kwargs.get("variable", self.mc_params[3]),
            ]
        elif mode == "s":
            self.sce_params = [
                kwargs.get("n_loops", self.sce_params[0]),
                kwargs.get("n_complexes", self.sce_params[1]),
                kwargs.get("n_offspring", self.sce_params[2]),
                kwargs.get("n_evolution", self.sce_params[3]),
                kwargs.get("variable", self.sce_params[4]),
            ]
        elif mode == "e":
            self.eval_params = [
                kwargs.get("n_runs", self.eval_params[0]),
                kwargs.get("variable", self.eval_params[1]),
            ]

    def set_objective_function(self, obj_id: int, *params: Any) -> None:
        """Sets the objective function ID and parameters.

        Args:
            obj_id (int): Objective function ID.
            *params: Objective function parameters.
        """
        self.obj_func = [obj_id, *params]

    def set_output_switches(
        self, soil: bool = True, unsat: bool = True, sat: bool = True
    ) -> None:
        """Sets which component output files should be written.

        Args:
            soil (bool): Write soil output (True/False).
            unsat (bool): Write unsaturated output (True/False).
            sat (bool): Write saturated output (True/False).
        """
        self.output_switches = [soil, unsat, sat]

    def load_parameters(
        self, calibration_results: Dict[str, pd.DataFrame], index: int = 0
    ) -> None:
        """Updates component parameters from calibration results.

        Args:
            calibration_results (Dict[str, pd.DataFrame]): Dictionary of DataFrames returned by get_results().
            index (int, optional): The row index in the results DataFrames to load. Defaults to 0.
        """
        mapping = [
            (self.soil_zone, "Soil_Params"),
            (self.unsat_zone, "Unsat_Params"),
            (self.sat_zone, "Sat_Params"),
        ]

        for comp, key in mapping:
            if comp and key in calibration_results:
                df = calibration_results[key]
                if not df.empty and index < len(df):
                    comp.parameters = {
                        str(k): v for k, v in df.iloc[index].to_dict().items()
                    }
                else:
                    warnings.warn(
                        f"Could not load parameters for {key}: result is empty or index out of range."
                    )

    def setup(self) -> None:
        """Prepares simulation files."""
        if not self.runner:
            raise ValueError("Runner must be set before calling setup()")
        self.runner.prepare()

    def run(self) -> None:
        """Executes the simulation."""
        if not self.runner:
            raise ValueError("Runner must be set before calling run()")
        self.runner.run()

    def get_results(self, run_number: int = 1) -> Dict[str, pd.DataFrame]:
        """Reads output files and returns them as DataFrames.

        In evaluation mode ('e'), it reads time series for the specified run_number.
        In calibration mode ('m' or 's'), it reads the parameter sets and fit metrics.

        Args:
            run_number (int, optional): Run number for time series. Defaults to 1.

        Returns:
            Dict[str, pd.DataFrame]: Dictionary of DataFrames containing model outputs.
        """
        output_dir = os.path.join(self.working_directory, "Output")
        results: Dict[str, pd.DataFrame] = {}

        components = [
            (self.soil_zone, "Soil"),
            (self.unsat_zone, "Unsat"),
            (self.sat_zone, "Sat"),
        ]

        # Read calibration parameter sets if in calibration mode
        if self.simulation_mode in ["m", "s"]:
            for comp, type_name in components:
                if comp:
                    comp_name = comp.__class__.__name__
                    file_name = f"{comp_name}_calib.out"
                    file_path = os.path.join(output_dir, file_name)
                    if os.path.exists(file_path):
                        results[f"{type_name}_Params"] = pd.read_csv(
                            file_path, sep=r"\s+"
                        )

        # Read time series results (only created in evaluation mode or if explicitly requested)
        if self.simulation_mode == "e":
            for comp, type_name in components:
                if comp:
                    comp_name = comp.__class__.__name__
                    file_name = f"{comp_name}_TimeSeries{run_number}.out"
                    file_path = os.path.join(output_dir, file_name)

                    if os.path.exists(file_path):
                        results[type_name] = pd.read_csv(file_path, sep=r"\s+")
                    else:
                        print(f"Warning: Time series file {file_path} not found.")

        # Also read fit_eval.out or fit_calib.out depending on mode
        fit_file = "fit_eval.out" if self.simulation_mode == "e" else "fit_calib.out"
        fit_path = os.path.join(output_dir, fit_file)
        if os.path.exists(fit_path):
            results["Fit"] = pd.read_csv(fit_path, sep=r"\s+")

        return results

__init__(model_name, executable_path, working_directory, exec_prefix=None, *, simulation_mode='e', spinup_time=365, obj_func=None, output_switches=None)

Initializes the model.

Parameters:

Name Type Description Default
model_name str

Name of the simulation.

required
executable_path str

Path to the AquiMod 2 binary.

required
working_directory str

Path to the working directory.

required
exec_prefix List[str]

Command prefix for running the binary (e.g., ["wine"]). Defaults to None.

None
simulation_mode str

'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA). Defaults to 'e'.

'e'
spinup_time int

Number of days for simulation spin-up. Defaults to 365.

365
obj_func List[Any]

Objective function ID and parameters. Defaults to [1].

None
output_switches List[bool]

Which component output files to write [soil, unsat, sat]. Defaults to [True, True, True].

None
Source code in src/aquimodpy/Model.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __init__(
    self,
    model_name: str,
    executable_path: str,
    working_directory: str,
    exec_prefix: Optional[List[str]] = None,
    *,
    simulation_mode: str = "e",
    spinup_time: int = 365,
    obj_func: Optional[List[Any]] = None,
    output_switches: Optional[List[bool]] = None,
) -> None:
    """Initializes the model.

    Args:
        model_name (str): Name of the simulation.
        executable_path (str): Path to the AquiMod 2 binary.
        working_directory (str): Path to the working directory.
        exec_prefix (List[str], optional): Command prefix for running the binary (e.g., ["wine"]). Defaults to None.
        simulation_mode (str): 'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA). Defaults to 'e'.
        spinup_time (int): Number of days for simulation spin-up. Defaults to 365.
        obj_func (List[Any], optional): Objective function ID and parameters. Defaults to [1].
        output_switches (List[bool], optional): Which component output files to write [soil, unsat, sat]. Defaults to [True, True, True].
    """

    self.model_name: str = model_name
    self.executable_path: str = os.path.abspath(os.path.expanduser(executable_path))
    self.working_directory: str = os.path.abspath(
        os.path.expanduser(working_directory)
    )
    self.exec_prefix: List[str] = exec_prefix if exec_prefix is not None else []

    self.soil_zone: Optional["SoilZone"] = None
    self.unsat_zone: Optional["UnsatZone"] = None
    self.sat_zone: Optional["SatZone"] = None

    self.observations: Optional["Observations"] = None
    self.runner: Optional["Runner"] = None

    # Configuration parameters
    self.simulation_mode: str = simulation_mode
    self.spinup_time: int = spinup_time
    self.obj_func: List[Any] = obj_func if obj_func is not None else [1]
    self.output_switches: List[bool] = (
        output_switches if output_switches is not None else [True, True, True]
    )

    # Simulation specific parameters (defaults)
    self.mc_params: List[Any] = [10000, 0.5, 100, "g"]
    self.sce_params: List[Any] = [100, 50, -1, -1, "g"]
    self.eval_params: List[Any] = [1, "g"]

add_component(component)

Categorizes and stores the component.

Parameters:

Name Type Description Default
component Component

An instance of a model component (SoilZone, UnsatZone, or SatZone).

required

Raises:

Type Description
ValueError

If the component type is unknown.

Source code in src/aquimodpy/Model.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def add_component(self, component: "Component") -> None:
    """Categorizes and stores the component.

    Args:
        component (Component): An instance of a model component (SoilZone, UnsatZone, or SatZone).

    Raises:
        ValueError: If the component type is unknown.
    """

    if isinstance(component, SoilZone):
        if self.soil_zone is not None:
            warnings.warn(
                f"Replacing existing SoilZone component: {type(self.soil_zone).__name__} with {type(component).__name__}"
            )
        self.soil_zone = component
    elif isinstance(component, UnsatZone):
        if self.unsat_zone is not None:
            warnings.warn(
                f"Replacing existing UnsatZone component: {type(self.unsat_zone).__name__} with {type(component).__name__}"
            )
        self.unsat_zone = component
    elif isinstance(component, SatZone):
        if self.sat_zone is not None:
            warnings.warn(
                f"Replacing existing SatZone component: {type(self.sat_zone).__name__} with {type(component).__name__}"
            )
        self.sat_zone = component
    else:
        raise ValueError(f"Unknown component type: {type(component)}")

get_results(run_number=1)

Reads output files and returns them as DataFrames.

In evaluation mode ('e'), it reads time series for the specified run_number. In calibration mode ('m' or 's'), it reads the parameter sets and fit metrics.

Parameters:

Name Type Description Default
run_number int

Run number for time series. Defaults to 1.

1

Returns:

Type Description
Dict[str, DataFrame]

Dict[str, pd.DataFrame]: Dictionary of DataFrames containing model outputs.

Source code in src/aquimodpy/Model.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def get_results(self, run_number: int = 1) -> Dict[str, pd.DataFrame]:
    """Reads output files and returns them as DataFrames.

    In evaluation mode ('e'), it reads time series for the specified run_number.
    In calibration mode ('m' or 's'), it reads the parameter sets and fit metrics.

    Args:
        run_number (int, optional): Run number for time series. Defaults to 1.

    Returns:
        Dict[str, pd.DataFrame]: Dictionary of DataFrames containing model outputs.
    """
    output_dir = os.path.join(self.working_directory, "Output")
    results: Dict[str, pd.DataFrame] = {}

    components = [
        (self.soil_zone, "Soil"),
        (self.unsat_zone, "Unsat"),
        (self.sat_zone, "Sat"),
    ]

    # Read calibration parameter sets if in calibration mode
    if self.simulation_mode in ["m", "s"]:
        for comp, type_name in components:
            if comp:
                comp_name = comp.__class__.__name__
                file_name = f"{comp_name}_calib.out"
                file_path = os.path.join(output_dir, file_name)
                if os.path.exists(file_path):
                    results[f"{type_name}_Params"] = pd.read_csv(
                        file_path, sep=r"\s+"
                    )

    # Read time series results (only created in evaluation mode or if explicitly requested)
    if self.simulation_mode == "e":
        for comp, type_name in components:
            if comp:
                comp_name = comp.__class__.__name__
                file_name = f"{comp_name}_TimeSeries{run_number}.out"
                file_path = os.path.join(output_dir, file_name)

                if os.path.exists(file_path):
                    results[type_name] = pd.read_csv(file_path, sep=r"\s+")
                else:
                    print(f"Warning: Time series file {file_path} not found.")

    # Also read fit_eval.out or fit_calib.out depending on mode
    fit_file = "fit_eval.out" if self.simulation_mode == "e" else "fit_calib.out"
    fit_path = os.path.join(output_dir, fit_file)
    if os.path.exists(fit_path):
        results["Fit"] = pd.read_csv(fit_path, sep=r"\s+")

    return results

load_parameters(calibration_results, index=0)

Updates component parameters from calibration results.

Parameters:

Name Type Description Default
calibration_results Dict[str, DataFrame]

Dictionary of DataFrames returned by get_results().

required
index int

The row index in the results DataFrames to load. Defaults to 0.

0
Source code in src/aquimodpy/Model.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def load_parameters(
    self, calibration_results: Dict[str, pd.DataFrame], index: int = 0
) -> None:
    """Updates component parameters from calibration results.

    Args:
        calibration_results (Dict[str, pd.DataFrame]): Dictionary of DataFrames returned by get_results().
        index (int, optional): The row index in the results DataFrames to load. Defaults to 0.
    """
    mapping = [
        (self.soil_zone, "Soil_Params"),
        (self.unsat_zone, "Unsat_Params"),
        (self.sat_zone, "Sat_Params"),
    ]

    for comp, key in mapping:
        if comp and key in calibration_results:
            df = calibration_results[key]
            if not df.empty and index < len(df):
                comp.parameters = {
                    str(k): v for k, v in df.iloc[index].to_dict().items()
                }
            else:
                warnings.warn(
                    f"Could not load parameters for {key}: result is empty or index out of range."
                )

run()

Executes the simulation.

Source code in src/aquimodpy/Model.py
215
216
217
218
219
def run(self) -> None:
    """Executes the simulation."""
    if not self.runner:
        raise ValueError("Runner must be set before calling run()")
    self.runner.run()

set_objective_function(obj_id, *params)

Sets the objective function ID and parameters.

Parameters:

Name Type Description Default
obj_id int

Objective function ID.

required
*params Any

Objective function parameters.

()
Source code in src/aquimodpy/Model.py
161
162
163
164
165
166
167
168
def set_objective_function(self, obj_id: int, *params: Any) -> None:
    """Sets the objective function ID and parameters.

    Args:
        obj_id (int): Objective function ID.
        *params: Objective function parameters.
    """
    self.obj_func = [obj_id, *params]

set_output_switches(soil=True, unsat=True, sat=True)

Sets which component output files should be written.

Parameters:

Name Type Description Default
soil bool

Write soil output (True/False).

True
unsat bool

Write unsaturated output (True/False).

True
sat bool

Write saturated output (True/False).

True
Source code in src/aquimodpy/Model.py
170
171
172
173
174
175
176
177
178
179
180
def set_output_switches(
    self, soil: bool = True, unsat: bool = True, sat: bool = True
) -> None:
    """Sets which component output files should be written.

    Args:
        soil (bool): Write soil output (True/False).
        unsat (bool): Write unsaturated output (True/False).
        sat (bool): Write saturated output (True/False).
    """
    self.output_switches = [soil, unsat, sat]

set_runner(runner)

Sets the runner for the model simulation.

Parameters:

Name Type Description Default
runner Runner

A runner instance (e.g., EvaluationRunner, CalibrationRunner).

required
Source code in src/aquimodpy/Model.py
86
87
88
89
90
91
92
def set_runner(self, runner: "Runner") -> None:
    """Sets the runner for the model simulation.

    Args:
        runner: A runner instance (e.g., EvaluationRunner, CalibrationRunner).
    """
    self.runner = runner

set_simulation_mode(mode, **kwargs)

Configures simulation mode and its parameters.

Parameters:

Name Type Description Default
mode str

'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA).

required
**kwargs Any

Parameters for the chosen mode (e.g., n_runs, threshold, variable).

{}

Raises:

Type Description
ValueError

If an invalid mode is provided.

Source code in src/aquimodpy/Model.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def set_simulation_mode(self, mode: str, **kwargs: Any) -> None:
    """Configures simulation mode and its parameters.

    Args:
        mode (str): 'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA).
        **kwargs: Parameters for the chosen mode (e.g., n_runs, threshold, variable).

    Raises:
        ValueError: If an invalid mode is provided.
    """
    if mode not in ["e", "m", "s"]:
        raise ValueError(
            "Mode must be 'e' (evaluation), 'm' (Monte Carlo), or 's' (SCE-UA)"
        )
    self.simulation_mode = mode
    if mode == "m":
        self.mc_params = [
            kwargs.get("n_runs", self.mc_params[0]),
            kwargs.get("threshold", self.mc_params[1]),
            kwargs.get("n_max", self.mc_params[2]),
            kwargs.get("variable", self.mc_params[3]),
        ]
    elif mode == "s":
        self.sce_params = [
            kwargs.get("n_loops", self.sce_params[0]),
            kwargs.get("n_complexes", self.sce_params[1]),
            kwargs.get("n_offspring", self.sce_params[2]),
            kwargs.get("n_evolution", self.sce_params[3]),
            kwargs.get("variable", self.sce_params[4]),
        ]
    elif mode == "e":
        self.eval_params = [
            kwargs.get("n_runs", self.eval_params[0]),
            kwargs.get("variable", self.eval_params[1]),
        ]

setup()

Prepares simulation files.

Source code in src/aquimodpy/Model.py
209
210
211
212
213
def setup(self) -> None:
    """Prepares simulation files."""
    if not self.runner:
        raise ValueError("Runner must be set before calling setup()")
    self.runner.prepare()

aquimodpy.Components

Component

Base class for all model components.

Attributes:

Name Type Description
model Model

The Model instance.

component_id int

Unique identifier for the component.

parameters Dict[str, Any]

Dictionary of component parameters.

Source code in src/aquimodpy/Components.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Component:
    """Base class for all model components.

    Attributes:
        model (Model): The Model instance.
        component_id (int): Unique identifier for the component.
        parameters (Dict[str, Any]): Dictionary of component parameters.
    """

    MAP: Dict[str, str] = {}
    REQUIRED_PARAMETERS: List[str] = []

    def __init__(self, model: "Model", component_id: int, **kwargs: Any) -> None:
        """Initializes a new component and registers it with the model.

        Args:
            model (Model): The Model instance.
            component_id (int): Unique identifier for the component.
            **kwargs: Component parameters as named arguments.
        """
        self.model: "Model" = model
        self.component_id: int = component_id
        self.parameters: Dict[str, Any] = {}

        # Map clean names to Aquimod names
        for clean_name, value in kwargs.items():
            if clean_name in self.MAP:
                self.parameters[self.MAP[clean_name]] = value
            else:
                # Allow passing the "unfriendly" name directly too
                self.parameters[clean_name] = value

        # Basic validation
        missing = [
            req for req in self.REQUIRED_PARAMETERS if req not in self.parameters
        ]
        if missing:
            raise ValueError(
                f"Missing required parameters for {self.__class__.__name__}: {missing}"
            )

        self.model.add_component(self)

__init__(model, component_id, **kwargs)

Initializes a new component and registers it with the model.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
component_id int

Unique identifier for the component.

required
**kwargs Any

Component parameters as named arguments.

{}
Source code in src/aquimodpy/Components.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def __init__(self, model: "Model", component_id: int, **kwargs: Any) -> None:
    """Initializes a new component and registers it with the model.

    Args:
        model (Model): The Model instance.
        component_id (int): Unique identifier for the component.
        **kwargs: Component parameters as named arguments.
    """
    self.model: "Model" = model
    self.component_id: int = component_id
    self.parameters: Dict[str, Any] = {}

    # Map clean names to Aquimod names
    for clean_name, value in kwargs.items():
        if clean_name in self.MAP:
            self.parameters[self.MAP[clean_name]] = value
        else:
            # Allow passing the "unfriendly" name directly too
            self.parameters[clean_name] = value

    # Basic validation
    missing = [
        req for req in self.REQUIRED_PARAMETERS if req not in self.parameters
    ]
    if missing:
        raise ValueError(
            f"Missing required parameters for {self.__class__.__name__}: {missing}"
        )

    self.model.add_component(self)

FAO

Bases: SoilZone

Soil FAO component.

Source code in src/aquimodpy/Components.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class FAO(SoilZone):
    """Soil FAO component."""

    REQUIRED_PARAMETERS = ["theta_fc(-)", "theta_wp(-)", "Z_r(mm)", "p(-)", "BFI(-)"]
    MAP = {
        "theta_fc": "theta_fc(-)",
        "theta_wp": "theta_wp(-)",
        "Z_r": "Z_r(mm)",
        "p": "p(-)",
        "BFI": "BFI(-)",
    }

    def __init__(
        self,
        model: "Model",
        theta_fc: float,
        theta_wp: float,
        Z_r: float,
        p: float,
        BFI: float,
    ) -> None:
        """Initializes the FAO soil component.

        Args:
            model (Model): The Model instance.
            theta_fc: Field capacity moisture content (-).
            theta_wp: Wilting point moisture content (-).
            Z_r: Rooting depth (mm).
            p: Fraction of available soil water (-).
            BFI: Baseflow index (-).
        """
        super().__init__(
            model, 1, theta_fc=theta_fc, theta_wp=theta_wp, Z_r=Z_r, p=p, BFI=BFI
        )

__init__(model, theta_fc, theta_wp, Z_r, p, BFI)

Initializes the FAO soil component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
theta_fc float

Field capacity moisture content (-).

required
theta_wp float

Wilting point moisture content (-).

required
Z_r float

Rooting depth (mm).

required
p float

Fraction of available soil water (-).

required
BFI float

Baseflow index (-).

required
Source code in src/aquimodpy/Components.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def __init__(
    self,
    model: "Model",
    theta_fc: float,
    theta_wp: float,
    Z_r: float,
    p: float,
    BFI: float,
) -> None:
    """Initializes the FAO soil component.

    Args:
        model (Model): The Model instance.
        theta_fc: Field capacity moisture content (-).
        theta_wp: Wilting point moisture content (-).
        Z_r: Rooting depth (mm).
        p: Fraction of available soil water (-).
        BFI: Baseflow index (-).
    """
    super().__init__(
        model, 1, theta_fc=theta_fc, theta_wp=theta_wp, Z_r=Z_r, p=p, BFI=BFI
    )

NSSS

Bases: SoilZone

NSSS soil component.

Source code in src/aquimodpy/Components.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
class NSSS(SoilZone):
    """NSSS soil component."""

    REQUIRED_PARAMETERS = [
        "theta_fc(-)",
        "theta_wp(-)",
        "Z_r(mm)",
        "FRACSTOR(-)",
        "p(-)",
        "BFI(-)",
    ]
    MAP = {
        "theta_fc": "theta_fc(-)",
        "theta_wp": "theta_wp(-)",
        "Z_r": "Z_r(mm)",
        "fracstor": "FRACSTOR(-)",
        "p": "p(-)",
        "BFI": "BFI(-)",
    }

    def __init__(
        self,
        model: "Model",
        theta_fc: float,
        theta_wp: float,
        Z_r: float,
        fracstor: float,
        p: float,
        BFI: float,
    ) -> None:
        """Initializes the NSSS soil component.

        Args:
            model (Model): The Model instance.
            theta_fc: Field capacity moisture content (-).
            theta_wp: Wilting point moisture content (-).
            Z_r: Rooting depth (mm).
            fracstor: Fraction of soil moisture that can be stored (-).
            p: Fraction of available soil water (-).
            BFI: Baseflow index (-).
        """
        super().__init__(
            model,
            2,
            theta_fc=theta_fc,
            theta_wp=theta_wp,
            Z_r=Z_r,
            fracstor=fracstor,
            p=p,
            BFI=BFI,
        )

__init__(model, theta_fc, theta_wp, Z_r, fracstor, p, BFI)

Initializes the NSSS soil component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
theta_fc float

Field capacity moisture content (-).

required
theta_wp float

Wilting point moisture content (-).

required
Z_r float

Rooting depth (mm).

required
fracstor float

Fraction of soil moisture that can be stored (-).

required
p float

Fraction of available soil water (-).

required
BFI float

Baseflow index (-).

required
Source code in src/aquimodpy/Components.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def __init__(
    self,
    model: "Model",
    theta_fc: float,
    theta_wp: float,
    Z_r: float,
    fracstor: float,
    p: float,
    BFI: float,
) -> None:
    """Initializes the NSSS soil component.

    Args:
        model (Model): The Model instance.
        theta_fc: Field capacity moisture content (-).
        theta_wp: Wilting point moisture content (-).
        Z_r: Rooting depth (mm).
        fracstor: Fraction of soil moisture that can be stored (-).
        p: Fraction of available soil water (-).
        BFI: Baseflow index (-).
    """
    super().__init__(
        model,
        2,
        theta_fc=theta_fc,
        theta_wp=theta_wp,
        Z_r=Z_r,
        fracstor=fracstor,
        p=p,
        BFI=BFI,
    )

Observations

Handles model observations and generates the required input file.

Source code in src/aquimodpy/Components.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
class Observations:
    """Handles model observations and generates the required input file."""

    def __init__(
        self, model: "Model", obs_df: pd.DataFrame, columns: Dict[str, str]
    ) -> None:
        """Initializes Observations component.

        Args:
            model (Model): The Model instance.
            obs_df: Pandas DataFrame containing observations.
            columns: Dictionary mapping required model columns to user DataFrame columns.
                     Required keys: DATE, RAIN, PET.
                     Optional keys: SOIL_VWC, GWL, ABS.
        """
        self.model: "Model" = model
        self.obs_df: pd.DataFrame = obs_df.copy()
        self.columns: Dict[str, str] = columns

        # Ensure required columns are present in mapping
        for req in ["DATE", "RAIN", "PET"]:
            if req not in columns:
                raise KeyError(
                    f"Required key '{req}' missing from columns mapping dictionary."
                )

            # Ensure mapped column exists in DataFrame
            if columns[req] not in obs_df.columns:
                raise KeyError(
                    f"Mapped {req} column '{columns[req]}' missing from DataFrame."
                )

        self.model.observations = self

    def write_obs_file(self) -> None:
        """Processes observations and writes them to 'Observations.txt' in the working directory."""
        # Create a working copy for processing
        df = self.obs_df.copy()

        # Map user columns to standard names used by the model
        rename_map = {v: k for k, v in self.columns.items()}
        df = df.rename(columns=rename_map)

        # Required columns for the output file
        headings = ["DAY", "MONTH", "YEAR", "RAIN", "PET", "SOIL_VWC", "GWL", "ABS"]

        # Validate numeric types and check for NaNs in required continuous columns
        for col in ["RAIN", "PET", "SOIL_VWC", "GWL", "ABS"]:
            if col in df.columns:
                if not pd.api.types.is_numeric_dtype(df[col]):
                    raise TypeError(f"Column '{col}' must be numeric.")

                if col in ["RAIN", "PET"] and df[col].isnull().any():
                    raise ValueError(
                        f"Column '{col}' contains missing values (NaNs). "
                        "Continuous values are required for RAIN and PET."
                    )

        df["DATE"] = pd.to_datetime(df["DATE"], format="mixed", errors="coerce")
        if df["DATE"].isnull().any():
            raise ValueError("DATE column contains invalid or missing dates.")

        # Create date component columns
        df["DAY"] = df["DATE"].dt.day
        df["MONTH"] = df["DATE"].dt.month
        df["YEAR"] = df["DATE"].dt.year

        # Define default fill values for missing columns or NaNs
        fill_values = {"SOIL_VWC": -9999, "GWL": -9999, "ABS": 0}

        # Ensure all required headings are present and fill missing values
        for col in headings:
            if col in ["DAY", "MONTH", "YEAR", "RAIN", "PET"]:
                continue
            if col not in df.columns:
                df[col] = fill_values[col]
            else:
                df[col] = df[col].fillna(fill_values[col])

        # Define output file path
        out_file = os.path.join(self.model.working_directory, "Observations.txt")

        # Write to file with CRLF for Windows/Wine compatibility
        with open(out_file, "w", newline="") as f:
            f.write("NUMBER OF OBSERVATIONS\r\n")
            f.write(f"{len(df)}\r\n")
            # Aquimod 2 expects tab-separated values in Observations.txt
            df[headings].to_csv(
                f, sep="\t", index=False, header=True, lineterminator="\r\n"
            )

__init__(model, obs_df, columns)

Initializes Observations component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
obs_df DataFrame

Pandas DataFrame containing observations.

required
columns Dict[str, str]

Dictionary mapping required model columns to user DataFrame columns. Required keys: DATE, RAIN, PET. Optional keys: SOIL_VWC, GWL, ABS.

required
Source code in src/aquimodpy/Components.py
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
def __init__(
    self, model: "Model", obs_df: pd.DataFrame, columns: Dict[str, str]
) -> None:
    """Initializes Observations component.

    Args:
        model (Model): The Model instance.
        obs_df: Pandas DataFrame containing observations.
        columns: Dictionary mapping required model columns to user DataFrame columns.
                 Required keys: DATE, RAIN, PET.
                 Optional keys: SOIL_VWC, GWL, ABS.
    """
    self.model: "Model" = model
    self.obs_df: pd.DataFrame = obs_df.copy()
    self.columns: Dict[str, str] = columns

    # Ensure required columns are present in mapping
    for req in ["DATE", "RAIN", "PET"]:
        if req not in columns:
            raise KeyError(
                f"Required key '{req}' missing from columns mapping dictionary."
            )

        # Ensure mapped column exists in DataFrame
        if columns[req] not in obs_df.columns:
            raise KeyError(
                f"Mapped {req} column '{columns[req]}' missing from DataFrame."
            )

    self.model.observations = self

write_obs_file()

Processes observations and writes them to 'Observations.txt' in the working directory.

Source code in src/aquimodpy/Components.py
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
def write_obs_file(self) -> None:
    """Processes observations and writes them to 'Observations.txt' in the working directory."""
    # Create a working copy for processing
    df = self.obs_df.copy()

    # Map user columns to standard names used by the model
    rename_map = {v: k for k, v in self.columns.items()}
    df = df.rename(columns=rename_map)

    # Required columns for the output file
    headings = ["DAY", "MONTH", "YEAR", "RAIN", "PET", "SOIL_VWC", "GWL", "ABS"]

    # Validate numeric types and check for NaNs in required continuous columns
    for col in ["RAIN", "PET", "SOIL_VWC", "GWL", "ABS"]:
        if col in df.columns:
            if not pd.api.types.is_numeric_dtype(df[col]):
                raise TypeError(f"Column '{col}' must be numeric.")

            if col in ["RAIN", "PET"] and df[col].isnull().any():
                raise ValueError(
                    f"Column '{col}' contains missing values (NaNs). "
                    "Continuous values are required for RAIN and PET."
                )

    df["DATE"] = pd.to_datetime(df["DATE"], format="mixed", errors="coerce")
    if df["DATE"].isnull().any():
        raise ValueError("DATE column contains invalid or missing dates.")

    # Create date component columns
    df["DAY"] = df["DATE"].dt.day
    df["MONTH"] = df["DATE"].dt.month
    df["YEAR"] = df["DATE"].dt.year

    # Define default fill values for missing columns or NaNs
    fill_values = {"SOIL_VWC": -9999, "GWL": -9999, "ABS": 0}

    # Ensure all required headings are present and fill missing values
    for col in headings:
        if col in ["DAY", "MONTH", "YEAR", "RAIN", "PET"]:
            continue
        if col not in df.columns:
            df[col] = fill_values[col]
        else:
            df[col] = df[col].fillna(fill_values[col])

    # Define output file path
    out_file = os.path.join(self.model.working_directory, "Observations.txt")

    # Write to file with CRLF for Windows/Wine compatibility
    with open(out_file, "w", newline="") as f:
        f.write("NUMBER OF OBSERVATIONS\r\n")
        f.write(f"{len(df)}\r\n")
        # Aquimod 2 expects tab-separated values in Observations.txt
        df[headings].to_csv(
            f, sep="\t", index=False, header=True, lineterminator="\r\n"
        )

Q1K1S1

Bases: SatZone

Q1K1S1 saturated zone component.

Source code in src/aquimodpy/Components.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
class Q1K1S1(SatZone):
    """Q1K1S1 saturated zone component."""

    REQUIRED_PARAMETERS = ["deltaX(m)", "S(-)", "K_1(m/d)", "z_1(m)", "alpha(-)"]
    MAP = {
        "dx": "deltaX(m)",
        "K1": "K_1(m/d)",
        "S": "S(-)",
        "z1": "z_1(m)",
        "alpha": "alpha(-)",
    }

    def __init__(
        self, model: "Model", dx: float, K1: float, S: float, z1: float, alpha: float
    ) -> None:
        """Initializes the Q1K1S1 saturated zone component.

        Args:
            model (Model): The Model instance.
            dx: Grid spacing (m).
            K1: Hydraulic conductivity of layer 1 (m/day).
            S: Storage coefficient (-).
            z1: Bottom depth of layer 1 (m).
            alpha: Saturated zone parameter (-).
        """
        super().__init__(model, 3, dx=dx, K1=K1, S=S, z1=z1, alpha=alpha)

__init__(model, dx, K1, S, z1, alpha)

Initializes the Q1K1S1 saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
dx float

Grid spacing (m).

required
K1 float

Hydraulic conductivity of layer 1 (m/day).

required
S float

Storage coefficient (-).

required
z1 float

Bottom depth of layer 1 (m).

required
alpha float

Saturated zone parameter (-).

required
Source code in src/aquimodpy/Components.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def __init__(
    self, model: "Model", dx: float, K1: float, S: float, z1: float, alpha: float
) -> None:
    """Initializes the Q1K1S1 saturated zone component.

    Args:
        model (Model): The Model instance.
        dx: Grid spacing (m).
        K1: Hydraulic conductivity of layer 1 (m/day).
        S: Storage coefficient (-).
        z1: Bottom depth of layer 1 (m).
        alpha: Saturated zone parameter (-).
    """
    super().__init__(model, 3, dx=dx, K1=K1, S=S, z1=z1, alpha=alpha)

Q1T1S1

Bases: SatZone

Q1T1S1 saturated zone component.

Source code in src/aquimodpy/Components.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
class Q1T1S1(SatZone):
    """Q1T1S1 saturated zone component."""

    REQUIRED_PARAMETERS = ["deltaX(m)", "S(-)", "T_1(m2/d)", "z_1(m)"]
    MAP = {"dx": "deltaX(m)", "T1": "T_1(m2/d)", "S": "S(-)", "z1": "z_1(m)"}

    def __init__(
        self, model: "Model", dx: float, T1: float, S: float, z1: float
    ) -> None:
        """Initializes the Q1T1S1 saturated zone component.

        Args:
            model (Model): The Model instance.
            dx: Grid spacing (m).
            T1: Transmissivity of layer 1 (m2/day).
            S: Storage coefficient (-).
            z1: Bottom depth of layer 1 (m).
        """
        super().__init__(model, 4, dx=dx, T1=T1, S=S, z1=z1)

__init__(model, dx, T1, S, z1)

Initializes the Q1T1S1 saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
dx float

Grid spacing (m).

required
T1 float

Transmissivity of layer 1 (m2/day).

required
S float

Storage coefficient (-).

required
z1 float

Bottom depth of layer 1 (m).

required
Source code in src/aquimodpy/Components.py
385
386
387
388
389
390
391
392
393
394
395
396
397
def __init__(
    self, model: "Model", dx: float, T1: float, S: float, z1: float
) -> None:
    """Initializes the Q1T1S1 saturated zone component.

    Args:
        model (Model): The Model instance.
        dx: Grid spacing (m).
        T1: Transmissivity of layer 1 (m2/day).
        S: Storage coefficient (-).
        z1: Bottom depth of layer 1 (m).
    """
    super().__init__(model, 4, dx=dx, T1=T1, S=S, z1=z1)

Q2K2S1

Bases: SatZone

Q2K2S1 saturated zone component.

Source code in src/aquimodpy/Components.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
class Q2K2S1(SatZone):
    """Q2K2S1 saturated zone component."""

    REQUIRED_PARAMETERS = [
        "deltaX(m)",
        "S(-)",
        "K_2(m/d)",
        "K_1(m/d)",
        "z_2(m)",
        "z_1(m)",
        "alpha(-)",
    ]
    MAP = {
        "dx": "deltaX(m)",
        "K2": "K_2(m/d)",
        "K1": "K_1(m/d)",
        "S": "S(-)",
        "z2": "z_2(m)",
        "z1": "z_1(m)",
        "alpha": "alpha(-)",
    }

    def __init__(
        self,
        model: "Model",
        dx: float,
        K2: float,
        K1: float,
        S: float,
        z2: float,
        z1: float,
        alpha: float,
    ) -> None:
        """Initializes the Q2K2S1 saturated zone component.

        Args:
            model (Model): The Model instance.
            dx: Grid spacing (m).
            K2: Hydraulic conductivity of layer 2 (m/day).
            K1: Hydraulic conductivity of layer 1 (m/day).
            S: Storage coefficient (-).
            z2: Bottom depth of layer 2 (m).
            z1: Bottom depth of layer 1 (m).
            alpha: Saturated zone parameter (-).
        """
        super().__init__(model, 2, dx=dx, K2=K2, K1=K1, S=S, z2=z2, z1=z1, alpha=alpha)

__init__(model, dx, K2, K1, S, z2, z1, alpha)

Initializes the Q2K2S1 saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
dx float

Grid spacing (m).

required
K2 float

Hydraulic conductivity of layer 2 (m/day).

required
K1 float

Hydraulic conductivity of layer 1 (m/day).

required
S float

Storage coefficient (-).

required
z2 float

Bottom depth of layer 2 (m).

required
z1 float

Bottom depth of layer 1 (m).

required
alpha float

Saturated zone parameter (-).

required
Source code in src/aquimodpy/Components.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
def __init__(
    self,
    model: "Model",
    dx: float,
    K2: float,
    K1: float,
    S: float,
    z2: float,
    z1: float,
    alpha: float,
) -> None:
    """Initializes the Q2K2S1 saturated zone component.

    Args:
        model (Model): The Model instance.
        dx: Grid spacing (m).
        K2: Hydraulic conductivity of layer 2 (m/day).
        K1: Hydraulic conductivity of layer 1 (m/day).
        S: Storage coefficient (-).
        z2: Bottom depth of layer 2 (m).
        z1: Bottom depth of layer 1 (m).
        alpha: Saturated zone parameter (-).
    """
    super().__init__(model, 2, dx=dx, K2=K2, K1=K1, S=S, z2=z2, z1=z1, alpha=alpha)

Q2K2S2

Bases: SatZone

Q2K2S2 saturated zone component.

Source code in src/aquimodpy/Components.py
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
class Q2K2S2(SatZone):
    """Q2K2S2 saturated zone component."""

    REQUIRED_PARAMETERS = [
        "deltaX(m)",
        "S_2(-)",
        "S_1(-)",
        "K_2(m/d)",
        "K_1(m/d)",
        "z_2(m)",
        "z_1(m)",
        "alpha(-)",
    ]
    MAP = {
        "dx": "deltaX(m)",
        "K2": "K_2(m/d)",
        "K1": "K_1(m/d)",
        "S2": "S_2(-)",
        "S1": "S_1(-)",
        "z2": "z_2(m)",
        "z1": "z_1(m)",
        "alpha": "alpha(-)",
    }

    def __init__(
        self,
        model: "Model",
        dx: float,
        K2: float,
        K1: float,
        S2: float,
        S1: float,
        z2: float,
        z1: float,
        alpha: float,
    ) -> None:
        """Initializes the Q2K2S2 saturated zone component.

        Args:
            model (Model): The Model instance.
            dx: Grid spacing (m).
            K2: Hydraulic conductivity of layer 2 (m/day).
            K1: Hydraulic conductivity of layer 1 (m/day).
            S2: Storage coefficient of layer 2 (-).
            S1: Storage coefficient of layer 1 (-).
            z2: Bottom depth of layer 2 (m).
            z1: Bottom depth of layer 1 (m).
            alpha: Saturated zone parameter (-).
        """
        super().__init__(
            model, 7, dx=dx, K2=K2, K1=K1, S2=S2, S1=S1, z2=z2, z1=z1, alpha=alpha
        )

__init__(model, dx, K2, K1, S2, S1, z2, z1, alpha)

Initializes the Q2K2S2 saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
dx float

Grid spacing (m).

required
K2 float

Hydraulic conductivity of layer 2 (m/day).

required
K1 float

Hydraulic conductivity of layer 1 (m/day).

required
S2 float

Storage coefficient of layer 2 (-).

required
S1 float

Storage coefficient of layer 1 (-).

required
z2 float

Bottom depth of layer 2 (m).

required
z1 float

Bottom depth of layer 1 (m).

required
alpha float

Saturated zone parameter (-).

required
Source code in src/aquimodpy/Components.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
def __init__(
    self,
    model: "Model",
    dx: float,
    K2: float,
    K1: float,
    S2: float,
    S1: float,
    z2: float,
    z1: float,
    alpha: float,
) -> None:
    """Initializes the Q2K2S2 saturated zone component.

    Args:
        model (Model): The Model instance.
        dx: Grid spacing (m).
        K2: Hydraulic conductivity of layer 2 (m/day).
        K1: Hydraulic conductivity of layer 1 (m/day).
        S2: Storage coefficient of layer 2 (-).
        S1: Storage coefficient of layer 1 (-).
        z2: Bottom depth of layer 2 (m).
        z1: Bottom depth of layer 1 (m).
        alpha: Saturated zone parameter (-).
    """
    super().__init__(
        model, 7, dx=dx, K2=K2, K1=K1, S2=S2, S1=S1, z2=z2, z1=z1, alpha=alpha
    )

Q3K3S1

Bases: SatZone

Q3K3S1 saturated zone component.

Source code in src/aquimodpy/Components.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
class Q3K3S1(SatZone):
    """Q3K3S1 saturated zone component."""

    REQUIRED_PARAMETERS = [
        "deltaX(m)",
        "S(-)",
        "K_3(m/d)",
        "K_2(m/d)",
        "K_1(m/d)",
        "z_3(m)",
        "z_2(m)",
        "z_1(m)",
        "alpha(-)",
    ]
    MAP = {
        "dx": "deltaX(m)",
        "K3": "K_3(m/d)",
        "K2": "K_2(m/d)",
        "K1": "K_1(m/d)",
        "S": "S(-)",
        "z3": "z_3(m)",
        "z2": "z_2(m)",
        "z1": "z_1(m)",
        "alpha": "alpha(-)",
    }

    def __init__(
        self,
        model: "Model",
        dx: float,
        K3: float,
        K2: float,
        K1: float,
        S: float,
        z3: float,
        z2: float,
        z1: float,
        alpha: float,
    ) -> None:
        """Initializes the Q3K3S1 saturated zone component.

        Args:
            model (Model): The Model instance.
            dx: Grid spacing (m).
            K3: Hydraulic conductivity of layer 3 (m/day).
            K2: Hydraulic conductivity of layer 2 (m/day).
            K1: Hydraulic conductivity of layer 1 (m/day).
            S: Storage coefficient (-).
            z3: Bottom depth of layer 3 (m).
            z2: Bottom depth of layer 2 (m).
            z1: Bottom depth of layer 1 (m).
            alpha: Saturated zone parameter (-).
        """
        super().__init__(
            model, 1, dx=dx, K3=K3, K2=K2, K1=K1, S=S, z3=z3, z2=z2, z1=z1, alpha=alpha
        )

__init__(model, dx, K3, K2, K1, S, z3, z2, z1, alpha)

Initializes the Q3K3S1 saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
dx float

Grid spacing (m).

required
K3 float

Hydraulic conductivity of layer 3 (m/day).

required
K2 float

Hydraulic conductivity of layer 2 (m/day).

required
K1 float

Hydraulic conductivity of layer 1 (m/day).

required
S float

Storage coefficient (-).

required
z3 float

Bottom depth of layer 3 (m).

required
z2 float

Bottom depth of layer 2 (m).

required
z1 float

Bottom depth of layer 1 (m).

required
alpha float

Saturated zone parameter (-).

required
Source code in src/aquimodpy/Components.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def __init__(
    self,
    model: "Model",
    dx: float,
    K3: float,
    K2: float,
    K1: float,
    S: float,
    z3: float,
    z2: float,
    z1: float,
    alpha: float,
) -> None:
    """Initializes the Q3K3S1 saturated zone component.

    Args:
        model (Model): The Model instance.
        dx: Grid spacing (m).
        K3: Hydraulic conductivity of layer 3 (m/day).
        K2: Hydraulic conductivity of layer 2 (m/day).
        K1: Hydraulic conductivity of layer 1 (m/day).
        S: Storage coefficient (-).
        z3: Bottom depth of layer 3 (m).
        z2: Bottom depth of layer 2 (m).
        z1: Bottom depth of layer 1 (m).
        alpha: Saturated zone parameter (-).
    """
    super().__init__(
        model, 1, dx=dx, K3=K3, K2=K2, K1=K1, S=S, z3=z3, z2=z2, z1=z1, alpha=alpha
    )

Q3K3S3

Bases: SatZone

Q3K3S3 saturated zone component.

Source code in src/aquimodpy/Components.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
class Q3K3S3(SatZone):
    """Q3K3S3 saturated zone component."""

    REQUIRED_PARAMETERS = [
        "deltaX(m)",
        "S_3(-)",
        "S_2(-)",
        "S_1(-)",
        "K_3(m/d)",
        "K_2(m/d)",
        "K_1(m/d)",
        "z_3(m)",
        "z_2(m)",
        "z_1(m)",
        "alpha(-)",
    ]
    MAP = {
        "dx": "deltaX(m)",
        "K3": "K_3(m/d)",
        "K2": "K_2(m/d)",
        "K1": "K_1(m/d)",
        "S3": "S_3(-)",
        "S2": "S_2(-)",
        "S1": "S_1(-)",
        "z3": "z_3(m)",
        "z2": "z_2(m)",
        "z1": "z_1(m)",
        "alpha": "alpha(-)",
    }

    def __init__(
        self,
        model: "Model",
        dx: float,
        K3: float,
        K2: float,
        K1: float,
        S3: float,
        S2: float,
        S1: float,
        z3: float,
        z2: float,
        z1: float,
        alpha: float,
    ) -> None:
        """Initializes the Q3K3S3 saturated zone component.

        Args:
            model (Model): The Model instance.
            dx: Grid spacing (m).
            K3: Hydraulic conductivity of layer 3 (m/day).
            K2: Hydraulic conductivity of layer 2 (m/day).
            K1: Hydraulic conductivity of layer 1 (m/day).
            S3: Storage coefficient of layer 3 (-).
            S2: Storage coefficient of layer 2 (-).
            S1: Storage coefficient of layer 1 (-).
            z3: Bottom depth of layer 3 (m).
            z2: Bottom depth of layer 2 (m).
            z1: Bottom depth of layer 1 (m).
            alpha: Saturated zone parameter (-).
        """
        super().__init__(
            model,
            6,
            dx=dx,
            K3=K3,
            K2=K2,
            K1=K1,
            S3=S3,
            S2=S2,
            S1=S1,
            z3=z3,
            z2=z2,
            z1=z1,
            alpha=alpha,
        )

__init__(model, dx, K3, K2, K1, S3, S2, S1, z3, z2, z1, alpha)

Initializes the Q3K3S3 saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
dx float

Grid spacing (m).

required
K3 float

Hydraulic conductivity of layer 3 (m/day).

required
K2 float

Hydraulic conductivity of layer 2 (m/day).

required
K1 float

Hydraulic conductivity of layer 1 (m/day).

required
S3 float

Storage coefficient of layer 3 (-).

required
S2 float

Storage coefficient of layer 2 (-).

required
S1 float

Storage coefficient of layer 1 (-).

required
z3 float

Bottom depth of layer 3 (m).

required
z2 float

Bottom depth of layer 2 (m).

required
z1 float

Bottom depth of layer 1 (m).

required
alpha float

Saturated zone parameter (-).

required
Source code in src/aquimodpy/Components.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
def __init__(
    self,
    model: "Model",
    dx: float,
    K3: float,
    K2: float,
    K1: float,
    S3: float,
    S2: float,
    S1: float,
    z3: float,
    z2: float,
    z1: float,
    alpha: float,
) -> None:
    """Initializes the Q3K3S3 saturated zone component.

    Args:
        model (Model): The Model instance.
        dx: Grid spacing (m).
        K3: Hydraulic conductivity of layer 3 (m/day).
        K2: Hydraulic conductivity of layer 2 (m/day).
        K1: Hydraulic conductivity of layer 1 (m/day).
        S3: Storage coefficient of layer 3 (-).
        S2: Storage coefficient of layer 2 (-).
        S1: Storage coefficient of layer 1 (-).
        z3: Bottom depth of layer 3 (m).
        z2: Bottom depth of layer 2 (m).
        z1: Bottom depth of layer 1 (m).
        alpha: Saturated zone parameter (-).
    """
    super().__init__(
        model,
        6,
        dx=dx,
        K3=K3,
        K2=K2,
        K1=K1,
        S3=S3,
        S2=S2,
        S1=S1,
        z3=z3,
        z2=z2,
        z1=z1,
        alpha=alpha,
    )

SA1D

Bases: SatZone

SA1D saturated zone component.

Source code in src/aquimodpy/Components.py
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
class SA1D(SatZone):
    """SA1D saturated zone component."""

    REQUIRED_PARAMETERS = ["A(m2)", "S(-)", "k(d-1)", "z1(m)"]
    MAP = {"A": "A(m2)", "k": "k(d-1)", "z1": "z1(m)", "S": "S(-)"}

    def __init__(self, model: "Model", A: float, k: float, z1: float, S: float) -> None:
        """Initializes the SA1D saturated zone component.

        Args:
            model (Model): The Model instance.
            A: Area (m2).
            k: Drainage coefficient (day^-1).
            z1: Bottom depth of layer 1 (m).
            S: Storage coefficient (-).
        """
        super().__init__(model, 8, A=A, k=k, z1=z1, S=S)

__init__(model, A, k, z1, S)

Initializes the SA1D saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
A float

Area (m2).

required
k float

Drainage coefficient (day^-1).

required
z1 float

Bottom depth of layer 1 (m).

required
S float

Storage coefficient (-).

required
Source code in src/aquimodpy/Components.py
582
583
584
585
586
587
588
589
590
591
592
def __init__(self, model: "Model", A: float, k: float, z1: float, S: float) -> None:
    """Initializes the SA1D saturated zone component.

    Args:
        model (Model): The Model instance.
        A: Area (m2).
        k: Drainage coefficient (day^-1).
        z1: Bottom depth of layer 1 (m).
        S: Storage coefficient (-).
    """
    super().__init__(model, 8, A=A, k=k, z1=z1, S=S)

SMAP

Bases: SoilZone

SMAP soil component.

Source code in src/aquimodpy/Components.py
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class SMAP(SoilZone):
    """SMAP soil component."""

    REQUIRED_PARAMETERS = [
        "theta_fc(-)",
        "theta_wp(-)",
        "Z_r(mm)",
        "q_ic(mm d-1)",
        "K_s(m d-1)",
        "eta(-)",
        "gamma(-)",
        "beta(mm-1)",
        "psi_a(mm)",
    ]
    MAP = {
        "theta_fc": "theta_fc(-)",
        "theta_wp": "theta_wp(-)",
        "Z_r": "Z_r(mm)",
        "q_ic": "q_ic(mm d-1)",
        "K_s": "K_s(m d-1)",
        "eta": "eta(-)",
        "gamma": "gamma(-)",
        "beta": "beta(mm-1)",
        "psi_a": "psi_a(mm)",
    }

    def __init__(
        self,
        model: "Model",
        theta_fc: float,
        theta_wp: float,
        Z_r: float,
        q_ic: float,
        K_s: float,
        eta: float,
        gamma: float,
        beta: float,
        psi_a: float,
    ) -> None:
        """Initializes the SMAP soil component.

        Args:
            model (Model): The Model instance.
            theta_fc: Field capacity moisture content (-).
            theta_wp: Wilting point moisture content (-).
            Z_r: Rooting depth (mm).
            q_ic: Constant infiltration rate (mm/day).
            K_s: Saturated hydraulic conductivity (m/day).
            eta: Infiltration parameter (-).
            gamma: Infiltration parameter (-).
            beta: Infiltration parameter (mm^-1).
            psi_a: Air entry suction (mm).
        """
        super().__init__(
            model,
            3,
            theta_fc=theta_fc,
            theta_wp=theta_wp,
            Z_r=Z_r,
            q_ic=q_ic,
            K_s=K_s,
            eta=eta,
            gamma=gamma,
            beta=beta,
            psi_a=psi_a,
        )

__init__(model, theta_fc, theta_wp, Z_r, q_ic, K_s, eta, gamma, beta, psi_a)

Initializes the SMAP soil component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
theta_fc float

Field capacity moisture content (-).

required
theta_wp float

Wilting point moisture content (-).

required
Z_r float

Rooting depth (mm).

required
q_ic float

Constant infiltration rate (mm/day).

required
K_s float

Saturated hydraulic conductivity (m/day).

required
eta float

Infiltration parameter (-).

required
gamma float

Infiltration parameter (-).

required
beta float

Infiltration parameter (mm^-1).

required
psi_a float

Air entry suction (mm).

required
Source code in src/aquimodpy/Components.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def __init__(
    self,
    model: "Model",
    theta_fc: float,
    theta_wp: float,
    Z_r: float,
    q_ic: float,
    K_s: float,
    eta: float,
    gamma: float,
    beta: float,
    psi_a: float,
) -> None:
    """Initializes the SMAP soil component.

    Args:
        model (Model): The Model instance.
        theta_fc: Field capacity moisture content (-).
        theta_wp: Wilting point moisture content (-).
        Z_r: Rooting depth (mm).
        q_ic: Constant infiltration rate (mm/day).
        K_s: Saturated hydraulic conductivity (m/day).
        eta: Infiltration parameter (-).
        gamma: Infiltration parameter (-).
        beta: Infiltration parameter (mm^-1).
        psi_a: Air entry suction (mm).
    """
    super().__init__(
        model,
        3,
        theta_fc=theta_fc,
        theta_wp=theta_wp,
        Z_r=Z_r,
        q_ic=q_ic,
        K_s=K_s,
        eta=eta,
        gamma=gamma,
        beta=beta,
        psi_a=psi_a,
    )

SatZone

Bases: Component

Base class for saturated zone components.

Source code in src/aquimodpy/Components.py
239
240
241
242
class SatZone(Component):
    """Base class for saturated zone components."""

    pass

SoilZone

Bases: Component

Base class for soil zone components.

Source code in src/aquimodpy/Components.py
53
54
55
56
class SoilZone(Component):
    """Base class for soil zone components."""

    pass

UnsatZone

Bases: Component

Base class for unsaturated zone components.

Source code in src/aquimodpy/Components.py
216
217
218
219
class UnsatZone(Component):
    """Base class for unsaturated zone components."""

    pass

VKD

Bases: SatZone

VKD saturated zone component.

Source code in src/aquimodpy/Components.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
class VKD(SatZone):
    """VKD saturated zone component."""

    REQUIRED_PARAMETERS = [
        "deltaX(m)",
        "S(-)",
        "K_1(m/d)",
        "m(d-1)",
        "z_1(m)",
        "z_p(m)",
    ]
    MAP = {
        "dx": "deltaX(m)",
        "K1": "K_1(m/d)",
        "m": "m(d-1)",
        "S": "S(-)",
        "z1": "z_1(m)",
        "zp": "z_p(m)",
    }

    def __init__(
        self,
        model: "Model",
        dx: float,
        K1: float,
        m: float,
        S: float,
        z1: float,
        zp: float,
    ) -> None:
        """Initializes the VKD saturated zone component.

        Args:
            model (Model): The Model instance.
            dx: Grid spacing (m).
            K1: Hydraulic conductivity (m/day).
            m: Drainage parameter (day^-1).
            S: Storage coefficient (-).
            z1: Bottom depth of layer 1 (m).
            zp: Depth to base (m).
        """
        super().__init__(model, 5, dx=dx, K1=K1, m=m, S=S, z1=z1, zp=zp)

__init__(model, dx, K1, m, S, z1, zp)

Initializes the VKD saturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
dx float

Grid spacing (m).

required
K1 float

Hydraulic conductivity (m/day).

required
m float

Drainage parameter (day^-1).

required
S float

Storage coefficient (-).

required
z1 float

Bottom depth of layer 1 (m).

required
zp float

Depth to base (m).

required
Source code in src/aquimodpy/Components.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def __init__(
    self,
    model: "Model",
    dx: float,
    K1: float,
    m: float,
    S: float,
    z1: float,
    zp: float,
) -> None:
    """Initializes the VKD saturated zone component.

    Args:
        model (Model): The Model instance.
        dx: Grid spacing (m).
        K1: Hydraulic conductivity (m/day).
        m: Drainage parameter (day^-1).
        S: Storage coefficient (-).
        z1: Bottom depth of layer 1 (m).
        zp: Depth to base (m).
    """
    super().__init__(model, 5, dx=dx, K1=K1, m=m, S=S, z1=z1, zp=zp)

Weibull

Bases: UnsatZone

Weibull unsaturated zone component.

Source code in src/aquimodpy/Components.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
class Weibull(UnsatZone):
    """Weibull unsaturated zone component."""

    REQUIRED_PARAMETERS = ["k(-)", "lambda(-)"]
    MAP = {"k": "k(-)", "lambda_": "lambda(-)"}  # lambda is a keyword in Python

    def __init__(self, model: "Model", k: float, lambda_: float) -> None:
        """Initializes the Weibull unsaturated zone component.

        Args:
            model (Model): The Model instance.
            k: Weibull shape parameter (-).
            lambda_: Weibull scale parameter (-).
        """
        super().__init__(model, 1, k=k, lambda_=lambda_)

__init__(model, k, lambda_)

Initializes the Weibull unsaturated zone component.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
k float

Weibull shape parameter (-).

required
lambda_ float

Weibull scale parameter (-).

required
Source code in src/aquimodpy/Components.py
228
229
230
231
232
233
234
235
236
def __init__(self, model: "Model", k: float, lambda_: float) -> None:
    """Initializes the Weibull unsaturated zone component.

    Args:
        model (Model): The Model instance.
        k: Weibull shape parameter (-).
        lambda_: Weibull scale parameter (-).
    """
    super().__init__(model, 1, k=k, lambda_=lambda_)

aquimodpy.Runner

Runner

Base class for all simulation runners.

Attributes:

Name Type Description
model Model

The Model instance.

Source code in src/aquimodpy/Runner.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class Runner:
    """Base class for all simulation runners.

    Attributes:
        model (Model): The Model instance.
    """

    def __init__(self, model: "Model") -> None:
        """Initializes the runner with a model instance.

        Args:
            model (Model): The Model instance.
        """
        self.model: "Model" = model

    def prepare(self) -> None:
        """Prepares the simulation (e.g., generates input files)."""
        # Ensure directories exist
        for folder in ["Evaluation", "Calibration", "Output"]:
            os.makedirs(
                os.path.join(self.model.working_directory, folder), exist_ok=True
            )

        # Generate Observations.txt if observations are provided
        if self.model.observations:
            self.model.observations.write_obs_file()

        # Generate Input.txt
        self.write_input_file()

    def write_input_file(self) -> None:
        """Writes the Input.txt file."""
        file_path = os.path.join(self.model.working_directory, "Input.txt")

        # Determine component IDs
        soil_id = self.model.soil_zone.component_id if self.model.soil_zone else 0
        unsat_id = self.model.unsat_zone.component_id if self.model.unsat_zone else 0
        sat_id = self.model.sat_zone.component_id if self.model.sat_zone else 0

        # Format output switches (Y/N) with spaces
        switches = " ".join(["Y" if s else "N" for s in self.model.output_switches])

        lines = [
            "Component IDs",
            f"{soil_id} {unsat_id} {sat_id}",
            "",
            "Simulation mode",
            f"{self.model.simulation_mode}",
            "",
            "Monte Carlo parameters",
            " ".join(map(str, self.model.mc_params)),
            "",
            "SCE-UA parameters",
            " ".join(map(str, self.model.sce_params)),
            "",
            "Evaluation parameters",
            " ".join(map(str, self.model.eval_params)),
            "",
            "Objective function and parameters",
            " ".join(map(str, self.model.obj_func)),
            "",
            "Spin-up period",
            f"{self.model.spinup_time}",
            "",
            "Write model output files",
            f"{switches}",
        ]

        # Use CRLF for Windows/Wine compatibility
        with open(file_path, "w", newline="") as f:
            f.write("\r\n".join(lines) + "\r\n")

    def run(self) -> None:
        """Executes the simulation."""
        print(
            f"Running {self.model.simulation_mode} for model: {self.model.model_name}"
        )

        command = self.model.exec_prefix + [
            self.model.executable_path,
            self.model.working_directory,
        ]

        try:
            result = subprocess.run(command, check=True, capture_output=True, text=True)
            print(result.stdout)
        except subprocess.CalledProcessError as e:
            print(f"Error running Aquimod 2: {e.stderr}")
            print(f"Output: {e.output}")
            raise

__init__(model)

Initializes the runner with a model instance.

Parameters:

Name Type Description Default
model Model

The Model instance.

required
Source code in src/aquimodpy/Runner.py
16
17
18
19
20
21
22
def __init__(self, model: "Model") -> None:
    """Initializes the runner with a model instance.

    Args:
        model (Model): The Model instance.
    """
    self.model: "Model" = model

prepare()

Prepares the simulation (e.g., generates input files).

Source code in src/aquimodpy/Runner.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def prepare(self) -> None:
    """Prepares the simulation (e.g., generates input files)."""
    # Ensure directories exist
    for folder in ["Evaluation", "Calibration", "Output"]:
        os.makedirs(
            os.path.join(self.model.working_directory, folder), exist_ok=True
        )

    # Generate Observations.txt if observations are provided
    if self.model.observations:
        self.model.observations.write_obs_file()

    # Generate Input.txt
    self.write_input_file()

run()

Executes the simulation.

Source code in src/aquimodpy/Runner.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def run(self) -> None:
    """Executes the simulation."""
    print(
        f"Running {self.model.simulation_mode} for model: {self.model.model_name}"
    )

    command = self.model.exec_prefix + [
        self.model.executable_path,
        self.model.working_directory,
    ]

    try:
        result = subprocess.run(command, check=True, capture_output=True, text=True)
        print(result.stdout)
    except subprocess.CalledProcessError as e:
        print(f"Error running Aquimod 2: {e.stderr}")
        print(f"Output: {e.output}")
        raise

write_input_file()

Writes the Input.txt file.

Source code in src/aquimodpy/Runner.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def write_input_file(self) -> None:
    """Writes the Input.txt file."""
    file_path = os.path.join(self.model.working_directory, "Input.txt")

    # Determine component IDs
    soil_id = self.model.soil_zone.component_id if self.model.soil_zone else 0
    unsat_id = self.model.unsat_zone.component_id if self.model.unsat_zone else 0
    sat_id = self.model.sat_zone.component_id if self.model.sat_zone else 0

    # Format output switches (Y/N) with spaces
    switches = " ".join(["Y" if s else "N" for s in self.model.output_switches])

    lines = [
        "Component IDs",
        f"{soil_id} {unsat_id} {sat_id}",
        "",
        "Simulation mode",
        f"{self.model.simulation_mode}",
        "",
        "Monte Carlo parameters",
        " ".join(map(str, self.model.mc_params)),
        "",
        "SCE-UA parameters",
        " ".join(map(str, self.model.sce_params)),
        "",
        "Evaluation parameters",
        " ".join(map(str, self.model.eval_params)),
        "",
        "Objective function and parameters",
        " ".join(map(str, self.model.obj_func)),
        "",
        "Spin-up period",
        f"{self.model.spinup_time}",
        "",
        "Write model output files",
        f"{switches}",
    ]

    # Use CRLF for Windows/Wine compatibility
    with open(file_path, "w", newline="") as f:
        f.write("\r\n".join(lines) + "\r\n")

aquimodpy.CalibrationRunner

CalibrationRunner

Bases: Runner

Runner for Monte Carlo and SCE-UA calibration simulations.

Source code in src/aquimodpy/CalibrationRunner.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class CalibrationRunner(Runner):
    """Runner for Monte Carlo and SCE-UA calibration simulations."""

    def prepare(self) -> None:
        """Prepares calibration simulation."""
        super().prepare()
        # Generate component calibration files
        self.write_component_calib_files()

    def write_component_calib_files(self) -> None:
        """Writes *_calib.txt files in the Calibration directory."""
        calib_dir = os.path.join(self.model.working_directory, "Calibration")

        components = [self.model.soil_zone, self.model.unsat_zone, self.model.sat_zone]

        for comp in components:
            if comp:
                comp_name = comp.__class__.__name__
                file_path = os.path.join(calib_dir, f"{comp_name}_calib.txt")

                # Ensure parameters are written in the correct order specified in REQUIRED_PARAMETERS
                if isinstance(comp.parameters, dict):
                    with open(file_path, "w", newline="") as f:
                        for param_name in comp.REQUIRED_PARAMETERS:
                            bounds = comp.parameters.get(param_name)
                            if bounds is None:
                                continue  # Should not happen due to validation

                            # Ensure bounds is a list/tuple of two values
                            if (
                                not isinstance(bounds, (list, tuple))
                                or len(bounds) != 2
                            ):
                                # If it's a single value, treat it as fixed
                                b = [bounds, bounds]
                            else:
                                b = list(bounds)

                            f.write(f"{param_name}\r\n")
                            f.write(f"{b[0]} {b[1]}\r\n\r\n")

prepare()

Prepares calibration simulation.

Source code in src/aquimodpy/CalibrationRunner.py
12
13
14
15
16
def prepare(self) -> None:
    """Prepares calibration simulation."""
    super().prepare()
    # Generate component calibration files
    self.write_component_calib_files()

write_component_calib_files()

Writes *_calib.txt files in the Calibration directory.

Source code in src/aquimodpy/CalibrationRunner.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def write_component_calib_files(self) -> None:
    """Writes *_calib.txt files in the Calibration directory."""
    calib_dir = os.path.join(self.model.working_directory, "Calibration")

    components = [self.model.soil_zone, self.model.unsat_zone, self.model.sat_zone]

    for comp in components:
        if comp:
            comp_name = comp.__class__.__name__
            file_path = os.path.join(calib_dir, f"{comp_name}_calib.txt")

            # Ensure parameters are written in the correct order specified in REQUIRED_PARAMETERS
            if isinstance(comp.parameters, dict):
                with open(file_path, "w", newline="") as f:
                    for param_name in comp.REQUIRED_PARAMETERS:
                        bounds = comp.parameters.get(param_name)
                        if bounds is None:
                            continue  # Should not happen due to validation

                        # Ensure bounds is a list/tuple of two values
                        if (
                            not isinstance(bounds, (list, tuple))
                            or len(bounds) != 2
                        ):
                            # If it's a single value, treat it as fixed
                            b = [bounds, bounds]
                        else:
                            b = list(bounds)

                        f.write(f"{param_name}\r\n")
                        f.write(f"{b[0]} {b[1]}\r\n\r\n")

aquimodpy.EvaluationRunner

EvaluationRunner

Bases: Runner

Runner for standard evaluation simulations.

Source code in src/aquimodpy/EvaluationRunner.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class EvaluationRunner(Runner):
    """Runner for standard evaluation simulations."""

    def prepare(self) -> None:
        """Prepares evaluation simulation."""
        super().prepare()
        # Generate component evaluation files
        self.write_component_eval_files()

    def write_component_eval_files(self) -> None:
        """Writes *_eval.txt files in the Evaluation directory."""
        eval_dir = os.path.join(self.model.working_directory, "Evaluation")

        components = [self.model.soil_zone, self.model.unsat_zone, self.model.sat_zone]

        for comp in components:
            if comp:
                comp_name = comp.__class__.__name__
                file_path = os.path.join(eval_dir, f"{comp_name}_eval.txt")

                if isinstance(comp.parameters, dict):
                    # Single parameter set - use REQUIRED_PARAMETERS order
                    headers = comp.REQUIRED_PARAMETERS
                    values = [str(comp.parameters.get(h)) for h in headers]
                    with open(file_path, "w", newline="") as f:
                        f.write("\t".join(headers) + "\r\n")
                        f.write("\t".join(values) + "\r\n")
                elif isinstance(comp.parameters, list):
                    # Multiple parameter sets (list of dicts)
                    if not comp.parameters:
                        continue
                    headers = comp.REQUIRED_PARAMETERS
                    with open(file_path, "w", newline="") as f:
                        f.write("\t".join(headers) + "\r\n")
                        for p_set in comp.parameters:
                            if isinstance(p_set, dict):
                                values = [str(p_set.get(h)) for h in headers]
                                f.write("\t".join(values) + "\r\n")

prepare()

Prepares evaluation simulation.

Source code in src/aquimodpy/EvaluationRunner.py
12
13
14
15
16
def prepare(self) -> None:
    """Prepares evaluation simulation."""
    super().prepare()
    # Generate component evaluation files
    self.write_component_eval_files()

write_component_eval_files()

Writes *_eval.txt files in the Evaluation directory.

Source code in src/aquimodpy/EvaluationRunner.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def write_component_eval_files(self) -> None:
    """Writes *_eval.txt files in the Evaluation directory."""
    eval_dir = os.path.join(self.model.working_directory, "Evaluation")

    components = [self.model.soil_zone, self.model.unsat_zone, self.model.sat_zone]

    for comp in components:
        if comp:
            comp_name = comp.__class__.__name__
            file_path = os.path.join(eval_dir, f"{comp_name}_eval.txt")

            if isinstance(comp.parameters, dict):
                # Single parameter set - use REQUIRED_PARAMETERS order
                headers = comp.REQUIRED_PARAMETERS
                values = [str(comp.parameters.get(h)) for h in headers]
                with open(file_path, "w", newline="") as f:
                    f.write("\t".join(headers) + "\r\n")
                    f.write("\t".join(values) + "\r\n")
            elif isinstance(comp.parameters, list):
                # Multiple parameter sets (list of dicts)
                if not comp.parameters:
                    continue
                headers = comp.REQUIRED_PARAMETERS
                with open(file_path, "w", newline="") as f:
                    f.write("\t".join(headers) + "\r\n")
                    for p_set in comp.parameters:
                        if isinstance(p_set, dict):
                            values = [str(p_set.get(h)) for h in headers]
                            f.write("\t".join(values) + "\r\n")