ThemeDataSetGenerator.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
// Some portions generated by Codex
package org.eclipse.rdf4j.benchmark.rio.util;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.function.Consumer;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.rio.RDFHandler;
import org.eclipse.rdf4j.rio.helpers.StatementCollector;

public final class ThemeDataSetGenerator {

	public enum Theme {
		MEDICAL_RECORDS,
		SOCIAL_MEDIA,
		LIBRARY,
		ENGINEERING,
		HIGHLY_CONNECTED,
		TRAIN,
		ELECTRICAL_GRID,
		PHARMA
	}

	private static final String BASE = "http://example.com/theme/";
	private static final String MEDICAL_NS = BASE + "medical/";
	private static final String SOCIAL_NS = BASE + "social/";
	private static final String LIBRARY_NS = BASE + "library/";
	private static final String ENGINEERING_NS = BASE + "engineering/";
	private static final String CONNECTED_NS = BASE + "connected/";
	private static final String TRAIN_NS = BASE + "train/";
	private static final String GRID_NS = BASE + "grid/";
	private static final String PHARMA_NS = BASE + "pharma/";

	private static final ValueFactory VF = SimpleValueFactory.getInstance();

	private static final String[] WORDS = new String[] {
			"alpha", "beta", "gamma", "delta", "epsilon", "omega", "vector", "node", "graph", "system",
			"signal", "sensor", "dataset", "record", "event", "network", "route", "station", "engine",
			"grid", "current", "voltage", "load", "train", "cable", "user", "profile", "post", "comment"
	};
	private static final long JITTER_SEED_XOR = 0x9E3779B97F4A7C15L;

	private ThemeDataSetGenerator() {
	}

	public static MedicalConfig medicalConfig() {
		return new MedicalConfig();
	}

	public static SocialMediaConfig socialMediaConfig() {
		return new SocialMediaConfig();
	}

	public static LibraryConfig libraryConfig() {
		return new LibraryConfig();
	}

	public static EngineeringConfig engineeringConfig() {
		return new EngineeringConfig();
	}

	public static HighlyConnectedConfig highlyConnectedConfig() {
		return new HighlyConnectedConfig();
	}

	public static TrainConfig trainConfig() {
		return new TrainConfig();
	}

	public static ElectricalGridConfig electricalGridConfig() {
		return new ElectricalGridConfig();
	}

	public static PharmaConfig pharmaConfig() {
		return new PharmaConfig();
	}

	public static Model generate(Theme theme) {
		return generateModel(handler -> generate(theme, handler));
	}

	public static void generate(Theme theme, RDFHandler handler) {
		Objects.requireNonNull(theme, "theme");
		Objects.requireNonNull(handler, "handler");
		switch (theme) {
		case MEDICAL_RECORDS:
			generateMedicalRecords(medicalConfig(), handler);
			break;
		case SOCIAL_MEDIA:
			generateSocialMedia(socialMediaConfig(), handler);
			break;
		case LIBRARY:
			generateLibrary(libraryConfig(), handler);
			break;
		case ENGINEERING:
			generateEngineering(engineeringConfig(), handler);
			break;
		case HIGHLY_CONNECTED:
			generateHighlyConnected(highlyConnectedConfig(), handler);
			break;
		case TRAIN:
			generateTrain(trainConfig(), handler);
			break;
		case ELECTRICAL_GRID:
			generateElectricalGrid(electricalGridConfig(), handler);
			break;
		case PHARMA:
			generatePharma(pharmaConfig(), handler);
			break;
		default:
			throw new IllegalArgumentException("Unsupported theme " + theme);
		}
	}

	public static Model generateMedicalRecords(MedicalConfig config) {
		return generateModel(handler -> generateMedicalRecords(config, handler));
	}

	public static void generateMedicalRecords(MedicalConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI patientType = iri(MEDICAL_NS, "Patient");
		IRI encounterType = iri(MEDICAL_NS, "Encounter");
		IRI conditionType = iri(MEDICAL_NS, "Condition");
		IRI medicationType = iri(MEDICAL_NS, "Medication");
		IRI practitionerType = iri(MEDICAL_NS, "Practitioner");
		IRI observationType = iri(MEDICAL_NS, "Observation");

		IRI hasEncounter = iri(MEDICAL_NS, "hasEncounter");
		IRI hasCondition = iri(MEDICAL_NS, "hasCondition");
		IRI hasMedication = iri(MEDICAL_NS, "hasMedication");
		IRI hasObservation = iri(MEDICAL_NS, "hasObservation");
		IRI handledBy = iri(MEDICAL_NS, "handledBy");
		IRI recordedOn = iri(MEDICAL_NS, "recordedOn");
		IRI hasName = iri(MEDICAL_NS, "name");
		IRI hasCode = iri(MEDICAL_NS, "code");
		IRI dosage = iri(MEDICAL_NS, "dosage");
		IRI observationValue = iri(MEDICAL_NS, "value");

		handler.startRDF();
		handler.handleNamespace("med", MEDICAL_NS);

		int practitionerCount = jitterInt(jitterRandom, config.practitionerCount);
		int patientCount = jitterInt(jitterRandom, config.patientCount);
		List<IRI> practitioners = new ArrayList<>(practitionerCount);
		for (int i = 0; i < practitionerCount; i++) {
			IRI practitioner = entity(MEDICAL_NS, "practitioner", i);
			practitioners.add(practitioner);
			add(handler, practitioner, RDF.TYPE, practitionerType);
			add(handler, practitioner, hasName, literal("Dr " + randomWord(contentRandom) + " " + i));
		}

		int medicationIndex = 0;
		int encounterIndex = 0;
		int conditionIndex = 0;
		int observationIndex = 0;
		for (int p = 0; p < patientCount; p++) {
			IRI patient = entity(MEDICAL_NS, "patient", p);
			add(handler, patient, RDF.TYPE, patientType);
			add(handler, patient, hasName, literal("Patient " + p));

			int medicationsPerPatient = jitterInt(jitterRandom, config.medicationsPerPatient);
			for (int m = 0; m < medicationsPerPatient; m++) {
				IRI medication = entity(MEDICAL_NS, "medication", medicationIndex++);
				add(handler, medication, RDF.TYPE, medicationType);
				add(handler, medication, hasCode, literal("MED-" + (1000 + m)));
				add(handler, medication, dosage, literal((1 + contentRandom.nextInt(3)) + "x daily"));
				add(handler, patient, hasMedication, medication);
			}

			int encountersPerPatient = jitterInt(jitterRandom, config.encountersPerPatient);
			for (int e = 0; e < encountersPerPatient; e++) {
				IRI encounter = entity(MEDICAL_NS, "encounter", encounterIndex++);
				add(handler, encounter, RDF.TYPE, encounterType);
				add(handler, patient, hasEncounter, encounter);
				add(handler, encounter, recordedOn,
						VF.createLiteral(LocalDate.of(2024, 1, 1).plusDays(contentRandom.nextInt(365))));
				IRI practitioner = practitioners.get(contentRandom.nextInt(practitioners.size()));
				add(handler, encounter, handledBy, practitioner);

				int conditionsPerEncounter = jitterInt(jitterRandom, config.conditionsPerEncounter);
				for (int c = 0; c < conditionsPerEncounter; c++) {
					IRI condition = entity(MEDICAL_NS, "condition", conditionIndex++);
					add(handler, condition, RDF.TYPE, conditionType);
					add(handler, condition, hasCode, literal("DX-" + (200 + c)));
					add(handler, encounter, hasCondition, condition);
				}

				int observationsPerEncounter = jitterInt(jitterRandom, config.observationsPerEncounter);
				for (int o = 0; o < observationsPerEncounter; o++) {
					IRI observation = entity(MEDICAL_NS, "observation", observationIndex++);
					add(handler, observation, RDF.TYPE, observationType);
					add(handler, observation, observationValue,
							VF.createLiteral(50 + contentRandom.nextInt(50)));
					add(handler, encounter, hasObservation, observation);
				}
			}
		}

		handler.endRDF();
	}

	public static Model generateSocialMedia(SocialMediaConfig config) {
		return generateModel(handler -> generateSocialMedia(config, handler));
	}

	public static void generateSocialMedia(SocialMediaConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI userType = iri(SOCIAL_NS, "User");
		IRI postType = iri(SOCIAL_NS, "Post");
		IRI commentType = iri(SOCIAL_NS, "Comment");
		IRI tagType = iri(SOCIAL_NS, "Tag");

		IRI follows = iri(SOCIAL_NS, "follows");
		IRI authored = iri(SOCIAL_NS, "authored");
		IRI likedBy = iri(SOCIAL_NS, "likedBy");
		IRI hasComment = iri(SOCIAL_NS, "hasComment");
		IRI hasTag = iri(SOCIAL_NS, "hasTag");
		IRI createdAt = iri(SOCIAL_NS, "createdAt");
		IRI hasName = iri(SOCIAL_NS, "name");
		IRI content = iri(SOCIAL_NS, "content");

		handler.startRDF();
		handler.handleNamespace("social", SOCIAL_NS);

		int userCount = jitterInt(jitterRandom, config.userCount);
		int tagCount = jitterInt(jitterRandom, config.tagCount);
		int[] cliqueSizes = jitterCliqueSizes(jitterRandom, config.cliqueSizes, userCount);

		List<IRI> users = new ArrayList<>(userCount);
		for (int u = 0; u < userCount; u++) {
			IRI user = entity(SOCIAL_NS, "user", u);
			users.add(user);
			add(handler, user, RDF.TYPE, userType);
			add(handler, user, hasName, literal("user" + u));
		}

		int cliqueStart = 0;
		for (int cliqueSize : cliqueSizes) {
			List<IRI> clique = users.subList(cliqueStart, cliqueStart + cliqueSize);
			for (int i = 0; i < clique.size(); i++) {
				IRI source = clique.get(i);
				for (int j = 0; j < clique.size(); j++) {
					if (i == j) {
						continue;
					}
					add(handler, source, follows, clique.get(j));
				}
			}
			cliqueStart += cliqueSize;
		}

		List<IRI> tags = new ArrayList<>(tagCount);
		for (int t = 0; t < tagCount; t++) {
			IRI tag = entity(SOCIAL_NS, "tag", t);
			tags.add(tag);
			add(handler, tag, RDF.TYPE, tagType);
			add(handler, tag, hasName, literal("tag" + t));
		}

		int postIndex = 0;
		int commentIndex = 0;
		for (IRI user : users) {
			int followsPerUser = jitterInt(jitterRandom, config.followsPerUser);
			for (int f = 0; f < followsPerUser; f++) {
				IRI target = users.get(contentRandom.nextInt(users.size()));
				if (!user.equals(target)) {
					add(handler, user, follows, target);
				}
			}

			int postsPerUser = jitterInt(jitterRandom, config.postsPerUser);
			for (int p = 0; p < postsPerUser; p++) {
				IRI post = entity(SOCIAL_NS, "post", postIndex++);
				add(handler, post, RDF.TYPE, postType);
				add(handler, post, authored, user);
				add(handler, post, content, literal(randomSentence(contentRandom, 5, 12)));
				add(handler, post, createdAt,
						VF.createLiteral(LocalDateTime.of(2024, 1, 1, 8, 0).plusHours(contentRandom.nextInt(500))));

				int tagsPerPost = jitterInt(jitterRandom, config.tagsPerPost);
				for (int t = 0; t < tagsPerPost; t++) {
					IRI tag = tags.get(contentRandom.nextInt(tags.size()));
					add(handler, post, hasTag, tag);
				}

				int likesPerPost = jitterInt(jitterRandom, config.likesPerPost);
				for (int l = 0; l < likesPerPost; l++) {
					IRI liker = users.get(contentRandom.nextInt(users.size()));
					add(handler, post, likedBy, liker);
				}

				int commentsPerPost = jitterInt(jitterRandom, config.commentsPerPost);
				for (int c = 0; c < commentsPerPost; c++) {
					IRI comment = entity(SOCIAL_NS, "comment", commentIndex++);
					IRI commenter = users.get(contentRandom.nextInt(users.size()));
					add(handler, comment, RDF.TYPE, commentType);
					add(handler, comment, authored, commenter);
					add(handler, comment, content, literal(randomSentence(contentRandom, 3, 8)));
					add(handler, post, hasComment, comment);
				}
			}
		}

		handler.endRDF();
	}

	public static Model generateLibrary(LibraryConfig config) {
		return generateModel(handler -> generateLibrary(config, handler));
	}

	public static void generateLibrary(LibraryConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI bookType = iri(LIBRARY_NS, "Book");
		IRI authorType = iri(LIBRARY_NS, "Author");
		IRI copyType = iri(LIBRARY_NS, "Copy");
		IRI memberType = iri(LIBRARY_NS, "Member");
		IRI loanType = iri(LIBRARY_NS, "Loan");
		IRI branchType = iri(LIBRARY_NS, "Branch");

		IRI writtenBy = iri(LIBRARY_NS, "writtenBy");
		IRI hasCopy = iri(LIBRARY_NS, "hasCopy");
		IRI locatedAt = iri(LIBRARY_NS, "locatedAt");
		IRI borrowedBy = iri(LIBRARY_NS, "borrowedBy");
		IRI loanedCopy = iri(LIBRARY_NS, "loanedCopy");
		IRI loanDate = iri(LIBRARY_NS, "loanDate");
		IRI dueDate = iri(LIBRARY_NS, "dueDate");
		IRI hasName = iri(LIBRARY_NS, "name");
		IRI title = iri(LIBRARY_NS, "title");

		handler.startRDF();
		handler.handleNamespace("lib", LIBRARY_NS);

		int authorCount = jitterInt(jitterRandom, config.authorCount);
		int branchCount = jitterInt(jitterRandom, config.branchCount);
		int bookCount = jitterInt(jitterRandom, config.bookCount);
		int memberCount = jitterInt(jitterRandom, config.memberCount);

		List<IRI> authors = new ArrayList<>(authorCount);
		for (int a = 0; a < authorCount; a++) {
			IRI author = entity(LIBRARY_NS, "author", a);
			authors.add(author);
			add(handler, author, RDF.TYPE, authorType);
			add(handler, author, hasName, literal("Author " + a));
		}

		List<IRI> branches = new ArrayList<>(branchCount);
		for (int b = 0; b < branchCount; b++) {
			IRI branch = entity(LIBRARY_NS, "branch", b);
			branches.add(branch);
			add(handler, branch, RDF.TYPE, branchType);
			add(handler, branch, hasName, literal("Branch " + b));
		}

		List<IRI> copies = new ArrayList<>();
		int copyIndex = 0;
		for (int b = 0; b < bookCount; b++) {
			IRI book = entity(LIBRARY_NS, "book", b);
			add(handler, book, RDF.TYPE, bookType);
			add(handler, book, title, literal("Book " + b + " " + randomWord(contentRandom)));

			int authorsPerBook = jitterInt(jitterRandom, config.authorsPerBook);
			for (int a = 0; a < authorsPerBook; a++) {
				IRI author = authors.get(contentRandom.nextInt(authors.size()));
				add(handler, book, writtenBy, author);
			}

			int copiesPerBook = jitterInt(jitterRandom, config.copiesPerBook);
			for (int c = 0; c < copiesPerBook; c++) {
				IRI copy = entity(LIBRARY_NS, "copy", copyIndex++);
				copies.add(copy);
				add(handler, copy, RDF.TYPE, copyType);
				add(handler, copy, locatedAt, branches.get(contentRandom.nextInt(branches.size())));
				add(handler, book, hasCopy, copy);
			}
		}

		List<IRI> members = new ArrayList<>(memberCount);
		for (int m = 0; m < memberCount; m++) {
			IRI member = entity(LIBRARY_NS, "member", m);
			members.add(member);
			add(handler, member, RDF.TYPE, memberType);
			add(handler, member, hasName, literal("Member " + m));
		}

		int loanIndex = 0;
		for (IRI member : members) {
			int loansPerMember = jitterInt(jitterRandom, config.loansPerMember);
			for (int l = 0; l < loansPerMember; l++) {
				IRI loan = entity(LIBRARY_NS, "loan", loanIndex++);
				IRI copy = copies.get(contentRandom.nextInt(copies.size()));
				add(handler, loan, RDF.TYPE, loanType);
				add(handler, loan, borrowedBy, member);
				add(handler, loan, loanedCopy, copy);
				LocalDate date = LocalDate.of(2024, 1, 1).plusDays(contentRandom.nextInt(90));
				add(handler, loan, loanDate, VF.createLiteral(date));
				add(handler, loan, dueDate, VF.createLiteral(date.plusDays(14)));
			}
		}

		handler.endRDF();
	}

	public static Model generateEngineering(EngineeringConfig config) {
		return generateModel(handler -> generateEngineering(config, handler));
	}

	public static void generateEngineering(EngineeringConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI componentType = iri(ENGINEERING_NS, "Component");
		IRI assemblyType = iri(ENGINEERING_NS, "Assembly");
		IRI requirementType = iri(ENGINEERING_NS, "Requirement");
		IRI testType = iri(ENGINEERING_NS, "TestCase");
		IRI measurementType = iri(ENGINEERING_NS, "Measurement");

		IRI partOf = iri(ENGINEERING_NS, "partOf");
		IRI dependsOn = iri(ENGINEERING_NS, "dependsOn");
		IRI satisfies = iri(ENGINEERING_NS, "satisfies");
		IRI verifiedBy = iri(ENGINEERING_NS, "verifiedBy");
		IRI measuredValue = iri(ENGINEERING_NS, "measuredValue");
		IRI hasName = iri(ENGINEERING_NS, "name");

		handler.startRDF();
		handler.handleNamespace("eng", ENGINEERING_NS);

		int assemblyCount = jitterInt(jitterRandom, config.assemblyCount);
		int componentCount = jitterInt(jitterRandom, config.componentCount);
		int requirementCount = jitterInt(jitterRandom, config.requirementCount);

		List<IRI> assemblies = new ArrayList<>(assemblyCount);
		for (int a = 0; a < assemblyCount; a++) {
			IRI assembly = entity(ENGINEERING_NS, "assembly", a);
			assemblies.add(assembly);
			add(handler, assembly, RDF.TYPE, assemblyType);
			add(handler, assembly, hasName, literal("Assembly " + a));
		}

		List<IRI> components = new ArrayList<>(componentCount);
		for (int c = 0; c < componentCount; c++) {
			IRI component = entity(ENGINEERING_NS, "component", c);
			components.add(component);
			add(handler, component, RDF.TYPE, componentType);
			add(handler, component, hasName, literal("Component " + c));
			add(handler, component, partOf, assemblies.get(contentRandom.nextInt(assemblies.size())));
			if (components.size() > 1) {
				IRI dependency = components.get(contentRandom.nextInt(components.size() - 1));
				add(handler, component, dependsOn, dependency);
			}
		}

		List<IRI> requirements = new ArrayList<>(requirementCount);
		for (int r = 0; r < requirementCount; r++) {
			IRI requirement = entity(ENGINEERING_NS, "requirement", r);
			requirements.add(requirement);
			add(handler, requirement, RDF.TYPE, requirementType);
			add(handler, requirement, hasName, literal("REQ-" + (1000 + r)));
			IRI component = components.get(contentRandom.nextInt(components.size()));
			add(handler, requirement, satisfies, component);
		}

		int testIndex = 0;
		int measurementIndex = 0;
		for (IRI requirement : requirements) {
			int testsPerRequirement = jitterInt(jitterRandom, config.testsPerRequirement);
			for (int t = 0; t < testsPerRequirement; t++) {
				IRI test = entity(ENGINEERING_NS, "test", testIndex++);
				add(handler, test, RDF.TYPE, testType);
				add(handler, requirement, verifiedBy, test);

				IRI measurement = entity(ENGINEERING_NS, "measurement", measurementIndex++);
				add(handler, measurement, RDF.TYPE, measurementType);
				add(handler, measurement, measuredValue, VF.createLiteral(0.8 + contentRandom.nextDouble() * 0.2));
				add(handler, test, verifiedBy, measurement);
			}
		}

		handler.endRDF();
	}

	public static Model generateHighlyConnected(HighlyConnectedConfig config) {
		return generateModel(handler -> generateHighlyConnected(config, handler));
	}

	public static void generateHighlyConnected(HighlyConnectedConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI nodeType = iri(CONNECTED_NS, "Node");
		IRI connectsTo = iri(CONNECTED_NS, "connectsTo");
		IRI linkWeight = iri(CONNECTED_NS, "weight");

		handler.startRDF();
		handler.handleNamespace("conn", CONNECTED_NS);

		int nodeCount = jitterInt(jitterRandom, config.nodeCount);
		int hubCount = jitterInt(jitterRandom, config.hubCount);
		double hubBias = jitterDouble(jitterRandom, config.hubBias, 0.0, 1.0);

		List<IRI> nodes = new ArrayList<>(nodeCount);
		for (int i = 0; i < nodeCount; i++) {
			IRI node = entity(CONNECTED_NS, "node", i);
			nodes.add(node);
			add(handler, node, RDF.TYPE, nodeType);
		}

		List<IRI> hubs = nodes.subList(0, Math.min(hubCount, nodes.size()));

		for (IRI node : nodes) {
			int edgesPerNode = jitterInt(jitterRandom, config.edgesPerNode);
			for (int e = 0; e < edgesPerNode; e++) {
				IRI target = contentRandom.nextDouble() < hubBias
						? hubs.get(contentRandom.nextInt(hubs.size()))
						: nodes.get(contentRandom.nextInt(nodes.size()));
				if (!node.equals(target)) {
					add(handler, node, connectsTo, target);
					add(handler, node, linkWeight, VF.createLiteral(1 + contentRandom.nextInt(10)));
				}
			}
		}

		handler.endRDF();
	}

	public static Model generateTrain(TrainConfig config) {
		return generateModel(handler -> generateTrain(config, handler));
	}

	public static void generateTrain(TrainConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI operationalPointType = iri(TRAIN_NS, "OperationalPoint");
		IRI lineType = iri(TRAIN_NS, "Line");
		IRI sectionType = iri(TRAIN_NS, "SectionOfLine");
		IRI trackSectionType = iri(TRAIN_NS, "TrackSection");
		IRI serviceType = iri(TRAIN_NS, "TrainService");

		IRI partOfLine = iri(TRAIN_NS, "partOfLine");
		IRI connectsOperationalPoint = iri(TRAIN_NS, "connectsOperationalPoint");
		IRI hasTrackSection = iri(TRAIN_NS, "hasTrackSection");
		IRI trackSectionOf = iri(TRAIN_NS, "trackSectionOf");
		IRI runsOnSection = iri(TRAIN_NS, "runsOnSection");
		IRI passesThrough = iri(TRAIN_NS, "passesThrough");
		IRI scheduledTime = iri(TRAIN_NS, "scheduledTime");
		IRI hasName = iri(TRAIN_NS, "name");

		handler.startRDF();
		handler.handleNamespace("train", TRAIN_NS);

		int stationCount = jitterInt(jitterRandom, config.stationCount);
		int routeCount = jitterInt(jitterRandom, config.routeCount);
		int trainCount = jitterInt(jitterRandom, config.trainCount);

		List<IRI> operationalPoints = new ArrayList<>(stationCount);
		for (int s = 0; s < stationCount; s++) {
			IRI operationalPoint = entity(TRAIN_NS, "operational-point", s);
			operationalPoints.add(operationalPoint);
			add(handler, operationalPoint, RDF.TYPE, operationalPointType);
			add(handler, operationalPoint, hasName, literal("OP " + s));
		}

		List<IRI> lines = new ArrayList<>(routeCount);
		for (int l = 0; l < routeCount; l++) {
			IRI line = entity(TRAIN_NS, "line", l);
			lines.add(line);
			add(handler, line, RDF.TYPE, lineType);
			add(handler, line, hasName, literal("Line " + l));
		}

		List<IRI> sections = new ArrayList<>();
		int sectionIndex = 0;
		int trackIndex = 0;
		int stationOffset = 0;
		for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
			IRI line = lines.get(lineIndex);
			int stopsPerRoute = jitterInt(jitterRandom, config.stopsPerRoute);
			for (int s = 0; s < stopsPerRoute; s++) {
				IRI section = entity(TRAIN_NS, "section", sectionIndex++);
				sections.add(section);
				add(handler, section, RDF.TYPE, sectionType);
				add(handler, section, partOfLine, line);

				IRI start = operationalPoints.get((stationOffset + s) % operationalPoints.size());
				IRI end = operationalPoints.get((stationOffset + s + 1) % operationalPoints.size());
				add(handler, section, connectsOperationalPoint, start);
				add(handler, section, connectsOperationalPoint, end);

				IRI trackSection = entity(TRAIN_NS, "track-section", trackIndex++);
				add(handler, trackSection, RDF.TYPE, trackSectionType);
				add(handler, section, hasTrackSection, trackSection);
				add(handler, trackSection, trackSectionOf, section);
			}
			stationOffset += stopsPerRoute;
		}

		int serviceIndex = 0;
		for (int t = 0; t < trainCount; t++) {
			IRI service = entity(TRAIN_NS, "service", serviceIndex++);
			add(handler, service, RDF.TYPE, serviceType);
			add(handler, service, hasName, literal("Service " + t));

			int tripsPerTrain = jitterInt(jitterRandom, config.tripsPerTrain);
			for (int tr = 0; tr < tripsPerTrain; tr++) {
				IRI section = sections.get(contentRandom.nextInt(sections.size()));
				IRI operationalPoint = operationalPoints.get(contentRandom.nextInt(operationalPoints.size()));
				add(handler, service, runsOnSection, section);
				add(handler, service, passesThrough, operationalPoint);
				add(handler, service, scheduledTime,
						VF.createLiteral(LocalTime.of(5, 0).plusMinutes(contentRandom.nextInt(600))));
			}
		}

		handler.endRDF();
	}

	public static Model generateElectricalGrid(ElectricalGridConfig config) {
		return generateModel(handler -> generateElectricalGrid(config, handler));
	}

	public static void generateElectricalGrid(ElectricalGridConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI substationType = iri(GRID_NS, "Substation");
		IRI transformerType = iri(GRID_NS, "Transformer");
		IRI lineType = iri(GRID_NS, "Line");
		IRI meterType = iri(GRID_NS, "Meter");
		IRI loadType = iri(GRID_NS, "Load");
		IRI generatorType = iri(GRID_NS, "Generator");

		IRI feeds = iri(GRID_NS, "feeds");
		IRI connectsTo = iri(GRID_NS, "connectsTo");
		IRI hasMeter = iri(GRID_NS, "hasMeter");
		IRI measures = iri(GRID_NS, "measures");
		IRI loadValue = iri(GRID_NS, "loadValue");
		IRI capacity = iri(GRID_NS, "capacity");
		IRI hasName = iri(GRID_NS, "name");

		handler.startRDF();
		handler.handleNamespace("grid", GRID_NS);

		int substationCount = jitterInt(jitterRandom, config.substationCount);

		List<IRI> substations = new ArrayList<>(substationCount);
		for (int s = 0; s < substationCount; s++) {
			IRI substation = entity(GRID_NS, "substation", s);
			substations.add(substation);
			add(handler, substation, RDF.TYPE, substationType);
			add(handler, substation, hasName, literal("Substation " + s));
		}

		int transformerIndex = 0;
		int lineIndex = 0;
		int meterIndex = 0;
		int loadIndex = 0;
		for (IRI substation : substations) {
			int transformersPerSubstation = jitterInt(jitterRandom, config.transformersPerSubstation);
			for (int t = 0; t < transformersPerSubstation; t++) {
				IRI transformer = entity(GRID_NS, "transformer", transformerIndex++);
				add(handler, transformer, RDF.TYPE, transformerType);
				add(handler, transformer, feeds, substation);

				int metersPerTransformer = jitterInt(jitterRandom, config.metersPerTransformer);
				for (int m = 0; m < metersPerTransformer; m++) {
					IRI meter = entity(GRID_NS, "meter", meterIndex++);
					IRI load = entity(GRID_NS, "load", loadIndex++);
					add(handler, meter, RDF.TYPE, meterType);
					add(handler, load, RDF.TYPE, loadType);
					add(handler, meter, measures, load);
					add(handler, transformer, hasMeter, meter);
					add(handler, load, loadValue, VF.createLiteral(50 + contentRandom.nextInt(150)));
				}
			}

			int linesPerSubstation = jitterInt(jitterRandom, config.linesPerSubstation);
			for (int l = 0; l < linesPerSubstation; l++) {
				IRI line = entity(GRID_NS, "line", lineIndex++);
				IRI target = substations.get(contentRandom.nextInt(substations.size()));
				add(handler, line, RDF.TYPE, lineType);
				add(handler, line, connectsTo, substation);
				add(handler, line, connectsTo, target);
			}

			IRI generator = entity(GRID_NS, "generator", substationIndex(substation));
			add(handler, generator, RDF.TYPE, generatorType);
			add(handler, generator, feeds, substation);
			add(handler, generator, capacity, VF.createLiteral(500 + contentRandom.nextInt(500)));
		}

		handler.endRDF();
	}

	public static Model generatePharma(PharmaConfig config) {
		return generateModel(handler -> generatePharma(config, handler));
	}

	public static void generatePharma(PharmaConfig config, RDFHandler handler) {
		Objects.requireNonNull(config, "config");
		Objects.requireNonNull(handler, "handler");
		config.validate();

		Random contentRandom = new Random(config.seed);
		Random jitterRandom = jitterRandom(config.seed);

		IRI drugType = iri(PHARMA_NS, "Drug");
		IRI moleculeType = iri(PHARMA_NS, "Molecule");
		IRI chemicalClassType = iri(PHARMA_NS, "ChemicalClass");
		IRI targetType = iri(PHARMA_NS, "Target");
		IRI pathwayType = iri(PHARMA_NS, "Pathway");
		IRI diseaseType = iri(PHARMA_NS, "Disease");
		IRI trialType = iri(PHARMA_NS, "ClinicalTrial");
		IRI armType = iri(PHARMA_NS, "TrialArm");
		IRI resultType = iri(PHARMA_NS, "TrialResult");
		IRI sideEffectType = iri(PHARMA_NS, "SideEffect");
		IRI biomarkerType = iri(PHARMA_NS, "Biomarker");
		IRI combinationType = iri(PHARMA_NS, "Combination");
		IRI comparatorType = iri(PHARMA_NS, "Comparator");

		IRI hasName = iri(PHARMA_NS, "name");
		IRI hasMolecule = iri(PHARMA_NS, "hasMolecule");
		IRI inClass = iri(PHARMA_NS, "inClass");
		IRI targets = iri(PHARMA_NS, "targets");
		IRI inPathway = iri(PHARMA_NS, "inPathway");
		IRI indicatedFor = iri(PHARMA_NS, "indicatedFor");
		IRI contraindicatedFor = iri(PHARMA_NS, "contraindicatedFor");
		IRI testedIn = iri(PHARMA_NS, "testedIn");
		IRI studiesDisease = iri(PHARMA_NS, "studiesDisease");
		IRI hasArm = iri(PHARMA_NS, "hasArm");
		IRI armDrug = iri(PHARMA_NS, "armDrug");
		IRI armComparator = iri(PHARMA_NS, "armComparator");
		IRI hasResult = iri(PHARMA_NS, "hasResult");
		IRI endpoint = iri(PHARMA_NS, "endpoint");
		IRI effectSize = iri(PHARMA_NS, "effectSize");
		IRI pValue = iri(PHARMA_NS, "pValue");
		IRI responseRate = iri(PHARMA_NS, "responseRate");
		IRI observedSideEffect = iri(PHARMA_NS, "observedSideEffect");
		IRI hasSideEffect = iri(PHARMA_NS, "hasSideEffect");
		IRI severity = iri(PHARMA_NS, "severity");
		IRI biomarker = iri(PHARMA_NS, "biomarker");
		IRI biomarkerValue = iri(PHARMA_NS, "biomarkerValue");
		IRI combinationOf = iri(PHARMA_NS, "combinationOf");
		IRI synergyScore = iri(PHARMA_NS, "synergyScore");
		IRI phase = iri(PHARMA_NS, "phase");

		String[] severities = new String[] { "Mild", "Moderate", "Severe" };
		String[] endpoints = new String[] { "OverallSurvival", "ProgressionFreeSurvival", "ResponseRate" };

		int chemicalClassCount = jitterInt(jitterRandom, config.chemicalClassCount);
		int pathwayCount = jitterInt(jitterRandom, config.pathwayCount);
		int targetCount = jitterInt(jitterRandom, config.targetCount);
		int moleculeCount = jitterInt(jitterRandom, config.moleculeCount);
		int diseaseCount = jitterInt(jitterRandom, config.diseaseCount);
		int sideEffectCount = jitterInt(jitterRandom, config.sideEffectCount);
		int biomarkerCount = jitterInt(jitterRandom, config.biomarkerCount);
		int drugCount = jitterInt(jitterRandom, config.drugCount);
		int comparatorCount = jitterInt(jitterRandom, config.comparatorCount);
		int trialCount = jitterInt(jitterRandom, config.trialCount);
		int combinationCount = jitterInt(jitterRandom, config.combinationCount);

		handler.startRDF();
		handler.handleNamespace("pharma", PHARMA_NS);

		List<IRI> chemicalClasses = new ArrayList<>(chemicalClassCount);
		for (int c = 0; c < chemicalClassCount; c++) {
			IRI chemicalClass = entity(PHARMA_NS, "class", c);
			chemicalClasses.add(chemicalClass);
			add(handler, chemicalClass, RDF.TYPE, chemicalClassType);
			add(handler, chemicalClass, hasName, literal("Class " + c));
		}

		List<IRI> pathways = new ArrayList<>(pathwayCount);
		for (int p = 0; p < pathwayCount; p++) {
			IRI pathway = entity(PHARMA_NS, "pathway", p);
			pathways.add(pathway);
			add(handler, pathway, RDF.TYPE, pathwayType);
			add(handler, pathway, hasName, literal("Pathway " + p));
		}

		List<IRI> targetsList = new ArrayList<>(targetCount);
		for (int t = 0; t < targetCount; t++) {
			IRI target = entity(PHARMA_NS, "target", t);
			targetsList.add(target);
			add(handler, target, RDF.TYPE, targetType);
			add(handler, target, hasName, literal("Target " + t));
			add(handler, target, inPathway, pathways.get(contentRandom.nextInt(pathways.size())));
		}

		List<IRI> molecules = new ArrayList<>(moleculeCount);
		for (int m = 0; m < moleculeCount; m++) {
			IRI molecule = entity(PHARMA_NS, "molecule", m);
			molecules.add(molecule);
			add(handler, molecule, RDF.TYPE, moleculeType);
			add(handler, molecule, hasName, literal("Molecule " + m));
			add(handler, molecule, inClass, chemicalClasses.get(contentRandom.nextInt(chemicalClasses.size())));
			int targetsPerMolecule = jitterInt(jitterRandom, config.targetsPerMolecule);
			for (int t = 0; t < targetsPerMolecule; t++) {
				add(handler, molecule, targets, targetsList.get(contentRandom.nextInt(targetsList.size())));
			}
		}

		List<IRI> diseases = new ArrayList<>(diseaseCount);
		for (int d = 0; d < diseaseCount; d++) {
			IRI disease = entity(PHARMA_NS, "disease", d);
			diseases.add(disease);
			add(handler, disease, RDF.TYPE, diseaseType);
			add(handler, disease, hasName, literal("Disease " + d));
		}

		List<IRI> sideEffects = new ArrayList<>(sideEffectCount);
		for (int s = 0; s < sideEffectCount; s++) {
			IRI sideEffect = entity(PHARMA_NS, "side-effect", s);
			sideEffects.add(sideEffect);
			add(handler, sideEffect, RDF.TYPE, sideEffectType);
			add(handler, sideEffect, hasName, literal("SideEffect " + s));
			add(handler, sideEffect, severity, literal(severities[s % severities.length]));
		}

		List<IRI> biomarkers = new ArrayList<>(biomarkerCount);
		for (int b = 0; b < biomarkerCount; b++) {
			IRI marker = entity(PHARMA_NS, "biomarker", b);
			biomarkers.add(marker);
			add(handler, marker, RDF.TYPE, biomarkerType);
			add(handler, marker, hasName, literal("Biomarker " + b));
		}

		List<IRI> drugs = new ArrayList<>(drugCount);
		for (int d = 0; d < drugCount; d++) {
			IRI drug = entity(PHARMA_NS, "drug", d);
			drugs.add(drug);
			add(handler, drug, RDF.TYPE, drugType);
			add(handler, drug, hasName, literal("Drug " + d));

			int moleculesPerDrug = jitterInt(jitterRandom, config.moleculesPerDrug);
			for (int m = 0; m < moleculesPerDrug; m++) {
				add(handler, drug, hasMolecule, molecules.get(contentRandom.nextInt(molecules.size())));
			}

			int targetsPerDrug = jitterInt(jitterRandom, config.targetsPerDrug);
			for (int t = 0; t < targetsPerDrug; t++) {
				add(handler, drug, targets, targetsList.get(contentRandom.nextInt(targetsList.size())));
			}

			int indicationsPerDrug = jitterInt(jitterRandom, config.indicationsPerDrug);
			for (int i = 0; i < indicationsPerDrug; i++) {
				add(handler, drug, indicatedFor, diseases.get(contentRandom.nextInt(diseases.size())));
			}

			add(handler, drug, contraindicatedFor, diseases.get(contentRandom.nextInt(diseases.size())));

			int sideEffectsPerDrug = jitterInt(jitterRandom, config.sideEffectsPerDrug);
			for (int s = 0; s < sideEffectsPerDrug; s++) {
				add(handler, drug, hasSideEffect, sideEffects.get(contentRandom.nextInt(sideEffects.size())));
			}
		}

		List<IRI> comparators = new ArrayList<>(comparatorCount);
		for (int c = 0; c < comparatorCount; c++) {
			IRI comparator = entity(PHARMA_NS, "comparator", c);
			comparators.add(comparator);
			add(handler, comparator, RDF.TYPE, comparatorType);
			add(handler, comparator, hasName, literal("Comparator " + c));
		}

		List<IRI> trials = new ArrayList<>(trialCount);
		int armIndex = 0;
		int resultIndex = 0;
		for (int t = 0; t < trialCount; t++) {
			IRI trial = entity(PHARMA_NS, "trial", t);
			trials.add(trial);
			add(handler, trial, RDF.TYPE, trialType);
			add(handler, trial, hasName, literal("Trial " + t));
			add(handler, trial, studiesDisease, diseases.get(contentRandom.nextInt(diseases.size())));
			add(handler, trial, phase, VF.createLiteral(1 + contentRandom.nextInt(3)));

			int armsPerTrial = jitterInt(jitterRandom, config.armsPerTrial);
			for (int a = 0; a < armsPerTrial; a++) {
				IRI arm = entity(PHARMA_NS, "arm", armIndex++);
				add(handler, arm, RDF.TYPE, armType);
				add(handler, trial, hasArm, arm);
				IRI drug = drugs.get(contentRandom.nextInt(drugs.size()));
				add(handler, arm, armDrug, drug);
				add(handler, drug, testedIn, trial);
				add(handler, arm, armComparator, comparators.get(contentRandom.nextInt(comparators.size())));

				IRI result = entity(PHARMA_NS, "result", resultIndex++);
				add(handler, result, RDF.TYPE, resultType);
				add(handler, arm, hasResult, result);
				add(handler, result, endpoint, literal(endpoints[contentRandom.nextInt(endpoints.length)]));
				add(handler, result, effectSize, VF.createLiteral(contentRandom.nextDouble()));
				add(handler, result, pValue, VF.createLiteral(contentRandom.nextDouble() * 0.1));
				add(handler, result, responseRate, VF.createLiteral(contentRandom.nextDouble()));
				add(handler, result, observedSideEffect, sideEffects.get(contentRandom.nextInt(sideEffects.size())));
				IRI marker = biomarkers.get(contentRandom.nextInt(biomarkers.size()));
				add(handler, result, biomarker, marker);
				add(handler, result, biomarkerValue, VF.createLiteral(0.1 + contentRandom.nextDouble() * 2.0));
			}
		}

		for (int c = 0; c < combinationCount; c++) {
			IRI combination = entity(PHARMA_NS, "combination", c);
			add(handler, combination, RDF.TYPE, combinationType);
			add(handler, combination, hasName, literal("Combination " + c));
			add(handler, combination, synergyScore, VF.createLiteral(contentRandom.nextDouble()));
			int members = jitterInt(jitterRandom, config.drugsPerCombination);
			for (int m = 0; m < members; m++) {
				add(handler, combination, combinationOf, drugs.get(contentRandom.nextInt(drugs.size())));
			}
			if (!trials.isEmpty()) {
				IRI trial = trials.get(contentRandom.nextInt(trials.size()));
				add(handler, combination, testedIn, trial);
			}
		}

		handler.endRDF();
	}

	private static Random jitterRandom(long seed) {
		return new Random(seed ^ JITTER_SEED_XOR);
	}

	private static int jitterInt(Random random, int base) {
		return jitterInt(random, base, 1);
	}

	private static int jitterInt(Random random, int base, int minValue) {
		int delta = base / 2;
		int min = Math.max(minValue, base - delta);
		int max = Math.max(min, base + delta);
		if (min == max) {
			return min;
		}
		return min + random.nextInt(max - min + 1);
	}

	private static double jitterDouble(Random random, double base, double minValue, double maxValue) {
		double delta = base * 0.5;
		double min = Math.max(minValue, base - delta);
		double max = Math.min(maxValue, base + delta);
		if (max < min) {
			double tmp = min;
			min = max;
			max = tmp;
		}
		if (max == min) {
			return min;
		}
		return min + random.nextDouble() * (max - min);
	}

	private static int[] jitterCliqueSizes(Random random, int[] baseSizes, int maxTotal) {
		Objects.requireNonNull(baseSizes, "baseSizes");
		List<Integer> sizes = new ArrayList<>(baseSizes.length);
		int remaining = maxTotal;
		for (int base : baseSizes) {
			if (remaining < 2) {
				break;
			}
			int size = jitterInt(random, base, 2);
			if (size > remaining) {
				size = remaining;
			}
			if (size < 2) {
				break;
			}
			sizes.add(size);
			remaining -= size;
		}
		int[] result = new int[sizes.size()];
		for (int i = 0; i < sizes.size(); i++) {
			result[i] = sizes.get(i);
		}
		return result;
	}

	private static int substationIndex(IRI substation) {
		String local = substation.getLocalName();
		int slash = local.lastIndexOf('/');
		String value = slash >= 0 ? local.substring(slash + 1) : local;
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException e) {
			return Math.abs(local.hashCode());
		}
	}

	private static Model generateModel(Consumer<RDFHandler> generator) {
		Model model = new LinkedHashModel();
		StatementCollector collector = new StatementCollector(model);
		generator.accept(collector);
		return model;
	}

	private static IRI iri(String namespace, String localName) {
		return VF.createIRI(namespace, localName);
	}

	private static IRI entity(String namespace, String category, int id) {
		return VF.createIRI(namespace, category + "/" + id);
	}

	private static Literal literal(String value) {
		return VF.createLiteral(value);
	}

	private static void add(RDFHandler handler, Resource subject, IRI predicate, Value object) {
		handler.handleStatement(VF.createStatement(subject, predicate, object));
	}

	private static String randomWord(Random random) {
		return WORDS[random.nextInt(WORDS.length)];
	}

	private static String randomSentence(Random random, int minWords, int maxWords) {
		int total = minWords + random.nextInt(Math.max(1, maxWords - minWords + 1));
		StringBuilder builder = new StringBuilder();
		for (int i = 0; i < total; i++) {
			if (i > 0) {
				builder.append(' ');
			}
			builder.append(randomWord(random));
		}
		return builder.toString();
	}

	public static final class MedicalConfig {
		private int patientCount = 10000;
		private int encountersPerPatient = 3;
		private int conditionsPerEncounter = 2;
		private int medicationsPerPatient = 2;
		private int observationsPerEncounter = 2;
		private int practitionerCount = 10000;
		private long seed = 42L;

		public MedicalConfig withPatientCount(int patientCount) {
			this.patientCount = requirePositive(patientCount, "patientCount");
			return this;
		}

		public MedicalConfig withEncountersPerPatient(int encountersPerPatient) {
			this.encountersPerPatient = requirePositive(encountersPerPatient, "encountersPerPatient");
			return this;
		}

		public MedicalConfig withConditionsPerEncounter(int conditionsPerEncounter) {
			this.conditionsPerEncounter = requirePositive(conditionsPerEncounter, "conditionsPerEncounter");
			return this;
		}

		public MedicalConfig withMedicationsPerPatient(int medicationsPerPatient) {
			this.medicationsPerPatient = requirePositive(medicationsPerPatient, "medicationsPerPatient");
			return this;
		}

		public MedicalConfig withObservationsPerEncounter(int observationsPerEncounter) {
			this.observationsPerEncounter = requirePositive(observationsPerEncounter, "observationsPerEncounter");
			return this;
		}

		public MedicalConfig withPractitionerCount(int practitionerCount) {
			this.practitionerCount = requirePositive(practitionerCount, "practitionerCount");
			return this;
		}

		public MedicalConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(patientCount, "patientCount");
			requirePositive(encountersPerPatient, "encountersPerPatient");
			requirePositive(conditionsPerEncounter, "conditionsPerEncounter");
			requirePositive(medicationsPerPatient, "medicationsPerPatient");
			requirePositive(observationsPerEncounter, "observationsPerEncounter");
			requirePositive(practitionerCount, "practitionerCount");
		}
	}

	public static final class SocialMediaConfig {
		private int userCount = 20000;
		private int postsPerUser = 15;
		private int commentsPerPost = 5;
		private int likesPerPost = 5;
		private int followsPerUser = 9;
		private int tagsPerPost = 4;
		private int tagCount = 50;
		private int[] cliqueSizes = new int[] { 3, 4, 5, 6 };
		private long seed = 42L;

		public SocialMediaConfig withUserCount(int userCount) {
			this.userCount = requirePositive(userCount, "userCount");
			return this;
		}

		public SocialMediaConfig withPostsPerUser(int postsPerUser) {
			this.postsPerUser = requirePositive(postsPerUser, "postsPerUser");
			return this;
		}

		public SocialMediaConfig withCommentsPerPost(int commentsPerPost) {
			this.commentsPerPost = requirePositive(commentsPerPost, "commentsPerPost");
			return this;
		}

		public SocialMediaConfig withLikesPerPost(int likesPerPost) {
			this.likesPerPost = requirePositive(likesPerPost, "likesPerPost");
			return this;
		}

		public SocialMediaConfig withFollowsPerUser(int followsPerUser) {
			this.followsPerUser = requirePositive(followsPerUser, "followsPerUser");
			return this;
		}

		public SocialMediaConfig withTagsPerPost(int tagsPerPost) {
			this.tagsPerPost = requirePositive(tagsPerPost, "tagsPerPost");
			return this;
		}

		public SocialMediaConfig withTagCount(int tagCount) {
			this.tagCount = requirePositive(tagCount, "tagCount");
			return this;
		}

		public SocialMediaConfig withCliqueSizes(int... cliqueSizes) {
			Objects.requireNonNull(cliqueSizes, "cliqueSizes");
			this.cliqueSizes = cliqueSizes.clone();
			return this;
		}

		public SocialMediaConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(userCount, "userCount");
			requirePositive(postsPerUser, "postsPerUser");
			requirePositive(commentsPerPost, "commentsPerPost");
			requirePositive(likesPerPost, "likesPerPost");
			requirePositive(followsPerUser, "followsPerUser");
			requirePositive(tagsPerPost, "tagsPerPost");
			requirePositive(tagCount, "tagCount");
			Objects.requireNonNull(cliqueSizes, "cliqueSizes");
			int cliqueTotal = 0;
			for (int size : cliqueSizes) {
				if (size < 2) {
					throw new IllegalArgumentException("cliqueSizes entries must be >= 2");
				}
				cliqueTotal += size;
			}
			if (cliqueTotal > userCount) {
				throw new IllegalArgumentException("cliqueSizes total exceeds userCount");
			}
		}
	}

	public static final class LibraryConfig {
		private int bookCount = 100000;
		private int authorCount = 30000;
		private int branchCount = 5;
		private int copiesPerBook = 3;
		private int authorsPerBook = 2;
		private int memberCount = 5000;
		private int loansPerMember = 2;
		private long seed = 42L;

		public LibraryConfig withBookCount(int bookCount) {
			this.bookCount = requirePositive(bookCount, "bookCount");
			return this;
		}

		public LibraryConfig withAuthorCount(int authorCount) {
			this.authorCount = requirePositive(authorCount, "authorCount");
			return this;
		}

		public LibraryConfig withBranchCount(int branchCount) {
			this.branchCount = requirePositive(branchCount, "branchCount");
			return this;
		}

		public LibraryConfig withCopiesPerBook(int copiesPerBook) {
			this.copiesPerBook = requirePositive(copiesPerBook, "copiesPerBook");
			return this;
		}

		public LibraryConfig withAuthorsPerBook(int authorsPerBook) {
			this.authorsPerBook = requirePositive(authorsPerBook, "authorsPerBook");
			return this;
		}

		public LibraryConfig withMemberCount(int memberCount) {
			this.memberCount = requirePositive(memberCount, "memberCount");
			return this;
		}

		public LibraryConfig withLoansPerMember(int loansPerMember) {
			this.loansPerMember = requirePositive(loansPerMember, "loansPerMember");
			return this;
		}

		public LibraryConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(bookCount, "bookCount");
			requirePositive(authorCount, "authorCount");
			requirePositive(branchCount, "branchCount");
			requirePositive(copiesPerBook, "copiesPerBook");
			requirePositive(authorsPerBook, "authorsPerBook");
			requirePositive(memberCount, "memberCount");
			requirePositive(loansPerMember, "loansPerMember");
		}
	}

	public static final class EngineeringConfig {
		private int componentCount = 120000;
		private int assemblyCount = 1500;
		private int requirementCount = 400;
		private int testsPerRequirement = 3;
		private long seed = 42L;

		public EngineeringConfig withComponentCount(int componentCount) {
			this.componentCount = requirePositive(componentCount, "componentCount");
			return this;
		}

		public EngineeringConfig withAssemblyCount(int assemblyCount) {
			this.assemblyCount = requirePositive(assemblyCount, "assemblyCount");
			return this;
		}

		public EngineeringConfig withRequirementCount(int requirementCount) {
			this.requirementCount = requirePositive(requirementCount, "requirementCount");
			return this;
		}

		public EngineeringConfig withTestsPerRequirement(int testsPerRequirement) {
			this.testsPerRequirement = requirePositive(testsPerRequirement, "testsPerRequirement");
			return this;
		}

		public EngineeringConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(componentCount, "componentCount");
			requirePositive(assemblyCount, "assemblyCount");
			requirePositive(requirementCount, "requirementCount");
			requirePositive(testsPerRequirement, "testsPerRequirement");
		}
	}

	public static final class HighlyConnectedConfig {
		private int nodeCount = 30000;
		private int hubCount = 10;
		private int edgesPerNode = 8;
		private double hubBias = 0.6;
		private long seed = 42L;

		public HighlyConnectedConfig withNodeCount(int nodeCount) {
			this.nodeCount = requirePositive(nodeCount, "nodeCount");
			return this;
		}

		public HighlyConnectedConfig withHubCount(int hubCount) {
			this.hubCount = requirePositive(hubCount, "hubCount");
			return this;
		}

		public HighlyConnectedConfig withEdgesPerNode(int edgesPerNode) {
			this.edgesPerNode = requirePositive(edgesPerNode, "edgesPerNode");
			return this;
		}

		public HighlyConnectedConfig withHubBias(double hubBias) {
			if (hubBias < 0.0 || hubBias > 1.0) {
				throw new IllegalArgumentException("hubBias must be between 0 and 1");
			}
			this.hubBias = hubBias;
			return this;
		}

		public HighlyConnectedConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(nodeCount, "nodeCount");
			requirePositive(hubCount, "hubCount");
			requirePositive(edgesPerNode, "edgesPerNode");
			if (hubBias < 0.0 || hubBias > 1.0) {
				throw new IllegalArgumentException("hubBias must be between 0 and 1");
			}
		}
	}

	public static final class TrainConfig {
		private int stationCount = 40000;
		private int routeCount = 6000;
		private int stopsPerRoute = 8;
		private int trainCount = 12000;
		private int tripsPerTrain = 3;
		private long seed = 42L;

		public TrainConfig withStationCount(int stationCount) {
			this.stationCount = requirePositive(stationCount, "stationCount");
			return this;
		}

		public TrainConfig withRouteCount(int routeCount) {
			this.routeCount = requirePositive(routeCount, "routeCount");
			return this;
		}

		public TrainConfig withStopsPerRoute(int stopsPerRoute) {
			this.stopsPerRoute = requirePositive(stopsPerRoute, "stopsPerRoute");
			return this;
		}

		public TrainConfig withTrainCount(int trainCount) {
			this.trainCount = requirePositive(trainCount, "trainCount");
			return this;
		}

		public TrainConfig withTripsPerTrain(int tripsPerTrain) {
			this.tripsPerTrain = requirePositive(tripsPerTrain, "tripsPerTrain");
			return this;
		}

		public TrainConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(stationCount, "stationCount");
			requirePositive(routeCount, "routeCount");
			requirePositive(stopsPerRoute, "stopsPerRoute");
			requirePositive(trainCount, "trainCount");
			requirePositive(tripsPerTrain, "tripsPerTrain");
		}
	}

	public static final class ElectricalGridConfig {
		private int substationCount = 12000;
		private int transformersPerSubstation = 3;
		private int linesPerSubstation = 2;
		private int metersPerTransformer = 4;
		private long seed = 42L;

		public ElectricalGridConfig withSubstationCount(int substationCount) {
			this.substationCount = requirePositive(substationCount, "substationCount");
			return this;
		}

		public ElectricalGridConfig withTransformersPerSubstation(int transformersPerSubstation) {
			this.transformersPerSubstation = requirePositive(transformersPerSubstation, "transformersPerSubstation");
			return this;
		}

		public ElectricalGridConfig withLinesPerSubstation(int linesPerSubstation) {
			this.linesPerSubstation = requirePositive(linesPerSubstation, "linesPerSubstation");
			return this;
		}

		public ElectricalGridConfig withMetersPerTransformer(int metersPerTransformer) {
			this.metersPerTransformer = requirePositive(metersPerTransformer, "metersPerTransformer");
			return this;
		}

		public ElectricalGridConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(substationCount, "substationCount");
			requirePositive(transformersPerSubstation, "transformersPerSubstation");
			requirePositive(linesPerSubstation, "linesPerSubstation");
			requirePositive(metersPerTransformer, "metersPerTransformer");
		}
	}

	public static final class PharmaConfig {
		private int drugCount = 5000;
		private int moleculeCount = 7500;
		private int chemicalClassCount = 200;
		private int targetCount = 600;
		private int pathwayCount = 150;
		private int diseaseCount = 400;
		private int trialCount = 1200;
		private int armsPerTrial = 3;
		private int sideEffectCount = 250;
		private int sideEffectsPerDrug = 2;
		private int biomarkerCount = 150;
		private int combinationCount = 600;
		private int moleculesPerDrug = 2;
		private int targetsPerDrug = 2;
		private int targetsPerMolecule = 2;
		private int indicationsPerDrug = 2;
		private int drugsPerCombination = 2;
		private int comparatorCount = 12;
		private long seed = 42L;

		public PharmaConfig withDrugCount(int drugCount) {
			this.drugCount = requirePositive(drugCount, "drugCount");
			return this;
		}

		public PharmaConfig withMoleculeCount(int moleculeCount) {
			this.moleculeCount = requirePositive(moleculeCount, "moleculeCount");
			return this;
		}

		public PharmaConfig withChemicalClassCount(int chemicalClassCount) {
			this.chemicalClassCount = requirePositive(chemicalClassCount, "chemicalClassCount");
			return this;
		}

		public PharmaConfig withTargetCount(int targetCount) {
			this.targetCount = requirePositive(targetCount, "targetCount");
			return this;
		}

		public PharmaConfig withPathwayCount(int pathwayCount) {
			this.pathwayCount = requirePositive(pathwayCount, "pathwayCount");
			return this;
		}

		public PharmaConfig withDiseaseCount(int diseaseCount) {
			this.diseaseCount = requirePositive(diseaseCount, "diseaseCount");
			return this;
		}

		public PharmaConfig withTrialCount(int trialCount) {
			this.trialCount = requirePositive(trialCount, "trialCount");
			return this;
		}

		public PharmaConfig withArmsPerTrial(int armsPerTrial) {
			this.armsPerTrial = requirePositive(armsPerTrial, "armsPerTrial");
			return this;
		}

		public PharmaConfig withSideEffectCount(int sideEffectCount) {
			this.sideEffectCount = requirePositive(sideEffectCount, "sideEffectCount");
			return this;
		}

		public PharmaConfig withSideEffectsPerDrug(int sideEffectsPerDrug) {
			this.sideEffectsPerDrug = requirePositive(sideEffectsPerDrug, "sideEffectsPerDrug");
			return this;
		}

		public PharmaConfig withBiomarkerCount(int biomarkerCount) {
			this.biomarkerCount = requirePositive(biomarkerCount, "biomarkerCount");
			return this;
		}

		public PharmaConfig withCombinationCount(int combinationCount) {
			this.combinationCount = requirePositive(combinationCount, "combinationCount");
			return this;
		}

		public PharmaConfig withMoleculesPerDrug(int moleculesPerDrug) {
			this.moleculesPerDrug = requirePositive(moleculesPerDrug, "moleculesPerDrug");
			return this;
		}

		public PharmaConfig withTargetsPerDrug(int targetsPerDrug) {
			this.targetsPerDrug = requirePositive(targetsPerDrug, "targetsPerDrug");
			return this;
		}

		public PharmaConfig withTargetsPerMolecule(int targetsPerMolecule) {
			this.targetsPerMolecule = requirePositive(targetsPerMolecule, "targetsPerMolecule");
			return this;
		}

		public PharmaConfig withIndicationsPerDrug(int indicationsPerDrug) {
			this.indicationsPerDrug = requirePositive(indicationsPerDrug, "indicationsPerDrug");
			return this;
		}

		public PharmaConfig withDrugsPerCombination(int drugsPerCombination) {
			this.drugsPerCombination = requirePositive(drugsPerCombination, "drugsPerCombination");
			return this;
		}

		public PharmaConfig withComparatorCount(int comparatorCount) {
			this.comparatorCount = requirePositive(comparatorCount, "comparatorCount");
			return this;
		}

		public PharmaConfig withSeed(long seed) {
			this.seed = seed;
			return this;
		}

		private void validate() {
			requirePositive(drugCount, "drugCount");
			requirePositive(moleculeCount, "moleculeCount");
			requirePositive(chemicalClassCount, "chemicalClassCount");
			requirePositive(targetCount, "targetCount");
			requirePositive(pathwayCount, "pathwayCount");
			requirePositive(diseaseCount, "diseaseCount");
			requirePositive(trialCount, "trialCount");
			requirePositive(armsPerTrial, "armsPerTrial");
			requirePositive(sideEffectCount, "sideEffectCount");
			requirePositive(sideEffectsPerDrug, "sideEffectsPerDrug");
			requirePositive(biomarkerCount, "biomarkerCount");
			requirePositive(combinationCount, "combinationCount");
			requirePositive(moleculesPerDrug, "moleculesPerDrug");
			requirePositive(targetsPerDrug, "targetsPerDrug");
			requirePositive(targetsPerMolecule, "targetsPerMolecule");
			requirePositive(indicationsPerDrug, "indicationsPerDrug");
			requirePositive(drugsPerCombination, "drugsPerCombination");
			requirePositive(comparatorCount, "comparatorCount");
		}
	}

	private static int requirePositive(int value, String name) {
		if (value <= 0) {
			throw new IllegalArgumentException(name + " must be > 0");
		}
		return value;
	}
}