diff --git a/.github/workflows/agentblocks.yml b/.github/workflows/agentblocks.yml new file mode 100644 index 0000000..07f31b7 --- /dev/null +++ b/.github/workflows/agentblocks.yml @@ -0,0 +1,34 @@ +# Standalone check for the AgentBlocks TIB reference implementation. +# This is the self-contained Java block from the RBB (agentblocks/TIB_BedDeM.java): +# no Repast, no Postgres, no build.xml -- just javac + java. It compiles the block +# and runs the example, and writes the output to the run summary. + +name: AgentBlocks TIB example + +on: + push: + branches: [ "master" ] + paths: [ "agentblocks/**", ".github/workflows/agentblocks.yml" ] + pull_request: + branches: [ "master" ] + paths: [ "agentblocks/**", ".github/workflows/agentblocks.yml" ] + +jobs: + tib-example: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + - name: Compile and run the TIB block + run: | + set -e + mkdir -p out + javac -encoding UTF-8 -d out agentblocks/TIB_BedDeM.java + echo "## AgentBlocks TIB example output" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + java -cp out agentblocks.tib.TIBExample | tee -a "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml index cfbcf97..9bd480d 100644 --- a/.github/workflows/ant.yml +++ b/.github/workflows/ant.yml @@ -1,4 +1,5 @@ -# This workflow will build a Java project with Ant +# This workflow builds the Java project with Ant, runs the tests, then runs the +# model headlessly and renders its real decision output onto the run summary. # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-ant name: BedDeM CI @@ -14,6 +15,13 @@ jobs: runs-on: ubuntu-latest + # Needed so the test-results step can post a check + comment on the PR. + permissions: + checks: write + pull-requests: write + issues: write + contents: read + steps: - uses: actions/checkout@v4 - name: Set up JDK 11 @@ -33,7 +41,75 @@ jobs: ant junitreport - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() # publish even when a test fails with: files: | junit/**/*.xml + check_name: "Test Results" + comment_mode: always # always post/update a comment on the PR + comment_title: "BedDeM test results" + - name: Upload JUnit report + uses: actions/upload-artifact@v4 + if: always() # keep the HTML/XML report even on failure + with: + name: junit-report + path: junit/ + # ---- Run the model and show its real decision output --------------------- + # The batch (headless) run drives DummyReporter, which writes + # output/decisions.csv. output/ is gitignored, so this is produced fresh in + # the pipeline on every run -- nothing is committed. + - name: Run model (headless) + continue-on-error: true # keep going so we can locate output + diagnose + timeout-minutes: 10 # don't hang the job if the run stalls + run: | + # Runs the dummy (CSV) model headless via the run-headless target + # (beddem_simulator.HeadlessRun) -- no Swing GUI, so it works with no + # display. Produces output/decisions.csv. + rm -f output/decisions.csv + ant run-headless "-DECLIPSE_HOME=$(pwd)" + - name: Locate decision output + if: always() + # Repast's batch runner executes each run in its own instance directory, so + # DummyReporter's relative "output/decisions.csv" may land there rather than + # at the repo root. Find whatever the run produced and bring it back. + run: | + echo "Files produced by the run:" + find . -name 'decisions.csv' -o -name 'DummyOutput*.txt' 2>/dev/null || true + latest=$(find . -name 'decisions.csv' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -1 | cut -d' ' -f2-) + if [ -n "$latest" ] && [ "$latest" != "./output/decisions.csv" ]; then + mkdir -p output && cp "$latest" output/decisions.csv + echo "Copied $latest -> output/decisions.csv" + fi + echo "--- output/ ---"; ls -la output/ 2>&1 || echo "(no output dir)" + - name: Set up Python + if: always() + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Summarise decisions + if: always() # still report (or note "no output") if the run failed + run: | + python analysis/summarize_decisions.py > decision_summary.md + cat decision_summary.md >> "$GITHUB_STEP_SUMMARY" + - name: Post decision results to PR + # Skipped automatically for PRs from forks (their token is read-only and + # cannot comment); the results still appear in the run summary + artifact. + if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + continue-on-error: true # never fail the build just over the comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: tib-decisions # one comment, updated on each run + path: decision_summary.md + - name: Render decision chart + if: always() + run: | + pip install matplotlib + python analysis/plot_tib.py + - name: Upload decision chart + if: always() + uses: actions/upload-artifact@v4 + with: + name: tib-decisions + path: analysis/tib_decisions.png + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 4345fe9..128e608 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ Repast.settings /testbin/ junit/* .idea/* + +# Generated TIB charts +analysis/*.png diff --git a/MessageCenter.log4j.properties b/MessageCenter.log4j.properties index c5cfa15..de67c67 100644 --- a/MessageCenter.log4j.properties +++ b/MessageCenter.log4j.properties @@ -44,6 +44,10 @@ log4j.logger.dummy.database.CSVReader = DEBUG, R log4j.logger.framework.agent.core.TaskExecutionAgent = DEBUG, R log4j.logger.framework.agent.reasoning.Determinant = DEBUG, R +# Per-determinant TIB scores (norm/role/self/emotion/facilitating/freq + time/cost). +# Comment this out to silence the determinant traces. +log4j.logger.dummy.agent.StandardDummyAgent = DEBUG, stdout, R + log4j.appender.R=org.apache.log4j.RollingFileAppender log4j.appender.R.File=debug.log log4j.appender.R.MaxFileSize=100KB diff --git a/README.md b/README.md index 3d54222..b71a14b 100644 --- a/README.md +++ b/README.md @@ -58,63 +58,16 @@ agentID,start_time,km,vehicle ``` This output corresponds to the `time_weight` set to 5 and `cost_weight` set to 1. -## Project structure -In this case we have example `` called `dummy`. The directory structure: -* **`src`** - - `.agent`: specific implementation of all agents for scenario and external inputs for processing - + CommunicationComponent: decision communication - + DecisionComponent: decision determinants definition - + MemoryComponent: memory definition and feelings - + PerceptionComponent: perception component of possibilities - + StandardDummyAgent: definition of the agent and its functions - - `.concept`: basic concepts used around model such as Task and Option available for agent - + EnvironmentalState: state of the environment and availability - + Feedback: feedback of the environment - + InternalState: state of the environment - + Option: environment options - + Task: one task in the agent schedule with all its constraints - + Vehicle: vehicle definition and properties - - `.context`: context definitions usually taken from the GlobVariables file - + AgentContext: context definition for agent - + LocationContext: context definition for location - - `.database`: data retrievers - + CSVReader: reader for csv files - - `.environment`: definitions of the environment - - `.report`: contains scenario specific reporter classes. - + Reporter classes: describes how what we want from the model. We can select which method to be run in Repast interface. - - `.simulator`: contain the Repast controller, scheduler and logger of the project. This is the core function of Repast and you should not modify it. The two simulator classes here are ContextManager (the simulator controller and entry points of the simulator) and ThreadAgentScheduler (schedules the Tasks and execute them). - + ContextManager: describes how the input should be read and can be modified to fit the project. Container of all the agents and locations. - - `framework.agent.core`: anything that is related to the agent's operation - + IAgent: the Interface of agent. Contains some methods that an agent should have. - + DefaultAgent: the standard agent that can be extended depend on the type of project. Its simulator operation is described in the step() method which is controlled by the Scheduler mentioned above. - + StandardTraveller: the class used specific for transportation demand. Contains the logic to evaluate an option. - - `framework.agent.reasoning`: containing the TPB and TIB model and determinant interfaces - + Determinant: standard determinant for evaluation of options - + LeafDeterminant: base determinant node in decision making - + ParentDeterminant: parent determinant in decision making model defines ranking options - + TIBModel: Triandis decision making model, definition all base determinants according to this model - + TPBModel: Theory of planned behavior model - - `framework.concept`: some basic concepts that are used around the model (i.e Task and Option available for the agent). Note that at the moment time is setup as hour and distance is in km. Also some classes that are implemented specifically for transportation demand. - + EnvironmentalState: state of environment - + Feedback: interface definition of feedback - + InternalState: interface for agent state - + Opinion: interface for agent opinion - + Option: option interface - + Task: task interface - - `framework.environment`: information of the environment where the agents reside in. (ex: available transportations, total demand, kms, spending for each mode, etc.). - + Environment: interface for location of the agent its environment - - `framework.exception`: all the exceptions and how to handle them. - -* **`data`** - - `csv_files`: two scenario example files one with `dummy` prefix and one without. Both are containing schedule for only for next day. - + agents: list of all the agent and their parameters. - + vehicle: list of all available transportation mode and their parameters. - + schedule: list of all the events to be assigned to agents. At the moment, the price point of car is added at the end of the file name (ex: 0.0 mean car_price * 0.0). This is for when we want to run in batch mode where we want to define different price points in the demand curve. - + location: list of all locations with available modes of transport - - testing: csv files for modular + system testing, will be generated if you run the testing classes. - - beddem_simulator.properties: locations of all the inputs, parameters and outputs of the model. -* **`your_project_name.rs`**: Define the inputs, outputs, parameters and observers for the model +## Scenario: Sion ↔ Sierre +The bundled scenario models a full TIB decision with **two locations** — Sion (`loc_id 1`) and Sierre (`loc_id 2`), both offering train + bus — and several agents with different attributes (weights and owned vehicles). Every TIB determinant is implemented in `StandardDummyAgent` (norm, role, self-concept, emotion, facilitating, habit, plus time/cost), each expressed as a **cost** (lower = better), so an agent picks the option with the lowest expected utility. +Everything is data-driven from `data/`: +- `agent.csv` — one row per agent: home location, funding, owned vehicles (`resources`), and the TIB weights. +- `location.csv` — `loc_id, train, bus, tram, name` (which transit each location offers). +- `schedule.0.csv` — the trips (`agent_id, start, km, time_limit, purpose`); a Sion↔Sierre trip is an 18 km row. +- `vehicle.csv` — the modes and their speed/cost. + +Add agents, locations, or trips by editing those CSVs — the `ContextManager` loads them at startup. To see each determinant's score per mode, enable `dummy.agent.StandardDummyAgent = DEBUG` in `MessageCenter.log4j.properties`. ## Tested with versions - Eclipse: 2024-03 @@ -129,5 +82,3 @@ To enable the debug logging for the BedDeM specific classes add or uncomment in ```bash log4j.logger.dummy.database.CSVReader = DEBUG, stdout, R ``` -Logger levels and the description of log4j architecture can be find [here](https://logging.apache.org/log4j/2.x/manual/architecture.html) - diff --git a/agentblocks/TIB_BedDeM.java b/agentblocks/TIB_BedDeM.java new file mode 100644 index 0000000..b3eb6cf --- /dev/null +++ b/agentblocks/TIB_BedDeM.java @@ -0,0 +1,213 @@ +/** + * TIB Agent Block: self-contained, illustrative reimplementation of the BedDeM + * TIB decision block. It mirrors the determinant hierarchy and the minimum- + * expected-utility choice rule of framework.agent.reasoning.TIBModel, and it + * implements the RBB's expected-utility equation: + * + * EU_d(option) = Σ_a ( EU_a(option) / Σ_o EU_a(o) ) · w_a + * + * Each determinant's value is normalised across all options, then weighted into + * its parent. The behaviour output is the weighted aggregate of the top-level + * determinants (Nguyen 2023, Eq. 4.7 and Table 4.2). + * + * Reference: https://github.com/SiLab-group/beddem_simulator + */ +package agentblocks.tib; + +import java.util.*; + +interface Option { + String getName(); + double getProperty(String propertyName); +} + +class SimpleOption implements Option { + private final String name; + private final Map properties = new HashMap<>(); + public SimpleOption(String name) { this.name = name; } + public String getName() { return name; } + public double getProperty(String propertyName) { + return properties.getOrDefault(propertyName, 0.0); + } + public void setProperty(String propertyName, double value) { + properties.put(propertyName, value); + } +} + +interface Determinant { + String getName(); + double getWeight(); + void setWeight(double weight); + /** + * Evaluate every option at this determinant and return its NORMALISED score + * (each option's share of the total across all options). Normalising here, at + * every level, is the Σ_o EU_a(o) term of the EU equation. + */ + Map evaluate(List options); +} + +abstract class AbstractDeterminant implements Determinant { + protected final String name; + protected double weight; + protected AbstractDeterminant(String name, double weight) { + this.name = name; this.weight = weight; + } + public String getName() { return name; } + public double getWeight() { return weight; } + public void setWeight(double weight) { this.weight = weight; } + + /** Divide each option's raw value by the sum across all options. */ + protected static Map normalise(Map raw) { + double sum = 0.0; + for (double v : raw.values()) sum += v; + Map out = new HashMap<>(); + for (Map.Entry e : raw.entrySet()) + out.put(e.getKey(), sum > 0 ? e.getValue() / sum : 0.0); + return out; + } +} + +/** Base determinant: reads one property off each option. */ +class LeafDeterminant extends AbstractDeterminant { + public LeafDeterminant(String name, double weight) { super(name, weight); } + @Override + public Map evaluate(List options) { + Map raw = new HashMap<>(); + for (Option opt : options) raw.put(opt, opt.getProperty(name)); + return normalise(raw); + } +} + +/** Aggregates children: Σ_child ( normalised child score · child weight ). */ +class ParentDeterminant extends AbstractDeterminant { + private final List children = new ArrayList<>(); + public ParentDeterminant(String name, double weight) { super(name, weight); } + public void addDeterminantChild(Determinant child) { children.add(child); } + public List getChildren() { return children; } + + /** Weighted sum of the normalised children (U_o(d) in the EU equation). */ + protected Map aggregate(List options) { + Map agg = new HashMap<>(); + for (Option opt : options) agg.put(opt, 0.0); + for (Determinant child : children) { + Map childScores = child.evaluate(options); + for (Option opt : options) + agg.put(opt, agg.get(opt) + child.getWeight() * childScores.get(opt)); + } + return agg; + } + + @Override + public Map evaluate(List options) { + // Normalised aggregate, used when this node is a child of another. + return normalise(aggregate(options)); + } +} + +/** + * Triandis' TIB decision model. Provide the eight base determinants and the five + * aggregate weights; the hierarchy below is wired exactly as in BedDeM. To model + * additional psychology, subclass and add determinants to the tree. + */ +class TIBModel extends ParentDeterminant { + public TIBModel(Determinant belief, Determinant evaluation, Determinant norm, Determinant role, + Determinant self_concept, Determinant emotion, Determinant facilitatingCond, Determinant freq, + double attitudeWeight, double socialWeight, double affectWeight, double intentionWeight, + double habitWeight) { + super("Triandis", 1); + ParentDeterminant intention = new ParentDeterminant("intention", intentionWeight); + ParentDeterminant habits = new ParentDeterminant("habits", habitWeight); + ParentDeterminant attitude = new ParentDeterminant("attitude", attitudeWeight); + ParentDeterminant social = new ParentDeterminant("social", socialWeight); + ParentDeterminant affect = new ParentDeterminant("affect", affectWeight); + + addDeterminantChild(intention); + addDeterminantChild(habits); + addDeterminantChild(facilitatingCond); + + habits.addDeterminantChild(freq); + intention.addDeterminantChild(affect); + intention.addDeterminantChild(social); + intention.addDeterminantChild(attitude); + affect.addDeterminantChild(emotion); + social.addDeterminantChild(norm); + social.addDeterminantChild(role); + social.addDeterminantChild(self_concept); + attitude.addDeterminantChild(belief); + attitude.addDeterminantChild(evaluation); + } + + /** Behaviour-output utility per option: the aggregate of the top-level determinants. */ + public Map evaluateOptions(List options) { + return aggregate(options); + } + + /** Select the option with the minimum expected utility, as in BedDeM. */ + public Option selectOption(List options) { + if (options == null || options.isEmpty()) + throw new IllegalArgumentException("Options list cannot be empty"); + Map eu = aggregate(options); + Option best = null; double min = Double.MAX_VALUE; + for (Option opt : options) { + double u = eu.get(opt); + if (u < min) { min = u; best = opt; } + } + return best; + } +} + +class TIBExample { + // Sion -> Sierre (18 km), Car / Train / Bike. + // All 13 weights are supplied from outside, exactly as in the BedDeM model: + // the eight base determinants are each constructed with their weight, and the + // five aggregate weights are passed straight into the TIBModel constructor. + // We only set weights + per-option values; TIBModel does the whole calculation + // (normalise each determinant, weight, aggregate up the tree, pick minimum EU). + public static void main(String[] args) { + // Base determinants (leaf), each carrying its own weight. + LeafDeterminant price = new LeafDeterminant("price", 2.0); + LeafDeterminant time = new LeafDeterminant("time", 4.0); + LeafDeterminant norm = new LeafDeterminant("norm", 3.0); + LeafDeterminant role = new LeafDeterminant("role", 2.0); + LeafDeterminant self = new LeafDeterminant("self", 3.0); + LeafDeterminant emotion = new LeafDeterminant("emotion", 1.0); + LeafDeterminant facilitating = new LeafDeterminant("facilitating", 2.0); + LeafDeterminant freq = new LeafDeterminant("freq", 3.0); + + // Aggregate weights, also supplied from outside. + TIBModel tib = new TIBModel( + price, time, norm, role, self, emotion, facilitating, freq, + 4.0, // attitude + 2.0, // social + 2.0, // affect + 4.0, // intention + 3.0); // habit + + String[] p = {"price","time","norm","role","self","emotion","facilitating","freq"}; + // price time norm role self emo facil freq + SimpleOption car = option("Car", p, new double[]{4.0, 0.3, 2, 3, 1, 1, 0, 0}); + SimpleOption train = option("Train", p, new double[]{3.0, 0.2, 1, 2, 2, 2, 1, 0}); + SimpleOption bike = option("Bike", p, new double[]{0.0, 1.0, 3, 1, 3, 3, 0, 1}); + List