pandid 0.1.0rc1__tar.gz
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.
- pandid-0.1.0rc1/.github/workflows/ci.yml +69 -0
- pandid-0.1.0rc1/.github/workflows/release.yml +65 -0
- pandid-0.1.0rc1/.gitignore +43 -0
- pandid-0.1.0rc1/.pre-commit-config.yaml +19 -0
- pandid-0.1.0rc1/CHANGELOG.md +156 -0
- pandid-0.1.0rc1/CONTRIBUTING.md +301 -0
- pandid-0.1.0rc1/LICENSE +122 -0
- pandid-0.1.0rc1/LICENSE-APACHE +202 -0
- pandid-0.1.0rc1/NOTICE +96 -0
- pandid-0.1.0rc1/PKG-INFO +294 -0
- pandid-0.1.0rc1/README.md +252 -0
- pandid-0.1.0rc1/docs/api.md +2450 -0
- pandid-0.1.0rc1/docs/gallery/01_ammonia_loop.png +0 -0
- pandid-0.1.0rc1/docs/gallery/01_ammonia_loop.svg +61 -0
- pandid-0.1.0rc1/docs/gallery/02_manual_layout.png +0 -0
- pandid-0.1.0rc1/docs/gallery/02_manual_layout.svg +43 -0
- pandid-0.1.0rc1/docs/gallery/03_distillation_train.png +0 -0
- pandid-0.1.0rc1/docs/gallery/03_distillation_train.svg +427 -0
- pandid-0.1.0rc1/docs/gallery/04_control_loop.png +0 -0
- pandid-0.1.0rc1/docs/gallery/04_control_loop.svg +93 -0
- pandid-0.1.0rc1/docs/gallery/05_reactor_recycle.png +0 -0
- pandid-0.1.0rc1/docs/gallery/05_reactor_recycle.svg +73 -0
- pandid-0.1.0rc1/docs/gallery/06_column_reflux.png +0 -0
- pandid-0.1.0rc1/docs/gallery/06_column_reflux.svg +78 -0
- pandid-0.1.0rc1/docs/gallery/07_metering_skid.png +0 -0
- pandid-0.1.0rc1/docs/gallery/07_metering_skid.svg +80 -0
- pandid-0.1.0rc1/docs/gallery/08_from_data.png +0 -0
- pandid-0.1.0rc1/docs/gallery/08_from_data.svg +324 -0
- pandid-0.1.0rc1/docs/gallery/09_line_numbers.png +0 -0
- pandid-0.1.0rc1/docs/gallery/09_line_numbers.svg +219 -0
- pandid-0.1.0rc1/docs/gallery/10_ethanol_pfd.png +0 -0
- pandid-0.1.0rc1/docs/gallery/10_ethanol_pfd.svg +586 -0
- pandid-0.1.0rc1/docs/gallery/11_ethanol_pid.png +0 -0
- pandid-0.1.0rc1/docs/gallery/11_ethanol_pid.svg +600 -0
- pandid-0.1.0rc1/docs/gallery/README.md +230 -0
- pandid-0.1.0rc1/examples/01_ammonia_loop.py +24 -0
- pandid-0.1.0rc1/examples/02_manual_layout.py +55 -0
- pandid-0.1.0rc1/examples/03_distillation_train.py +159 -0
- pandid-0.1.0rc1/examples/04_control_loop.py +95 -0
- pandid-0.1.0rc1/examples/05_reactor_recycle.py +47 -0
- pandid-0.1.0rc1/examples/06_column_reflux.py +106 -0
- pandid-0.1.0rc1/examples/07_metering_skid.py +88 -0
- pandid-0.1.0rc1/examples/08_from_data.py +178 -0
- pandid-0.1.0rc1/examples/09_line_numbers.py +138 -0
- pandid-0.1.0rc1/examples/10_ethanol_pfd.py +272 -0
- pandid-0.1.0rc1/examples/11_ethanol_pid.py +440 -0
- pandid-0.1.0rc1/examples/_bootstrap.py +29 -0
- pandid-0.1.0rc1/pandid/__init__.py +19 -0
- pandid-0.1.0rc1/pandid/__main__.py +12 -0
- pandid-0.1.0rc1/pandid/cli.py +296 -0
- pandid-0.1.0rc1/pandid/components.py +15 -0
- pandid-0.1.0rc1/pandid/document.py +379 -0
- pandid-0.1.0rc1/pandid/flowsheet.py +917 -0
- pandid-0.1.0rc1/pandid/geometry.py +156 -0
- pandid-0.1.0rc1/pandid/layout/__init__.py +70 -0
- pandid-0.1.0rc1/pandid/layout/attach.py +171 -0
- pandid-0.1.0rc1/pandid/layout/coordinates.py +181 -0
- pandid-0.1.0rc1/pandid/layout/cycles.py +77 -0
- pandid-0.1.0rc1/pandid/layout/faces.py +148 -0
- pandid-0.1.0rc1/pandid/layout/layering.py +66 -0
- pandid-0.1.0rc1/pandid/layout/ordering.py +126 -0
- pandid-0.1.0rc1/pandid/loops.py +124 -0
- pandid-0.1.0rc1/pandid/portgeom.py +412 -0
- pandid-0.1.0rc1/pandid/ports.py +32 -0
- pandid-0.1.0rc1/pandid/py.typed +0 -0
- pandid-0.1.0rc1/pandid/render/__init__.py +12 -0
- pandid-0.1.0rc1/pandid/render/_vendored_symbols.py +951 -0
- pandid-0.1.0rc1/pandid/render/furniture.py +488 -0
- pandid-0.1.0rc1/pandid/render/svg.py +1455 -0
- pandid-0.1.0rc1/pandid/render/symbols.py +1243 -0
- pandid-0.1.0rc1/pandid/routing/__init__.py +127 -0
- pandid-0.1.0rc1/pandid/routing/astar.py +144 -0
- pandid-0.1.0rc1/pandid/routing/separation.py +193 -0
- pandid-0.1.0rc1/pandid/routing/visibility.py +266 -0
- pandid-0.1.0rc1/pandid/spec.py +1104 -0
- pandid-0.1.0rc1/pandid/state.py +24 -0
- pandid-0.1.0rc1/pandid/stations.py +211 -0
- pandid-0.1.0rc1/pandid/streams.py +110 -0
- pandid-0.1.0rc1/pandid/units.py +1448 -0
- pandid-0.1.0rc1/pandid/validate.py +274 -0
- pandid-0.1.0rc1/pyproject.toml +80 -0
- pandid-0.1.0rc1/scripts/mxgraph_to_svg.py +303 -0
- pandid-0.1.0rc1/scripts/symbol_sheet.py +81 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/agitators.xml +190 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/compressors.xml +373 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/driers.xml +235 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/filters.xml +429 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/fittings.xml +770 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/flow_sensors.xml +328 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/heat_exchangers.xml +809 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/instruments.xml +571 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/mixers.xml +97 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/piping.xml +882 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/pumps.xml +600 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/separators.xml +594 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/valves.xml +846 -0
- pandid-0.1.0rc1/scripts/vendor_data/drawio/vessels.xml +1847 -0
- pandid-0.1.0rc1/scripts/vendor_symbols.py +1265 -0
- pandid-0.1.0rc1/tests/conftest.py +47 -0
- pandid-0.1.0rc1/tests/golden/01_ammonia_loop.svg +61 -0
- pandid-0.1.0rc1/tests/golden/02_manual_layout.svg +43 -0
- pandid-0.1.0rc1/tests/golden/03_distillation_train.svg +427 -0
- pandid-0.1.0rc1/tests/golden/04_control_loop.svg +93 -0
- pandid-0.1.0rc1/tests/golden/05_reactor_recycle.svg +73 -0
- pandid-0.1.0rc1/tests/golden/06_column_reflux.svg +78 -0
- pandid-0.1.0rc1/tests/golden/07_metering_skid.svg +80 -0
- pandid-0.1.0rc1/tests/golden/08_from_data.svg +324 -0
- pandid-0.1.0rc1/tests/golden/09_line_numbers.svg +219 -0
- pandid-0.1.0rc1/tests/golden/README.md +36 -0
- pandid-0.1.0rc1/tests/test_cli.py +302 -0
- pandid-0.1.0rc1/tests/test_conveyor.py +286 -0
- pandid-0.1.0rc1/tests/test_custom_units.py +237 -0
- pandid-0.1.0rc1/tests/test_faces.py +199 -0
- pandid-0.1.0rc1/tests/test_fail_position.py +376 -0
- pandid-0.1.0rc1/tests/test_flowsheet.py +181 -0
- pandid-0.1.0rc1/tests/test_geometry.py +36 -0
- pandid-0.1.0rc1/tests/test_geometry_model.py +71 -0
- pandid-0.1.0rc1/tests/test_golden.py +566 -0
- pandid-0.1.0rc1/tests/test_gravity_orientation.py +214 -0
- pandid-0.1.0rc1/tests/test_instruments.py +557 -0
- pandid-0.1.0rc1/tests/test_integration.py +57 -0
- pandid-0.1.0rc1/tests/test_layout.py +129 -0
- pandid-0.1.0rc1/tests/test_line_numbers.py +398 -0
- pandid-0.1.0rc1/tests/test_location_reference.py +150 -0
- pandid-0.1.0rc1/tests/test_loops.py +295 -0
- pandid-0.1.0rc1/tests/test_model.py +35 -0
- pandid-0.1.0rc1/tests/test_normally_closed.py +447 -0
- pandid-0.1.0rc1/tests/test_packaging.py +61 -0
- pandid-0.1.0rc1/tests/test_recycle.py +36 -0
- pandid-0.1.0rc1/tests/test_reducer.py +261 -0
- pandid-0.1.0rc1/tests/test_render.py +223 -0
- pandid-0.1.0rc1/tests/test_render_api.py +416 -0
- pandid-0.1.0rc1/tests/test_route_invariants.py +246 -0
- pandid-0.1.0rc1/tests/test_routing.py +99 -0
- pandid-0.1.0rc1/tests/test_routing_quality.py +381 -0
- pandid-0.1.0rc1/tests/test_scaffold.py +4 -0
- pandid-0.1.0rc1/tests/test_serialize.py +49 -0
- pandid-0.1.0rc1/tests/test_spec.py +756 -0
- pandid-0.1.0rc1/tests/test_state.py +38 -0
- pandid-0.1.0rc1/tests/test_stream_numbering.py +136 -0
- pandid-0.1.0rc1/tests/test_symbol_invariants.py +1504 -0
- pandid-0.1.0rc1/tests/test_tee.py +455 -0
- pandid-0.1.0rc1/tests/test_titleblock.py +546 -0
- pandid-0.1.0rc1/tests/test_transforms.py +369 -0
- pandid-0.1.0rc1/tests/test_units.py +253 -0
- pandid-0.1.0rc1/tests/test_units_api.py +56 -0
- pandid-0.1.0rc1/tests/test_utility_headers.py +213 -0
- pandid-0.1.0rc1/tests/test_validate.py +150 -0
- pandid-0.1.0rc1/tests/test_valve_station.py +433 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
# The release workflow calls these same gates on the tagged commit, so a
|
|
8
|
+
# release runs the matrix that is actually maintained rather than a copy.
|
|
9
|
+
workflow_call:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
# Cancel superseded runs on the same ref (e.g. rapid pushes to a PR branch).
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
lint:
|
|
21
|
+
name: Lint (ruff)
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
- name: Install
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
pip install -e ".[dev]"
|
|
32
|
+
- name: ruff check
|
|
33
|
+
run: ruff check .
|
|
34
|
+
- name: ruff format (tests must stay formatted)
|
|
35
|
+
run: ruff format --check tests
|
|
36
|
+
|
|
37
|
+
typecheck:
|
|
38
|
+
name: Type check (mypy)
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- uses: actions/setup-python@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: "3.12"
|
|
45
|
+
- name: Install
|
|
46
|
+
run: |
|
|
47
|
+
python -m pip install --upgrade pip
|
|
48
|
+
pip install -e ".[dev]"
|
|
49
|
+
- name: mypy
|
|
50
|
+
run: mypy pandid
|
|
51
|
+
|
|
52
|
+
test:
|
|
53
|
+
name: Tests (py${{ matrix.python-version }})
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
strategy:
|
|
56
|
+
fail-fast: false
|
|
57
|
+
matrix:
|
|
58
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
- uses: actions/setup-python@v5
|
|
62
|
+
with:
|
|
63
|
+
python-version: ${{ matrix.python-version }}
|
|
64
|
+
- name: Install
|
|
65
|
+
run: |
|
|
66
|
+
python -m pip install --upgrade pip
|
|
67
|
+
pip install -e ".[dev]"
|
|
68
|
+
- name: pytest
|
|
69
|
+
run: pytest -q
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing is driven by the tag, not by a branch: pushing `v0.1.0` cuts the
|
|
4
|
+
# release. The tag must name the version in pandid/__init__.py — the build job
|
|
5
|
+
# below refuses to go on if it does not.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
# Re-run the full CI matrix against the tagged commit: a tag can point at a
|
|
15
|
+
# commit that never went through a pull request.
|
|
16
|
+
gates:
|
|
17
|
+
name: Gates
|
|
18
|
+
uses: ./.github/workflows/ci.yml
|
|
19
|
+
|
|
20
|
+
build:
|
|
21
|
+
name: Build sdist + wheel
|
|
22
|
+
needs: [gates]
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
- name: Tag must match pandid.__version__
|
|
30
|
+
run: |
|
|
31
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
32
|
+
src="$(python -c 'import pandid; print(pandid.__version__)')"
|
|
33
|
+
if [ "$tag" != "$src" ]; then
|
|
34
|
+
echo "::error::tag $GITHUB_REF_NAME does not name pandid.__version__ ($src)"
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
- name: Build
|
|
38
|
+
run: |
|
|
39
|
+
python -m pip install --upgrade pip
|
|
40
|
+
pip install build twine
|
|
41
|
+
python -m build
|
|
42
|
+
- name: Check the metadata PyPI will render
|
|
43
|
+
run: twine check --strict dist/*
|
|
44
|
+
- uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
|
|
49
|
+
publish:
|
|
50
|
+
name: Publish to PyPI
|
|
51
|
+
needs: [build]
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
# Named environment so the repository owner can require a manual approval,
|
|
54
|
+
# and so the PyPI trusted publisher can be scoped to it, before any upload.
|
|
55
|
+
environment:
|
|
56
|
+
name: pypi
|
|
57
|
+
url: https://pypi.org/p/pandid
|
|
58
|
+
permissions:
|
|
59
|
+
id-token: write # mint the OIDC token PyPI trusted publishing exchanges
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/download-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist/
|
|
65
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Byte-compiled / optimized files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
|
|
5
|
+
# Packaging / build
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
|
|
14
|
+
# Tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
|
|
19
|
+
# uv lockfile (not committed for this library)
|
|
20
|
+
uv.lock
|
|
21
|
+
|
|
22
|
+
# Generated diagram output (reproducible from examples/)
|
|
23
|
+
*.svg
|
|
24
|
+
# ...but the golden fixtures are source, not output: the regression suite has
|
|
25
|
+
# nothing to compare against if these are ignored.
|
|
26
|
+
!tests/golden/*.svg
|
|
27
|
+
# ...and the gallery is documentation: docs/gallery/README.md has nothing to
|
|
28
|
+
# show if these are ignored. Regenerate by running examples/ (see that README).
|
|
29
|
+
!docs/gallery/*.svg
|
|
30
|
+
|
|
31
|
+
# Agent / harness working files
|
|
32
|
+
.agents/
|
|
33
|
+
.superpowers/
|
|
34
|
+
skills-lock.json
|
|
35
|
+
|
|
36
|
+
# local render artifacts
|
|
37
|
+
renders/
|
|
38
|
+
|
|
39
|
+
# Reference drawings (third-party coursework; kept local, not redistributed)
|
|
40
|
+
professional_examples/
|
|
41
|
+
|
|
42
|
+
# Agent worktrees (created per-run under .claude/, never part of the project)
|
|
43
|
+
.claude/worktrees/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Local commit-time checkers, mirroring CI. Enable with:
|
|
2
|
+
# pip install pre-commit && pre-commit install
|
|
3
|
+
repos:
|
|
4
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
5
|
+
rev: v0.15.14
|
|
6
|
+
hooks:
|
|
7
|
+
- id: ruff # lint the whole tree (auto-fixes what it safely can)
|
|
8
|
+
args: [--fix]
|
|
9
|
+
- id: ruff-format # keep the test suite formatted
|
|
10
|
+
files: ^tests/
|
|
11
|
+
|
|
12
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
13
|
+
rev: v5.0.0
|
|
14
|
+
hooks:
|
|
15
|
+
- id: trailing-whitespace
|
|
16
|
+
- id: end-of-file-fixer
|
|
17
|
+
- id: check-yaml
|
|
18
|
+
- id: check-toml
|
|
19
|
+
- id: check-merge-conflict
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-07-28
|
|
9
|
+
|
|
10
|
+
First public release. Nothing was published before it, so everything below is the
|
|
11
|
+
initial feature set rather than a change to something users have, and the entries
|
|
12
|
+
are headlines rather than a description of the library. `README.md` is the tour,
|
|
13
|
+
[`docs/api.md`](docs/api.md) is the reference, and
|
|
14
|
+
[`docs/gallery/README.md`](docs/gallery/README.md) walks the eleven examples.
|
|
15
|
+
Later releases will list changes rather than capabilities.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Topology.** `Flowsheet` is the container and the single source of truth for
|
|
20
|
+
connectivity, with `add()`, `connect()`, `add_component()` and
|
|
21
|
+
`add_annotation()`. 28 typed `Unit` classes declare named ports reachable as
|
|
22
|
+
`unit.ports[name]` or as attributes (`pump.suction`), and a typo raises an
|
|
23
|
+
error naming the real ports. `connect()` validates every connection: outlet to
|
|
24
|
+
inlet only, both units on the same flowsheet, one stream per port, and signal
|
|
25
|
+
against process. A `Unit` subclass of your own declaring its `kind` and
|
|
26
|
+
`PORTS` is laid out, routed and drawn like a shipped class.
|
|
27
|
+
- **Assemblies and branching.** `Tee` is the pipe tee, drawn as bare pipe and
|
|
28
|
+
scheduled nowhere, so a bypass, a drain, a vent or a PSV takeoff no longer puts
|
|
29
|
+
equipment on the sheet that the plant does not contain.
|
|
30
|
+
`Flowsheet.add_valve_station()` builds the eight devices and four tees a
|
|
31
|
+
control valve is installed in, in one call.
|
|
32
|
+
- **Numbering.** Automatic stream numbers carry one number *through* inline
|
|
33
|
+
valves, reducers and fittings. Line numbers (`size`, `schedule`, `service`,
|
|
34
|
+
`spec`, `insulation`, plus an auto-filled `sequence`) are assembled by
|
|
35
|
+
`line_numbering_scheme` and assigned by the same pass, so a line number
|
|
36
|
+
survives an in-line fitting and breaks where the spec break is marked.
|
|
37
|
+
- **Layout.** Sugiyama-style automatic layout: cycle breaking, layer assignment,
|
|
38
|
+
crossing reduction and coordinate assignment, with the main flow line
|
|
39
|
+
straightened onto one axis. The geometry model separates intent (`Pin`, written
|
|
40
|
+
only by `Unit.pin()`) from result (`Frame`, written only by the engine), which
|
|
41
|
+
is what makes `layout()` idempotent. `pin()` takes a grid cell or exact
|
|
42
|
+
coordinates, a quarter turn, a mirror, and `port=` to place a **nozzle** rather
|
|
43
|
+
than a corner. Port faces and equipment tags are then chosen automatically from
|
|
44
|
+
where each peer actually landed, and `nozzle()` overrides that pick where a
|
|
45
|
+
drawing convention has to be stated.
|
|
46
|
+
- **Routing.** Orthogonal A\* over a visibility graph, with port anchors projected
|
|
47
|
+
onto unit boundaries, used-edge penalties so runs do not overlap, crossing
|
|
48
|
+
jump-gaps, and separation of co-located parallel runs. `Stream.via([...])`
|
|
49
|
+
forces explicit waypoints. `route()` places attached instruments and re-routes
|
|
50
|
+
until the two agree rather than trading a fixed number of passes.
|
|
51
|
+
- **Rendering.** SVG with **no runtime dependencies**; `.pdf` and `.png` go
|
|
52
|
+
through the optional `cairosvg` backend. `page_size="A4"`..`"A0"` draws a sheet
|
|
53
|
+
of exactly that ISO 216 size, `border="zone"` rules the ASME-style zone frame,
|
|
54
|
+
and `diagram="p&id"` draws process lines without arrowheads. Signal lines are
|
|
55
|
+
drawn at half the weight of process pipe, the 2:1 ratio ISO 15519-1 §6.2
|
|
56
|
+
requires. A `TitleBlock`, `Revision` rows, and docked `Annotation` / `TableBox`
|
|
57
|
+
furniture (`equipment_list()`, `notes()`, `legend()`) are sheet furniture drawn
|
|
58
|
+
whatever the border, and every cell is measured before it is written into, so
|
|
59
|
+
text that cannot fit is reported on `fs.warnings` rather than drawn across a
|
|
60
|
+
rule.
|
|
61
|
+
- **Symbols.** 139 registered `(kind, variant)` pairs across 28 kinds, generated
|
|
62
|
+
from the draw.io / diagrams.net P&ID stencils by `scripts/vendor_symbols.py`
|
|
63
|
+
and matched to ISO 10628-2 where a symbol exists. Feed/Product flags, the
|
|
64
|
+
variable-port Mixer and Splitter, the pipe tee and the ANSI/ISA-5.1 balloons are
|
|
65
|
+
hand-drawn originals. Every port is checked to land on drawn ink, at every box
|
|
66
|
+
shape a unit can be given. `variant=` is checked against the registry, so a
|
|
67
|
+
typo raises naming the nearest match rather than reaching the printer.
|
|
68
|
+
- **Instrumentation (ISA-5.1).** `add_instrument()` and the `Instrument` unit
|
|
69
|
+
draw the functional letters over a loop number, in six balloon variants plus
|
|
70
|
+
the two trip squares ANSI/ISA-5.1-2009 distinguishes. `Instrument.attach()`
|
|
71
|
+
anchors a balloon to the stream or equipment it reads, with an impulse line to
|
|
72
|
+
the tap. Typed signal lines (`electric`, `pneumatic`, `data`, `software`,
|
|
73
|
+
`capillary`) are legal only between two signal connections.
|
|
74
|
+
`Flowsheet.add_loop()` declares a control loop, so the loop number is typed
|
|
75
|
+
once and each balloon's own letters are checked against it.
|
|
76
|
+
- **Valve marking.** `Valve(normal_position="closed")` darkens the body
|
|
77
|
+
(PIP PIC001 4.2.2.7) or writes `NC` beside it where a filled body would hide
|
|
78
|
+
the device (ISO 15519-1 §11.4.5), and refuses on a control or relief valve
|
|
79
|
+
(clause 4.2.2.10). `Valve(fail=...)` writes the six ANSI/ISA-5.1-2009 Table
|
|
80
|
+
5.4.4 codes beside an actuated valve. `Fitting(variant="blind")` takes the same
|
|
81
|
+
`normal_position` and changes shape rather than fill.
|
|
82
|
+
- **Validation.** `Flowsheet.validate()` returns `Issue` records, errors first.
|
|
83
|
+
Errors such as overlapping pinned units raise from `render()` rather than emit
|
|
84
|
+
a silently wrong drawing; warnings such as a route crossing a unit body, a tag
|
|
85
|
+
whose letters are out of ISO 15519-2 §5.2.4 order, or a gravity-dependent
|
|
86
|
+
symbol given a quarter turn collect on `fs.warnings`.
|
|
87
|
+
- **Spec format.** `Flowsheet.to_dict()` / `from_dict()` round-trip the whole
|
|
88
|
+
topology as JSON-safe data, and `pandid.spec` reads the same shape from YAML or
|
|
89
|
+
JSON. An unknown key is rejected rather than ignored, and the message names the
|
|
90
|
+
key it was probably meant to be.
|
|
91
|
+
- **Command line.** A `pandid` command, installed with the distribution:
|
|
92
|
+
`pandid draw plant.yaml -o plant.pdf --page-size A3 --border zone`,
|
|
93
|
+
`pandid validate plant.yaml`, and `pandid symbols --kind valve`. Exit codes a
|
|
94
|
+
build script can gate on, and every user-provokable failure is one line on
|
|
95
|
+
stderr rather than a traceback. Built on `argparse`, so the package still has
|
|
96
|
+
no runtime dependencies.
|
|
97
|
+
- **Packaging and tooling.** `pandid/py.typed` (PEP 561), a golden-SVG regression
|
|
98
|
+
suite, symbol- and route-invariant suites, CI on Python 3.10 to 3.13, and a
|
|
99
|
+
release workflow that checks the tag against `pandid.__version__` and publishes
|
|
100
|
+
over PyPI Trusted Publishing. `pandid.__version__` is the only place the version
|
|
101
|
+
is written; the build backend reads it from there.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
Two attributes were renamed on the way to this release. Nothing was published, so
|
|
106
|
+
there is no alias and the old spellings simply do not exist. **Both are also spec
|
|
107
|
+
keys**, so a YAML or JSON file written against a pre-release checkout has to be
|
|
108
|
+
edited: the reader rejects an unknown key rather than ignoring it, and names the
|
|
109
|
+
new one.
|
|
110
|
+
|
|
111
|
+
- `Unit.significant` is now **`Unit.new_line_number`**, and the unit spec key
|
|
112
|
+
`significant:` is now `new_line_number:`. It marks the inline item at which a
|
|
113
|
+
line number breaks and a new one starts, which is what the new name says and
|
|
114
|
+
the old one did not.
|
|
115
|
+
- `connect(tear_hint=...)` and `Stream.tear_hint` are now
|
|
116
|
+
**`draw_as_recycle`**, and the stream spec key `tear_hint:` is now
|
|
117
|
+
`draw_as_recycle:`. It marks a stream to be drawn as a recycle loop.
|
|
118
|
+
|
|
119
|
+
Neither rename changes a drawn sheet: every golden fixture and all eleven example
|
|
120
|
+
SVGs are byte-identical across it.
|
|
121
|
+
|
|
122
|
+
### Removed
|
|
123
|
+
|
|
124
|
+
The deprecated aliases, on the same reasoning: nothing has been published, so
|
|
125
|
+
keeping them preserves compatibility with code that cannot exist. This follows the
|
|
126
|
+
clean breaks already made for `hot_*` to `shell_*` on heat-exchanger nozzles.
|
|
127
|
+
|
|
128
|
+
- `styling=` on `to_svg()` / `render()`. Use `border="zone"` and
|
|
129
|
+
`diagram="p&id"`, which are independent and name the two things it bundled.
|
|
130
|
+
- `anchor=` on `Annotation`, `TableBox`, `equipment_list()`, `notes()` and
|
|
131
|
+
`legend()`. Use `align=`.
|
|
132
|
+
- `Unit.port_face()`. Use `Unit.nozzle()`, whose `face` is the compass point on
|
|
133
|
+
the finished sheet. The two disagreed on a rotated or mirrored unit, since
|
|
134
|
+
`port_face()` read its face in the symbol's own frame, so **rewrite each call
|
|
135
|
+
site with the face the reader sees** rather than substituting the name (#26).
|
|
136
|
+
- `Symbol.port_alts` and `Symbol.free_ports`. Declare the whole menu, home
|
|
137
|
+
placement included, in `Symbol.port_faces`; the faceless set is
|
|
138
|
+
`Symbol.faceless_ports`.
|
|
139
|
+
- `Unit._PORTS`. Use `Unit.PORTS`, the same list of `(name, direction, role)`
|
|
140
|
+
tuples under a name that does not tell the one attribute a subclass must set
|
|
141
|
+
that it is private.
|
|
142
|
+
|
|
143
|
+
### Licence
|
|
144
|
+
|
|
145
|
+
Licensed under the **PolyForm Small Business License 1.0.0**: free for
|
|
146
|
+
individuals, research, teaching, and companies under 100 people and 1,000,000 USD
|
|
147
|
+
revenue; a commercial licence is required above either threshold. Source-available
|
|
148
|
+
rather than OSI open source.
|
|
149
|
+
|
|
150
|
+
The vendored draw.io symbol geometry remains **Apache-2.0**, as that licence
|
|
151
|
+
requires. The stencil artwork carries one additional field-of-use restriction on
|
|
152
|
+
top of that grant, naming Atlassian products and marketplace distribution and
|
|
153
|
+
excluding diagram output; `NOTICE` reproduces it in full, lists exactly which
|
|
154
|
+
files fall under which licence, and both texts ship in the distribution.
|
|
155
|
+
|
|
156
|
+
[0.1.0]: https://github.com/Alpha9463/pandid/releases/tag/v0.1.0
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Contributing to `pandid`
|
|
2
|
+
|
|
3
|
+
Thanks for helping. This covers setup, the checks that have to pass, and the
|
|
4
|
+
four things about this codebase a new contributor will otherwise get wrong.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
Python 3.10+ (CI tests 3.10 – 3.13). From a checkout:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install -e '.[dev]' # pytest, ruff, mypy
|
|
12
|
+
pip install -e '.[dev,pdf]' # ...plus cairosvg, if you need PDF/PNG export
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package has **zero runtime dependencies** and must stay that way: `pandid/` may
|
|
16
|
+
import only the Python standard library. `cairosvg` is optional and is imported
|
|
17
|
+
lazily inside `Flowsheet.render()`, only when the output path ends in `.pdf` or
|
|
18
|
+
`.png`.
|
|
19
|
+
|
|
20
|
+
Optionally install the commit hooks, which mirror the CI lint gates:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pre-commit && pre-commit install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The gates
|
|
27
|
+
|
|
28
|
+
All four must pass. CI (`.github/workflows/ci.yml`) runs the same four.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pytest -q # tests (CI: pytest -q on 3.10/3.11/3.12/3.13)
|
|
32
|
+
python -m ruff check . # lint the whole tree
|
|
33
|
+
python -m ruff format --check tests # tests/ must stay ruff-formatted
|
|
34
|
+
python -m mypy pandid # type check (blocking, not advisory)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`ruff format` is enforced on `tests/` only. The rest of the tree is linted but
|
|
38
|
+
not auto-formatted, so don't reformat `pandid/` in a feature PR.
|
|
39
|
+
|
|
40
|
+
Keep a PR to one concern. If a change touches rendering, say so and show what
|
|
41
|
+
moved (see *Goldens* below).
|
|
42
|
+
|
|
43
|
+
## Where things live
|
|
44
|
+
|
|
45
|
+
1. **Topology** (`pandid/flowsheet.py`, `pandid/units.py`, `pandid/ports.py`,
|
|
46
|
+
`pandid/streams.py`) holds units, ports and stream connectivity.
|
|
47
|
+
2. **Geometry.** `pandid/layout/` runs Sugiyama layering, then ordering, then
|
|
48
|
+
coordinates, emitting each unit's resolved `Frame`, and finally port-face
|
|
49
|
+
selection and label placement. `pandid/portgeom.py` is the single source of
|
|
50
|
+
truth for port geometry, and `pandid/routing/` is the visibility graph and
|
|
51
|
+
the A\* search over it.
|
|
52
|
+
3. **Render** (`pandid/render/`) produces the SVG output and the symbol
|
|
53
|
+
registry, with `pandid/validate.py` and `pandid/document.py` beside it.
|
|
54
|
+
|
|
55
|
+
Geometry separates *intent* (`Pin`, written by `pin()`) from *result* (`Frame`,
|
|
56
|
+
written by the layout engine), which is what makes `layout()` idempotent.
|
|
57
|
+
|
|
58
|
+
`scripts/vendor_symbols.py` generates the symbol library into
|
|
59
|
+
`pandid/render/_vendored_symbols.py`, converting mxGraph stencil XML to SVG via
|
|
60
|
+
`scripts/mxgraph_to_svg.py`. `scripts/symbol_sheet.py` renders a catalogue.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 1. Symbols come from the vendored stencils, never from hand-editing
|
|
65
|
+
|
|
66
|
+
`pandid/render/_vendored_symbols.py` is **generated**, and its own docstring says
|
|
67
|
+
so: draw.io-derived equipment symbols under Apache-2.0, generated by
|
|
68
|
+
`scripts/vendor_symbols.py`, not to be edited by hand, with attribution in
|
|
69
|
+
`NOTICE`. The file is regenerated wholesale, so a hand edit is lost the next
|
|
70
|
+
time anyone runs the generator and the drawing silently reverts.
|
|
71
|
+
|
|
72
|
+
The pipeline is:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
scripts/vendor_data/drawio/*.xml mxGraph stencil XML, vendored from jgraph/drawio
|
|
76
|
+
│
|
|
77
|
+
├── scripts/mxgraph_to_svg.py stencil XML -> plain SVG
|
|
78
|
+
│
|
|
79
|
+
└── scripts/vendor_symbols.py KIND_MAP: (kind, variant) -> stencil shape + ports
|
|
80
|
+
│
|
|
81
|
+
▼
|
|
82
|
+
pandid/render/_vendored_symbols.py generated; registered last, overriding
|
|
83
|
+
the hand-drawn defaults of the same kind
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To add or change an equipment symbol:
|
|
87
|
+
|
|
88
|
+
1. Add an entry to `KIND_MAP` in `scripts/vendor_symbols.py`, naming the stencil
|
|
89
|
+
file, the shape, and where each port lands. A port is either a named draw.io
|
|
90
|
+
`<constraint>` anchor (`"W"`, `"E"`, `"N"`, `"S"`, …) or an explicit
|
|
91
|
+
placement on a bounding-box edge.
|
|
92
|
+
2. Regenerate:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python scripts/vendor_symbols.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
3. Review it visually. `python scripts/symbol_sheet.py out.svg` draws every
|
|
99
|
+
registered `(kind, variant)` in a labelled grid with a red dot on each port
|
|
100
|
+
anchor. It is a dev tool, not part of the package.
|
|
101
|
+
4. Commit the regenerated `_vendored_symbols.py` with your `KIND_MAP` change,
|
|
102
|
+
never one without the other.
|
|
103
|
+
|
|
104
|
+
The shape's `aspect` comes across with it, as `Symbol.stretchable`. The stencil
|
|
105
|
+
author has already answered whether the drawing may be reshaped to fill a box of
|
|
106
|
+
another shape (`aspect="variable"`, mxGraph's default) or has to keep its
|
|
107
|
+
proportions (`aspect="fixed"`), and that is the same question a unit given an
|
|
108
|
+
explicit `width` and `height` asks. Every shape `KIND_MAP` names today is a
|
|
109
|
+
variable one, because the 24 fixed shapes in the stencil set are draw.io's own
|
|
110
|
+
instrument balloons, which `pandid` draws itself, so no generated symbol carries
|
|
111
|
+
the keyword. Do not set it by hand on one: change the stencil mapping, or say so
|
|
112
|
+
in `symbols.py` if the symbol is hand-drawn.
|
|
113
|
+
|
|
114
|
+
Only a handful of symbols are hand-drawn in `pandid/render/symbols.py`: the
|
|
115
|
+
Feed/Product boundary flags, the variable-port Mixer and Splitter, and the
|
|
116
|
+
ISA-5.1 instrument balloons. Those are original primitives, not stencils. New
|
|
117
|
+
*equipment* should come from the stencils, so the sheet stays visually
|
|
118
|
+
consistent and `NOTICE` remains the whole attribution story.
|
|
119
|
+
|
|
120
|
+
### Symbols that have to stretch
|
|
121
|
+
|
|
122
|
+
There is a third case, and today it has one member. The generator emits one
|
|
123
|
+
fixed-size `Symbol` per shape, and a fixed drawing lands in whatever box its
|
|
124
|
+
unit is given: reshaped to fill it, or held to its own proportions and centred
|
|
125
|
+
in it. Some artwork survives neither. Stretching a belt conveyor draws its
|
|
126
|
+
rollers as ellipses; holding its proportions instead draws a short conveyor
|
|
127
|
+
adrift in a long box. A symbol the user is meant to resize along *one* axis,
|
|
128
|
+
leaving the rest of it alone, cannot come out of the generator at all.
|
|
129
|
+
|
|
130
|
+
Such a symbol is written as a *builder* in `pandid/render/symbols.py`, taking the
|
|
131
|
+
size and returning a `Symbol` drawn at it, and `SymbolRegistry.for_unit()`
|
|
132
|
+
resolves it against the unit. Because the artwork is built to the size, the
|
|
133
|
+
`<defs>` entry is per size and the placed box is exactly the box it was drawn
|
|
134
|
+
in, so the scale factor is 1 and every rotation and mirror still works the way
|
|
135
|
+
it does for a fixed symbol.
|
|
136
|
+
|
|
137
|
+
It is still stencil-derived and still Apache-2.0. Adapting one means:
|
|
138
|
+
|
|
139
|
+
1. write the builder in `pandid/render/symbols.py`, with a comment naming the
|
|
140
|
+
stencil file, the shape, and what was changed;
|
|
141
|
+
2. record it in `ADAPTED_ELSEWHERE` in `scripts/vendor_symbols.py`, so the
|
|
142
|
+
mapping table stays the place provenance is looked up;
|
|
143
|
+
3. name it and its file in `NOTICE` section 2. It is not an original primitive
|
|
144
|
+
and does not belong in the section 1 paragraph that lists those.
|
|
145
|
+
|
|
146
|
+
Do not reach for this to avoid vendoring. A symbol with a fixed size goes
|
|
147
|
+
through `KIND_MAP` like everything else.
|
|
148
|
+
|
|
149
|
+
## 2. Every port must land on drawn ink
|
|
150
|
+
|
|
151
|
+
A nozzle floating in whitespace draws a stream that fails to touch its
|
|
152
|
+
equipment. `tests/test_symbol_invariants.py` enforces this over **every**
|
|
153
|
+
registered `(kind, variant)`, not just the ones the examples use:
|
|
154
|
+
|
|
155
|
+
- the symbol SVG parses and declares a `stroke-width`;
|
|
156
|
+
- every port, and every alternate face in `port_faces`, lies inside the symbol's
|
|
157
|
+
declared bounding box;
|
|
158
|
+
- every port and alternate is within `GEOM_TOL` (2 units) of the nearest drawn
|
|
159
|
+
stroke, measured against the SVG primitives flattened to line segments;
|
|
160
|
+
- no two *different* ports resolve to the same point, which would stack two
|
|
161
|
+
streams on one pixel;
|
|
162
|
+
- and the same again on a *rendered sheet*, with every unit forced into box
|
|
163
|
+
shapes nothing is drawn at: much wider than tall, much taller than wide,
|
|
164
|
+
square. A unit given an explicit `width`/`height` is drawn at that box, so
|
|
165
|
+
this is where the artwork and the ports can drift apart: the rectangle the
|
|
166
|
+
drawing lands in is read back out of the SVG and the resolved nozzle is
|
|
167
|
+
measured against it.
|
|
168
|
+
|
|
169
|
+
That last one is what `Symbol.stretchable` answers. A symbol that may be
|
|
170
|
+
reshaped fills the box, and its ports map onto it linearly. One that may not,
|
|
171
|
+
an instrument balloon being a circle because ISA-5.1 says a circle, keeps its
|
|
172
|
+
aspect and is centred, and `pandid.portgeom.ink_box` is what keeps its ports on
|
|
173
|
+
the drawing rather than out in the whitespace beside it. The vendored symbols
|
|
174
|
+
take the answer from the stencil's own `aspect` attribute, as section 1 above
|
|
175
|
+
describes.
|
|
176
|
+
|
|
177
|
+
If that suite fails on your symbol, **move the port onto the geometry**. Adding
|
|
178
|
+
the symbol to `_KNOWN_GEOMETRY_GAPS` is not the fix, and a PR that does it will
|
|
179
|
+
be sent back. That set fences off three pre-existing, *deliberate* cases: the
|
|
180
|
+
pump and compressor suction nozzles, which sit in the mouth of an opening the
|
|
181
|
+
casing is drawn around.
|
|
182
|
+
|
|
183
|
+
The one legitimate escape hatch is `Symbol.faceless_ports`. An instrument
|
|
184
|
+
balloon is a circle, so its signal connections have no face of their own and may
|
|
185
|
+
offer the same faces as each other. Faceless ports are still checked against the
|
|
186
|
+
nozzles that do own a face, and equipment nozzles are never faceless.
|
|
187
|
+
|
|
188
|
+
## 3. Goldens: a changed golden means changed rendering
|
|
189
|
+
|
|
190
|
+
`tests/golden/*.svg` are committed SVG fixtures, one per fixed scenario, and
|
|
191
|
+
`tests/test_golden.py` renders each scenario and compares. They are *source*,
|
|
192
|
+
not build output, which is why `.gitignore`'s blanket `*.svg` rule carries a
|
|
193
|
+
`!tests/golden/*.svg` negation.
|
|
194
|
+
|
|
195
|
+
The comparison runs on normalized text (`_normalize` canonicalizes the `<defs>`
|
|
196
|
+
ordering, which depends on the process hash seed and on nothing about the
|
|
197
|
+
diagram). Every other line compares verbatim, so a real regression fails.
|
|
198
|
+
|
|
199
|
+
After an *intentional* rendering change:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
PANDID_UPDATE_GOLDEN=1 python -m pytest tests/test_golden.py -q
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
(on Windows PowerShell: `$env:PANDID_UPDATE_GOLDEN=1; python -m pytest tests/test_golden.py -q`)
|
|
206
|
+
|
|
207
|
+
Then **read `git diff tests/golden/` before committing**. A golden diff is the
|
|
208
|
+
diagram changing for every user, so the PR has to say what changed and why, with
|
|
209
|
+
the before/after visible. "the test was red" is not a reason. If you cannot
|
|
210
|
+
explain the diff, you have found a bug, not a golden that needed updating.
|
|
211
|
+
|
|
212
|
+
`tests/golden/README.md` explains why the fixtures are rebuilt inline in
|
|
213
|
+
`test_golden.py` rather than by running `examples/*.py`. Those scripts write
|
|
214
|
+
into `examples/`, and `03`'s title block would stamp today's date into the
|
|
215
|
+
fixture. Read it before adding a scenario.
|
|
216
|
+
|
|
217
|
+
## 4. Port geometry goes through `pandid/portgeom.py`
|
|
218
|
+
|
|
219
|
+
Layout, routing, and rendering must never compute a port position
|
|
220
|
+
independently. They all call into `pandid/portgeom.py`, the single source of truth
|
|
221
|
+
for a unit's resolved size and its port coordinates.
|
|
222
|
+
|
|
223
|
+
The bug it prevents actually happened: the renderer forgot a mirror flip the
|
|
224
|
+
router had applied, and every mirrored unit's streams drew visually detached
|
|
225
|
+
from their nozzles.
|
|
226
|
+
|
|
227
|
+
- `resolve_size(unit)` gives the intrinsic `(w, h)` of a unit's placed box.
|
|
228
|
+
- `symbol_to_box(...)` maps a point from the symbol's own coordinates into the
|
|
229
|
+
placed box, applying mirroring **first** and then the clockwise quarter turn,
|
|
230
|
+
which is exactly the order the renderer's SVG transform composes in.
|
|
231
|
+
- `ink_box(...)` gives the rectangle inside that box the artwork actually
|
|
232
|
+
occupies: the whole of it for a symbol that may be reshaped, and the centred,
|
|
233
|
+
aspect-preserving part of it for one that may not. Ports are resolved against
|
|
234
|
+
*that*, so a symbol drawn smaller than its box keeps its nozzles on the
|
|
235
|
+
drawing. `unit_box()` stays the full placed box, since that is the space the
|
|
236
|
+
unit occupies and what the router has to route around.
|
|
237
|
+
- `port_point(unit, frame, name)` is where a stream visually attaches.
|
|
238
|
+
- `port_anchor(unit, frame, name)` is the routing anchor, projected onto the
|
|
239
|
+
bounding-box edge, plus the outward direction.
|
|
240
|
+
- `unit_box(unit, frame)` is the true drawn bounding box the router treats as an
|
|
241
|
+
obstacle.
|
|
242
|
+
|
|
243
|
+
Every one of these takes the resolved box explicitly rather than reading
|
|
244
|
+
`unit.frame`, so the same code works during layout (on a `_Slot`) and afterwards
|
|
245
|
+
(on a `Frame`). If you find yourself writing `frame.x + something` to find a
|
|
246
|
+
nozzle, stop and use `portgeom`.
|
|
247
|
+
|
|
248
|
+
The related invariant on the geometry model: `Pin` is *intent*, written only by
|
|
249
|
+
`Unit.pin()` and never by the engine, and `Frame` is *result*, written only by
|
|
250
|
+
the layout engine. The solver reseeds from `Pin` on every run, which is what
|
|
251
|
+
makes `layout()` idempotent. Don't write coordinates back onto a `Pin`.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Examples and the gallery
|
|
256
|
+
|
|
257
|
+
`examples/*.py` are runnable scripts and are checked by `ruff`. They render into
|
|
258
|
+
`examples/` (gitignored). The gallery under `docs/gallery/` is generated by
|
|
259
|
+
running them and copying the output, as `docs/gallery/README.md` describes. If
|
|
260
|
+
you add an example, add it to the gallery in the same PR, and consider adding
|
|
261
|
+
the scenario to `tests/test_golden.py`.
|
|
262
|
+
|
|
263
|
+
## Releasing
|
|
264
|
+
|
|
265
|
+
The version is written in exactly one place, `pandid/__init__.py`. `pyproject.toml`
|
|
266
|
+
declares it `dynamic` and the build backend reads that literal, so there is no
|
|
267
|
+
second copy to forget. To cut a release, bump `__version__`, move the CHANGELOG
|
|
268
|
+
`[Unreleased]` heading to the new number, merge, then push the matching tag.
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`.github/workflows/release.yml` takes it from there. It re-runs the four gates
|
|
275
|
+
against the tagged commit, refuses to build if the tag and `pandid.__version__`
|
|
276
|
+
disagree, builds the sdist and wheel, and publishes to PyPI over Trusted
|
|
277
|
+
Publishing. Nothing is uploaded from a laptop and there is no API token to leak.
|
|
278
|
+
|
|
279
|
+
## Reporting a bug
|
|
280
|
+
|
|
281
|
+
Include the flowsheet that reproduces it (the topology is usually 15 lines) and
|
|
282
|
+
the SVG you got. `fs.validate()` returns the engine's own findings, errors first
|
|
283
|
+
and then warnings, and is usually the fastest way to describe what went wrong.
|
|
284
|
+
|
|
285
|
+
## Licence
|
|
286
|
+
|
|
287
|
+
The engine is under the [PolyForm Small Business License
|
|
288
|
+
1.0.0](https://polyformproject.org/licenses/small-business/1.0.0), free for
|
|
289
|
+
individuals, research, teaching and small companies. A commercial licence is
|
|
290
|
+
required above 100 people or 1,000,000 USD revenue. It is source-available
|
|
291
|
+
rather than OSI open source.
|
|
292
|
+
|
|
293
|
+
By opening a pull request you agree that your contribution is licensed under
|
|
294
|
+
those same terms, and that the maintainer may also offer it under a commercial
|
|
295
|
+
licence. If that is not something you want, say so in the PR and it will not be
|
|
296
|
+
merged rather than left ambiguous.
|
|
297
|
+
|
|
298
|
+
Symbols derived from the draw.io / diagrams.net P&ID stencils are **Apache-2.0**
|
|
299
|
+
and stay that way. That licence permits the surrounding work to be licensed
|
|
300
|
+
differently only while its own terms and attribution are preserved. If you
|
|
301
|
+
vendor anything new, add it to the file list in `NOTICE` in the same PR.
|