Clojure library for URPX (Utility Rate Plan Exchange) — parse, validate, and resolve prices from URPX rate plan documents (JSON-LD).
URPX is an LF Energy open standard for exchanging utility rate plan information using semantic web technologies (RDF, OWL, SHACL, JSON-LD). It enables precise, machine-readable data exchange between utilities, regulators, vendors, and researchers.
The standard's source of truth — ontology, SHACL shapes, JSON-LD context, tutorial, and reference test cases — lives at github.com/urpx-org/urpx; its design-decision logs are maintained in urpx-org/urpx-dev. This library targets the same version-pinned ontology snapshot that fixtures cite via urpx:targetURPXVersion.
BigDecimal, LocalDate, LocalTime, LocalDateTime, ZonedDateTime, OffsetTime, Duration, Period, Year, integer month numbersurpx.dsl — per-entity builders with auto-coercion, validation-on-construct, and keyword refs scoped via dsl-scopeflowchart LR
JSONLD["URPX JSON-LD<br/>document"]
PARSED["parsed<br/><i>(string-keyed)</i>"]
COERCED["coerced entity<br/><i>(typed Clojure)</i>"]
RESOLVED["resolved prices<br/><i>at an instant</i>"]
SCHEDULE["price schedule<br/><i>over a window</i>"]
EMITTED["URPX JSON-LD<br/>output"]
DSL["urpx.dsl<br/><i>(authoring)</i>"]
JSONLD -->|urpx.core| PARSED
PARSED -->|urpx.coerce| COERCED
COERCED -->|urpx.price| RESOLVED
COERCED -->|urpx.schedule| SCHEDULE
COERCED -->|urpx.emit| EMITTED
DSL --> COERCED
Three paths share the same typed entity model:
deps.edn:
{:deps {energy.grid-coordination/clj-urpx {:mvn/version "0.54.0"}}}
A five-minute walkthrough using the vendored PG&E E-1 fixture (a real Schedule E-1 filing with block-tier pricing and a sibling vintage-PCIA modifier in a single JSON-LD @graph). Runs from a cold clone — no env vars, no sibling checkouts.
git clone https://github.com/grid-coordination/clj-urpx.git
cd clj-urpx
clojure -M:nrepl # nREPL on the port written to .nrepl-port
In the REPL:
(require '[urpx.core :as core]
'[urpx.coerce :as coerce]
'[urpx.price :as price]
'[urpx.test-fixtures :as fixtures])
(import '[java.time ZoneId ZonedDateTime])
;; 1. Load the vendored PG&E E-1 fixture: a urpx:RatePlan + sibling urpx:RatePlanModifier
(def plan (fixtures/load-rate-plan "pge-e-1-2026-03-01"))
(def modifier (fixtures/load-modifier "pge-e-1-2026-03-01"))
;; 2. Resolve prices for July 4 2026 at 5pm Pacific (Summer, weekend, holiday)
(def la (ZoneId/of "America/Los_Angeles"))
(def july-4-evening (ZonedDateTime/of 2026 7 4 17 0 0 0 la))
(def resolved
(price/resolve-prices-with-modifiers plan [modifier] july-4-evening))
(:urpx.resolved/season-name resolved)
;; => "Summer"
(mapv :urpx.resolved/ledger-name (:urpx.resolved/ledgers resolved))
;; => ["Energy Supply Prices" "Energy Delivery Prices"
;; "Conservation Incentive Adjustment" "Adjustment Prices"
;; "Base Services Charge"
;; "Vintaged Power Charge Indifference Adjustment"]
;; 3. Marginal $/kWh — sum of the lowest-tier unit-prices across energy ledgers.
;; The vintage-PCIA modifier prices one ledger per departure vintage. Those
;; are mutually-exclusive ALTERNATIVES, exactly one of which applies to a
;; customer, so say which one: without a selection `marginal-unit-rate`
;; refuses rather than pick arbitrarily.
(def with-vintage
(price/resolve-prices-with-modifiers
plan [modifier] july-4-evening
{:profile-alternatives {"pge:pa-departure-vintage" "pge:pa-vintage-2020"}}))
(price/marginal-unit-rate with-vintage)
;; => 0.36193M
Calling marginal-unit-rate on resolved instead, with no selection, throws ExceptionInfo naming the ledger: an aggregate that silently took one of eighteen mutually-exclusive prices would be reporting an arbitrary number as the marginal rate. Reading :urpx.resolved/tiers directly needs no selection, and still surfaces all eighteen.
That covers the consume side end-to-end. For emit and authoring, jump to Writing URPX JSON-LD and Authoring URPX entities — both compose with the coerced entity above.
urpx:CoverageClaimTargets the URPX v0.5.1 release while continuing to accept v0.2.x in full. Release history is in CHANGELOG.md. doc/status.md lists what is modeled and the known gaps.
| Namespace | Purpose |
|---|---|
urpx.core | JSON-LD parsing and loading (parse-rate-plan, load-rate-plan, load-rate-plan-version, parse-graph, load-graph, expand-document-entities) |
urpx.schema | Malli schemas for the coerced URPX entity model; schemas, registry and schema-for-type by @type; ranges, instance-of?, mistyped-references |
urpx.coverage | Optional reader for urpx:CoverageClaim: of-version, of-model, descriptions, coverage-interval, verdict-applies?, claim. Requires urpx.index only |
urpx.load | document-xf, documents, scan: sources to coerced documents, with the discards reportable |
urpx.coerce | Schema-driven coercion: strings → typed values; coerce-doc dispatches on @type; validate, explain; uncoerced? and check-coerced! tell a coerced document from a raw one |
urpx.index | build-index, resolve-ref, get-entity for @id references; versions-by-plan for v0.5.1 back-pointers; single and entities-of-type; mistyped-references over a set of documents |
urpx.price | resolve-prices, resolve-modifier-prices, resolve-prices-with-modifiers, marginal-unit-rate, measurement-kinds, has-energy-ledger?; version-effective-at, version-expired-by? and version-covers? are the single readers of a version window |
urpx.schedule | price-segments (exact boundaries, derived); price-schedule, price-schedule-days, price-schedule-with-modifiers, price-schedule-days-with-modifiers (stepped) |
urpx.tz-inference | infer-zone (opt-in fallback when urpx:timezoneIdentifier is absent) |
urpx.emit | Write-side: write-rate-plan, write-rate-plan-modifier, write-urpx-document, write-urpx-package, write-scenario-cost-model, write-program-enrollment |
urpx.validate | Single-file CLI checker (clojure -M:validate <file.jsonld>), tri-state exit |
urpx.dsl | Authoring DSL: dsl-scope, make-builder, named builders for every entity type |
urpx.defs | Typed definitions: defurpx, kind-schema, defs-of-kind |
urpx.scenario-cost-model | Optional enumerator: enumerate, enumerate-pairs, identity-tuple, plan-versions, applicable-modifiers, offered-by, identity-status-at, the one-modifier-at-most / every-subset, named-versions-narrow / named-versions-add and repeated-ids-refuse / repeated-ids-union policies, and resolve-at / segments-for / schedule-for for prices. Requires urpx.price; nothing in the library requires it |
Check any single URPX JSON-LD file, which is what a conversion smoke test needs while a set of filings migrates utility by utility:
clojure -M:validate path/to/filing.jsonld [more.jsonld ...]
Wrappers are descended, so a urpx:URPXDocument or urpx:URPXPackage is checked through to the urpx:RatePlan inside it rather than at the wrapper metadata alone. Exit codes are tri-state so a run that checked nothing cannot read as success: 0 everything valid, 1 at least one entity failed, broke a rule the standard states, or holds a reference or inline node of the wrong type for its property (judged within the file), 2 refused because a path was unreadable or a file yielded no entity this library models.
doc/dsl-tutorial.md — worked DSL examples mirroring sections of the upstream URPX tutorialIssues, Discussions, and pull requests are welcome — see CONTRIBUTING.md for the workflow (and the dev commands: tests, lint, nREPL), and doc/RELEASING.md for cutting a release. In short:
Copyright (c) 2026 Clark Communications Corporation. MIT License.
The vendored test fixtures under test/resources/urpx-fixtures/ and the vendored test/resources/corpus.schema.json are licensed CDLA-Permissive-2.0.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |