owlplanner 2025.12.20__py3-none-any.whl → 2026.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
owlplanner/__init__.py CHANGED
@@ -1,6 +1,25 @@
1
+ """
2
+ Owl planner package initialization and public API exports.
3
+
4
+ Copyright (C) 2025-2026 The Owlplanner Authors
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+ """
19
+
1
20
  from owlplanner.plan import Plan # noqa: F401
2
21
  from owlplanner.plan import clone # noqa: F401
3
- from owlplanner.config import readConfig # noqa: F401
22
+ from owlplanner.config import readConfig, saveConfig # noqa: F401
4
23
  from owlplanner.rates import getRatesDistributions # noqa: F401
5
24
  from owlplanner.version import __version__ # noqa: F401
6
25
 
owlplanner/abcapi.py CHANGED
@@ -1,25 +1,25 @@
1
1
  """
2
+ Abstract API for building linear programming constraint matrices.
2
3
 
3
- Owl/abcapi
4
- ---
4
+ This module provides a solver-neutral API to build constraint matrices and
5
+ objective functions line by line, abstracting the building process to enable
6
+ use of various solvers (MOSEK, HiGHS, etc.) for comparison. The name ABCAPI
7
+ refers to A (matrix), B (bounds), C (constraints).
5
8
 
6
- A retirement planner using linear programming optimization.
9
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
10
 
8
- See companion document for a complete explanation and description
9
- of all variables and parameters.
11
+ This program is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
10
15
 
11
- This file contains basic functions to build a constraint matrix and
12
- objective function line by line. This is used to abstract the
13
- building of the constraint matrix in order to be able to use various
14
- solvers for comparison.
15
-
16
- This approach has been successful with the MOSEK and the HiGHS solvers.
17
- A for matrix, B for bounds, C for constraints. Thus the name ABCAPI.
18
-
19
- Copyright &copy; 2024 - Martin-D. Lacasse
20
-
21
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
16
+ This program is distributed in the hope that it will be useful,
17
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ GNU General Public License for more details.
22
20
 
21
+ You should have received a copy of the GNU General Public License
22
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
23
  """
24
24
 
25
25
  import numpy as np
@@ -176,7 +176,7 @@ class Bounds:
176
176
  self.ind.append(ii)
177
177
  self.lb.append(lb)
178
178
  self.ub.append(ub)
179
- if lb == ub:
179
+ if np.isclose(lb, ub):
180
180
  self.key.append("fx")
181
181
  elif ub == np.inf and lb == -np.inf:
182
182
  self.key.append("fr")
@@ -204,6 +204,7 @@ class Bounds:
204
204
  return lb, ub
205
205
 
206
206
  def integralityArray(self):
207
+ # All variables continuous by default.
207
208
  integrality = np.zeros(self.nvars, dtype=int)
208
209
  for ii in range(len(self.integrality)):
209
210
  integrality[self.integrality[ii]] = 1
@@ -0,0 +1,50 @@
1
+ # OWLCLI - a Command Line Interface for OWL
2
+
3
+ OWLCLI is a command line interface tool to streamline the listing, running and experimenting with OWL plan files outside the streamlit interface.
4
+
5
+ ## Installation
6
+
7
+ OWLCLI is installed with the owlplanner module. Once OWLPLANNER is installed, owlcli can be run from the command line.
8
+
9
+ ## Usage
10
+
11
+ At present, OWLCLI provides two commands: `list` and `run`.
12
+
13
+ ```bash
14
+ ❯ owlcli list
15
+ ```
16
+
17
+ This command will list all the available OWL .toml plan files in the current directory.
18
+
19
+ To list all the available OWL plan files in the `examples` directory, relative to the current directory, use:
20
+
21
+ ```bash
22
+ ❯ owlcli list examples
23
+ FILE PLAN NAME TIME LISTS FILE
24
+ --------------------------------------------------------------------------------
25
+ Case_jack+jill jack+jill ✓HFP_jack+jill.xlsx
26
+ Case_joe joe ✓HFP_joe.xlsx
27
+ Case_john+sally john+sally ✓HFP_john+sally.xlsx
28
+ Case_jon+jane Jon+Jane ✗HFP_jon+jane.xslx
29
+ Case_kim+sam-bequest kim+sam-bequest ✓HFP_kim+sam.xlsx
30
+ Case_kim+sam-spending kim+sam-spending ✓HFP_kim+sam.xlsx
31
+ case_drawdowncalc-comparison-1 drawdowncalc-com... ✗edited values
32
+ ```
33
+
34
+ The listing shows the file name, plan name and Household Financial Plan file (timeListsFile) associated with each plan.
35
+
36
+ ✓ indicates that the Household Financial Plan file listed in the OWL Plan file exists.
37
+ ✗ indicates that the Household Financial Plan file was not found.
38
+ *edited values* indicates that the plan file may have been changed since the Household Financial Plan file was created.
39
+
40
+
41
+ To run an OWL plan file, use the `run` command followed by the plan file name:
42
+
43
+ ```bash
44
+ ❯ owlcli run examples/Case_kim+sam-spending
45
+ Case status: solved
46
+ Results saved to: examples/Case_kim+sam-spending_results.xlsx
47
+ ```
48
+
49
+ This example runs the `Case_kim+sam-spending` plan file located in the `examples` directory. The results of the run are saved to a new Excel file with `_results.xlsx` appended to the original plan file name. A copy of the input OWL plan file is saved as the new first tab in the Excel file.
50
+
@@ -0,0 +1,52 @@
1
+ """
2
+ Command-line interface main entry point for Owl retirement planner.
3
+
4
+ This module provides the main CLI group and command registration for the
5
+ Owl command-line interface.
6
+
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+ """
22
+
23
+ import click
24
+
25
+
26
+ from .cli_logging import configure_logging, LOG_LEVELS
27
+ from .cmd_list import cmd_list
28
+ from .cmd_run import cmd_run
29
+
30
+
31
+ @click.group()
32
+ @click.option(
33
+ "--log-level",
34
+ type=click.Choice(LOG_LEVELS, case_sensitive=False),
35
+ default="INFO",
36
+ show_default=True,
37
+ help="Set logging verbosity.",
38
+ )
39
+ @click.pass_context
40
+ def cli(ctx, log_level: str):
41
+ """SSG command-line interface."""
42
+ ctx.ensure_object(dict)
43
+ ctx.obj["log_level"] = log_level.upper()
44
+
45
+ configure_logging(log_level)
46
+
47
+
48
+ cli.add_command(cmd_list)
49
+ cli.add_command(cmd_run)
50
+
51
+ if __name__ == "__main__":
52
+ cli()
@@ -0,0 +1,56 @@
1
+ """
2
+ CLI logging configuration utilities.
3
+
4
+ This module provides logging configuration functions for the command-line
5
+ interface, including log level management and formatting.
6
+
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+ """
22
+
23
+ import sys
24
+ from loguru import logger
25
+
26
+ LOG_LEVELS = {
27
+ "TRACE",
28
+ "DEBUG",
29
+ "INFO",
30
+ "SUCCESS",
31
+ "WARNING",
32
+ "ERROR",
33
+ "CRITICAL",
34
+ }
35
+
36
+
37
+ def configure_logging(log_level: str = "INFO"):
38
+ log_level = log_level.upper()
39
+
40
+ if log_level not in LOG_LEVELS:
41
+ raise ValueError(f"Invalid log level: {log_level}")
42
+
43
+ logger.remove() # remove default handler
44
+
45
+ logger.add(
46
+ sys.stderr,
47
+ level=log_level,
48
+ format=(
49
+ # "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
50
+ "<level>{level:8}</level> | "
51
+ "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
52
+ "<level>{message}</level>"
53
+ ),
54
+ backtrace=(log_level == "TRACE"),
55
+ diagnose=(log_level == "TRACE"),
56
+ )
@@ -0,0 +1,83 @@
1
+ """
2
+ CLI command for listing retirement planning case files.
3
+
4
+ This module provides the 'list' command for discovering and displaying
5
+ information about retirement planning case files in a directory.
6
+
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+ """
22
+
23
+ import click
24
+ from pathlib import Path
25
+ from loguru import logger
26
+
27
+ import owlplanner as owl
28
+
29
+
30
+ @click.command(name="list")
31
+ @click.argument(
32
+ "directory",
33
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
34
+ default=".",
35
+ )
36
+ def cmd_list(directory):
37
+ """
38
+ List OWL plan files in a directory.
39
+
40
+ DIRECTORY defaults to the current directory.
41
+ """
42
+ logger.debug(f"Listing plans in directory: {directory}")
43
+
44
+ toml_files = sorted(directory.glob("*.toml"))
45
+
46
+ if not toml_files:
47
+ click.echo("No .toml files found.")
48
+ return
49
+
50
+ plans = []
51
+
52
+ for filename in toml_files:
53
+ try:
54
+ logger.debug(f"Loading plan from {filename}")
55
+ plan = owl.readConfig(str(filename), logstreams="loguru", readContributions=False)
56
+ plans.append((filename.stem, plan))
57
+ except Exception as e:
58
+ logger.warning(f"Failed to load {filename}: {e}")
59
+
60
+ if not plans:
61
+ click.echo("No valid OWL plans found.")
62
+ return
63
+
64
+ click.echo(f"{'FILE':<30} {'PLAN NAME':<20} {' TIME LISTS FILE':<30}")
65
+ click.echo("-" * 80)
66
+
67
+ CHECK = "✓"
68
+ CROSS = "✗"
69
+
70
+ plan_dir = directory
71
+
72
+ for stem, plan in plans:
73
+ # Truncate plan name if needed
74
+ plan_name = plan._name
75
+ if len(plan_name) > 20:
76
+ plan_name = plan_name[:16] + "..."
77
+
78
+ # Check if timeListsFileName exists in current directory
79
+ tl_name = plan.timeListsFileName
80
+ exists = (plan_dir / tl_name).exists() if tl_name else False
81
+ mark = CHECK if exists else CROSS
82
+
83
+ click.echo(f"{stem:<30} {plan_name:<20} {mark}{tl_name:<30}")
@@ -0,0 +1,86 @@
1
+ """
2
+ CLI command for running retirement planning cases.
3
+
4
+ This module provides the 'run' command for executing retirement planning
5
+ optimization from the command line.
6
+
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+ """
22
+
23
+ import click
24
+ from loguru import logger
25
+ import owlplanner as owl
26
+ from pathlib import Path
27
+
28
+
29
+ def validate_toml(ctx, param, value: Path):
30
+ if value is None:
31
+ return None
32
+
33
+ # If no suffix, append .toml
34
+ if value.suffix == "":
35
+ value = value.with_suffix(".toml")
36
+
37
+ # Enforce .toml extension
38
+ if value.suffix.lower() != ".toml":
39
+ raise click.BadParameter("File must have a .toml extension")
40
+
41
+ # Check existence AFTER normalization
42
+ if not value.exists():
43
+ raise click.BadParameter(f"File '{value}' does not exist")
44
+
45
+ if not value.is_file():
46
+ raise click.BadParameter(f"'{value}' is not a file")
47
+
48
+ return value
49
+
50
+
51
+ @click.command(name="run")
52
+ @click.argument(
53
+ "filename",
54
+ type=click.Path(exists=False, dir_okay=False, path_type=Path),
55
+ callback=validate_toml,
56
+ )
57
+ @click.option(
58
+ "--with-config",
59
+ "with_config",
60
+ type=click.Choice(["no", "first", "last"], case_sensitive=False),
61
+ default="first",
62
+ show_default=True,
63
+ help="Include config TOML sheet at the first or last position.",
64
+ )
65
+ def cmd_run(filename: Path, with_config: str):
66
+ """Run the solver for an input OWL plan file.
67
+
68
+ FILENAME is the OWL plan file to run. If no extension is provided,
69
+ .toml will be appended. The file must exist.
70
+
71
+ An output Excel file with results will be created in the current directory.
72
+ The output filename is derived from the input filename by appending
73
+ '_results.xlsx' to the stem of the input filename.
74
+
75
+ Optionally include the case configuration as a TOML worksheet.
76
+
77
+ """
78
+ logger.debug(f"Executing the run command with file: {filename}")
79
+
80
+ plan = owl.readConfig(str(filename), logstreams="loguru", readContributions=True)
81
+ plan.solve(plan.objective, plan.solverOptions)
82
+ click.echo(f"Case status: {plan.caseStatus}")
83
+ if plan.caseStatus == "solved":
84
+ output_filename = filename.with_name(filename.stem + "_results.xlsx")
85
+ plan.saveWorkbook(basename=output_filename, overwrite=True, with_config=with_config)
86
+ click.echo(f"Results saved to: {output_filename}")