-
Notifications
You must be signed in to change notification settings - Fork 0
Variable Types
Every design variable in HSDS implements the same three-method contract. This page covers the four built-in primitive types — Continuous, Discrete, Integer, and Categorical — explains how callable bounds work, and describes what sample, filter, and neighbor actually do inside the optimizer loop.
Domain-specific types (ACIRebar, SteelSection, ConcreteGrade, etc.) follow the same contract and are covered in Engineering Spaces.
Every variable — whether built-in or custom — must implement exactly three methods:
| Method | Signature | When the optimizer calls it |
|---|---|---|
sample(ctx) |
→ value |
During initialization and when HMCR misses — draws a fresh random value from the full domain. |
filter(candidates, ctx) |
→ List[value] |
During HMCR memory consideration — receives all values for this variable currently in the harmony memory and returns only those that are feasible given the current context. |
neighbor(value, ctx) |
→ value |
During pitch adjustment (PAR step) — returns a value adjacent to value within the current domain. Must always return a feasible value. |
The ctx argument is a plain dict mapping the names of all variables defined before this one to their already-assigned values in the current harmony. Variables that have no dependencies simply ignore it. Variables with dependent bounds use it to resolve their bounds at call time.
One additional key, ctx["__bw__"], is injected automatically by the optimizer and carries the current pitch-adjustment bandwidth (a float between bw_min and bw_max). Continuous reads this to scale its Gaussian perturbation step; all other built-in types ignore it.
Uniformly distributed real-valued variable over a closed interval [lo, hi].
Continuous(lo, hi)
Both lo and hi accept either a fixed float or a callable lambda ctx: .... Static bounds are validated at construction time; callable bounds are resolved each time the variable is called.
-
sample — returns
random.uniform(lo, hi). -
filter — keeps candidates satisfying
lo ≤ v ≤ hi. -
neighbor — perturbs the value by a Gaussian step of size
bw × (hi − lo), then clamps to[lo, hi]. The bandwidthbwis read fromctx["__bw__"]and defaults to 0.05 if absent.
from hsds import DesignSpace, Continuous
space = DesignSpace()
# Fixed bounds
space.add("x", Continuous(-5.0, 5.0))
# One fixed bound, one dependent
space.add("h", Continuous(0.30, 1.20))
space.add("bf", Continuous(lo=lambda ctx: ctx["h"] * 0.4,
hi=lambda ctx: ctx["h"] * 2.0))
# Both bounds dependent on the same earlier variable
space.add("d", Continuous(0.40, 0.80))
space.add("tw", Continuous(lo=lambda ctx: ctx["d"] / 50,
hi=lambda ctx: ctx["d"] / 10))
Any real-valued design parameter — dimensions, angles, force magnitudes, stiffnesses. Use callable bounds whenever the valid range of a variable is physically constrained by another variable already in the space.
Variable taking values on a regular grid {lo, lo+step, lo+2·step, …, hi}. All three parameters may be fixed scalars or callables.
Discrete(lo, step, hi)
- sample — draws uniformly from the grid.
- filter — keeps candidates that fall on the grid within a tolerance of 1e-9 (guards against floating-point drift).
-
neighbor — steps one position left or right on the grid. Bandwidth has no effect on
Discrete.
from hsds import Discrete
# Thickness in 5 mm increments from 10 mm to 50 mm
space.add("t", Discrete(10.0, 5.0, 50.0))
# Spacing dependent on another variable
space.add("s", Discrete(lo=lambda ctx: ctx["d"] * 0.1,
step=0.05,
hi=lambda ctx: ctx["d"] * 0.5))
Parameters that must come from a regular grid — standard plate thicknesses, bolt spacings, mesh resolutions, any value that increments by a fixed amount. For integer steps of 1, use Integer instead.
Integer-valued variable over the closed interval {lo, lo+1, …, hi}. A convenience wrapper around Discrete(lo, 1, hi) that returns and accepts int values rather than floats.
Integer(lo, hi)
Both bounds accept fixed values or callables.
-
sample — returns
int(random.randint(lo, hi)). -
filter — keeps integer candidates in
[lo, hi]. -
neighbor — steps ±1, clamped to
[lo, hi].
from hsds import Integer
# Number of stories
space.add("n_stories", Integer(1, 20))
# Number of bolts — at least 4, at most dependent on available length
space.add("n_bolts", Integer(lo=4,
hi=lambda ctx: int(ctx["L"] / 0.05)))
Countable quantities — number of bars, stories, load cases, grid points, iterations. Prefer Integer over Discrete(lo, 1, hi) for clarity.
Variable taking values from an unordered finite set of labels. There is no meaningful notion of adjacency for nominal data, so neighbor returns a different randomly chosen member of the set.
Categorical(choices)
choices is any sequence — list of strings, numbers, or any hashable objects. The set is fixed at construction time and does not support callable filtering.
-
sample — returns
random.choice(choices). -
filter — keeps candidates that appear in
choices. -
neighbor — returns a uniformly random choice from
choicesexcluding the current value (or the current value itself if it is the only option).
from hsds import Categorical
# Steel grade selection
space.add("grade", Categorical(["S235", "S275", "S355", "S420"]))
# Structural system type
space.add("system", Categorical(["moment_frame", "braced_frame", "shear_wall"]))
# Numeric labels are fine too
space.add("config", Categorical([1, 2, 4, 8]))
Nominal choices with no natural ordering — material grades, cross-section families, load combinations, configuration flags. If there is a natural ordering (e.g. concrete grades C25 → C30 → C35), consider ConcreteGrade or a Discrete index instead, so that neighbor can explore meaningfully.
Any bound parameter of Continuous, Discrete, or Integer can be replaced with a callable that accepts ctx and returns a number. This is the mechanism that makes dependent spaces possible.
space.add("h", Continuous(0.30, 1.20))
space.add("b", Continuous(lo=lambda ctx: ctx["h"] * 0.3,
hi=lambda ctx: ctx["h"] * 1.0))
space.add("tw", Continuous(lo=lambda ctx: ctx["b"] / 30,
hi=lambda ctx: ctx["b"] / 10))
space.add("n", Integer(lo=4,
hi=lambda ctx: max(4, int(ctx["b"] * 1000 / 80))))
A few things to keep in mind:
-
Order matters. Variables are sampled in definition order. A callable can only reference variables defined before it. Referencing a variable defined later will raise a
KeyErrorat runtime. -
The callable is called every time.
sample,filter, andneighboreach call the bound callable independently. Keep the expression lightweight — avoid expensive lookups inside bounds lambdas. -
Validation is deferred. Static bounds (
lo > hi) are caught at construction. Callable bounds are only checked when the variable is actually called; an invalid bound at runtime (e.g.lo > hidue to an unusual context) will raise aValueError. - Circular dependencies are not supported. If variable A's bounds depend on B and B's bounds depend on A, you cannot encode both as dependent bounds. Encode one direction as a dependent bound and handle the other with a penalty term.
| Your parameter is… | Use |
|---|---|
| A real number in a range | Continuous |
| A real number on a fixed grid (e.g. 5 mm steps) | Discrete |
| A whole number count | Integer |
| A label from an unordered set | Categorical |
| A standard steel section (IPE, HEA, HEB, W) | SteelSection |
| An ACI 318 rebar arrangement |
ACIRebar / ACIDoubleRebar
|
| An EN 206 concrete grade | ConcreteGrade |
| An SPT-N soil profile class | SoilSPT |
| A TBDY 2018 seismic zone | SeismicZoneTBDY |
| Something domain-specific not listed above | Custom Variables |
- Dependent Spaces — when and why to use callable bounds, benchmark evidence
- Engineering Spaces — domain-specific variable types with built-in feasibility rules
-
Custom Variables — subclassing
Variable, the factory function, and the plugin registry -
Optimizer Parameters — how
hmcr,par,bw_max, andbw_mininteract withsample,filter, andneighbor