|
| 1 | +# MWL Server |
| 2 | + |
| 3 | +DICOM [Modality Worklist (MWL)](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_K) server for managing scheduled breast screening appointments and providing worklist information to imaging modalities. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +The MWL server is a lightweight, production-ready DICOM worklist solution that: |
| 8 | + |
| 9 | +- Provides scheduled procedure information via [DICOM C-FIND](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_C) protocol |
| 10 | +- Stores worklist items in SQLite database |
| 11 | +- Supports filtering by modality, date, and patient ID |
| 12 | +- Runs in a separate container alongside the [PACS Server](../pacs/README.md) |
| 13 | + |
| 14 | +## Architecture |
| 15 | + |
| 16 | +### Components |
| 17 | + |
| 18 | +``` |
| 19 | +┌─────────────────────────────────────────────────────────────┐ |
| 20 | +│ MWL Server (Port 4243) │ |
| 21 | +├─────────────────────────────────────────────────────────────┤ |
| 22 | +│ │ |
| 23 | +│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ |
| 24 | +│ │ C-FIND │─────▶│ Storage │─────▶│ SQLite │ │ |
| 25 | +│ │ Handler │ │ Layer │ │ Database │ │ |
| 26 | +│ └──────────────┘ └──────────────┘ └──────────┘ │ |
| 27 | +│ │ ▲ │ |
| 28 | +│ │ │ │ |
| 29 | +│ └──────────────────────┘ │ |
| 30 | +│ Query & Response │ |
| 31 | +└─────────────────────────────────────────────────────────────┘ |
| 32 | + ▲ ▲ |
| 33 | + │ │ |
| 34 | + ┌──────┴──────┐ ┌───────┴────────┐ |
| 35 | + │ Modality │ │ Relay Listener │ |
| 36 | + │ (SCU) │ │ (Populates DB) │ |
| 37 | + └─────────────┘ └────────────────┘ |
| 38 | +``` |
| 39 | + |
| 40 | +### Workflow |
| 41 | + |
| 42 | +1. **Worklist Creation**: Relay listener receives appointments from web app and creates worklist items (NB not yet implemented; worklist items must be created programmatically via `scripts/add_worklist_item.py`) |
| 43 | +2. **Worklist Query**: Modality sends C-FIND request to MWL server |
| 44 | +3. **Filtering**: MWL server filters by modality, date, patient ID, status |
| 45 | +4. **Response**: Server returns matching worklist items to modality |
| 46 | +5. **Status Updates**: [MPPS (Modality Performed Procedure Step)](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_F) updates procedure status (NB not yet implemented) |
| 47 | + |
| 48 | +## Running the MWL Server |
| 49 | + |
| 50 | +The MWL server runs in a separate container: |
| 51 | + |
| 52 | +```bash |
| 53 | +# Start both PACS and MWL servers |
| 54 | +docker compose up -d |
| 55 | + |
| 56 | +# Start only MWL server |
| 57 | +docker compose up -d mwl |
| 58 | + |
| 59 | +# View logs |
| 60 | +docker compose logs -f mwl |
| 61 | + |
| 62 | +# Stop servers |
| 63 | +docker compose down |
| 64 | + |
| 65 | +# Reset databases |
| 66 | +docker compose down -v |
| 67 | +``` |
| 68 | + |
| 69 | +## Configuration |
| 70 | + |
| 71 | +Environment variables: |
| 72 | + |
| 73 | +| Variable | Default | Description | |
| 74 | +|----------|---------|-------------| |
| 75 | +| `MWL_AET` | `MWL_SCP` | Application Entity Title | |
| 76 | +| `MWL_PORT` | `4243` | DICOM service port | |
| 77 | +| `MWL_DB_PATH` | `/var/lib/pacs/worklist.db` | SQLite database path | |
| 78 | +| `LOG_LEVEL` | `INFO` | Logging level | |
| 79 | + |
| 80 | +## Example query |
| 81 | + |
| 82 | +```python |
| 83 | +from pynetdicom import AE, QueryRetrievePresentationContexts |
| 84 | +from pydicom import Dataset |
| 85 | + |
| 86 | +ae = AE() |
| 87 | +ae.requested_contexts = QueryRetrievePresentationContexts |
| 88 | + |
| 89 | +# Create query dataset |
| 90 | +ds = Dataset() |
| 91 | +ds.PatientID = '9876543210' |
| 92 | +ds.PatientName = '' |
| 93 | +ds.AccessionNumber = '' |
| 94 | + |
| 95 | +# Scheduled procedure step query |
| 96 | +sps = Dataset() |
| 97 | +sps.Modality = 'MG' |
| 98 | +sps.ScheduledProcedureStepStartDate = '20260108' |
| 99 | +ds.ScheduledProcedureStepSequence = [sps] |
| 100 | + |
| 101 | +# Send C-FIND with Worklist Information Model ('W') |
| 102 | +assoc = ae.associate('localhost', 4243, ae_title='MWL_SCP') |
| 103 | +responses = assoc.send_c_find(ds, query_model='W') |
| 104 | +for (status, identifier) in responses: |
| 105 | + if status.Status in (0xFF00, 0xFF01): |
| 106 | + print(f"Found: {identifier.PatientName}") |
| 107 | +assoc.release() |
| 108 | +``` |
| 109 | + |
| 110 | +## Verification |
| 111 | + |
| 112 | +Check worklist items: |
| 113 | + |
| 114 | +```bash |
| 115 | +docker compose exec gateway sqlite3 /var/lib/pacs/worklist.db \ |
| 116 | + "SELECT accession_number, patient_name, scheduled_date, status FROM worklist_items;" |
| 117 | +``` |
| 118 | + |
| 119 | +Add test worklist item: |
| 120 | + |
| 121 | +```bash |
| 122 | +docker compose exec gateway sqlite3 /var/lib/pacs/worklist.db <<EOF |
| 123 | +INSERT INTO worklist_items ( |
| 124 | + accession_number, patient_id, patient_name, patient_birth_date, |
| 125 | + scheduled_date, scheduled_time, modality, study_description |
| 126 | +) VALUES ( |
| 127 | + 'ACC001', '9876543210', 'TEST^PATIENT', '19800101', |
| 128 | + '20260108', '100000', 'MG', 'Bilateral Screening Mammogram' |
| 129 | +); |
| 130 | +EOF |
| 131 | +``` |
| 132 | + |
| 133 | +## Integration testing |
| 134 | + |
| 135 | +**Running integration tests:** |
| 136 | + |
| 137 | +```bash |
| 138 | +uv run pytest tests/integration/test_c_find_returns_worklist_items.py -v |
| 139 | +uv run pytest tests/integration/test_request_cfind_on_worklist.py -v |
| 140 | +``` |
| 141 | + |
| 142 | +## Multi-container architecture |
| 143 | + |
| 144 | +The PACS and MWL servers run in separate containers. See [ADR-003: Separate containers for PACS and MWL](../adr/ADR-003_Separate_containers_for_PACS_and_MWL.md) for the architectural decision and trade-offs. |
| 145 | + |
| 146 | +**Docker Compose services:** |
| 147 | + |
| 148 | +```yaml |
| 149 | +services: |
| 150 | + pacs: |
| 151 | + container_name: pacs-server |
| 152 | + command: ["uv", "run", "python", "-m", "pacs_main"] |
| 153 | + ports: |
| 154 | + - "4244:4244" |
| 155 | + |
| 156 | + mwl: |
| 157 | + container_name: mwl-server |
| 158 | + command: ["uv", "run", "python", "-m", "mwl_main"] |
| 159 | + ports: |
| 160 | + - "4243:4243" |
| 161 | +``` |
| 162 | +
|
| 163 | +Each server: |
| 164 | +
|
| 165 | +- Runs in its own container |
| 166 | +- Has its own Application Entity (AE) |
| 167 | +- Uses a separate SQLite database |
| 168 | +- Can be scaled and deployed independently |
| 169 | +- Handles different DICOM operations (C-STORE vs C-FIND) |
0 commit comments