Add a PumpX-2000 datasheet and render every configured pump in the OpenUSD twin - #4138
Conversation
Give the PumpDeviceIntegrationServer sample an official-style product datasheet for its simulated asset and align the server to it, so the document and the running address space cannot drift apart. New DATASHEET.md specifies the fictitious SimPump Corp PumpX-2000: nameplate mapped to OPC UA browse paths and namespaces, best-efficiency duty point, characteristic curves, performance table, operating limits, motor and materials data, instrument list, alarm trip points, the deterministic simulation profile, and a Mermaid process schematic. The server now implements those numbers. PumpDatasheet.cs holds the datasheet as compile-time constants; MaterialiseNameplate adds the 19 optional identification properties through the generator-emitted AddXxx(context) helpers so each carries its declared browse name, namespace and DataType. Both pumps become units of the same product (SN-001, SN-002) rather than two unrelated vendors. The simulation is rebuilt around the datasheet curves: volumetric flow is the only independent variable and differential pressure, mass flow, efficiency and shaft power are derived from it, so P = dp * Q / eta holds at every tick. Previously each measurement was an independent sine and the published duty point was not physically self-consistent (5 kW of shaft power at 0.05 kg/s and 2 bar). Engineering ranges follow the datasheet, OverTempAlarm reports the BearingTemperature chain instead of a boolean, and the Cavitation and MotorOverheat supervision states are derived from suction level and bearing temperature with hysteresis. The sample README gains a simulated-device summary, a link to the datasheet, and three Mermaid diagrams: address space, startup and hosting sequence, and the simulation and alarm dataflow. PumpDatasheetConformanceTests pins the nameplate, engineering ranges, trip points, value envelope and hydraulic consistency against the datasheet, and the pump node-surface baseline covers the new identification properties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR adds an “official-style” PumpX-2000 datasheet to the PumpDeviceIntegrationServer sample and aligns the sample server + tests so the simulated address space, ranges, alarms, and hydraulics remain consistent with the published documentation.
Changes:
- Added a full PumpX-2000 datasheet (markdown + Mermaid diagrams) and updated sample docs to reference it.
- Rebuilt the sample simulation to derive all process values from datasheet curves and added compile-time datasheet constants (
PumpDatasheet.cs). - Added conformance/regression tests that pin nameplate fields, ranges, alarm trip points, and hydraulic consistency to the datasheet.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Opc.Ua.Di.Tests/PumpInstanceNodeIdRegressionTests.cs | Extends the node-surface baseline to include newly materialized Identification properties. |
| tests/Opc.Ua.Di.Tests/PumpDatasheetConformanceTests.cs | Adds deterministic conformance tests to prevent drift between datasheet and address space/simulation. |
| samples/PumpDeviceIntegrationServer/README.md | Adds datasheet link + diagrams and documents the new simulation/alarm behavior. |
| samples/PumpDeviceIntegrationServer/PumpNodeManager.cs | Materializes optional nameplate properties via generator helpers to match model-defined names/namespaces/datatypes. |
| samples/PumpDeviceIntegrationServer/PumpNodeManager.Configure.cs | Aligns nameplate values, EURanges, alarm source/limits, and simulation math with the datasheet. |
| samples/PumpDeviceIntegrationServer/PumpDatasheet.cs | Introduces a single source of truth for datasheet constants used by the sample and tests. |
| samples/PumpDeviceIntegrationServer/Program.cs | Updates the declarative Pump #2 identification wiring to match the datasheet across namespaces. |
| samples/PumpDeviceIntegrationServer/DATASHEET.md | Adds the vendor-style PumpX-2000 datasheet (nameplate, curves, ranges, alarms, simulation profile). |
| docs/SourceGeneratedNodeManagers.md | Updates docs examples to match new naming/ranges and correct property CLR types. |
| docs/DeviceIntegration.md | Links to the datasheet and updates the topology-element builder example identification wiring. |
Reconcile the PumpX-2000 datasheet work with the address-space compliance changes from OPCFoundation#4117, which reshaped the same sample. Conflict resolutions: - WithIdentification: keep the upstream per-pump signature (builder, pump, pumpNumber) and NodeId-based lookup, and apply the full 21-field datasheet nameplate through it. Unit-specific fields (serial, asset id, component name, location, fabrication number) are now derived from the pump number, so every instance materialised by --pumps N gets a consistent nameplate instead of only the first two. - Program.cs: take upstream. Identification is configured centrally in WithIdentification for every pump, so the per-pump nameplate block in the ConfigureDevicesFor delegate is superseded; the delegate keeps upstream's loop that adds the Diagnostics functional group. - PumpSimulationState.Publish: keep upstream's status code and source timestamp plumbing and publish the datasheet curve values through it. - Supervision booleans: keep upstream's WireBoolean helper (TrueState / FalseState text and history) and drop the alarm's MonitorVariable on the boolean, because the alarm now sources BearingTemperature, which is what its Kelvin trip points describe. - Node-surface baseline: union of both identification lists. - docs/SourceGeneratedNodeManagers.md was consolidated upstream into docs/NodeManagers.md; the datasheet-related snippet fixes were ported there and the old file deleted. Documentation and tests follow the upstream rename of the pump BrowseNames to Pump_1 / Pump_2 (DisplayName "Pump #1" / "Pump #2"). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
Address PR review feedback on PumpDatasheetConformanceTests: the consistency check divided by the published efficiency and shaft power without checking them first, so a sample read before the simulation had published a coherent set of values would have produced NaN or Infinity and failed on startup timing rather than on the hydraulic model. Skip and retry an incomplete sample instead, and assert explicitly that a complete sample was observed before asserting the deviation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4138 +/- ##
==========================================
- Coverage 80.23% 79.91% -0.32%
==========================================
Files 1515 1515
Lines 209980 209980
Branches 36213 36213
==========================================
- Hits 168479 167811 -668
- Misses 28867 29568 +701
+ Partials 12634 12601 -33 🚀 New features to boost your workflow:
|
Reconcile the PumpX-2000 datasheet work with the OpenUSD, Robotics and Positioning drop, which added an OpenUSD twin to the same pump sample. Conflict resolutions: - PumpSimulationState keeps the datasheet-driven phase offset and gains upstream's tick interval, which the shaft integration needs. - Publish() keeps the datasheet curve values and adds upstream's shaft angle integration on top of them. - The shaft speed reference is rescaled: upstream integrated the angle from a NominalFlow of 0.05 kg/s, which was the pre-datasheet mass-flow scale. Rated mass flow is now 6.93 kg/s, so the constants move into PumpDatasheet as Hydraulics.RatedMassFlow and Hydraulics.RatedSpeed (2900 min-1, the value the datasheet already published in section 5). Without this the twin would spin the shaft about 139 times too fast. - The node-surface baseline is the union of the datasheet nameplate and the OpenUSD representation nodes. The sample README picks up the OpenUSD twin section from upstream; its address-space and dataflow diagrams and the console transcript are refreshed for the merged server. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
The twin was hard-wired to a single machine: PumpPrimPath was the constant /Plant/Pumps/P101 and every pump's representation, component bindings and live bindings used it, so N pumps drove one prim. The signal Variables lived in single fields that each pump overwrote, ShaftAngle and AlarmActive both read m_primarySimulation - the first pump - and only the first representation was organised into the discovery registry. A server started with --pumps 4 rendered one pump, spun by pump 1 and alarmed by pump 4. Each pump now owns a PumpTwin: its prim path (/Plant/Pumps/<BrowseName>), its signal Variables, its representation and its simulation. Every representation is registered in the Representations folder, and a plant-level representation on the DeviceSet declares one Many component binding scoped to PumpType, so a connector composes one full-fidelity pump.usda reference per configured pump. The arc is Reference rather than Instance because an instanceable prim turns its descendants into a shared prototype, which cannot carry per-pump rotation or colour. Each pump publishes its bay as a ThreeDCartesianCoordinates value bound to xformOp:translate, so any number of pumps lays out in a row without the stage authoring anything per pump. The binding set now covers the machine rather than four signals. Added: motor cooling fan, discharge-pressure and bearing-temperature gauge needles, a suction vessel whose liquid surface rides on the published level, suction-line tint from fluid temperature, mass-flow, efficiency and start-count readouts, and distinct cavitation and motor-overheat indications at the place on the machine where each fault is. Scales and offsets are derived from PumpDatasheet, so a datasheet change moves the needles with it. Efficiency is a readout rather than a colour: the DisplayColor ramp models a temperature. The geometry those bindings need is authored into the P101 master and regenerated into pump.usda and remote-pump.usda. Beacon and StatusLight move into the component asset, because signalling is now per machine. The master is left in the layer but deactivated: it is the generator's input, and rendering it too would show a phantom machine no OPC UA object drives. The OverTempAlarm condition is deliberately not bound. The fluent alarm builder leaves the condition's state children on their standard namespace-0 declaration NodeIds, so every pump's alarm shares one ActiveState, Severity and AckedState node and binding them would light every beacon at once. The per-pump supervision states are the alarm indication instead, and they are what drives the condition through ActivatesAlarm in the first place. The plant aggregation is not declared dynamic. The configured pump set is fixed by --pumps at start-up, the ProductionLine already demonstrates dynamic add/remove, and declaring it dynamic made the connector's stale-prim sweep deactivate the Impeller and Bearing component prims that now live under /Plant/Pumps. Verified against a running server: with --pumps 4 the connector composes four referenced pumps at bays 0/2.4/4.8/7.2 m, each with its own shaft angle, fan, gauge needles, casing colour, fluid level and alarm halos. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
Three things made the rendered hall misread. The aggregation demo shared the main aisle. Its line pumps sit on 1.2 m centres while the configured pumps march along the same axis on 2.4 m centres, so the two rows interleaved and the hall showed more machines than a client could browse - the reported "5 pumps when only 4 started". Line1 now stands in its own aisle behind the suction vessels, which is also what it is: a composition demo, not part of the configured line. Every machine wore a permanent alarm halo. StatusLight was authored visibility = "inherited", so a pump nothing is bound to - an aggregated line pump, or the one federated from another server - stood there showing an alarm it never raised. It now defaults to invisible and only a live supervision binding reveals it, which is what makes the halo mean something. The camera was a gantry shot down the line, framed for a single pump at the origin. It is now an operator's viewpoint: eye height 1.65 m in the aisle on the discharge side, pitched 7 degrees below horizontal, framing every configured pump at a three-quarter angle with the vessels behind them and the back aisle further back again. The framing was solved against the projection rather than guessed, and holds from one pump up to eight, so it does not need retuning for --pumps N. The floor is widened to match; the old one ended at y = 4.2 and left the far pumps hanging over black. Verified in the viewport with --pumps 5: five machines in shot, each turning at its own speed, no alarm showing unless its own supervision state asserts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
The hall contained geometry that no longer belonged there. A demo production line authored three empty pump bodies plus a "remote" pump representing a machine on another server, none of which the connected server simulates, so the viewport showed more pumps than the server had and the extra ones never moved. Remove them. `/Plant/Line1` and `remote-pump.usda` are gone, the production line now aggregates plain topology objects without a representation, and the stage serves two assets instead of three. What renders is exactly the set of pumps the server simulates. Replace the alarm light bulb with an alarm ring. A red annulus is authored around each pump and made visible while the pump is in an error condition, so the fault reads at a glance from any angle instead of depending on a small emissive lamp. The bearing overheat halo moves onto the bearing bracket where the heat actually is, and the differential pressure lamp binding retires with the lamp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
Only one pump was visible in the viewport no matter how many the server simulated. The bay positions were correct on the wire - each pump published its own translation and the connector wrote it - but nothing moved, so five machines rendered on top of each other. A connector accumulates Translation, Rotation and Scale into a single xformOp:transform matrix rather than authoring the three ops separately. It has to: xformOpOrder is uniform, so an op order authored in the asset layer cannot be cleared from the stronger layer the connector edits. USD in turn evaluates only the ops named in xformOpOrder. The pump asset named xformOp:translate, and its root prim named nothing at all, so every matrix written to it was discarded in silence. Declare xformOp:transform on the two prims a Translation target drives: the pump root, which the bay layout positions, and the suction vessel's fluid surface, which rides on the published level. The surface keeps its authored base height by baking it into the identity matrix. Add TransformBindingsTargetDeclaredXformOpsAsync, which resolves every transform binding against the served asset and fails when a bound prim does not declare the op a connector will author. Checked against real OpenUSD composition: with the previous asset four of five pumps resolve to (0,0,0); with this one each resolves to its own bay. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
The net472 build of Opc.Ua.Di.Tests failed with four CS0234/CS0246 errors, and took the CodeQL analysis down with it as a configuration error. PumpDeviceIntegrationServer matches AppTargetFrameworks and so does not exist on net472; the test project already drops its project reference and every Pump fixture that depends on it there. PumpDatasheetConformanceTests was added to this branch without being added to that list, so on net472 it alone still reached for Opc.Ua.Pumps, PumpNodeManager and PumpState. Add it to the exclusion list next to the fixtures it belongs with. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
Two defects found in code review, both in the OpenUSD live-signal path. Signal registration ran once per pump as it was materialised, but walked every twin in the dictionary - and the pump being materialised was already in that dictionary. So pump 1 was registered N times for N pumps, pump 2 N-1 times, and so on. The publish set grew as N(N+1)/2, and every tick re-wrote those Variables and called ClearChangeMasks on them once per duplicate. Register the pump that was passed in instead. The publish set was a plain List, unlike the sibling collections that were made concurrent for exactly this reason. CreatePumpAsync adds a pump to an already-running simulation, so appending to that list raced the 250 ms tick enumerating it: the tick would throw and stop the simulation for every pump. Hold it as an immutable snapshot that TrackSignal replaces under the existing registration lock, which leaves the tick both allocation-free and lock-free. EveryPumpPublishesItsShaftAngleExactlyOnce pins the first defect; it fails with the previous registration loop. Also drop a stale reference to the deleted remote-pump.usda from the Plant.usda header. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe608993-f3c9-4779-a6b9-9a9eabfc24ba
Description
Two related improvements to the
PumpDeviceIntegrationServersample: an official-style product datasheet that the server is aligned to, and an OpenUSD twin that actually renders every configured pump.Part 1 — the PumpX-2000 datasheet
The sample had no product documentation, a thin nameplate, and a duty point that was not physically self-consistent.
New:
samples/PumpDeviceIntegrationServer/DATASHEET.md— a vendor-style datasheet for the fictitious SimPump Corp PumpX-2000 (clearly marked as a simulated device): nameplate mapped to OPC UA browse paths and namespaces, BEP duty point, characteristic curves, performance table, operating limits, motor/materials/dimensions, instrument list, trip points, simulation-profile formulas and a Mermaid process schematic.Server aligned to it:
PumpDatasheet.cs— the datasheet as compile-time constants.MaterialiseNameplate— 19 optional identification properties via generator-emittedAddXxx(context)helpers, so each carries the browse name, namespace and DataType declared by the model (21 nameplate fields per pump). Unit-level fields are derived from the pump number, so--pumps Nyields a consistent nameplate per instance.H(Q) = 32 − 0.0104·Q²,η(Q) = 72·(1 − 0.6·((Q−25)/25)²)andP = ρ·g·Q·H/η, soP = Δp·Q/ηholds at every tick. Previously each measurement was an independent sine and the sample published 5 kW of shaft power for 0.05 kg/s at 2 bar.EURangefrom the datasheet;OverTempAlarmsourcesBearingTemperature(the correctSourceNodefor an alarm whose limits are in Kelvin);CavitationandMotorOverheatderived from suction level and bearing temperature with hysteresis.PumpDatasheetConformanceTestspins nameplate, ranges, trip points, value envelope and hydraulic consistency against the document.Part 2 — one OpenUSD twin per pump
The twin was hard-wired to a single machine.
PumpPrimPathwas the constant/Plant/Pumps/P101and every pump's representation, component bindings and live bindings used it; the signal Variables lived in single fields each pump overwrote;ShaftAngle/AlarmActiveboth read the first pump's simulation; and only the first representation was organised into the discovery registry.--pumps 4rendered one pump, spun by pump 1 and alarmed by pump 4.PumpTwin: its prim path/Plant/Pumps/<BrowseName>, its signal Variables, its representation and its simulation. Every representation is registered for discovery.Manycomponent binding scoped toPumpType, so a connector composes one full-fidelity@pump.usda@</Pump>reference per configured pump. The arc isReference, notInstance— an instanceable prim turns its descendants into a shared prototype, which cannot carry per-pump rotation or colour.ThreeDCartesianCoordinatesvalue bound toxformOp:translate, so any N lays out in a row with no per-pump stage authoring.Level, suction-line tint from fluid temperature, mass-flow/efficiency/start-count readouts, and distinct cavitation and overheat indications at the place on the machine where each fault is. Scales and offsets are derived fromPumpDatasheet.pump.usda. The master stays in the layer butactive = false— it is the generator's input, and rendering it too would show a phantom machine no OPC UA object drives.The hall shows the pumps, and only the pumps
Three follow-up problems surfaced once the scene actually rendered N machines, all fixed here:
Phantom pumps. A demo
ProductionLineauthored three empty pump bodies on the same aisle at a different pitch, and aRemotePumpstood in for a machine on another server. Neither is simulated by the server you connect to, so the viewport showed more pumps than existed and the extras never moved./Plant/Line1andremote-pump.usdaare gone; the production line still demonstrates aggregation, but over plain topology objects with no representation. The stage now serves two assets and renders exactly the set of pumps the server simulates.Phantom alarms.
StatusLightdefaulted tovisibility = "inherited", so any machine without a live binding wore a permanent alarm halo. Indicator geometry now defaults to invisible and is only revealed by a binding.Error indication. The alarm light bulb is replaced by an alarm ring — a red annulus authored around the machine and made visible while that pump is in an error condition, so the fault reads at a glance from any angle instead of depending on a small emissive lamp. The bearing overheat halo moves onto the bearing bracket, where the heat actually is.
Phantom placement. Only one pump was visible however many the server simulated. The bay positions were correct on the wire, but a connector accumulates
Translation/Rotation/Scaleinto a singlexformOp:transformmatrix — it must, becausexformOpOrderisuniformand an op order authored in the asset layer cannot be cleared from the stronger layer a connector edits — while USD evaluates only the ops named inxformOpOrder. The asset namedxformOp:translate, and its root prim named nothing at all, so every matrix written to it was discarded in silence and all N pumps resolved to the origin. The two prims aTranslationtarget drives (the pump root and the fluid surface) now declarexformOp:transform.The stage camera is an operator viewpoint: standing in front of the row at eye height, angled 7° down, with the suction vessels behind the machines. It holds the whole row in frame for 1–8 pumps without moving.
Two deliberate non-changes, both explained in code and README:
OverTempAlarmcondition is not bound. The fluent alarm builder leaves the condition's state children on their standard namespace-0 declaration NodeIds (whichPumpInstanceNodeIdRegressionTestspins), so every pump's alarm shares oneActiveState/Severity/AckedStatenode — binding them would light every ring at once. The per-pump supervision states are the alarm indication instead, and they are what drives the condition throughActivatesAlarm.dynamic. The pump set is fixed by--pumpsat start-up, and declaring it dynamic made the connector's stale-prim sweep deactivate theImpellerandBearingcomponent prims that now live under/Plant/Pumps.No changes to
src/— this is sample, test and documentation only.Related Issues
No tracking issue; sample-quality and documentation improvements to the pump reference sample.
Checklist
Verification
dotnet buildsample + tests (Release, net10.0): 0 warnings, 0 errorsdotnet test tests/Opc.Ua.Di.Tests -f net10.0: 371/371 passeddotnet test tests/Opc.Ua.OpenUsd.Tests -f net10.0: 635/635 passeddotnet test tests/Opc.Ua.Di.Tests -f net48 --filter Pump: 63/63 passeddotnet build tests/Opc.Ua.Di.Tests -f net472(Debug and Release): 0 warnings, 0 errors.PumpDeviceIntegrationServermatchesAppTargetFrameworksand does not exist on net472, so the test project drops its project reference and every dependent Pump fixture there;PumpDatasheetConformanceTestsis now on that list too.EveryConfiguredPumpIsAnIndependentTwinAsyncasserts N distinct prims, no cross-pump binding targets, and N distinct shaft angles.OnlySimulatedPumpsAreComposedAsyncasserts the composed set is exactly the simulated set.--pumps 5and the 3D viewport: five referenced pumps compose at bays 0 / 2.4 / 4.8 / 7.2 / 9.6 m; sampling the live override layer twice, ~25 s apart, shows every shaft advancing independently (Δ 3255° / 3363° / 3463° / 3528° / 3542°), and only the pump in an error condition has itsAlarmRingvisible. Noremote-pump,Line1orP_20xreferences remain in the composed stage. Live read also confirmsΔp·(ṁ/ρ)/ηmatches the published shaft power.(0,0,0)while this one resolves each to its own bay.TransformBindingsTargetDeclaredXformOpsAsyncpins the rule that produced it.Listappended to by the runtime-add path while the 250 ms tick enumerated it, which would throw on the timer thread and stop the simulation for every pump.EveryPumpPublishesItsShaftAngleExactlyOncepins the first.