planaco 0.2.1__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.
planaco/__init__.py ADDED
@@ -0,0 +1,158 @@
1
+ """
2
+ Planaco: Probabilistic Project Planning with Monte Carlo Simulation.
3
+
4
+ Planaco is a Python library for estimating project completion times using
5
+ Monte Carlo simulation. It models task durations as probability distributions
6
+ and calculates project timelines while accounting for uncertainty and
7
+ task dependencies.
8
+
9
+ Key Features
10
+ ------------
11
+ - **Multiple distribution types**: Choose from 6 probability distributions
12
+ to model task duration uncertainty.
13
+ - **Task dependencies**: Define complex workflows with parallel and
14
+ sequential task execution.
15
+ - **Critical path analysis**: Identify which tasks drive your timeline.
16
+ - **Monte Carlo simulation**: Run thousands of simulations to understand
17
+ probability distributions.
18
+ - **Statistical analysis**: Get mean, median, percentiles, and confidence
19
+ intervals.
20
+ - **Visualization**: Generate histograms, cumulative plots, and dependency
21
+ graphs.
22
+ - **Configuration files**: Define projects in YAML for easy sharing.
23
+ - **CLI tool**: Run simulations from the command line.
24
+
25
+ Main Components
26
+ ---------------
27
+ Distribution classes (from ``planaco.distributions``):
28
+ - ``TriangularDistribution``: Three-point estimate (min, mode, max).
29
+ Best for tasks where you can estimate optimistic, likely, and
30
+ pessimistic durations.
31
+ - ``UniformDistribution``: Equal probability between bounds.
32
+ Use when all durations in a range are equally likely.
33
+ - ``NormalDistribution``: Gaussian/bell curve with optional truncation.
34
+ Good for well-understood, repeatable tasks.
35
+ - ``PERTDistribution``: Smooth version of triangular (Program Evaluation
36
+ Review Technique). More weight on the mode than triangular.
37
+ - ``LogNormalDistribution``: Right-skewed distribution.
38
+ Good for tasks that may have unexpected delays.
39
+ - ``BetaDistribution``: Highly flexible with alpha/beta parameters.
40
+ For advanced modeling when you need specific shapes.
41
+
42
+ Task:
43
+ Represents a single task with probabilistic duration. Tasks are the
44
+ building blocks of projects.
45
+
46
+ Project:
47
+ Collection of tasks with optional dependencies. Provides Monte Carlo
48
+ simulation, statistics, critical path analysis, and visualization.
49
+
50
+ Quick Start
51
+ -----------
52
+ >>> from planaco import Project, Task
53
+ >>>
54
+ >>> # Create tasks with uncertainty (min, mode, max)
55
+ >>> design = Task(name="Design", min_duration=5, mode_duration=7, max_duration=14)
56
+ >>> develop = Task(name="Develop", min_duration=10, mode_duration=15, max_duration=25)
57
+ >>> test = Task(name="Test", min_duration=3, mode_duration=5, max_duration=10)
58
+ >>>
59
+ >>> # Create project with dependencies
60
+ >>> project = Project(name="My Project", unit="days")
61
+ >>> project.add_task(design)
62
+ >>> project.add_task(develop, depends_on=[design])
63
+ >>> project.add_task(test, depends_on=[develop])
64
+ >>>
65
+ >>> # Run simulation and get statistics
66
+ >>> stats = project.statistics(n=10000)
67
+ >>> print(f"Expected duration: {stats['mean']:.1f} days")
68
+ >>> print(f"P85 estimate: {stats['percentiles']['p85']:.1f} days")
69
+
70
+ Using Distribution Objects
71
+ --------------------------
72
+ >>> from planaco import Task, PERTDistribution
73
+ >>>
74
+ >>> # Use PERT distribution for smoother estimates
75
+ >>> dist = PERTDistribution(minimum=5, mode=7, maximum=14)
76
+ >>> task = Task(name="Design", distribution=dist)
77
+
78
+ Configuration Files
79
+ -------------------
80
+ Projects can be defined in YAML configuration files for easy sharing
81
+ and version control. See ``planaco.config`` for loading configurations.
82
+
83
+ Example YAML::
84
+
85
+ project:
86
+ name: "Website Redesign"
87
+ unit: "days"
88
+
89
+ tasks:
90
+ - name: "Design"
91
+ distribution:
92
+ type: "pert"
93
+ minimum: 5
94
+ mode: 7
95
+ maximum: 14
96
+
97
+ - name: "Development"
98
+ depends_on: ["Design"]
99
+ distribution:
100
+ type: "triangular"
101
+ minimum: 10
102
+ mode: 15
103
+ maximum: 25
104
+
105
+ Command Line Interface
106
+ ----------------------
107
+ Planaco provides a CLI for running simulations::
108
+
109
+ $ planaco init # Create template config file
110
+ $ planaco run config.yaml # Run simulation
111
+ $ planaco stats config.yaml # Show statistics
112
+ $ planaco plot config.yaml # Generate visualization
113
+ $ planaco graph config.yaml # Show dependency graph
114
+
115
+ See Also
116
+ --------
117
+ planaco.distributions : Probability distribution classes.
118
+ planaco.task : Task class for individual tasks.
119
+ planaco.project : Project class for task collections.
120
+ planaco.config : YAML configuration loading.
121
+ planaco.cli : Command-line interface.
122
+
123
+ Version
124
+ -------
125
+ 0.2.1
126
+ """
127
+
128
+ # Import order matters: distributions -> task -> project to avoid circular imports
129
+ from planaco.distributions import (
130
+ BetaDistribution,
131
+ Distribution,
132
+ LogNormalDistribution,
133
+ NormalDistribution,
134
+ PERTDistribution,
135
+ TriangularDistribution,
136
+ UniformDistribution,
137
+ )
138
+ from planaco.task import Task
139
+ from planaco.project import Project
140
+
141
+ __version__ = "0.2.1"
142
+
143
+ __all__ = [
144
+ # Core classes
145
+ "Project",
146
+ "Task",
147
+ # Distribution base
148
+ "Distribution",
149
+ # Distribution types
150
+ "BetaDistribution",
151
+ "LogNormalDistribution",
152
+ "NormalDistribution",
153
+ "PERTDistribution",
154
+ "TriangularDistribution",
155
+ "UniformDistribution",
156
+ # Version
157
+ "__version__",
158
+ ]
planaco/cli.py ADDED
@@ -0,0 +1,336 @@
1
+ """Planaco CLI - Monte Carlo simulation for project estimation."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Optional, Tuple
7
+
8
+ import click
9
+
10
+ from planaco import __version__
11
+ from planaco.config import (
12
+ ConfigError,
13
+ build_project_from_config,
14
+ get_seed_from_config,
15
+ get_template_config,
16
+ load_config,
17
+ )
18
+
19
+
20
+ def _setup_seed(config: dict, cli_seed: Optional[int]) -> None:
21
+ """Set up random seed from config or CLI, with warning if none specified.
22
+
23
+ Priority: CLI --seed > YAML config seed > random (no seed)
24
+
25
+ Parameters
26
+ ----------
27
+ config : dict
28
+ Parsed configuration dictionary
29
+ cli_seed : Optional[int]
30
+ Seed from CLI --seed option (takes priority)
31
+ """
32
+ import random
33
+
34
+ # CLI seed takes priority over config seed
35
+ if cli_seed is not None:
36
+ random.seed(cli_seed)
37
+ return
38
+
39
+ # Try config seed
40
+ config_seed = get_seed_from_config(config)
41
+ if config_seed is not None:
42
+ random.seed(config_seed)
43
+ return
44
+
45
+ # No seed specified - show warning
46
+ click.secho(
47
+ "Note: No seed specified. Results will vary between runs.",
48
+ fg="yellow",
49
+ err=True,
50
+ )
51
+ click.secho(
52
+ " Add 'seed: <number>' to your project config for reproducible results.",
53
+ fg="yellow",
54
+ err=True,
55
+ )
56
+
57
+
58
+ @click.group()
59
+ @click.version_option(version=__version__)
60
+ def main() -> None:
61
+ """Planaco - Monte Carlo simulation for project estimation.
62
+
63
+ Define projects and tasks in YAML, run simulations, and get
64
+ probabilistic completion time estimates.
65
+
66
+ Examples:
67
+
68
+ planaco init my_project.yaml
69
+
70
+ planaco stats my_project.yaml
71
+
72
+ planaco plot my_project.yaml -o chart.png
73
+ """
74
+ pass
75
+
76
+
77
+ @main.command()
78
+ @click.argument("output", default="planaco_project.yaml")
79
+ @click.option(
80
+ "--name", "-n", default="My Project", help="Project name for the template"
81
+ )
82
+ def init(output: str, name: str) -> None:
83
+ """Create a template YAML project file.
84
+
85
+ OUTPUT: Path for the new YAML file (default: planaco_project.yaml)
86
+
87
+ Example:
88
+ planaco init my_project.yaml --name "Web App Development"
89
+ """
90
+ output_path = Path(output)
91
+
92
+ if output_path.exists():
93
+ click.secho(f"Error: File '{output}' already exists", fg="red", err=True)
94
+ sys.exit(1)
95
+
96
+ template = get_template_config(name)
97
+
98
+ with open(output_path, "w") as f:
99
+ f.write(template)
100
+
101
+ click.secho(f"Created template project file: {output}", fg="green")
102
+ click.echo("\nEdit the file to define your tasks, then run:")
103
+ click.echo(f" planaco stats {output}")
104
+
105
+
106
+ @main.command()
107
+ @click.argument("config_file", type=click.Path(exists=True))
108
+ @click.option("-n", "--simulations", default=10000, help="Number of simulations to run")
109
+ @click.option(
110
+ "-o", "--output", default=None, help="Output file for results (default: stdout)"
111
+ )
112
+ @click.option(
113
+ "-f",
114
+ "--format",
115
+ "output_format",
116
+ type=click.Choice(["json", "csv"]),
117
+ default="json",
118
+ help="Output format",
119
+ )
120
+ @click.option("--seed", default=None, type=int, help="Random seed for reproducibility")
121
+ def run(
122
+ config_file: str,
123
+ simulations: int,
124
+ output: Optional[str],
125
+ output_format: str,
126
+ seed: Optional[int],
127
+ ) -> None:
128
+ """Run Monte Carlo simulation and export results.
129
+
130
+ CONFIG_FILE: Path to the YAML project configuration file.
131
+
132
+ Example:
133
+ planaco run project.yaml -n 10000 -o results.json
134
+ """
135
+ try:
136
+ config = load_config(config_file)
137
+ _setup_seed(config, seed)
138
+ project = build_project_from_config(config)
139
+
140
+ if output:
141
+ project.export_results(n=simulations, format=output_format, output=output)
142
+ click.secho(f"Results exported to {output}", fg="green")
143
+ else:
144
+ stats = project.statistics(n=simulations)
145
+ _print_stats(project.name, stats)
146
+
147
+ except (ConfigError, FileNotFoundError) as e:
148
+ click.secho(f"Error: {e}", fg="red", err=True)
149
+ sys.exit(1)
150
+ except Exception as e:
151
+ click.secho(f"Error: {e}", fg="red", err=True)
152
+ sys.exit(1)
153
+
154
+
155
+ @main.command()
156
+ @click.argument("config_file", type=click.Path(exists=True))
157
+ @click.option("-n", "--simulations", default=10000, help="Number of simulations to run")
158
+ @click.option(
159
+ "--json", "as_json", is_flag=True, help="Output as JSON instead of formatted text"
160
+ )
161
+ @click.option(
162
+ "--seed",
163
+ default=None,
164
+ type=int,
165
+ help="Random seed for reproducibility (overrides config)",
166
+ )
167
+ def stats(
168
+ config_file: str, simulations: int, as_json: bool, seed: Optional[int]
169
+ ) -> None:
170
+ """Calculate and display project statistics.
171
+
172
+ CONFIG_FILE: Path to the YAML project configuration file.
173
+
174
+ Example:
175
+ planaco stats project.yaml -n 10000
176
+ """
177
+ try:
178
+ config = load_config(config_file)
179
+ _setup_seed(config, seed)
180
+ project = build_project_from_config(config)
181
+
182
+ stats_result = project.statistics(n=simulations)
183
+
184
+ if as_json:
185
+ click.echo(json.dumps(stats_result, indent=2))
186
+ else:
187
+ _print_stats(project.name, stats_result)
188
+
189
+ except (ConfigError, FileNotFoundError) as e:
190
+ click.secho(f"Error: {e}", fg="red", err=True)
191
+ sys.exit(1)
192
+ except Exception as e:
193
+ click.secho(f"Error: {e}", fg="red", err=True)
194
+ sys.exit(1)
195
+
196
+
197
+ @main.command()
198
+ @click.argument("config_file", type=click.Path(exists=True))
199
+ @click.option("-n", "--simulations", default=1000, help="Number of simulations to run")
200
+ @click.option(
201
+ "-o", "--output", default=None, help="Save plot to file instead of displaying"
202
+ )
203
+ @click.option(
204
+ "--cumulative",
205
+ is_flag=True,
206
+ help="Show cumulative distribution instead of histogram",
207
+ )
208
+ @click.option("--kde", is_flag=True, help="Show kernel density estimate on histogram")
209
+ @click.option(
210
+ "-p",
211
+ "--percentile",
212
+ "percentiles",
213
+ multiple=True,
214
+ type=int,
215
+ help="Percentile markers to show (e.g., -p 50 -p 85 -p 95)",
216
+ )
217
+ @click.option(
218
+ "--seed",
219
+ default=None,
220
+ type=int,
221
+ help="Random seed for reproducibility (overrides config)",
222
+ )
223
+ def plot(
224
+ config_file: str,
225
+ simulations: int,
226
+ output: Optional[str],
227
+ cumulative: bool,
228
+ kde: bool,
229
+ percentiles: Tuple[int, ...],
230
+ seed: Optional[int],
231
+ ) -> None:
232
+ """Generate visualization of simulation results.
233
+
234
+ CONFIG_FILE: Path to the YAML project configuration file.
235
+
236
+ Examples:
237
+ planaco plot project.yaml -o chart.png
238
+
239
+ planaco plot project.yaml --cumulative
240
+
241
+ planaco plot project.yaml -p 50 -p 85 -p 95
242
+ """
243
+ try:
244
+ config = load_config(config_file)
245
+ _setup_seed(config, seed)
246
+ project = build_project_from_config(config)
247
+
248
+ percentiles_list = list(percentiles) if percentiles else None
249
+ show_percentiles = bool(percentiles_list)
250
+
251
+ project.plot(
252
+ n=simulations,
253
+ hist=not cumulative,
254
+ kde=kde,
255
+ save_path=output,
256
+ show_percentiles=show_percentiles,
257
+ percentiles=percentiles_list,
258
+ )
259
+
260
+ if output:
261
+ click.secho(f"Plot saved to {output}", fg="green")
262
+
263
+ except (ConfigError, FileNotFoundError) as e:
264
+ click.secho(f"Error: {e}", fg="red", err=True)
265
+ sys.exit(1)
266
+ except Exception as e:
267
+ click.secho(f"Error: {e}", fg="red", err=True)
268
+ sys.exit(1)
269
+
270
+
271
+ @main.command()
272
+ @click.argument("config_file", type=click.Path(exists=True))
273
+ @click.option(
274
+ "-o", "--output", default=None, help="Save graph to file instead of displaying"
275
+ )
276
+ @click.option("--no-durations", is_flag=True, help="Hide duration information on nodes")
277
+ def graph(config_file: str, output: Optional[str], no_durations: bool) -> None:
278
+ """Visualize the task dependency graph.
279
+
280
+ CONFIG_FILE: Path to the YAML project configuration file.
281
+
282
+ Examples:
283
+ planaco graph project.yaml
284
+
285
+ planaco graph project.yaml -o dependencies.png
286
+ """
287
+ try:
288
+ config = load_config(config_file)
289
+ project = build_project_from_config(config)
290
+
291
+ project.plot_dependency_graph(save_path=output, show_durations=not no_durations)
292
+
293
+ if output:
294
+ click.secho(f"Dependency graph saved to {output}", fg="green")
295
+
296
+ except (ConfigError, FileNotFoundError) as e:
297
+ click.secho(f"Error: {e}", fg="red", err=True)
298
+ sys.exit(1)
299
+ except Exception as e:
300
+ click.secho(f"Error: {e}", fg="red", err=True)
301
+ sys.exit(1)
302
+
303
+
304
+ def _print_stats(project_name: Optional[str], stats: dict) -> None:
305
+ """Print formatted statistics to stdout."""
306
+ click.echo()
307
+ click.secho(f"Project: {project_name or 'Unnamed'}", fg="cyan", bold=True)
308
+ click.secho("=" * 50, fg="cyan")
309
+ click.echo()
310
+
311
+ click.echo(f"Simulations: {stats['n_simulations']:,}")
312
+ click.echo(f"Time Unit: {stats['unit']}")
313
+ click.echo()
314
+
315
+ click.secho("Duration Estimates:", bold=True)
316
+ click.echo(f" Mean: {stats['mean']:.1f} {stats['unit']}")
317
+ click.echo(f" Median (P50): {stats['median']:.1f} {stats['unit']}")
318
+ click.echo(f" Std Deviation: {stats['std_dev']:.1f} {stats['unit']}")
319
+ click.echo(f" Min: {stats['min']:.1f} {stats['unit']}")
320
+ click.echo(f" Max: {stats['max']:.1f} {stats['unit']}")
321
+ click.echo()
322
+
323
+ click.secho("Percentiles:", bold=True)
324
+ for key, value in stats["percentiles"].items():
325
+ label = key.upper()
326
+ click.echo(f" {label}: {value:.1f} {stats['unit']}")
327
+ click.echo()
328
+
329
+ ci = stats["confidence_intervals"]["95%"]
330
+ click.secho("Confidence Interval:", bold=True)
331
+ click.echo(f" 95% CI: [{ci[0]:.1f}, {ci[1]:.1f}] {stats['unit']}")
332
+ click.echo()
333
+
334
+
335
+ if __name__ == "__main__":
336
+ main()