Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ permissions:


env:
DEFAULT_PYTHON_VERSION: '3.11'
DEFAULT_PYTHON_VERSION: '3.13'


jobs:
Expand All @@ -23,7 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ${{ github.ref == 'refs/heads/main' && fromJson('["3.8", "3.9", "3.10", "3.11"]') || fromJson('["default"]') }}
python-version: ${{ github.ref == 'refs/heads/main' && fromJson('["3.11", "3.12", "3.13"]') || fromJson('["default"]') }}
os: ${{ github.ref == 'refs/heads/main' && fromJson('["ubuntu-latest", "macos-latest", "windows-latest"]') || fromJson('["ubuntu-latest"]') }}

runs-on: ${{ matrix.os }}
Expand All @@ -37,11 +37,9 @@ jobs:
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version == 'default' && env.DEFAULT_PYTHON_VERSION || matrix.python-version }}

- uses: corriander/gha/uv/test@main
env:
UV_PYTHON: ${{ matrix.python-version == 'default' && env.DEFAULT_PYTHON_VERSION || matrix.python-version }}

release:
name: 🚀 Release
Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Features
- Requirements weighting with a Binary Weighting Matrix
- Programmatic or Spreadsheet based model creation (via Excel
workbooks or Google Sheets).
- Command-line interface for quick model inspection and interactive weighting


Install
Expand All @@ -30,6 +31,34 @@ Install
pip install vdd


CLI
---

`vdd` ships with a command-line interface for the two core workflows.

### Inspect a CODA model

Load a CODA model from an Excel file and print the overall design merit
alongside a per-requirement satisfaction summary:

vdd coda path/to/model.xlsx

By default the compact Excel format is assumed. Pass `--parser full` for the
standard format:

vdd coda path/to/model.xlsx --parser full

### Weight requirements interactively

Step through a pairwise comparison of requirements and print the resulting
normalised scores:

vdd requirements weight "Lightweight" "Stiff" "Durable"

Questions are shuffled by default. Pass `--no-shuffle` to work through them
in a fixed order.


Documentation
-------

Expand All @@ -42,10 +71,9 @@ Currently just stored in the repo.
Development
-----------

`poetry` must be installed in the local development environment as a pre-requisite. In the repository root:

In the repository root:

poetry install
uv sync


### Releases
Expand Down
16 changes: 9 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@ authors = [
]
license = {text = "MIT"}
readme = "README.md"
requires-python = ">=3.8,<3.13"
requires-python = ">=3.11"
dependencies = [
"platformdirs>=2.6.2",
"pygsheets>=2.0.5,<3",
"pandas>=1.3.5",
"numpy>=1.21.1",
"pandas>=2.0",
"numpy>=1.26",
"xlrd>=2.0,<3",
"pytz>=2022.6",
"openpyxl>=3.0.10,<4",
"typer[all]>=0.12",
]

[project.scripts]
vdd = "vdd.cli:app"

[dependency-groups]
dev = [
"pytest>=7.1.2,<8",
"pytest-mock>=3.8.2,<4",
"pytest>=8",
"pytest-mock>=3.8.2",
"coverage>=7",
"ddt>=1.6.0,<2",
"pytest-cov>=4",
]

Expand Down
1,277 changes: 359 additions & 918 deletions uv.lock

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions vdd/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""vdd command-line interface."""
from pathlib import Path
from typing import Annotated

import typer
from rich.console import Console
from rich.table import Table

app = typer.Typer(help="Value-Driven Design tools.")
requirements_app = typer.Typer(help="Requirements weighting tools.")
app.add_typer(requirements_app, name="requirements")

console = Console()


def _bar(value: float, width: int = 10) -> str:
filled = round(value * width)
return "█" * filled + "░" * (width - filled) + f" {value:.2f}"


@app.command()
def coda(
file: Annotated[Path, typer.Argument(help="Excel model file")],
parser: Annotated[str, typer.Option(help="Parser: compact or full")] = "compact",
) -> None:
"""Load a CODA model from an Excel file and display results."""
from vdd.coda import CODA
from vdd.coda.io import CompactExcelParser, ExcelParser

parser_cls = CompactExcelParser if parser == "compact" else ExcelParser
model = CODA.from_excel(file, parser_class=parser_cls)

# Default unset characteristic values to the midpoint of their bounds.
for char in model.characteristics:
try:
char.value # noqa: B018
except AttributeError:
lo, hi = char.limits
char.value = ((lo or 0.0) + (hi or 1.0)) / 2

console.print(f"Merit: {model.merit:.2f}")

table = Table(show_header=True, header_style="bold")
table.add_column("Requirement")
table.add_column("Weight", justify="right")
table.add_column("Satisfaction")

weights = model.weight[:, 0]
satisfactions = model.satisfaction[:, 0]

for req, w, s in zip(model.requirements, weights, satisfactions):
table.add_row(req.name, f"{w:.2f}", _bar(s))

console.print(table)


@requirements_app.command("weight")
def weight(
reqs: Annotated[list[str], typer.Argument(help="Requirement names")],
shuffle: Annotated[bool, typer.Option(help="Shuffle question order")] = True,
) -> None:
"""Interactively weight requirements using pairwise comparison."""
from vdd.requirements.models import BinWM

bwm = BinWM(*reqs)
bwm.prompt(shuffle=shuffle)

table = Table(show_header=True, header_style="bold")
table.add_column("Requirement")
table.add_column("Score")

for req, score in zip(bwm.requirements, bwm.score):
table.add_row(req, _bar(score))

console.print(table)
Binary file modified vdd/coda/data/sample_compact.xlsx
Binary file not shown.
Loading