Skip to main content

PMDT — Project Management Digital Twin

A lightweight, pure‑Python toolkit for building and analyzing “digital twins” of projects:

  • Calendars (working days, holidays, workday math)
  • CPM scheduling (ES/EF/LS/LF, slack, criticality, FS/SS/FF/SF links with lag)
  • Cost model (direct + overhead/indirect)
  • EVM / Earned Schedule style performance tracking (PV/EV/AC, CPI/SPI, ES, SV(t), SPI(t), time & cost EACs)
  • Monte Carlo schedule/cost simulation with percentile summaries
  • Portfolio rollups across multiple projects (+ optional “AI” forecasting helper)

Requirements

Core module:

  • Python 3.10+
  • numpy
  • pandas

Optional (only for Portfolio.init_aip()):

  • scikit-learn

Install basics:

pip install numpy pandas
# optional:
pip install scikit-learn

Core concepts

Calendar

Defines working days (0=Mon … 6=Sun), holidays, and “workday/networkdays” utilities.

  • Default working_days is all 7 days; pass Mon–Fri explicitly if needed.

Resource

Represents labor/material/cost with a unit cost and (currently non-enforced) availability.

  • resource_type in {"work","material","cost"}

Activity

A task with:

  • Dependencies (predecessors)
  • Resources & duration (supports effort-driven duration scaling)
  • Costs (direct + overhead)
  • Tracking records (planned + actual/progress)
  • Monte Carlo distribution parameters (duration & cost)

ControlAccount

Auto-created per activity-resource allocation. Holds cost tracking (AC) and EVA metrics.

Project

A collection of activities with:

  • CPM schedule (project.schedule())
  • Time-phased baseline (PMB / PV curve)
  • Resource usage time series
  • EVA rollups + Earned Schedule style metrics
  • Monte Carlo simulation (project.mc())

Portfolio

A set of projects with portfolio-level EVA dataframe, and optional ML-based helper (init_aip).


Quickstart

Create a small project, schedule it, and view dataframes:

from pmdt import Calendar, Resource, Activity, Project

cal = Calendar(
    name="Mon-Fri",
    working_days=[0, 1, 2, 3, 4],
    holidays=[20251225],  # YYYYMMDD int, or date/datetime/ISO strings
)

dev = Resource(name="Dev", resource_type="work", unit_cost=120.0)   # €/day (example)
mat = Resource(name="Parts", resource_type="material", unit_cost=50.0)

a1 = Activity(
    name="Design",
    baseline_duration=5,
    baseline_resources={"Dev": (dev, 1.0)},
    resources={"Dev": (dev, 1.0)},
)

a2 = Activity(
    name="Build",
    baseline_duration=3,
    baseline_resources={"Dev": (dev, 1.0), "Parts": (mat, 10)},
    resources={"Dev": (dev, 1.0), "Parts": (mat, 10)},
    predecessors={"Design": (a1, "fs", 0)},  # FS + 0 day lag
)

proj = Project(
    name="Demo",
    activities=[a1, a2],
    calendar=cal,
    start_date=20260105,     # YYYYMMDD
    tracking_freq="D",       # pandas date_range freq: D/W/M/...
)

proj.schedule()

print(proj.df_project())
print(proj.df_activities())
print(proj.df_resources())
print(proj.df_controlaccounts())

Dependencies (links)

Activities store predecessors as:

predecessors = {
  "SomeKey": (predecessor_activity, rel_type, lag_days)
}

Supported rel_type values used by CPM:

  • "fs": Finish → Start
  • "ff": Finish → Finish
  • "ss": Start → Start
  • "sf": Start → Finish

lag_days is a float; positive pushes successors later, negative pulls earlier.


Earned Value + Earned Schedule tracking

How tracking works

When you create/schedule a project it initializes tracking “records” at tracking_dates. Keys are YYYYMMDD integers derived from those dates.

Planned values (PV) are computed; actual progress and cost are inputs:

  • WP (work performed, 0–1) is stored on each Activity record
  • AC (actual cost) is stored on each ControlAccount record

Then proj.df_eva() recalculates all derived fields and updates project rollups.

Minimal example: update progress and cost

# pick a tracking date key that exists in proj.records
date_key = 20260112

# 40% complete on "Build"
proj.activities["Build"].records[date_key]["WP"] = 0.40

# add cost on the Build-Dev control account
ca_name = "Build-Dev"  # ActivityName-ResourceName by default
proj.control_accounts[ca_name].records[date_key]["AC"] = 1500.0

df_eva = proj.df_eva()
print(df_eva.tail())

Useful columns you’ll see

Typical EVA columns include:

  • PV, EV, AC, CV, SV, CPI, SPI
  • EAC_CV, EAC_CPI
  • time EACs like EAC(t)_SPI[Days] / EAC(t)_SPI[Date]
  • Earned Schedule fields at project level: ES[Days], SV(t), SPI(t), etc.

Performance Measurement Baseline (PMB / PV curve)

Generate time-phased PV:

proj.pmb()
df_daily_pv = proj.df_pmb_project      # daily PV
df_cum_pv   = proj.df_pmb_project_cuml # cumulative PV

Resource usage

Compute daily resource usage (units per day):

proj.resource_usage()
print(proj.df_resource_usage.head())       # daily usage by resource
print(proj.df_resource_usage_cuml.head())  # cumulative

Monte Carlo simulation

Each activity supports separate distributions for duration and cost.

Supported distributions include: fixed, uniform, exponential, normal, log-normal, triangular, pert, beta, gamma, weibull, discrete.

Example: triangular duration + normal cost

for a in proj.activities.values():
    a.duration_distribution = "triangular"
    a.duration_params = {"left": 3, "mode": 5, "right": 9}

    a.cost_distribution = "normal"
    a.cost_mean = a.total_cost
    a.cost_stdev = 200.0

proj.mc(n_simulations=2000, track_pmb=True)

print(proj.df_mc.describe(percentiles=[0.05, 0.5, 0.95]))
print(proj.df_mc_pmb_project.head())        # PV percentiles per time bucket (if track_pmb=True)
print(proj.df_mc_pmb_project_cuml.head())   # cumulative PV percentiles

Portfolio

from pmdt import Portfolio

pf = Portfolio([proj], name="My Portfolio")
print(pf.df_projects())
print(pf.df_eva().tail())

Optional: AI-powered helper (init_aip)

Portfolio.init_aip() is an experimental helper that prepares EVA data, optionally interpolates by WP steps, and runs leave-one-project-out modeling using scikit‑learn.

# requires: pip install scikit-learn
df0, df1, df2, df3, df4, df_out, dfr_model, dfr_wp, dfr_project = pf.init_aip(
    target="cost",          # or "time"
    method="direct",        # or "indirect"
    model="LinearRegression"  # or "MLPRegressor"
)

Notes & limitations

  • CPM scheduling does not level resources; Resource.availability is currently informational.
  • Records are easiest to work with if you keep date keys consistent with the project’s tracking_dates (use the existing YYYYMMDD keys created by the project).

Metadata

Release files for pmdt 1.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pmdt 1.1.2
File Size Uploaded
pmdt-1.1.2.tar.gz 20.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pmdt 1.1.2
File Interpreter ABI Platform
pmdt-1.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 41.3 kB

Release files / pmdt-1.1.2.tar.gz

Download URL pmdt-1.1.2.tar.gz
Size 20.5 kB
Tags Source
SHA-256 checksum
How to use checksums
9a15031d10a8b9a93cdad7623c9d541a67cccfce7a2af81e2e24b72aa7d881e0
BLAKE2b-256 checksum
How to use checksums
7310fe317da5530480b2b5b0040bb873176c099c4f6448098c446d956ce879af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jan 30, 2026.

Transparency log

Release files / pmdt-1.1.2-py3-none-any.whl

Download URL pmdt-1.1.2-py3-none-any.whl
Size 20.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
018008b42e81efb8ca10778a8b5658202f27d10896c174ff3f307451e40eeee6
BLAKE2b-256 checksum
How to use checksums
0516ca8e18cc0d1ae1c003a3570b5166910ee17e5cfe9ae56b90b822d08c07b3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jan 30, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.2 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page