Skip to content

Variable Types

Abdulkadir Özcan edited this page Mar 26, 2026 · 2 revisions

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.


The variable contract

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.


Continuous

Uniformly distributed real-valued variable over a closed interval [lo, hi].

Constructor

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.

Behavior

  • 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 bandwidth bw is read from ctx["__bw__"] and defaults to 0.05 if absent.

Examples

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))

When to use

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.


Discrete

Variable taking values on a regular grid {lo, lo+step, lo+2·step, …, hi}. All three parameters may be fixed scalars or callables.

Constructor

Discrete(lo, step, hi)

Behavior

  • 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.

Examples

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))

When to use

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

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.

Constructor

Integer(lo, hi)

Both bounds accept fixed values or callables.

Behavior

  • sample — returns int(random.randint(lo, hi)).
  • filter — keeps integer candidates in [lo, hi].
  • neighbor — steps ±1, clamped to [lo, hi].

Examples

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)))

When to use

Countable quantities — number of bars, stories, load cases, grid points, iterations. Prefer Integer over Discrete(lo, 1, hi) for clarity.


Categorical

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.

Constructor

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.

Behavior

  • sample — returns random.choice(choices).
  • filter — keeps candidates that appear in choices.
  • neighbor — returns a uniformly random choice from choices excluding the current value (or the current value itself if it is the only option).

Examples

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]))

When to use

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.


Callable bounds in depth

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 KeyError at runtime.
  • The callable is called every time. sample, filter, and neighbor each 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 > hi due to an unusual context) will raise a ValueError.
  • 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.

Which type should I use?

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

See also

  • 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, and bw_min interact with sample, filter, and neighbor

Clone this wiki locally