Rule-Based Household

The RuleBasedHousehold is a deterministic-policy alternative to an LLM household. It combines stylized models of sleep, hunger, meals, inventory replenishment, store choice, and spatial movement. Random store choice uses the household’s injected random.Random instance, so a fixed seed and fixed observation order are reproducible.

This model is intended as a transparent simulation heuristic. Its parameters are not estimated automatically and its \((s,S)\) rule is not the solution of a household dynamic program.

Configuration overview

A typical configuration has the following structure:

"Household": {
    "type": "RuleBasedHousehold",
    "isHousehold": true,
    "foodItems": ["Rice", "Chocolate", "Sushi"],
    "cashName": "Yen",
    "startHour": 0.0,
    "stepHours": 1.0,
    "sleepRule": { ... },
    "mealRule": { ... },
    "sSinventoryRule": { ... },
    "budgetRule": { ... },
    "pricePriors": { ... },
    "itemImportance": { ... },
    "storeChoice": { ... }
}

The agent should request the observations required by its policy. In particular, others_pos supplies store locations and others_inventory supplies offers observed after co-location. Using "requestObs": ["all"] includes both built-in observations.

Decision priority

At each step, the household updates store beliefs from co-located offers and then applies a strict priority order. Active environment sleep is honored first; critical hunger, sleep, return travel, scheduled meals, shopping, and ordinary return-home behavior follow. Only the first applicable core branch emits an action. Actions listed in disabledActions are removed from the feasible policy.

SNS behavior

SNS behavior is optional. Set socialRule.enabled to true to create a SocialMediaPolicy and register it as a supplemental policy. It is evaluated through the same supplemental-policy interface as ProposalReactionPolicy, after the core household decision. Its fragment may contain follow, unfollow, and tweet alongside the core economic action.

The policy does not modify or replace the social-network recommender. It only consumes recommended_follows from the configured recommender. For example, TwoHopRecommenderSystem.temperature continues to control how uniformly randomized recommendations are sampled, including choices among candidates with equal scores.

A complete household and text-service configuration is:

"Household": {
    "type": "RuleBasedHousehold",
    "isHousehold": true,
    "requestObs": ["all"],
    "socialRule": {
        "enabled": true,
        "topicPriority": [
            "health", "finances", "shopping", "social",
            "consumption", "mobility", "daily_life"
        ],
        "tweet": {
            "textGeneratorService": "tweetTextClient",
            "baseIntensity": 0.0001,
            "selfExcitation": 0.1,
            "decayRate": 0.8,
            "memoryExcitation": 0.0001,
            "stressExcitationScale": 0.000001,
            "maxMemoryExcerptCharacters": 320,
            "language": "English",
            "maxCharacters": 280
        },
        "follow": {
            "probability": 0.0005,
            "cooldownSteps": 24
        },
        "unfollow": {
            "probability": 0.0001,
            "cooldownSteps": 48,
            "emptyTweetSteps": 168
        }
    }
},
"tweetTextClient": {
    "type": "TransformersTextClient",
    "modelName": "HuggingFaceTB/SmolLM2-360M-Instruct",
    "device": "cpu",
    "dtype": "float32",
    "maxModelParameters": 1000000000,
    "maxPromptTokens": 384,
    "maxNewTokens": 32,
    "temperature": 0.4,
    "topP": 0.9,
    "repetitionPenalty": 1.1,
    "numThreads": 4,
    "maxConcurrentGenerations": 1
}

Tweet timing

Tweet occurrence follows a discrete exponential-kernel Hawkes process rather than a minimum posting interval. Before sampling step \(t\), the policy updates

\[h_t=h_{t^-}\exp(-\beta\Delta t) +w_M M_t+w_S S_t, \qquad \lambda_t=\mu+h_t,\]

and emits a tweet with probability

\[P(\text{tweet at }t)=1-\exp(-\lambda_t).\]

Here baseIntensity is \(\mu\), decayRate is \(\beta\), memoryExcitation contributes \(w_M\) for each changed memory category, and stressExcitationScale scales the corresponding summarized stress. After a tweet is successfully rendered, selfExcitation is added to \(h_t\). Consequently, successive tweets can occur in adjacent steps and form bursts; there is no minIntervalSteps setting.

Tweet content

Rules first construct a structured TweetIntent containing topic, sentiment, style, and a compact memory excerpt. Topic is selected from changed memory categories in topicPriority order. Summarized stress and negative language influence sentiment, while rules map topic and sentiment to style. Only after the intent has been fixed does TweetRenderer ask the configured text service to realize its wording.

The default output language is English. maxCharacters is a configurable upper bound, not a fixed 140-character Twitter limit; the baseline example uses 280. The renderer normalizes whitespace, removes a leading Tweet: or Post: label, rejects empty text and an exact repeat of self_tweet, and otherwise preserves the Tiny LM wording. It does not apply a factual-grounding fallback, so a very small model may paraphrase or supplement details from the memory excerpt.

TransformersTextClient is a plain-text service separate from the existing schema-based TransformersClient. At startup it counts model parameters and raises ValueError if the model exceeds maxModelParameters. Setting that limit to 1000000000 enforces the intended maximum of one billion parameters. The baseline uses SmolLM2-360M-Instruct.

Candidate tweet interventions

An experiment may replace ordinarily generated tweet text with a line from a plain-text candidate file during a configured step range:

"candidateTweetIntervention": {
    "path": "tweet_candidates/positive_information.txt",
    "startStep": 144,
    "endStep": 720,
    "probability": 0.5
}

The path is read when the household is initialized. Blank lines and lines starting with # are ignored; every remaining line must fit the configured tweet.maxCharacters limit. The active interval is half-open. The probability is conditional on the normal Hawkes process already deciding to tweet, so this option changes content without increasing posting frequency. The immediately preceding exact tweet is excluded when another candidate is available. Candidate tweets are recorded normally and contribute the same Hawkes self-excitation as generated tweets.

Memory used by SNS rules

Content and external Hawkes excitation use the summarized memory mapping already present in the household observation. Consumption, purchases, sales, state evaluations, sleep, movement, social actions, and inner thoughts are mapped to suitable tweet topics. TweetLog entries are also retained as tweet_history by MemoryHandler, allowing an agent’s earlier posts to become social-topic memory. The stress-aware summarizer exposes tweet history but assigns it no stress score.

Follow and unfollow rules

A follow is sampled with follow.probability after its cooldown and is chosen from valid recommended_follows candidates. Self-following, already followed agents, the same-step unfollow target, and choices beyond follow_cap are excluded. If an unfollow in the same step frees capacity, a replacement follow may be emitted.

An unfollow becomes eligible when a visible followee has produced an empty timeline entry for emptyTweetSteps, or its message contains a configured negativeKeywords value. The fallback random-unfollow probability is unfollow.probability. Sleeping households make no SNS action, and entries listed in disabledActions remain unavailable.

Sleep and hunger

sleepRule configures an exponential homeostatic sleep stock plus a sinusoidal circadian contribution. mealRule configures hunger accumulation, meal-time signals, and minimum spacing between meals. See PhysiologyModel for the complete state equations and default values.

Meal composition

mealRule.composition is the fraction of current hunger energy assigned to each food in one meal. For hunger \(G_t\), configured share \(s_i\), energy per unit \(e_i\), and inventory \(I_t(i)\), consumption is

\[c_{i,t}=\min\left\{I_t(i), \frac{\max(0,G_t)s_i}{\max(e_i,10^{-12})}\right\}.\]

For example:

"mealRule": {
    "composition": {
        "Rice": 0.7,
        "Eggs": 0.2,
        "Chocolate": 0.1
    },
    "energyPerUnit": {
        "Rice": 1.0,
        "Eggs": 1.0,
        "Chocolate": 1.0
    }
}

If composition omits an item, its default share is \(1/F\), where \(F\) is the number of configured foodItems. Consequently, explicitly assigning 0.043478260869565216 to each of 23 foods is simply equal sharing:

\[0.043478260869565216 = \frac{1}{23}.\]

The implementation deliberately remains simple:

  • shares are not normalized or validated to sum to one;

  • a meal may consume every configured food simultaneously;

  • unavailable food is not replaced by another food; and

  • unallocated hunger remains after the meal.

These properties make equal shares useful as a neutral placeholder, but they are usually not a realistic diet model. Simulations that distinguish staples, prepared meals, and preference goods should configure meaningful shares or provide a custom physiology model.

Inventory replenishment

The shopping trigger is an \((s,S)\)-style rule. For reorder point \(s_i\), target stock \(T_i\), and current inventory \(I_t(i)\), a shopping trip is due when

\[\exists i:\ I_t(i)\leq s_i,\]

and desired replenishment is

\[q_i=\max(0,T_i-I_t(i)).\]

These parameters are configured per item:

"sSinventoryRule": {
    "reorderPoints": {"Rice": 1.0, "Chocolate": 0.5},
    "targetStocks": {"Rice": 4.0, "Chocolate": 1.0}
}

Shopping budget

For cash \(Y_t\), reserve \(R\), and maximum basket share \(\alpha\), the available shopping budget is

\[B_t=\max\{0,\min(Y_t-R,\alpha Y_t)\}.\]
"budgetRule": {
    "cashReserve": 20000.0,
    "maxBasketShare": 0.1
}

The budget affects both store choice and the quantities that can actually be ordered.

Item importance

itemImportance gives each food a nonnegative replenishment weight. A larger weight makes availability for that shortage more important in store choice. When the budget is tight, orders are also processed in descending importance, with foodItems order as the stable tie break.

"itemImportance": {
    "Rice": 3.0,
    "Chocolate": 0.5,
    "Sushi": 0.25
}

An omitted item defaults to 1.0 and a negative configured value is clamped to zero. Reorder points and target stocks remain independent parameters, so itemImportance does not by itself change when the shopping trigger fires.

Stores and household beliefs

Every visible agent whose observation has is_household: false is treated as a store. Agent names and name prefixes do not affect this classification. Households are excluded from store choice, belief updates, and order targets.

Each household has private, store-specific mappings:

expected_price[store_id][item]
expected_availability[store_id][item]

For an unvisited store, expected price starts at the item’s pricePriors value and expected availability starts at initialAvailability. A co-located offer updates the beliefs by exponential smoothing:

\[\hat p_{ij}\leftarrow (1-\rho_p)\hat p_{ij}+\rho_p p_{ij},\]
\[\hat a_{ij}\leftarrow (1-\rho_a)\hat a_{ij}+\rho_a a_{ij}.\]

An offered item with a positive or masked amount has observed availability \(a_{ij}=1\); a missing item has \(a_{ij}=0\). Price is updated only for offered items. beliefLearningRate supplies the default for both learning rates, while priceLearningRate and availabilityLearningRate can override it separately.

Store-choice utility

For item importance \(w_i\), desired replenishment \(q_i\), expected availability \(\hat a_{ij}\), expected price \(\hat p_{ij}\), and price prior \(\bar p_i\), define weighted need

\[W_t=\sum_i w_iq_i.\]

Expected coverage and relative price savings are

\[C_{tj}=\frac{\sum_iw_iq_i\hat a_{ij}}{W_t},\]
\[R_{tj}=\frac{\sum_iw_iq_i\hat a_{ij} (\bar p_i-\hat p_{ij})/\bar p_i}{W_t}.\]

The price term measures a store’s expected price relative to the household’s prior for the same item. An expensive luxury item is therefore not penalized merely for costing more than rice. Absolute affordability is represented by expected expenditure and budget pressure:

\[E_{tj}=\sum_iq_i\hat a_{ij}\max(0,\hat p_{ij}), \qquad L_{tj}=E_{tj}/B_t.\]

The distance \(D_{tj}\) is Chebyshev grid distance. Deterministic utility is

\[V_{tj}=\beta_P R_{tj}+\beta_A C_{tj} -\beta_B L_{tj}-\beta_DD_{tj}.\]

A store is sampled with multinomial-logit probability

\[P(J_t=j)=\frac{\exp(V_{tj})}{\sum_r\exp(V_{tr})}.\]

The budget-pressure term means that the same expected premium basket deters a cash-constrained household more strongly than a wealthy household. Coverage is retained as a separate positive term so that a store cannot appear attractive merely because carrying fewer desired goods makes its expected expenditure small.

storeChoice parameters

Key

Default

Meaning

betaPrice

1.0

Weight on expected relative price savings \(R_{tj}\).

betaAvailability

2.0

Weight on importance-adjusted expected coverage \(C_{tj}\).

betaBudgetPressure

1.0

Penalty weight on expected expenditure divided by shopping budget.

betaDistance

0.1

Penalty per unit of Chebyshev grid distance.

initialAvailability

0.5

Initial availability belief, clamped to [0, 1].

beliefLearningRate

0.5

Default smoothing rate for both beliefs, clamped to [0, 1].

priceLearningRate

beliefLearningRate

Optional price-specific smoothing rate.

availabilityLearningRate

beliefLearningRate

Optional availability-specific smoothing rate.

With identical initial beliefs, price, availability, and budget-pressure terms are initially common across stores; early choices are therefore mainly distance-driven. Store differentiation emerges as households observe offers.

Ordering at the selected store

After arrival, the household uses the seller’s observed price and stock rather than its beliefs. Items are considered by descending importance. For positive price \(p_i\), available stock \(A_i\), remaining budget \(B_i\), and desired amount \(q_i^*\), the affordable amount is bounded by

\[\min\{q_i^*,A_i,B_i/p_i\}.\]

The current implementation truncates the emitted order amount to an integer. Unspent budget can then be used by the next item in importance order.

Extending the policy

The adapter, decision arbiter, and stylized models are separate classes. Custom behavior can replace the default HouseholdDecisionPolicy or add a supplemental policy without modifying the environment action protocol. See the API reference for econsimulacra.agents.households.household, econsimulacra.agents.households.policy, and econsimulacra.agents.households.stylized_models.