Assignments
Assignments are the primary statement form. They define or update addressable bindings; a cell is the coordinate-addressed kind of binding. See concepts.md for the vocabulary used here.
Assignments
Assignments are the primary statement form. They define or update addressable
bindings; a cell is the coordinate-addressed kind of binding. See
concepts.md for the vocabulary used here.
This doc covers all assignment shapes. For rule blocks, see
rules-and-schedules.md. For the
language as a whole, see reference.md.
1. Anatomy
<target> IS <type-tag> <op> <expression>
<type-tag> <target> <op> <expression>
| Part | Required | Notes |
|---|---|---|
<target> |
yes | A cell reference, named value, or finite range |
<type-tag> |
no | Semantic or native-kind contract, e.g. currency, List of String, or Tensor[?, 4] |
<op> |
yes | At top level, one of =, ~=, ?=. Compound operators are contextual: arithmetic in rule actions, colon-predicate add/remove (optionally with declared fields), and typed mutations such as table updates. |
<expression> |
yes | Any expression — see reference.md |
Examples:
A1 = 10
A2 is currency = A1 * 100
currency A3 = A1 * 100
A3 ~= EXPENSIVE_FETCH()
A4 ?= MAYBE_FAIL()
A5 += :reviewed
A6:A10 = B1
When a model assigns the same target more than once at top level, the later
assignment replaces the earlier one. Here answer is 23:
answer = 11
answer = 23
2. Assignment Operators
2.1 = (Eager)
The default. The cell recomputes every time any of its inputs change.
A1 = SUM(B1:B10)
When any of B1 through B10 change, A1 recomputes immediately.
2.2 ~= (Lazy)
The cell does not compute until something reads it. Useful for expensive or external work.
A2 ~= ML_SCORE([0.1, 0.2, 0.3, 0.4])
Reads happen when the cell is displayed, inspected, or referenced by another
cell. A downstream eager cell that references A2 triggers A2 to compute.
Once computed, the value is cached. The cache invalidates when an input changes, returning the cell to the lazy state.
2.3 ?= (Conditional Eager)
Same as =, but if the right-hand side evaluates to an error, the
assignment is skipped — the existing value (if any) is preserved.
A3 ?= MAYBE_FAIL()
If MAYBE_FAIL() returns #N/A, A3 keeps its previous value rather
than becoming #N/A.
2.4 Compound Assignments
WHEN increment_requested THEN
A4 += 5
B4 -= 5
C4 *= 2
D4 /= 2
END
These work only on single-cell targets. The right-hand side is evaluated against the current value of the target.
Arithmetic compound assignment is valid only in rule actions, where the target
has a committed value and the rule transaction supplies a real prior-value
source. At top level, Grid rejects it with
GRID_TOP_LEVEL_COMPOUND_ASSIGNMENT; use a reactive rule action for state
changes. Typed mutation contexts such as table updates retain their documented
operator meanings.
2.5 Graph handle declarations
graph is an exact named-handle declaration rather than a cell type. It uses
plain = and a graph-producing expression:
graph roads = GRAPH(edges AS edges, "id" AS edge_id, "from" AS from, "to" AS to)
graph reversed = roads.REVERSE()
Graph handles cannot target cells or ranges and do not support decorators, mutation operators, lazy/conditional assignment, or schedules. See Graph Authoring for construction and query semantics.
2.6 Table mutations (type-dispatched)
When the assignment target is a table-typed named wire, compound
operators mean record mutations against the model's embedded table
store — not arithmetic. Grid applies them as record operations rather
than binary expressions.
| Operator | Meaning on table symbol |
|---|---|
+= |
Insert row(s) |
^= |
Upsert (merge on row _id) |
*= |
Update (patch by _id or filter) |
-= |
Delete (by _id or filter) |
table Orders += { name: "Acme", amount: 100 }
table Orders += COLLECT(inbound_frame)
table Orders ^= { id: "rec01HXYZ", status: "Shipped" }
table Orders *= { id: "rec01HXYZ", amount: 200 }
table Orders -= { id: "rec01HXYZ" }
Inside rule actions, cells and numeric named wires use += / -= / *= /
/= for arithmetic read-modify-write. Ordinary top-level arithmetic compound
assignment is rejected. ^= remains the table upsert operator in this typed
mutation context; exponentiation remains an expression operator.
A standalone top-level table-mutation statement is an explicit host command:
it commits once when submitted, rather than becoming a recalculating formula.
The same four forms are valid inside WHEN/EVERY/AT bodies. There the RHS
uses the rule's pre-commit snapshot and the durable table write runs after the
rule's atomic cell-state commit.
A table's schema can carry write rules that every mutation must satisfy, such as
the Ledger tables. On a table with a soft-delete column,
-= marks the matching rows instead of removing them. A mutation that would
change a fixed column, name a missing or archived referenced row, or leave a
balanced group unequal is refused as a whole, and nothing is written.
3. Targets
3.1 Single Cell
A1 = 10
$A$1 = 10
Sheet1!A1 = 10
'Q4 Revenue'!B2 = 10
Revenue = 10 /* named reference */
Workbook surfaces
Surfaces are namespaces that store component values with the same
namespace specifier syntax as sheets (Namespace!Component).
Dataset_1!type = "dataset"
Dataset_1!config = <toml>
version = 1
kind = "dataset"
title = "Dataset_1"
</toml>
Dataset_1!layout = ""
Dataset_1!data = ""
Use a language block for surface configuration. <toml>, <yaml>, and
<json> produce structured values that the surface checks against its
configuration schema. The block identifies its encoding; it does not choose
the surface kind, which still comes from !type. Plain TOML strings remain
supported for existing models. Use <jsx>…</jsx> for authored Component source.
The tab Dataset_1 appears when Dataset_1!type names a known surface
kind. The frontend reads component values (especially !config) and renders
the matching surface UI.
See ../product/surfaces.md for the surface families.
3.2 Named Address Aliases
A plain named assignment whose entire right-hand side is one absolute reference adds another address for the existing binding:
A1 = 125000
Revenue = A1
ReportedRevenue = Revenue
A1, Revenue, and ReportedRevenue address the same reactive binding. An
external write or rule override through any of those names changes the value
seen through all of them. They also share history and explanation identity;
the aliases do not add calculation steps.
This rule applies only when the target is a name. Coordinate-to-coordinate assignment retains spreadsheet behavior:
B1 = A1 /* a second cell whose formula reads A1 */
Tax = Revenue * tax_rate /* a separate named calculation */
The direct-reference form is deliberately recognizable from source. It is an alias when all of the following are true:
- the target is a named address;
- the right-hand side is exactly one absolute cell or named reference;
- the assignment is ordinary eager
=; - it has no type contract,
default,STATE, schedule, lazy mode, or conditional mode.
input and output only declare the external surface, so they may label an
alias without creating another calculation:
output Revenue = A1
input Assumption = B2
When a name needs its own type, state, schedule, or error-preservation policy, Grid creates an independent named binding even if the expression is a single reference. Unary predicates remain properties of the authored name rather than flowing through the aliased value.
3.3 Finite Ranges
A range target broadcasts the right-hand side to each cell in the range. The range must be a finite rectangle.
A1:A10 = B1 /* writes B1 to each of A1..A10 */
A1:C3 = 0 /* writes 0 to all 9 cells */
A1:A5 = [10, 20, 30, 40, 50] /* writes the array elementwise */
Full-column (A:A) and full-row (1:1) targets are not allowed.
3.4 Destructuring
Bind multiple symbols from a single array-returning expression:
[total, avg, min, max] = SUMMARY(B1:B10)
Rest capture binds the tail:
[head, ...tail] = ARRAY_OF_VALUES
The number of named bindings before ... must not exceed the array
length.
4. Type Tags
A type tag attaches semantic meaning to the value. The grammar is:
<target> IS [A | AN] <type-tag> = <expression>
Equivalently, prefix notation is accepted:
<type-tag> <target> = <expression>
A1 IS currency = 100000
A1 IS A currency = 100000
A1 IS AN fx_rate = 1.0825
currency A1 = 100000
currency x = 10
A2 is percentage = 21pct
A3 is date = d"2026-04-10"
A4 is bps = 25bps
A5 is score = ML_SCORE(features)
A6 is unit:m/s = distance / duration
Under strict, a value that
cannot inhabit the tag's representation root is a #TYPE! — tagging
is currency with a word-string errors, while a numeric string still
coerces. loose (the default) applies the tag as an overlay without
checking. This rides the coercion axis, so strict except coercion turns
it off.
Tags form a hierarchy (USD is a currency is a number), and a leaf
implies its ancestors — is USD needs no separate currency. See
reference.md for the full tree.
Unit and currency tags are load-bearing in dimensional analysis. Physical
units are built in (unit:m, unit:C, unit:delta_C) and currencies take
the currency: qualifier (currency:USD, or bare USD); models may also
declare scale-only custom units near the top of the source:
unit smoot = 1.7018 m
distance is unit:smoot = 2
Engineering conversions share the same unit vocabulary as physical tags.
psi, bbl (petroleum barrel), darcy, mD/md (millidarcy), and
cP (centipoise) are built in. Their names and explicit aliases are
case-insensitive, as with other named units; SI prefixes retain their existing
case-sensitive rules. Use m^2, m^3, and Pa*s for compound targets.
Division after a power is supported (m^3/s), as are rational exponents
(m^1/2/s). Shorthands m2, m3, and Pa.s are not unit expressions.
| Unit | Accepted aliases | SI conversion |
|---|---|---|
psi |
psi |
approximately 6894.757293168361 Pa |
bbl |
petroleum_barrel, petroleum_barrels |
0.158987294928 m^3 |
darcy |
darcies |
9.869233e-13 m^2 |
mD |
md, millidarcy, millidarcies |
9.869233e-16 m^2 |
cP |
centipoise |
0.001 Pa*s |
The factors follow NIST SP 811, Appendix B.8 and Appendix B.9. The barrel is exactly 42 U.S. liquid gallons; the darcy factor retains the published precision. Permeability has the dimension of area, but a unit tag alone does not prove that an area is a physically meaningful permeability.
# Convert a pressure difference; no atmospheric offset is implied.
pressure_drop_pa = CONVERT(1500, "psi", "Pa")
viscosity_pa_s = CONVERT(2, "cP", "Pa*s")
volume_rate_m3_s = CONVERT(1000, "bbl/day", "m^3/s")
psi is a pressure scale and does not select absolute or gauge pressure.
State that convention in the model and supply an atmospheric reference when
needed. bbl converts volume without selecting reservoir or standard
conditions. Formation-volume factors and gas reference conditions remain
explicit model assumptions. psia, psig, stb, rb, and scf are not
silently treated as equivalent built-in units.
Custom declarations cannot shadow built-ins. If an older model declares one of these newly built-in names, remove an equivalent declaration or rename a different convention and all its references. Grid reports the conflict rather than silently changing the declaration's scale.
Native value kinds and optional contracts
Native structures use the same IS relationship. The shortest useful type is
the kind itself:
items IS List = VECTOR(1, "two", {name: "three"})
pending IS Deque = DEQUE()
weights IS Tensor = A1:D1000
Those declarations do not demand homogeneous elements or a predeclared shape. The constructor, inferred expression, and resident runtime value establish the kind; an annotation merely adds a contract when the author wants one. Omitting the annotation entirely remains equally valid.
Parameterized contracts participate in Grid's coercion axis. strict checks
their contents recursively, warn keeps the value and emits a coercion
diagnostic, and the default loose mode does not scan collection contents.
Bare kinds always remain open. Type nesting is bounded at 64 levels so hostile
source cannot exhaust the compiler stack.
Use of to add an element contract and to for a keyed value contract. Types
nest without switching to a punctuation-heavy mini-language:
names IS List of String = VECTOR("Ada", "Edsger")
jobs IS Deque of Job = DEQUE()
orders IS Map of Customer Id to List of Order = MAP_OF()
routes IS Trie of Route = TRIE()
latency IS Quantile Sketch = KLL()
List is the language name for Grid's persistent indexed sequence. The legacy
spellings Vector<String> and Map<String, Vector<Order>> remain accepted,
but grid fmt emits List of String and Map of String to List of Order.
Tensor follows the same progression. Shape, semantic element type, and precision are independent, optional contracts:
samples IS Tensor = A1:D1000
points IS Tensor[?, 3] = A1:C1000
distances IS Tensor[?] of unit:m = A1:A1000
weights IS Tensor[2, 2] with precision 32 = PARAMETER([[0.1, 0], [0, 0.1]])
score IS Tensor[] = AVERAGE(weights)
Omitted precision uses ordinary Grid Number semantics. Precision 32 is an
explicit storage/computation policy; precision 64 is available when authors
want to pin the default. Low-bit 2/4/8 storage is quantization, not floating
precision, and is deliberately not disguised as a numeric type.
Tensor contracts are persisted independently from backend promotion. Shape, precision, and semantic element metadata therefore survive compilation even when a formula takes a general evaluation path; runtime publication restores the semantic element envelope without copying resident Tensor storage.
4.3 Dynamic cast inside expressions
Declaration-level IS attaches a tag to the assignment target. To tag a
value inside an expression — for intermediate results or nested assignments —
use infix INTO:
B1 = A1 * fx_rate INTO currency
C1 = ROUND(amount, 2) INTO currency
This lowers to TYPE_TAG(value, "<tag>") and applies the same semantic overlay
as target IS <tag> = …. Use IS on the target when the whole cell carries
the tag; use INTO when only part of the expression should be tagged.
4.1 Tags On Range Targets
The tag applies to every cell in the range:
A1:A10 is currency = 0
4.2 Tags Are Sticky
Once a cell carries a type tag, the tag persists across recomputations until a different assignment overwrites it (or removes it by omitting the tag).
A1 is currency = 100 /* A1 is currency */
A1 = A1 + 1 /* A1 is still currency */
A1 is percentage = 0.5 /* A1 is now percentage */
A1 = "hello" /* A1 is now plain string (tag removed) */
4.3 Unary predicates
Where a type tag says what a value is, a predicate says what is true
about the symbol. Colon predicates form an open, multi-valued set orthogonal
to the type. Fieldless predicates are ad hoc and need no declaration. Use
predicate to declare a reusable typed field schema shared by unary
attachments and explicit model-graph relationship payloads:
predicate reviewed(by: string, score: number)
x is :happy USD = 1.00 /* type USD, plus predicate :happy */
x is a :draft :urgent = 5 /* several predicates, no type */
x is a :reviewed(reviewer, 0.95) USD = 1.00
reviewer1 = PREDICATES(x).reviewed.by
reviewer2 = x.predicates.reviewed.by /* equivalent */
The a or an after declaration-side is is optional surface sugar; the
leading colon identifies a predicate. A declaration is single-valued by
default, with an optional explicit one designation. A multi-valued predicate
declares the field tuple that distinguishes its instances:
predicate reviewed(id: symbol, by: symbol, at: datetime) many by (id)
x is :reviewed(:r1, :ada, NOW()) :reviewed(:r2, :lin, NOW()) = report
review_ids = PREDICATES(x).reviewed.id /* [:r1, :r2] */
reviewers = x.predicates.reviewed.by /* [:ada, :lin] */
The declared field count and types apply wherever :reviewed(...) is attached.
An undeclared predicate may not carry fields. Field access is schema-checked;
PREDICATES(x).reviewed and x.predicates.reviewed query Boolean presence,
while a field of an absent predicate is BLANK. A many-valued field returns an array in
stable first-insertion order. Declared field types fail closed with #TYPE!
even in loose mode. For a single predicate, += :reviewed(...) replaces its
atomic state and bare -= :reviewed removes it. For many, += supplies the
full payload and upserts by the declared identity; -= :reviewed(key, ...)
supplies keys in many by order and removes that instance; bare
-= :reviewed removes all instances. These mutations are reactive inside
WHEN, EVERY, and AT rules; a predicate introduced only by a rule starts
absent until an add action fires. Unary predicates describe the symbol, not the
value, so they don't flow through arithmetic. See the
predicate guide and
reference.md.
Reverse lookup is available through the unified reactive Graph receiver
MODEL.PREDICATES:
all_reviewed = MODEL.PREDICATES.NEIGHBORS(:reviewed)
predicates_on_x = MODEL.PREDICATES.IN_NEIGHBORS("x")
Within this view, colon symbols are predicate vertices and quoted canonical
names are model-symbol vertices. The colon therefore prevents :reviewed from
colliding with a model symbol named reviewed. Each directed edge has kind
:applies; structured fields live on that edge. Authored binary Predicate
facts occupy the same view. A many unary predicate has one parallel edge
per identity, though NEIGHBORS still returns each target symbol once. Rule
mutations revise the view automatically. It is deliberately separate from
MODEL.GRAPH, so unary-predicate membership does not change model dependency
or dependency algorithms. The carrier-owned membership ID is an opaque
versioned string, currently tag-membership:v2:<sha256>, and stays stable when
an upsert changes payload fields without changing its typed identity.
TAGS(x), x.tags, and the unary-only MODEL.TAGS view remain accepted for
source compatibility; new source should use the Predicate spellings above.
5. Ownership, State, And Model Boundaries
Every writable binding has two conceptual value layers:
- The base is the value produced by its top-level declaration.
- A sticky override is a committed write from an admitted external writer or a rule action. While present, it wins over the base.
The value authors read is the effective value: the override when one is
present, otherwise the base. RESET removes an override and reveals the base;
CLEAR installs a blank override. Aliases share these layers because they are
additional addresses for the same binding.
Declarations state who is supposed to write:
| Form | Intended owner | Base value | Other writers |
|---|---|---|---|
A1 = expr |
formula | expr |
none; a rule set earns GRID_MIXED_OWNERSHIP |
input A1 = expr |
external caller | expr |
admitted external writes |
default A1 = expr |
formula plus override layer | expr |
rules; external writes when also declared input |
STATE A1 = expr |
model rules | initial/reset value expr |
rule actions |
| rule-only target | model rules | BLANK |
rule actions |
output does not grant write authority. It publishes a binding on the model's
read surface.
Two optional decorator keywords may appear at the start of an assignment:
input <target>[IS <tag>] = <expression>
output <target>[IS <tag>] <op> <expression>
They label bindings as part of the model's external write surface
(input) and read surface (output).
5.1 input Decorator
input marks a binding as externally writable.
input price = 0
input qty IS integer = 0
input items = []
Restrictions — input may only appear on a plain eager =
assignment with a default value. The parser rejects:
| Form | Why |
|---|---|
input A1 ~= … |
Lazy bindings have no initial value to act as a default |
input A1 ?= … |
Conditional bindings skip on error — undefined initial state |
input A1 += 1 (and -=, *=, /=) |
Arithmetic compound assignment belongs in a rule action, not a declaration |
input A1 = … ONCE (and EVERY, AT) |
Scheduled bindings are driven by time, not external writes |
Inside WHEN … THEN … END rule actions |
Rule actions are model-owned; declare input on the top-level binding instead |
The default value (the right-hand side of =) is the value the binding
holds before any external write arrives. It can be any expression —
including a function call — as long as it does not depend on other
declared inputs that have no default of their own.
5.2 output Decorator
output marks a binding as externally readable.
output total = price * qty
output history ~= EXPENSIVE_AGGREGATE(transactions)
output last_seen = NOW() ONCE
output default running_log = []
output may decorate top-level single-cell and named assignments. Rule actions
do not declare model boundaries; declare the target as an output at the top
level, or publish a semantic alias for it. Use outputs deliberately to define
what the model promises to consumers.
5.3 Combining Decorators
input and output may appear together, in either order:
input output A1 = 0 /* writable input that also surfaces as a read */
output input A1 = 0 /* equivalent */
Each keyword may appear at most once per assignment. Decorators
are case-insensitive (input, INPUT, Input all parse the same).
5.4 default Decorator
default marks a formula as an overridable base value. The formula
computes normally, but a rule action (or an external input) may install
an override on top; the override wins and is sticky (it persists until
cleared), and RESET
restores the base.
default price = model_estimate /* computed default, may be overridden */
WHEN incident THEN price = manual_number END /* override wins, sticky */
WHEN resolved THEN RESET price END /* base governs again */
Without default, a cell is owned by either its formula or its
rule actions, never both: a rule that sets a plain formula cell shadows
it silently and earns the GRID_MIXED_OWNERSHIP warning (see
errors.md). default is the opt-in that makes the
override intentional and inspectable — WHY reports the value as
overridden rather than silently tracing the shadowed formula.
default applies to single-cell and named formula targets. It is not
supported on range or table-mutation targets. It composes with type tags
(default B1 IS currency = …). Like input/output, it is
case-insensitive and appears at the start of the assignment.
It may compose with input when both external callers and model rules are
intentional writers:
input default price = model_estimate
5.5 STATE Declaration
STATE declares model-owned mutable state. Its right-hand side is the initial
value and the value restored by RESET; rule actions install sticky revisions
on top of it.
STATE retry_count = 0
WHEN request_failed THEN retry_count += 1 END
WHEN request_succeeded THEN RESET retry_count END
output retries = retry_count
STATE is distinct from an ordinary formula. A formula describes what a value
is now from its dependencies; state remembers what prior rule waves did. It is
also distinct from input: rules own state, while callers own inputs.
A state declaration uses a single-cell or named target with eager =. It
cannot itself be decorated input or output. Publish state through an
output alias or a derived output, as above. STATE uses the same preserved
base and override machinery as default, but keeps its authored state identity
for formatting and tooling.
5.6 Declared Surface
Use decorators to make the model boundary explicit:
- Inputs are the cells a caller or collaborator is expected to change.
- Outputs are the cells a caller or collaborator is expected to read.
- Any referenced but undeclared cell is just an implementation detail of the model.
Even when a workspace does not enforce the boundary, the declarations remain valuable documentation for people and tools.
5.7 Common Mistakes
input A1 += 1 /* rejected: input requires plain `=` with a default */
input A1 ~= 0 /* rejected: input requires plain `=` with a default */
A1 = input /* rejected: `input` is a reserved word */
input input A1=0 /* rejected: duplicate `input` decorator */
STATE input A1=0 /* rejected: state is model-owned, not an external input */
To attach input to a counter you intend to increment from rules,
declare the input separately:
input default counter = 0 /* external surface plus intentional rule writes */
WHEN trigger > 0 THEN
counter += 1
END
output current_counter = counter /* publish the binding at top level */
6. Schedule Modifiers
A single-action assignment can carry a schedule modifier. This is equivalent to wrapping the assignment in a single-action rule block.
G1 = NOW() EVERY cron"0 * * * *" BACKFILL
G2 = TRUE AT dt"2026-12-31T23:59:00Z"
G3 = NOW() ONCE
Schedule modifiers are sugar for:
EVERY cron"0 * * * *" BACKFILL THEN
G1 = NOW()
END
AT dt"2026-12-31T23:59:00Z" SKIP MISSED THEN
G2 = TRUE
END
Three forms are accepted:
| Form | Behavior |
|---|---|
<expr> EVERY <duration|cron> [SKIP MISSED|BACKFILL] |
Wraps as an EVERY rule block |
<expr> AT <datetime> [SKIP MISSED|BACKFILL] |
Wraps as an AT rule block |
<expr> ONCE |
Capture the first successful evaluation and do not recompute it. Useful for snapshot values such as B1 = NOW() ONCE. An unchanged assignment keeps its captured value when the same resident model is replaced; changing the expression starts a new capture. |
6.1 Restrictions
Schedule modifiers attach only to eager = assignments. Grid rejects:
~=lazy assignments?=conditional eager assignments+=/-=/*=//=compound assignments
A model that combines any schedule modifier with ~=, ?=, or a compound
operator builds with GRID_ASSIGNMENT_SCHEDULE_OPERATOR. The diagnostic names
the attempted scheduled form, explains why schedules require eager writes,
identifies an eager top-level assignment as the valid context, and shows an
= rewrite. See contextual grammar diagnostics.
6.2 Semantics
The canonical style prefers explicit rule blocks for shared models; modifiers
are a convenience for short single-action cases. An inline EVERY or AT
target exists as BLANK before its first firing. Its RHS is evaluated only by
the scheduler, not as an ordinary base formula.
6.3 ONCE semantics
ONCE cells follow these rules:
- The expression evaluates on the first evaluation wave that includes the cell (normally model application or its first read).
- After the first successful evaluation, invalidations from upstream inputs or calculations do not trigger recomputation. The cell stays at its captured value.
- Same-id replacement or runtime-host restart preserves the prior
ONCEvalue if the cell's expression is unchanged. If the expression changes, the cell recomputes once with the new expression. - If the first attempt errors (e.g.
#DIV/0!), the result is not sticky — the cell retries on the next invalidation wave.
7. Nested External Calls
When an external call is nested inside a larger formula, Grid tracks that call separately so the fetched value can be cached and retried independently:
A1 = ROUND(FX_RATE("EUR", "USD") + 0.01, 4)
For readability, prefer naming the external boundary when it is reused:
eur_usd = FX_RATE("EUR", "USD") DEFAULT 1.08
A1 = ROUND(eur_usd + 0.01, 4)
8. Restrictions Summary
| Constraint | Where it applies |
|---|---|
| Range targets must be finite rectangles | Top-level and rule-action assignments |
Arithmetic compound += / -= / *= / /= is valid only on a single-cell or named target inside a rule action |
Rule action body |
Value-first <value> AS <parameter> binds call arguments by name; it is not assignment |
All contexts |
Inside rule actions, =, ?=, and compound (+=/-=/*=//=) are allowed; ~= and nested rules are rejected |
Rule action body |
| Rule action RHS may not contain external async calls because rule actions must commit atomically with the rule wave | Rule action body |
| Namespace specifiers use single quotes for spaces; identifiers don't need them | All contexts |
input decorator requires a plain = assignment with a default value; rejected on ~=, ?=, compound, scheduled, and rule-action targets |
All contexts |
VALIDATE FIELD requires a uniquely owned local eager, unconditional, unscheduled input; simple aliases share its contract |
Standalone field declaration; see record field rules |
output decorates top-level single-cell or named assignments; rule actions do not declare the model boundary |
Top level |
STATE uses an eager = single-cell or named declaration and cannot also be input or output |
Top level |
Each decorator (input, output) may appear at most once per assignment |
All contexts |
9. Common Mistakes
9.1 Missing Operator
A1 10 /* parse error */
A1 = 10 /* correct */
9.2 Single-Quoted String
A1 = 'hello' /* 'hello' is parsed as a namespace specifier, not a string */
A1 = "hello" /* correct */
9.3 Range Target Too Large
A:A = 0 /* rejected — full-column ranges not allowed as targets */
A1:A1000 = 0 /* works, but explodes the dependency graph */
A1:A1000 ~= 0 /* ~= not allowed in range target broadcasting */
Use an anchor assignment with an array-valued RHS instead. For constant fills the dedicated helpers are clearest:
A1 = ZEROS(1000, 1) /* column of zeros */
A1 = ONES(1000, 1) /* column of ones */
A1 = FILL("TBD", 1000, 1) /* column of any constant value */
A1 = REPEAT(0, 1000) /* equivalent shorthand for column of zeros */
When each output element depends on its row/column index, prefer an array
comprehension — it lowers to a single MAKEARRAY binding but reads more
naturally:
A1 = [r * 2 FOR r IN 1..1000]
A1 = [r * c FOR r IN 1..100, c IN 1..10]
Reach for MAKEARRAY directly when you need three or more index
variables, want to share heavy intermediates via LET, or are computing
the dimensions dynamically. For constant fills, prefer
FILL / ZEROS / ONES / REPEAT.
9.4 Incompatible Type Tag
A1 is currency = "hello" /* #TYPE! — string can't be currency */
A1 is currency = "100" /* OK — string coerces to number */
A1 is currency = 100 /* canonical */
9.5 Cycle
A1 = A2 + 1
A2 = A1 + 1 /* both cells become #CIRCULAR_REF! */
Cycles are detected and reported as #CIRCULAR_REF! unless iterative
calculation is enabled for a convergent cycle.
Lazy bindings (~=) participate in cycle detection.
10. Examples
A complete, well-typed model:
MODEL "Pricing"
VERSION "1.0.0"
# Declared inputs (writable surface in protected mode)
input A1 is currency = 100000 /* base price */
input A2 is percentage = 7pct /* discount */
input A3 is percentage = 21pct /* tax rate */
# Declared outputs (readable surface in protected mode)
output B1 is currency = A1 * (1 - A2) /* discounted price */
output B2 is currency = B1 * (1 + A3) /* gross price */
# Lazy expensive lookup, exposed as output
output C1 ~= FX_RATE("USD", "EUR")
# Conditional output (only if FX succeeds)
output D1 is currency ?= B2 * C1
# Internal counter (not part of the external surface)
default E1 = 0
WHEN increment_requested THEN E1 += 1 END
# Range broadcast (every cell carries the `output` decorator)
output F1:F5 = 0
# Destructuring (decorator broadcasts to each binding)
output [min, max, avg] = SUMMARY([100, 200, 300, 400])
END MODEL
This model is safe to deploy with protectedMode: true: every
implicit input is declared (the model has no implicit inputs), and
the model declares both an input and an output.
11. SOLVE Statements
A SOLVE statement declares a goal-seek or bounded optimization over
workbook cells. The result cell receives the value of the variable cell
that satisfies the goal; the variable cell itself is never mutated, so the
sheet stays consistent and auditable.
SOLVE Z9 = A2 IN [0, 1] GOAL A3 = 200000
SOLVE B9 = A1 IN [0, 5] MINIMIZE A2
SOLVE B9 = A1 IN [0, 5] MAXIMIZE A2
SOLVE Recommended = Candidate IN [Minimum, Capacity] GOAL Residual = Target
Z9— result cell (receives the solved variable value).A2— variable cell the solver varies inside the bounds[lo, hi](fractional bounds are fine).GOAL A3 = 200000drives cellA3to the target by root finding;MINIMIZE/MAXIMIZEoptimize the objective cell instead.- The goal may reach the variable through intermediate cells (chains and diamonds compose by the chain rule); other referenced cells participate as data parameters at their current values.
- Result, variable, and objective can also be named wires. Bounds and goal targets can be expressions, evaluated once at the current input values before solving. They must be finite numbers with ordered bounds.
- Solves are reactive: editing an upstream input re-solves on the next resolve.
Current limits: one variable per statement, default-sheet addresses, and
expression-backed chains (a chain crossing a descriptor-compacted range
refuses). A statement the compiler cannot lower reports
#CALC! (SOLVE_NOT_COMPILED); a solve that fails at runtime (no root in
the bounds, non-smooth objective) reports #CALC! (SOLVE_FAILED) on the
result cell without failing the rest of the model.
Solves execute on the solver-region lane and write back unconditionally —
the result cell has no other evaluator. The resolve response carries a
structured solverWrites report (cell, values, boundDuals), where
boundDuals is the goal's marginal at the solution (∂goal/∂variable) —
the shadow-price view of the solve; the same values appear in the solve's
diagnostics.
12. Named and Indexed Optimization
name = SOLVE … END binds an optimization result, with local unknowns and
linear constraints. SOLVE name … END is an equivalent alternative spelling.
Both produce the same owning AST and one existing LP/MIP report call; reactive
inputs and the result run through the ordinary compiled evaluator. Source formatting
preserves the chosen spelling; source-free canonical rendering uses assignment.
These notation bindings currently require a plain, unqualified top-level name
and =. Cell/range targets, decorators, other assignment operators and nested
blocks are not supported. Ordinary references and function calls named plan
or solve retain their expression grammar.
input capacity = 4
allocation = SOLVE
VARIABLE x IN [0, 2]
VARIABLE y IN [0, 3]
REQUIRE total_capacity: x + y <= capacity
MAXIMIZE 3*x + 2*y
END
chosen_x = allocation.values.x
objective = allocation.objective
status = allocation.status
This model returns approximately x = 2, y = 2, and objective 10.
Changing capacity re-solves the model without mutating either unknown into an
outer input. The earlier single-variable SOLVE result = … form is unchanged.
VARIABLE name [IS continuous|integer|binary] IN [lower, upper]declares an unknown. Continuous is the default. Binary bounds may be omitted for[0,1]. Bounds must be finite and satisfylower <= upper, including negative and fixed bounds. Binary bounds must stay within[0, 1]. Unbounded/free unknowns are outside this version.REQUIRE label: left <=|>=|= rightdeclares a named constraint. Equality becomes two labeled inequalities. Labels and unknown names are unique within their respective namespaces, compared without case.- Exactly one
MINIMIZEorMAXIMIZEgives the objective. Unknown-bearing expressions allow addition, subtraction, multiplication by a parameter, and division by a parameter. Objectives additionally admit the convexABS/MAXpenalties described below. Products of unknowns and other nonlinear functions of unknowns are rejected. - Parameters are previously declared, untagged numeric inputs or pure numeric
formulas. A bounded numeric function subset includes
ABS,MIN,MAX,SUM,PRODUCT, roots, logarithms, exponentials, rounding, and trigonometry. User callables, effectful/volatile calls, tagged quantities, and arbitrary record access and arbitrary array operations are not admitted. Fixed-shape numeric arrays support the checkedINDEXreads described below. Parameter expressions use lexical snapshots; invalid or nonfinite inputs produce a value error before solver backend execution.
Use finite domains to name families of unknowns and constraints:
workers = [:ana, :ben]
tasks = [:cut, :pack]
assignment = SOLVE
VARIABLE assigned[task IN tasks, worker IN workers] IS binary
REQUIRE covered[task IN tasks]: SUM(worker IN workers, assigned[task, worker]) = 1
REQUIRE available[worker IN workers]: SUM(task IN tasks, assigned[task, worker]) <= 1
MINIMIZE 3*assigned[:cut, :ana] + 5*assigned[:cut, :ben]
+ 4*assigned[:pack, :ana] + 2*assigned[:pack, :ben]
END
Domains are preceding constant row or column arrays, including constant aliases. Keys must be unique, nonempty text, symbols, or exact integers, with one key representation per domain. Input-dependent domains and Frames are rejected; domain changes require recompilation. Nested binders cannot shadow active indices, unknowns, or declared parameters. An index tuple is meaningful on a SOLVE unknown or declared parameter. Declaration and domain order determine columns; the last index varies fastest.
Newly compiled blocks retain the wrapper schema grid.named-optimization.v1:
| Field | Meaning |
|---|---|
status |
Exact status from the existing optimization report. |
values |
Record keyed by unknown name; indexed unknowns are arrays in domain order. Blank when no candidate exists. |
axes |
Ordered domain arrays for each indexed unknown; an empty record for scalar-only models. |
objective |
Candidate objective including the variable-independent constant; blank without a candidate. |
report |
grid.optimization.report.v2 sparse report, including variable/row labels and solver evidence. Its objective excludes the constant. |
assignment.values.assigned is a task-by-worker matrix and
assignment.axes.assigned carries those two ordered domains. Reading fields
does not launch additional solves. Budget-limited candidates retain their actual
status; a candidate does not by itself establish feasibility or optimality.
Authored bounds are native column bounds, exposed as each variable's
lower_bound and upper_bound. They do not add constraint rows. For example,
VARIABLE balance IN [-5, 3] allows a negative balance directly. Fixed variables
such as VARIABLE reserve IN [-2, -2] are supported.
The report's constraint_coefficients is a grid.sparse-matrix.csr.v1 record:
row_count, column_count, row_offsets, column_indices, and coefficients.
Offsets and column indices are zero-based; vectors are row arrays. Consecutive
offsets delimit each row's sorted coefficient entries. Empty rows repeat their
offset; a model with no encoded constraints has offsets [0]. The compiler and
report never materialize a dense constraint matrix. >= constraints retain the
negated encoding and equality retains its two labeled opposite inequalities.
The original positional LP/MIP report functions still return dense v1 reports.
The named wrapper and its objective-constant convention are unchanged.
Fixed-shape parameter arrays
Declare a local PARAMETER to read array values by domain keys:
products = [:chair, :desk]
input unit_profit = [3, 2]
input production_limit = [2, 3]
allocation = SOLVE
PARAMETER profit[product IN products] = unit_profit
PARAMETER limit[product IN products] = production_limit
VARIABLE quantity[product IN products] IN [0, limit[product]]
REQUIRE capacity: SUM(product IN products, quantity[product]) <= 4
MAXIMIZE SUM(product IN products, profit[product] * quantity[product])
END
A parameter is a read-only local view of data. It does not introduce an unknown,
global assignment, or result field. One axis maps an exact-length row or column
vector; two axes map rows then columns. Domain order determines storage order:
with keys [20, 10], profit[10] reads the second value. Keys may be symbols,
text, or exact integers, using the same rules as unknown domains. Reads need
the declared number of keys, supplied as literals or active index binders.
Values remain reactive; axes and dimensions require recompilation to change.
Initializers and domains read the outer scope before local declarations become
visible, so PARAMETER costs[k IN products] = costs can name a view of an outer
array. Local parameter and unknown names must be unique without regard to case.
Parameter-to-parameter initializers, scalar parameters, more than two axes,
dynamic domains, and keys outside a domain are refused. Each declaration retains
the complete array contract even when no term reads it or terms cancel.
Static domain and index constants must have one definition and no reactive rule
writes, including through aliases. Local unknowns and parameters cannot shadow
those constants. This keeps compiled labels and positions consistent with the
module; parameter array values may still be updated by inputs or rules.
The named parameter example
includes usage[resource, product] for a coefficient matrix and
capacity[resource] for right-hand sides. These accesses lower to fixed
coordinates in the same checked array path as explicit INDEX calls below.
The transportation example
uses two indexed shipment dimensions, stock constraints and exact order
fulfilment. Changing costs or stock re-solves the allocation; insufficient total
stock produces the native infeasible result rather than a partial shipment plan.
Use INDEX(data, row, column) for scalar coefficients, right-hand sides, and
variable bounds. All three arguments are required; positions are one-based
compile-time positive integers, including expanded numeric binders and constant
arithmetic with +, -, *, and /. The array can be a rectangular literal or a preceding
binding/alias whose initializer has a known rectangular shape. Its elements
must be pure numeric expressions. Inputs may replace values without recompiling.
products = [1, 2]
input unit_profit = [3, 2]
input production_limit = [2, 3]
allocation = SOLVE
VARIABLE quantity[product IN products] IN [0, INDEX(production_limit, 1, product)]
REQUIRE capacity: SUM(product IN products, quantity[product]) <= 4
MAXIMIZE SUM(product IN products, INDEX(unit_profit, 1, product) * quantity[product])
END
Row vectors, column vectors, and two-dimensional arrays use the same explicit row/column convention. The complete example includes indexed resource rows and a coefficient matrix. Values and signed bounds remain reactive through ordinary reactive dependencies. Domains, dimensions, and positions stay fixed; change them by recompiling the source.
The selected report adapter validates every supplied array, including unread
cells, before solving. Shape changes, ragged arrays, nested arrays, blanks,
booleans, text, tagged quantities, and nonfinite elements return #VALUE!;
a subsequent valid update can solve again. Array-valued coefficients, dynamic
index positions, array-producing functions, Frames, and record lookups are not
supported by this profile. Each SOLVE admits at most 1,000 parameter arrays and
1,000,000 total declared cells, subject to existing compiler work budgets.
The compiler uses the single-record overload LINEAR_PROGRAM.REPORT(model) or
MIXED_INTEGER_PROGRAM.REPORT(model). Its model schema is
grid.linear-program.sparse.v1, with numeric vectors objective, row_offsets,
column_indices, coefficients, rhs, lower_bounds and upper_bounds;
boolean maximize; and label vectors variable_labels, row_labels and
original_senses (<= or >=). MIP also requires a numeric integer_mask of
zeros and ones; the continuous overload refuses a nonblank mask. Coefficients encode
A x <= rhs; original_senses is presentation metadata for negated >= rows,
not an instruction to change the solved inequality. Missing variable or
row labels may be blank to generate defaults. CSR rows require sorted unique
zero-based columns; coefficients that evaluate to zero are omitted from the
native model. The same finite-value, bound-order and size checks apply to direct
calls. The adapter permits up to 20,000 encoded rows because each of the 10,000
authored equality constraints may contribute two rows. Array-backed models select
grid.linear-program.sparse.v2, which requires a nonempty parameter_arrays
row/column array of records with name, values, rows, and columns.
values must match the declared positive dimensions and contain only finite
untagged numbers. The ingress version makes older runtimes refuse rather than
ignore array validation. Scalar-only models retain sparse v1 ingress; both
versions produce the existing sparse v2 report and named v1 wrapper.
Integer/binary unknowns require a solver-mip runtime build. Missing capability
produces the existing invalid_problem report with an
integer-backend capability warning, without relaxing integrality.
Expansion is limited to 1,000 total variable columns and 10,000 constraint instances, eight indices per declaration, and 1,000,000 stored constraint coefficients, including both equality rows. Bounds do not count as constraint coefficients. Expression expansion and native compiler/solver budgets also apply. Indexed report labels must fit 480 UTF-8 bytes. Sparse coefficient arrays and native bounds feed the existing solver model.
The V3 owning syntax retains declarations, bounds, objectives, index domains, and constraints. Canonical admission constructs native expressions directly; it does not generate and reparse Grid source.
Owning transport, formatting, free-name inspection, and checked expression edits use that same admission path. Local parameter names support checked renames, including their reads; they do not define public result keys. Unknown/index declaration renames refuse in this first version: unknown names also define public result fields and report labels, and index declarations need distinct rename identities. Edit a numeric coefficient, bound, or constraint expression through the checked AST workflow.
Keyed parameter inputs
Use BY ... VALUE ... when source rows carry their own keys. Grid aligns them
to the declared axes once, before solving:
products = ["alpha", "beta"]
input forecasts = {
product: ["beta", "alpha"],
demand: [4, 6],
margin: [2, 3]
}
allocate = SOLVE(rows, capacity)
PARAMETER demand[p IN products] = rows BY product VALUE demand
PARAMETER margin[p IN products] = rows BY product VALUE margin
VARIABLE quantity[p IN products] IN [0, demand[p]]
REQUIRE capacity_limit: SUM(p IN products, quantity[p]) <= capacity
MAXIMIZE SUM(p IN products, margin[p] * quantity[p])
END
allocation = allocate(forecasts, 8)
This returns quantity = [6, 2] in product order, with objective 22. Changing
source row order does not change the result. The alternative SOLVE allocate(rows, capacity) header has the same meaning. Ordinary local scope,
named arguments, partial application, imports and reusable calls also apply.
Two axes use two key columns in the same order as the axes:
PARAMETER cost[o IN origins, d IN destinations] =
routes BY origin, destination VALUE cost
The first axis becomes matrix rows; the second becomes columns. The selectors
following BY and VALUE are literal column identifiers, so renaming a Grid
variable does not rename them. Axis domains remain fixed at compilation, just
as for positional parameters. Incoming values and row order may change.
The ordinary function exposes the same admission operation:
demand = KEYED.VALUES(forecasts, ["product"], [products], "demand")
inputs = KEYED.VALUES(forecasts, ["product"], [products], ["demand", "margin"])
A single value-column string returns a numeric row or matrix. A vector of value column names returns a record containing those arrays, sharing one selected read and alignment. Within one SOLVE invocation, parameters using the same source binding, key columns and domains share that operation automatically. Independent source expressions are evaluated independently.
Sources may be column records, matrices with a header row, native vectors of row records, declared Tables, already materialized model-local Frames, or resident graph-query Frames in the current resolve. Only selected key and value columns participate. Physical Int64 Frame columns admit only lossless safe integers; semantic tags remain intact and tagged numerical coefficients refuse. Declared Table mutations invalidate dependent calls; explicit Table handle inputs must be replaced with the current handle after a mutation. Frames retain their immutable snapshot and must be replaced explicitly. Resident graph-query references must match the current Frame revision; references captured before a graph mutation are rejected. Admission never runs lazy ETL or creates a job.
Every domain tuple must appear exactly once. Missing, duplicate, outside-domain or wrong-kind keys fail, even if repeated rows contain identical values. Keys are nonempty text, symbols or exact safe integers; text is case-sensitive and these three kinds stay distinct. Values must be finite untagged numbers: blank, text, boolean, nested, nonfinite and tagged values fail, including in unused parameters. No rows are dropped, filled or aggregated implicitly.
Admission permits at most 100,000 rows/tuples, 1,000,000 selected cells, 128-byte keys, two axes and 1,024 value columns. Resident Table projections additionally have a 64 MiB selected snapshot budget; Frame IPC snapshots must fit within 64 MiB, including unselected columns. Products and carrier bounds are checked before output allocation; existing tighter solver limits still apply. Sparse coverage, implicit unit conversion and inferred axes are outside this first profile. See the complete example.
Soft constraints and convex piecewise-linear objectives
Keep compulsory requirements in REQUIRE. Express a preference by charging for
its violation in the objective:
input demand = 8
input capacity = 6
input unit_cost = 2
input shortage_penalty = 10
allocation = SOLVE
VARIABLE production IN [0, capacity]
MINIMIZE unit_cost * production
+ shortage_penalty * MAX(0, demand - production)
END
The model produces six units and pays for a two-unit shortage. Making
production >= demand a REQUIRE instead would make this example infeasible.
There is no automatic relaxation of hard constraints.
| Preference | Penalty expression |
|---|---|
| Meet or exceed a target | weight * MAX(0, target - actual) |
| Stay below a preferred ceiling | weight * MAX(0, actual - ceiling) |
| Stay close to a target | weight * ABS(actual - target) |
| Minimize the worst of several affine costs | MAX(cost_a, cost_b, cost_c) |
ABS accepts an affine decision expression. MAX accepts scalar affine
expressions and nonnegative weighted sums of admitted penalties; for example,
MAX(2 * ABS(x - 1), ABS(x + 1)). Binder SUM supports indexed penalty terms
and named parameter data. Bounds, target values, affine coefficients and
penalty weights retain the existing reactive numeric parameter contract.
MINIMIZE may add nonnegative weighted penalties; MAXIMIZE may subtract them.
A zero weight is valid. Negative weights and nonconvex nesting are refused;
a reactive weight that changes to an invalid sign returns #VALUE! until
corrected. ABS(MAX(x, 0)), products of unknown expressions and general
nonlinear objectives remain outside this profile. ABS/MAX over parameters
alone keep their ordinary numeric meaning.
The compiler introduces continuous helper variables and linear constraints, then calls the existing LP or MIP report function once. Helpers share the 1,000-column, 10,000-constraint and 1,000,000-stored-coefficient ceilings with authored declarations. Their conservative finite bounds are derived from the authored bounds at each resolve; overflowing helper-bound arithmetic refuses the resolve. Integer authored decisions still require the MIP capability; helper variables remain continuous.
values and axes contain only authored unknowns. Diagnostic report.variables
and report.constraints also include reserved @penalty[...] labels for the
lowered formulation. The named objective evaluates the authored objective at
the candidate decisions. report.objective describes the encoded linear
objective and excludes its constant; it can additionally differ when a
budget-limited candidate has loose helper variables. Neither a candidate nor
its evaluated cost establishes feasibility or optimality: always inspect
status. Source-free owning artifacts retain the authored ABS/MAX expressions,
not generated helper declarations.
Lexicographic objectives
Use THEN to prioritize goals. Each later stage optimizes within protection
bounds established by completed earlier stages:
allocation = SOLVE
VARIABLE x IN [0, 2]
VARIABLE y IN [0, 3]
REQUIRE demand: x + y >= 4
MINIMIZE MAX(0, 3-y) TOLERANCE ABSOLUTE 0.001 RELATIVE 0
THEN MINIMIZE x + 5*y
BUDGET 1000 ITERATIONS
END
THEN expression inherits the preceding direction; THEN MINIMIZE expression
and THEN MAXIMIZE expression select it explicitly. Two through sixteen
objectives support indexed affine terms and the existing convex ABS/MAX forms.
Both assignment and declaration-first headers work. All stages share the existing
expanded-model limits, including helper columns and protection rows. Hard
requirements and variable bounds retain their meanings.
Optional TOLERANCE ABSOLUTE a RELATIVE r belongs to the preceding objective.
Omitted tolerances are zero. Values may be reactive pure numeric parameters,
must be finite and nonnegative, and cannot depend on unknowns. For an achieved
objective value z, later stages permit degradation a + r * abs(z). The
constant offset is included in z. Minimization adds the allowance;
maximization subtracts it. Zero means no requested degradation; native numerical
feasibility and optimality tolerances still apply separately and are reported.
Overflow refuses rather than removing the protection bound. Triangular epigraph
helpers are tightened to their admitted affine maxima before measuring an
achieved objective, so helper slack cannot enlarge a relative allowance.
BUDGET n ITERATIONS is available with multiple objectives. It reserves a shared
integer effort allowance (0–1,000,000,000), capped by the host's existing
iteration/evaluation limits. Each stage receives the remaining allowance divided
by the number of remaining stages, rounding up. Reservations are not measured
work and are not refunded. The allowance limits native LP iterations and, for
HiGHS, MIP nodes/leaves separately; it is not a combined instruction count.
Presolve, wall time and peak memory are not bounded by this clause. Omission uses
the host caps; zero performs no backend solve and reports budget_exhausted.
Only native convergence at the requested solver accuracy, followed by an
independent finite, bound, row and integrality check, permits advancing. A reduced-accuracy
result retains its feasible candidate but cannot certify a priority. No lower objective runs after an unfinished
stage. A prior feasible candidate may survive a later failure, but the overall
status remains unfinished. A failed later protected stage never labels the
original model infeasible. Cancellation is checked at native stage boundaries.
Integer models still require solver-mip.
Multiple objectives select grid.named-optimization.v2. values and axes
retain their meanings. objective evaluates the first authored objective at
the final candidate; objectives is a row array evaluating every authored
objective there. Missing candidates yield blank objective values. Generated
helpers never substitute for authored ABS/MAX evaluation in these public values.
The versioned grid.optimization.lexicographic-report.v1 report adds:
completed_stages: the completed prefix length;candidate_stage: the one-based stage supplying the retained candidate.stages: status (not_startedwhen unattempted), completion, direction, achieved optimum, degradation tolerances and reserved allowance.reserved_iterationsandbudget_note: explicit effort accounting.
Its sparse model describes the last attempted stage, including @priority[...]
protection rows. Its scalar objective and marginals concern that stage; they
are not joint multi-objective sensitivity. Incomplete results omit dual claims.
Older runtimes reject the new formulation schema instead of ignoring priorities.
Single-objective payloads and result schemas remain unchanged.
13. Planning declarations
fabrication = PLAN … END defines a native scheduling problem and binds a
model-local reference to it. PLAN fabrication … END is an equivalent
alternative, including the same reference binding. The existing Planning
operation solves it and publishes requested outputs. Targeted reads of the
reference or its aliases do not run scheduling search or publish results.
Explicit whole-module compiled execution still runs its Planning regions, as before.
fabrication = PLAN TICK 1min
RESOURCE laser CAPACITY 1
RESOURCE welder CAPACITY 2
CALENDAR laser = [[0min, 8h], [9h, 17h]]
ACTIVITY cut DURATION 4h USING laser
ACTIVITY assemble DURATION 5h USING welder AFTER cut
ACTIVITY inspect DURATION 30min USING welder AFTER assemble
RELEASE 0min DEADLINE 17h
MINIMIZE makespan
RESULTS TABLE fabrication_schedule
END
selected_plan = fabrication
plan_name = selected_plan.name
The reference record has schema grid.planning.reference.v1 and fields name,
tickSecondsNumerator and tickSecondsDenominator (exact decimal strings).
Its name resolves to the model's native resident PlanHandle; transient numeric
handle IDs are not portable reference identities. Pass reference.name to the
existing resolvePlanning(modelId, name) operation. It refers to the current
named definition in that model, including resident repairs; it is not a frozen
copy, a cross-model reference, or a solved schedule. Bytecode retains both the
reference and native definition without requiring source text.
The plan name is an immutable module binding. Collisions with another binding
or callable, duplicate definitions and rule writes are errors. Previously,
PLAN name occupied a separate namespace; models that reused that name for
an unrelated value must rename one of them. Resource and activity namespaces
remain local and distinct.
This schedule starts at ticks 0, 240 and 540 and finishes at 570. One tick is one
minute. resolvePlanning(modelId, "fabrication") selects this definition;
resolvePlanning(modelId) retains the existing all-plans operation. Legacy
region-index patch and definition APIs keep their existing meaning.
| Clause | Meaning |
|---|---|
TICK duration |
Required positive fixed duration. Schedule timestamps and durations must be exact nonnegative multiples. |
RESOURCE name [CAPACITY integer] |
Declared resource; capacity defaults to 1. |
CALENDAR name = [[start, end], ...] |
Ordered, nonoverlapping half-open availability windows. Omitted means always available; [] means unavailable. |
ACTIVITY name DURATION duration USING resource |
Required nonpreemptive work with positive duration. |
ALTERNATIVES [resource, ...] |
Ordered alternative resources for that activity. |
DEMAND integer |
Positive resource demand, default 1. |
SETUP class |
Static setup label on an activity, used by its selected resources. |
CHANGEOVER resource FROM class TO class DURATION duration |
Directed transition time on a unary resource. Initial use and unspecified transitions take zero setup time. |
CHOICE (role = resource, ...) DURING [start, end] |
Offer one complete resource combination within a finite occupancy window, including setup. |
NO CHOICES |
Explicitly offer no placement for this activity. |
FIXED AT start USING (role = resource, ...) |
Pin the work start and complete resource combination to an offered choice. |
AFTER predecessor or AFTER [a, b] |
Dependencies, including references to later activities. Cycles are refused. |
RELEASE duration, DEADLINE duration |
Earliest start and latest completion, in time from the plan origin. |
OPTIONAL PENALTY integer |
Permit omission, paying the stated cost. Compatible with total cost or explicit objective priorities. |
DUE duration PENALTY integer PER duration |
Soft completion target and lateness rate. Compatible with total cost or explicit objective priorities. |
MINIMIZE makespan or MINIMIZE total_cost |
Exactly one objective; total cost adds omission and lateness penalties to makespan in ticks. |
MINIMIZE omission_cost THEN tardiness_cost THEN makespan |
Exact supported priority order, with finite deadlines and explicit windowed choices. |
BUDGET integer NODES |
Native search budget; omitted uses 100,000. Zero retains the greedy incumbent without proof search. |
REPAIR BUDGET integer NODES |
Native repair budget; omitted uses 2,000. |
RESULTS FRAME name, RESULTS TABLE name |
Optional existing publication destinations. Names must not collide with model bindings or another plan's output. |
Resource and activity names occupy separate, case-insensitive namespaces. Every
resource must be declared. Time operands accept fixed duration suffixes
(ms, s, min, h, d, w), fixed ISO duration literals, and pure constant
arithmetic (+, -, *, /, parentheses and preceding constant bindings).
Bare numbers are not times. Fractional ticks, overflow, reactive inputs,
month/year durations and calls are refused. Compile-time arithmetic is checked
within signed 128-bit rational bounds; native counts must fit their u32/u64
contracts. Changing ordinary model inputs does not rewrite immutable plan data.
Preceding bindings retain normal Grid literal semantics; numeric bindings above
the safe integer range are refused. Write large node budgets directly in the
PLAN block to preserve their exact integer spelling.
Reports retain the native feasibility/budget status, assignments, makespan and
repair evidence. The optional authoring metadata gives the plan name and a
reduced exact tick scale in seconds (tickSecondsNumerator and
tickSecondsDenominator, decimal strings). The compiled artifact carries this data,
so loading an archive does not require the original Grid source.
Simultaneous-resource search prunes a branch when its optimistic cost cannot
improve the current schedule. The bound accounts for release times, durations,
soft due dates and optional omission penalties; resource delays can only raise
it. A matching bound can prove optimality without enumerating activity orders.
Canonical tie-breaking still examines equal-cost alternatives. BUDGET 0 NODES
continues to return an incumbent without a proof, and difficult plans may still
exhaust a positive budget with status feasible.
Simultaneous resource requirements
An activity can require several resources at once. Give each requirement a role name and a list of alternatives:
workshop = PLAN TICK 1min
RESOURCE ana
RESOURCE ben
RESOURCE laser
RESOURCE mill
ACTIVITY machine_part DURATION 30min
USING operator FROM [ana, ben]
AND machine FROM [laser, mill]
MINIMIZE makespan
END
The scheduler chooses one operator and one machine and reserves both for the
entire activity. FROM lists independent alternatives for a role; AND adds
another required role. This differs from scalar USING laser ALTERNATIVES [mill],
which reserves only a machine. A role may have one option, such as FROM [laser].
Splitting a simultaneous activity into separate activities would change its meaning.
Each role accepts DEMAND positive_integer, default 1. For example,
USING crew FROM [day_team, night_team] DEMAND 2 reserves two capacity units on
one selected team; it does not select two different resources. Demands on a
resource shared by several roles add together. Roles are case-insensitive and
unique within one activity. Resource and activity namespaces stay separate from
roles and ordinary constant bindings. Every resource must be declared, and every listed option must have capacity for that
role’s demand. Empty lists and repeated options are errors. Finish the requirement list before
AFTER, RELEASE, DEADLINE, SETUP, OPTIONAL or DUE. Scalar and grouped forms cannot be mixed in one
activity, but different activities in one plan may use either form.
A plan containing groups selects the existing simultaneous v5 contract. Native admission bounds apply: at most 16 roles per activity, 64 alternatives per role, 4,096 combinations per activity and 65,536 combinations across a plan, plus 4,096 activities/resources and search/repair budgets no greater than 1,000,000. The native contract validates capacity and graph structure; infeasibility and budget exhaustion remain distinct runtime results. All times and demands still use immutable compile-time data. Reports keep every role assignment, the source name and exact tick scale; reference reads still do not solve or publish. See the complete workshop example.
Scalar-only plans retain v4 payloads; simultaneous groups use v5. Explicit choices, occupancy windows and fixed appointments select v6, as described below. Planning does not require the optional MIP backend.
Sequence-dependent setup
Use SETUP labels when preparation time depends on the previous activity on a
resource. Directed CHANGEOVER declarations can have different durations:
painting = PLAN TICK 1min
RESOURCE booth
CHANGEOVER booth FROM red TO blue DURATION 15min
CHANGEOVER booth FROM blue TO red DURATION 10min
ACTIVITY first_batch DURATION 20min USING booth SETUP red
ACTIVITY second_batch DURATION 20min USING booth SETUP blue
AFTER first_batch
MINIMIZE makespan
END
The first batch finishes at minute 20. Setup then occupies the booth for 15 minutes, and the second batch runs from 35 to 55. Reversing the two class labels finishes at minute 50. Initial use, a same-class transition without an explicit entry, and any other unspecified transition have zero setup duration.
Class names are case-insensitive identifier labels, not expressions or model
bindings. SETUP applies to scalar and simultaneous activities, including an
entire indexed activity family. Each selected resource uses its own directed
transition table. Alternatives can avoid a changeover by selecting another
resource. Resources with changeovers must have capacity 1; native admission
rejects higher capacity rather than silently ignoring setup.
Setup is non-anticipatory: it follows the activity's release and predecessors.
The whole setup-plus-work occupancy must fit resource availability. For a booth
calendar [[0min, 30min], [40min, 100min]], the example's second setup starts at
40, work starts at 55, and work finishes at 75. Reports retain setup start and
duration separately from work start and end.
Durations use the same immutable data and exact tick rules as other PLAN time
operands. A keyed resource such as booth[:east] selects one member;
CHANGEOVER booth[*] FROM red TO blue DURATION 15min applies to every member.
Duplicate directed transitions are errors, including overlapping wildcard and
member declarations. Expansion is capped at 65,536 transitions per plan.
Checked binding renames update resource and activity references while leaving
class labels intact. The compiled payload reuses native setup fields; reference reads
still do not solve or publish. See the parameterized painting example.
Allowed combinations, occupancy windows and appointments
Independent resource groups permit every combination of their alternatives.
Use CHOICE when only particular combinations are allowed:
workshop = PLAN TICK 1min
RESOURCE ana
RESOURCE ben
RESOURCE laser
RESOURCE mill
ACTIVITY machine_part DURATION 30min
USING operator FROM [ana, ben] AND equipment FROM [laser, mill]
CHOICE (operator = ana, equipment = laser) DURING [0min, 4h]
CHOICE (operator = ben, equipment = mill) DURING [1h, 8h]
FIXED AT 2h USING (operator = ben, equipment = mill)
MINIMIZE makespan
END
This activity can use Ana with the laser, or Ben with the mill. The appointment
chooses Ben and the mill, starts work at minute 120, and finishes at minute 150.
A tuple supplies every role exactly once; its resource must belong to that
role's alternatives. Plain USING bench has the native role resource, written
CHOICE (resource = bench) DURING [0min, 1h].
DURING bounds the entire half-open occupancy, including setup before work.
FIXED AT pins the work start. Calendars, release times, hard deadlines,
predecessors, capacity and setup still apply. A fixed tuple absent from the
offered choices is invalid; a well-formed appointment that cannot fit its
window or other hard constraints produces an infeasible schedule.
Every activity in a windowed plan needs CHOICE declarations or NO CHOICES.
No window is inferred. NO CHOICES allows optional work to be omitted, makes
mandatory work infeasible, and cannot satisfy a fixed appointment. Repeating a
tuple with different windows is allowed; duplicate tuple/window pairs are
errors. Windows and fixed times use exact immutable duration expressions,
including named parameters and activity-family binders. Resource keys select
one member; tuples cannot contain [*].
The compiler reserves expansion before copying offers: at most 4,096 choices per activity, 65,536 total choices, and 65,536 tuple role entries including fixed appointments. Eligible-resource references have a separate 65,536-entry bound. Explicit choices avoid constructing the groups' Cartesian product. Source-free owning artifacts and checked renaming preserve role, resource and family-binder identity. These clauses select the existing native v6 compiled definition for total cost, or v7 for tuple priorities/diagnosis; reading the plan reference still does not solve it.
Objective priorities
MINIMIZE omission_cost THEN tardiness_cost THEN makespan minimizes the
weighted omission penalties first, then weighted lateness, then completion time.
Any nonempty subset and permutation of these components is supported. Weights
come from OPTIONAL PENALTY and DUE … PENALTY … PER; zero weights are allowed.
Hard deadlines and explicit windows are optional. Search compares exact integer
tuples directly, and reports the selected components in priorities.
objective_cost remains the ordinary makespan-plus-penalties total for reporting;
it does not rank a priority objective. With an optional ten-minute job and an
omission penalty of one, omission-first priorities favor completing the job when
feasible even if its lateness is expensive. Makespan-first priorities can prefer
omission. See General PLAN priorities for examples,
budget behavior and compatibility with older serialized plans.
Optional activities and soft due dates
Use a due date when late work remains useful, and an omission penalty when work may be skipped. Both apply to scalar and simultaneous activities:
dispatch = PLAN TICK 1min
RESOURCE machine
ACTIVITY urgent DURATION 2min USING machine
DUE 2min PENALTY 10 PER 1min
ACTIVITY routine DURATION 3min USING machine
DUE 4min PENALTY 1 PER 1min
ACTIVITY polish DURATION 10min USING machine
OPTIONAL PENALTY 2
MINIMIZE total_cost
END
The best schedule completes urgent work at minute 2 and routine work at minute
5, and omits polishing. Its cost is 5 + 1 + 2 = 8: five ticks of makespan,
one unit of lateness cost, and two units of omission cost.
total_cost is the existing native weighted sum: makespan in ticks plus
omission penalties plus max(0, completion - due) times each admitted lateness
rate. It does not express strict priority or minimize tardiness alone.
DEADLINE still forbids late completion; an activity may have both a soft due
date and a hard deadline. A required activity cannot depend on an optional
predecessor. An omitted optional predecessor prevents scheduling its optional
successors under the existing native contract.
Penalties are nonnegative integers, including zero. The PER duration must be
positive. The compiler converts its rate to an exact integer cost per tick:
PENALTY 10 PER 1min with TICK 30s becomes five per tick. An inexact rate is
refused rather than rounded. Due dates must fall on whole ticks; the rate's
period need not, provided its conversion is exact. All operands use the same
immutable constant rules as other PLAN clauses. Changing TICK changes the
makespan contribution relative to omission costs; rates retain their authored
physical time meaning. Costs and native arithmetic retain each selected
scheduler contract's integer limits.
These clauses support MINIMIZE total_cost or explicit objective priorities, including zero penalties.
Older MINIMIZE makespan plans keep their meaning and native payloads. Owning
syntax preserves cost operands for rendering and checked edits. A PLAN still
binds a resident definition reference; explicit operations solve and publish.
See the complete example.
13.3 Named parameter data
Use PARAMETER to attach readable domain keys to immutable arrays:
jobs = [:cut, :pack]
times = [20min, 10min]
batch = PLAN TICK 1min
PARAMETER length[job IN jobs] = times
RESOURCE machine
ACTIVITY cut DURATION length[:cut] USING machine
ACTIVITY pack DURATION length[:pack] USING machine AFTER cut
MINIMIZE makespan
END
One axis addresses a row or column vector. Two axes address matrix rows and
columns: PARAMETER length[stage IN stages, job IN jobs] = times, read as
length[:cut, "large"]. Keys are symbols, text or exact integers; integer keys
are labels, not array positions. Each domain must be nonempty, unique and use
one representation. Array order defines the correspondence with domain order.
Use the same key representation when reading a parameter.
PLAN captures all data at admission. Initializers and domains read the outer module scope, even if a parameter has the same name as an outer binding. Parameters cannot initialize other local parameters. Inputs, state, defaults, reassigned bindings and rule-written dependencies are refused, including transitive aliases and unused data. A changed definition requires compilation and explicit replacement; reading a PLAN reference still performs no search. SOLVE's parameter values remain reactive under its existing contract.
Parameter arrays contain exact numeric or duration constants and pure arithmetic; each array uses one of those two kinds. Indexed reads can appear in numeric and time fields, including capacities, demands, calendar endpoints, penalties and budgets. Normal field rules still apply: positive durations and capacities, whole ticks, and exact integer costs. An unknown key, wrong shape or arity, optional access, slice, or nonconstant data refuses admission. There are at most 1,024 parameters, 4,096 keys per axis and 65,536 data cells across a PLAN.
Parameter data and member declarations are separate. See the parameter example, which combines named data with optional work and soft due dates. The two-stage flow-shop example uses a matrix of processing times and has a seven-minute optimum.
13.4 Indexed resources and activities
Declare a family when several resources or activities share a structure:
batches = [:small, :large]
stages = [:cut, :pack]
processing_times = [2min, 4min; 1min, 2min]
flowshop = PLAN TICK 1min
PARAMETER length[stage IN stages, batch IN batches] = processing_times
RESOURCE station[stage IN stages]
ACTIVITY cut[batch IN batches] DURATION length[:cut, batch]
USING station[:cut]
ACTIVITY pack[batch IN batches] DURATION length[:pack, batch]
USING station[:pack] AFTER cut[batch]
MINIMIZE makespan
END
Each declaration expands in domain order when the definition is admitted. One or two axes are supported; the last axis varies fastest. Resource capacities and activity operands can use their declaration's index names. Domains read the outer immutable scope, and an index cannot shadow a local parameter or another index in the same declaration. Outer aliases retain their original meaning even when an index has the same name as one of their dependencies.
station[:cut] selects a resource by its typed key. AFTER cut[batch] selects
the matching predecessor; AFTER cut[*] waits for every member. In a resource
group, USING operator FROM [worker[*]] makes every worker an alternative for
that role. CALENDAR worker[*] = [[2min, 10min]] gives every worker the same
availability; CALENDAR worker[:ana] selects one. A resource may have only one
calendar declaration, including overlaps between keyed and whole-family forms.
[*] selects a whole family where a list is expected. It cannot select a single
resource after USING, mix with explicit keys, or partially slice a two-axis
family. Bare family names, unknown keys, wrong key arity and duplicate expanded
references are errors. Symbols, text and integer keys remain distinct; the
expanded native names retain those distinctions.
Expansion preserves the existing scalar or simultaneous scheduling contract. A PLAN admits at most 4,096 resources and 4,096 activities; expanded names must fit the native 128-byte identifier limit. Resource groups retain their limits of 16 groups, 64 alternatives per group and 4,096 combinations per activity. Total combinations, predecessor relations and calendar windows are each bounded at 65,536. Excessive expansion is refused rather than truncated.
The assignment still binds a resident definition reference. Reading it does not run the scheduler; explicit Planning operations solve, repair and publish. Changing a family domain or captured data requires recompilation. See the indexed flow shop.
14. See Also
reference.mdfor the type system.rules-and-schedules.mdfor rule blocks.external-functions.mdfor~=deep dive.errors.mdfor#TYPE!,#CIRCULAR_REF!, etc.
Reusable SOLVE definitions
allocate = SOLVE(capacity)
VARIABLE x IN [0, 2]
VARIABLE y IN [0, 3]
REQUIRE capacity_limit: x + y <= capacity
MAXIMIZE 3*x + 2*y
END
small = allocate(4)
large = allocate(5 AS capacity)
The results have objectives 10 and 12. SOLVE allocate(capacity) is the
alternative header. Parentheses create an ordinary callable, including the
zero-argument SOLVE() form; a header without parentheses retains its existing
reactive result binding. Unused definitions perform no solver work.
Constructors support up to 64 unique case-insensitive formal names, inferred callable types, named arguments, aliases, partial application, imports and re-exports. They can be declared inside DO/function bodies and passed to other callables. They cannot escape as cell/record/STATE data. Immutable lexical constants may be captured; pass cells, inputs, defaults, STATE, Tables and rule-written values as arguments. Aliases and pure helper dependencies obey the same rule.
Scalar arguments must be finite, untagged numbers, even when unused or cancelled from the objective. An array argument gets its exact shape from a PARAMETER with fixed domains:
allocate_products = SOLVE(demand, margin)
PARAMETER need[p IN ["alpha", "beta"]] = demand
PARAMETER value[p IN ["alpha", "beta"]] = margin
VARIABLE quantity[p IN ["alpha", "beta"]] IN [0, need[p]]
REQUIRE capacity: SUM(p IN ["alpha", "beta"], quantity[p]) <= 4
MAXIMIZE SUM(p IN ["alpha", "beta"], value[p] * quantity[p])
END
allocation = allocate_products([2, 3], [3, 2])
One-axis formal data uses a row vector; two-axis data uses the declared row and column counts. The existing sparse solver ingress checks the complete shape and all numeric entries on every invocation. Domains and variable/constraint counts remain static; a formal cannot supply a structural domain. Constructor formals cannot also name local variables or PARAMETER views. All existing affine, convex ABS/MAX and ordered objective forms retain their contracts, reports and explicit backend failures. Calling a constructor creates no retained job.
See reusable FIT definitions for the same callable syntax applied to statistical models.
Bounded SOLVE and PLAN diagnosis
DIAGNOSE requests additional investigation explicitly:
allocation = SOLVE
VARIABLE supply IN [0, 10]
REQUIRE demand: supply >= 12
MINIMIZE supply
DIAGNOSE BUDGET 50 SOLVES
END
allocation.report.diagnosis names the conflicting requirement and upper bound,
including their source byte offsets. Indexed members retain their expanded labels.
irreducible means every retained encoded row or bound is needed for that
conflict, with integrality fixed. It does not mean the smallest possible conflict.
Equality requirements have two encoded rows. Generated objective rows have no
invented source location. Numerical feasibility tolerances still apply.
A diagnosis budget counts feasibility calls, each subject to native effort limits.
It is separate from lexicographic iteration reservations and is not a wall-time
or peak-memory guarantee. Limits are 4,096 candidate members and 4,097 calls.
Zero calls do no investigation. budget_exhausted, cancelled, inconclusive,
and invalid_problem never claim irreducibility. infeasibility_proven says
whether the retained subsystem was proved infeasible before stopping. The
investigation excludes objective protection rows added by lexicographic stages.
Byte spans are present when compiled from source text and survive stripped compiled models.
Source-free owning artifacts retain indexed labels but omit unavailable text
positions; a missing position is never presented as byte zero.
Ordinary SOLVE reports perform no diagnostic search.
PLAN accepts DIAGNOSE BUDGET 1000 NODES, with a maximum of one million nodes.
Its selected result adds diagnosis: bounded search events with activity names,
source offsets and a small partial-schedule context. These events describe
local dead ends, not a conflict core or instructions for removing constraints.
Only a complete infeasible diagnostic search sets infeasibilityProven.
The report separately records its node count, budget and truncated event/context
lists. A validated feasible schedule needs no extra diagnostic search.
General PLAN priorities
Author any nonempty subset of these distinct components in the desired order:
MINIMIZE tardiness_cost THEN omission_cost THEN makespan
omission_cost is the sum of penalties for omitted activities, tardiness_cost
is the weighted lateness cost, and makespan is the final completion tick.
A later component breaks ties in earlier components. Search compares tuples
and optimistic tuples directly; it does not multiply priorities by large weights.
Finite deadlines and positive omission penalties are no longer prerequisites.
A zero omission weight is a deliberate tie, not mandatory presence. Native
integer limits still apply to each component and the reported total; removing
scalar multipliers does not permit overflowing those values.
MINIMIZE total_cost retains the weighted-sum contract. With soft clauses,
MINIMIZE makespan minimizes only makespan and can omit optional work even
when omission is expensive. Result objectiveCost remains the ordinary sum
of makespan, omission and tardiness; it is not a scalar encoding of the priority
tuple. The selected result adds priorities, with each authored component and its achieved integer value. feasible means the search found a schedule but did not finish its proof.
optimal requires completion or a valid optimistic bound.
New priority sources select Planning schema/dialect 7. Previously compiled schema-6 scalarized priorities keep their original objective and tie contract. New tuples retain the first optimum in deterministic search order; they do not spend additional search effort choosing a canonical assignment among equal optima.
Configured PLAN instances
Parentheses make a top-level PLAN definition callable:
input cut_time = 8min
fabricate = PLAN(cut_duration, pack_duration) TICK 1min
RESOURCE bench
ACTIVITY cut DURATION cut_duration USING bench
ACTIVITY pack DURATION pack_duration USING bench AFTER cut
MINIMIZE makespan
END
request = fabricate(cut_time, 4min)
other = fabricate(2min, 3min)
Calls construct independently validated, immutable instance DATA. Reading either
binding runs no scheduler and publishes no Frame or Table. Changing cut_time
reconstructs request; other keeps its independent values. The tick scale is
fixed by the definition. Duration fields require typed fixed durations and exact
whole ticks; capacities, demands, penalties and budgets require exact nonnegative
integers. Rate periods can be fractional ticks if the resulting per-tick cost is
an exact integer. Invalid values produce a PLANNING/INSTANCE error.
This constructor profile supports flat activities, simultaneous resource groups,
calendars, setup classes and changeovers, optional work, due dates, objective priorities and diagnosis. Indexed
families, explicit choices/fixed appointments, and RESULTS
remain static PLAN features in this profile; constructor admission refuses them.
Local PLAN definitions inside DO are not supported by this initial profile.
Changeover durations can be constructor arguments or pure expressions, while resource names and setup classes remain fixed:
paint = PLAN(wash_time) TICK 1min
RESOURCE booth
CHANGEOVER booth FROM red TO blue DURATION wash_time
ACTIVITY first_batch DURATION 3min USING booth SETUP red
ACTIVITY second_batch DURATION 3min USING booth SETUP blue AFTER first_batch
MINIMIZE makespan
END
request = paint(2min)
This instance needs eight ticks: three for each batch and two for the changeover. Transitions are directed; author the reverse transition separately if needed. Duplicate resource/from/to transitions and unknown resources are refused. Changeover durations use the same exact whole-tick admission as activity durations.
The existing resolvePlanning operation accepts [modelId, "request"] to solve
the current instance. To publish through the existing output owners, pass a third
argument such as {"table":"request_schedule"} or {"frame":"request_frame"}.
Output names are explicit operation inputs. Native prepared-operation tokens are
not serializable and are bound to the resident model image and input revision;
a foreign model, reload or intervening input change requires selecting the live
binding again. Public instance DATA is not an operation token.