Skip to content

Commit 5f3d25b

Browse files
committed
fix: align nodal-allocation projection docs and tuple reduction lowering
1 parent d58b8ec commit 5f3d25b

4 files changed

Lines changed: 146 additions & 9 deletions

File tree

crates/arco-kdl/src/compile/expressions.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,80 @@ fn expand_reduction_bindings(
319319
entrypoint: &Path,
320320
) -> Result<Vec<LinearizationBindings>, CompileError> {
321321
let reverse_aliases = build_reverse_alias_lookup(program);
322+
323+
if let Some(first) = bindings.first() {
324+
let same_domain = bindings.iter().all(|binding| binding.domain == first.domain);
325+
let name_bindings = bindings
326+
.iter()
327+
.map(|binding| match &binding.pattern {
328+
crate::algebra::BindingPattern::Name(name) => Some(name.clone()),
329+
crate::algebra::BindingPattern::Tuple(_) => None,
330+
})
331+
.collect::<Option<Vec<_>>>();
332+
333+
if same_domain {
334+
if let (Some(names), Some(set)) =
335+
(name_bindings, program.set_registry.get(&first.domain))
336+
{
337+
if let (Some(tuple_components), Some(tuple_rows)) =
338+
(set.tuple_components.as_ref(), set.tuple_rows.as_ref())
339+
{
340+
let mut component_to_binding = BTreeMap::new();
341+
for name in &names {
342+
let component = name
343+
.strip_suffix("_r")
344+
.unwrap_or(name.as_str())
345+
.to_string();
346+
component_to_binding.insert(component, name.clone());
347+
}
348+
349+
let mut scopes = Vec::new();
350+
for row in tuple_rows {
351+
if row.len() != tuple_components.len() {
352+
return Err(CompileError::InvalidFormulation {
353+
message: format!(
354+
"tuple row arity mismatch in reduction over `{}`: expected `{}`, received `{}`",
355+
first.domain,
356+
tuple_components.len(),
357+
row.len()
358+
),
359+
path: entrypoint.to_path_buf(),
360+
});
361+
}
362+
363+
let mut scope = current.clone();
364+
let mut matches_anchor = true;
365+
for (component, value) in tuple_components.iter().zip(row.iter()) {
366+
if let Some(binding_name) = component_to_binding.get(component) {
367+
scope.values.insert(
368+
binding_name.clone(),
369+
FilterValue::String(value.clone()),
370+
);
371+
continue;
372+
}
373+
374+
if let Some(existing) = current.values.get(component) {
375+
let tuple_value = FilterValue::String(value.clone());
376+
if existing != &tuple_value {
377+
matches_anchor = false;
378+
break;
379+
}
380+
}
381+
}
382+
383+
if matches_anchor {
384+
scopes.push(scope);
385+
}
386+
}
387+
388+
if !scopes.is_empty() {
389+
return Ok(scopes);
390+
}
391+
}
392+
}
393+
}
394+
}
395+
322396
let mut scopes = vec![current.clone()];
323397

324398
for binding in bindings {

crates/arco-kdl/tests/compile_suite.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -955,7 +955,8 @@ scenario "S1" {
955955
assert!(
956956
message.contains("empty constraint-relevant tuple subset for `capacity_target`")
957957
);
958-
assert!(message.contains("1,solar; 2,solar; 2,wind"));
958+
assert!(message.contains("1,solar"));
959+
assert!(message.contains("2,wind"));
959960
}
960961
other => panic!("expected InvalidFormulation, got {other:?}"),
961962
}

docs/arco-spec.md

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This document defines the low-level Arco DSL profile authored in KDL 2.0.
1717
Scope of this specification:
1818

1919
- [`set`](#4-set-declaration-top-level) declarations (explicit domains)
20+
- [`projection`](#45-projection-declaration-top-level) declarations (named tuple-domain projections)
2021
- [`data`](#5-data-declaration) declarations (CSV-backed namespaces)
2122
- [`param`](#31-inline-scalar-parameters) declarations (inline scalar constants)
2223
- [`param`](#54-param-inside-data) declarations (CSV-backed projection,
@@ -40,6 +41,7 @@ Scope of this specification:
4041
- [3. Top-level declarations](#3-top-level-declarations)
4142
- [3.1 Inline scalar parameters](#31-inline-scalar-parameters)
4243
- [4. `set` declaration (top-level)](#4-set-declaration-top-level)
44+
- [4.5 `projection` declaration (top-level)](#45-projection-declaration-top-level)
4345
- [5. `data` declaration](#5-data-declaration)
4446
- [5.1 `map`](#51-map)
4547
- [5.2 `set` (inside `data`)](#52-set-inside-data)
@@ -98,7 +100,7 @@ supported. Slashdash (`/-`) comments out an entire node, property, or argument,
98100
which is useful for toggling declarations during development.
99101

100102
Unknown nodes: Implementations MUST reject unknown top-level node types
101-
(anything other than `set`, `data`, `param`, `model`, `scenario`). Inside
103+
(anything other than `set`, `projection`, `data`, `param`, `model`, `scenario`). Inside
102104
blocks, unknown child node types MUST also fail validation. This ensures forward
103105
compatibility is explicit: new node types require a spec version bump.
104106

@@ -343,9 +345,8 @@ indexed or scalar via a single-row CSV). Inline scalars MUST NOT have `index`,
343345

344346
## 4. `set` declaration (top-level)
345347

346-
A top-level `set` declares a named domain with explicit members listed inline.
347-
This is useful for sets that are not backed by a CSV file, for example index
348-
ranges, scenario labels, or piecewise segments.
348+
A top-level `set` declares a named domain. It supports either explicit inline
349+
members or subset/tuple-set forms built from existing sets.
349350

350351
Explicit member list:
351352

@@ -369,6 +370,22 @@ Alias:
369370
set time alias=t { 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20; 21; 22; 23; 24 }
370371
```
371372

373+
Subset/tuple-set form:
374+
375+
```kdl
376+
set "priority_links" {
377+
in "feasible_links"
378+
index "a" { in "area" }
379+
index "i" { in "tech" }
380+
index "g" { in "generators" }
381+
index "b" { in "buses" }
382+
filter { area == "south" }
383+
}
384+
```
385+
386+
In this form, `in` identifies the parent tuple set, `index` declares the tuple
387+
signature, and `filter` (optional) narrows rows.
388+
372389
Top-level sets are globally visible, just like sets declared inside `data`
373390
blocks. They can be used in any model, constraint, or algebra expression.
374391

@@ -377,6 +394,26 @@ MUST NOT have the same name as a set declared inside a `data` block.
377394

378395
---
379396

397+
## 4.5 `projection` declaration (top-level)
398+
399+
`projection` declares a named lower-dimensional tuple domain derived from a
400+
parent tuple set.
401+
402+
```kdl
403+
projection "ai" {
404+
from "feasible_links"
405+
to "a" "i"
406+
}
407+
```
408+
409+
Semantics:
410+
411+
- `from` MUST reference an existing tuple-domain set.
412+
- `to` MUST list one or more tuple component names present on the parent tuple
413+
signature, in parent declaration order.
414+
- Projected rows are deduplicated by value.
415+
- Projection names MUST be unique in the top-level namespace.
416+
380417
## 5. `data` declaration
381418

382419
`data` declares one CSV-backed namespace. Sets and parameters declared inside
@@ -417,6 +454,7 @@ Allowed children:
417454
```
418455
map <logical_name>
419456
map <logical_name> from=<source_header>
457+
alias <logical_name> column=<source_header>
420458
```
421459

422460
Semantics:
@@ -425,6 +463,8 @@ Semantics:
425463
MUST exist in the CSV.
426464
- Mapping is optional. Unmapped columns remain available.
427465
- Duplicate logical targets MUST fail validation.
466+
- `alias <logical_name> column=<source_header>` is accepted as an equivalent
467+
spelling of `map <logical_name> from=<source_header>`.
428468

429469
### 5.2 `set` (inside `data`)
430470

@@ -438,6 +478,7 @@ constraint, or algebra expression in the document.
438478
```
439479
set <name>
440480
set <name> alias=<short>
481+
set <name> as=<short>
441482
set <name> {
442483
in <parent_set>
443484
}
@@ -450,8 +491,8 @@ set <name> {
450491
Semantics:
451492

452493
- `set class` extracts unique values from the `class` column.
453-
- `alias` provides a short set reference that MAY be used wherever a set
454-
reference is expected (`index=` property, `index` children,
494+
- `alias` (or equivalent `as`) provides a short set reference that MAY be used
495+
wherever a set reference is expected (`index=` property, `index` children,
455496
`index ... { in ... }`, and algebra iteration domains). Example:
456497
`set asset_id alias=a` allows `param capacity { index a }`,
457498
`index a_idx { in a }`, and `dispatch[a,t]`.
@@ -1124,6 +1165,27 @@ the point of use. When an expression is referenced inside a constraint with
11241165
`index` clauses, the constraint's iteration variables are in scope for the
11251166
expression body.
11261167

1168+
Projection-reduce expression form:
1169+
1170+
```kdl
1171+
expression "investment_by_area_tech" {
1172+
reduce "ai" {
1173+
sum "investment"
1174+
}
1175+
}
1176+
```
1177+
1178+
This form aggregates a higher-dimensional control/expression onto a named
1179+
projection domain.
1180+
1181+
Rules:
1182+
1183+
- the projection name (`"ai"` above) MUST resolve to a declared `projection`.
1184+
- the body MUST contain exactly one reduction operation.
1185+
- currently supported operation is `sum`.
1186+
- the reduction target (`"investment"` above) MUST have a tuple signature
1187+
compatible with the projection source and target dimensions.
1188+
11271189
### 6.5 `constraint`
11281190

11291191
Two supported forms.

examples/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ For full language-level guidance on subset/filter patterns, see:
111111

112112
### Why this matters in V1
113113

114-
- `dispatch[a,i,g,b]` is instantiated only for members of the tuple subset you
114+
- `investment[a,i,g,b]` is instantiated only for members of the tuple subset you
115115
bind to (for example, `feasible_links`). There is no Cartesian fallback.
116116
- Reduced scopes must be named explicitly (for example, `priority_links`). V1
117117
does not auto-project high-dimensional tuple domains.
@@ -120,7 +120,7 @@ For full language-level guidance on subset/filter patterns, see:
120120

121121
```text
122122
index order mismatch for `NodalAllocationDay.NodalAllocation.constraint_1` over tuple domain `feasible_links`
123-
empty constraint-relevant tuple subset for `NodalAllocationDay.NodalAllocation.capacity_target` at keys: north,solar; north,wind; south,gas; south,solar
123+
empty constraint-relevant tuple subset for `NodalAllocationDay.NodalAllocation.enforce_mw_target` at keys: north,solar; north,wind; south,gas; south,solar
124124
```
125125

126126
## Current caveats

0 commit comments

Comments
 (0)