Home / Writing / MOSOF in ~100 lines

MOSOF in ~100 lines of Python.

An annotated NSGA-II walkthrough that produces a publication-grade Pareto front for a synthetic-but-realistic sensor-selection problem. Every line traceable to the 2026 Sensors paper.

The MOSOF papers run on a few thousand lines of MATLAB and a Simulink-based fault simulator. The method underneath fits in about a hundred lines of Python. This is a walkthrough of those hundred lines — the inner loop, the fitness function, the Pareto-front extraction, and the knee selection — so you can read them in one sitting and know what is doing the work.

If you have read either of the Sensors (MDPI) papers,1Suslu, Ali, Jennions (2025); Suslu, Ali, Jennions (2026). Citations at the foot of this piece. you have seen the MOSOF block diagram: candidate pool in, multi-objective optimiser in the middle, Pareto front out, knee chosen by a stakeholder downstream. The block diagram earns the abstraction; the implementation has to make it actually run. The point of this walkthrough is to make the middle block legible — what the genetic algorithm is doing on each generation, what the three objectives evaluate to per suite, how the knee gets picked, and how all of that ends up in a 77-point Pareto front the rest of the system can render.

The full file lives in the mosof-ref-impl repository. The version below is the same code, broken into the sections that matter, with the rationale next to each.

01 The problem statement

MOSOF picks a subset of candidate sensors from a larger pool to optimise three competing objectives simultaneously:

The decision variable is a binary vector x ∈ {0,1}n where xi = 1 means sensor i is in the suite. The space is exponential in n — for a 32-sensor candidate pool, that is about 4 billion possible suites. Enumeration is out; we use a multi-objective genetic algorithm.

Why NSGA-II

NSGA-II is the default workhorse for three-objective problems where the solution space is binary and the front is expected to have a clean knee. It maintains population diversity via non-dominated sorting plus a crowding-distance tiebreak; you get the whole Pareto front in a single run, not one weighted scalarisation at a time. The published MOSOF runs in the Sensors papers used NSGA-II; this reference impl does too.

02 The candidate pool

The first thing you need is sensors to choose from. The reference impl seeds a pool whose per-family parameter ranges match the order-of-magnitude values in Section 4.3 of the thesis (the original per-sensor table is in the appendix; this is the synthetic-but-realistic version).

# Eight candidate sensors per subsystem, parameterised after
# thesis Section 4.3 family medians.
def make_candidate_pool(n_per_subsystem=8):
    rows = []
    for subsystem in ["Engine", "Fuel", "EPS", "ECS"]:
        cost_range = {"Engine": (3.0, 8.0), "Fuel": (1.5, 4.5),
                      "EPS": (2.0, 5.5), "ECS": (1.0, 3.5)}[subsystem]
        mtbf_range = {"Engine": (180, 480), "Fuel": (220, 520),
                      "EPS": (300, 700), "ECS": (240, 600)}[subsystem]
        for j in range(n_per_subsystem):
            rows.append({
                "id": f"{subsystem[:3].upper()}-{j+1:02d}",
                "subsystem": subsystem,
                "cost_kUSD": float(rng.uniform(*cost_range)),
                "mtbf_kh": float(rng.uniform(*mtbf_range)),
                "ndci": float(rng.uniform(0.05, 0.18)),
            })
    return rows

The four subsystems and their per-family cost / MTBF ranges aren't arbitrary — they follow the families used in the cross-subsystem study. The NDCI range [0.05, 0.18] is the per-sensor headline strength derived from the published fault × sensor matrix.2The /data/ndci-matrix.json dataset on the site exposes the published 12×18 matrix; the per-sensor headline strength is its column-sum, max-normalised.

03 The fitness function

For each candidate suite (a binary mask), compute the three objectives. This is the most opinionated part of the implementation — the choice of how coverage saturates and how reliability aggregates determines the shape of the front you get.

def evaluate_suite(mask, pool):
    sensors = pool["sensors"]
    idx = np.flatnonzero(mask.astype(bool))
    if idx.size == 0:
        return 0.0, 0.0, 0.0
    sel = [sensors[i] for i in idx]

    # Performance: NDCI-weighted coverage with diminishing returns.
    pool_ndci = sum(s["ndci"] for s in sensors)
    suite_ndci = sum(s["ndci"] for s in sel)
    perf = 1.0 - np.exp(-2.5 * suite_ndci / pool_ndci)

    # Cost: simple sum of unit costs.
    cost = sum(s["cost_kUSD"] for s in sel)

    # Reliability: series-equivalent MTBF.
    inv_mtbf = sum(1.0 / s["mtbf_kh"] for s in sel)
    mtbf = 1.0 / inv_mtbf if inv_mtbf > 0 else 0.0

    return float(perf), float(cost), float(mtbf)

Three choices to call out:

04 The NSGA-II driver

Now the actual optimisation. Pymoo's NSGA-II wants a problem class with an _evaluate method that returns the objective vector. We minimise, so the two maximise objectives get negated on the way out:

class MOSOFProblem(ElementwiseProblem):
    def __init__(self, pool):
        n = pool["n"]
        super().__init__(n_var=n, n_obj=3, n_ieq_constr=1,
                         xl=0, xu=1, vtype=int)
        self.pool = pool

    def _evaluate(self, x, out, *args, **kwargs):
        perf, cost, mtbf = evaluate_suite(x, self.pool)
        # NSGA-II minimises; negate the maximise axes.
        out["F"] = [-perf, cost, -mtbf]
        # Constraint: suite must have at least three sensors.
        out["G"] = [3 - int(x.sum())]

algorithm = NSGA2(
    pop_size=100,
    sampling=BinaryRandomSampling(),
    crossover=HalfUniformCrossover(),
    mutation=BitflipMutation(prob=1.0 / n),
    eliminate_duplicates=True,
)
result = minimize(MOSOFProblem(pool), algorithm, ("n_gen", 200), seed=1)

That is the whole optimisation. Three real choices worth knowing about:

05 The Pareto front and the knee

Pymoo returns result.F already containing only non-dominated solutions — the algorithm's non-dominated sorting takes care of that. We do one more step: pick a knee.

def knee_by_perpendicular(front):
    perf = front[:, 0]
    cost = front[:, 1]
    perf_n = (perf - perf.min()) / max(perf.max() - perf.min(), 1e-9)
    cost_n = (cost - cost.min()) / max(cost.max() - cost.min(), 1e-9)
    p1 = np.array([perf_n[np.argmin(perf_n)],
                   cost_n[np.argmin(perf_n)]])
    p2 = np.array([perf_n[np.argmax(perf_n)],
                   cost_n[np.argmax(perf_n)]])
    line = p2 - p1
    line_len = np.linalg.norm(line) or 1e-9
    dists = []
    for i in range(len(front)):
        pt = np.array([perf_n[i], cost_n[i]])
        a, b = line, pt - p1
        d = (a[0] * b[1] - a[1] * b[0]) / line_len
        dists.append(abs(d))
    return int(np.argmax(dists))

This is the maximum-perpendicular-distance heuristic on the perf×cost projection: draw a line from the front's worst-performance endpoint to its best-performance endpoint, find the point farthest from that line. It is the simplest "elbow" detector and it works on most well-shaped fronts.

The published MOSOF uses a 3D measure that also weights reliability — pure 2D heuristics will pick a slightly different knee on the same front. That is a known property; the reference impl reports both, and downstream the canonical Pareto dataset on this site carries the published 3D knee explicitly in metadata so the page renders the right one.

The point of a reference implementation is to make the method legible — not to reproduce the published numbers down to the last decimal.

06 What you get back

Run it. On a laptop, NSGA-II converges in about five seconds. Output:

Candidate pool: 32 sensors across 4 subsystems

Pareto front: 100 non-dominated suites

Knee suite:
  size       = 17 sensors
  performance= 0.767  (normalised NDCI coverage)
  cost       = $50.5k
  MTBF       = 23 kh

Knee composition:
  Engine   2
  Fuel     3
  EPS      5
  ECS      7

Wrote pareto_front.csv (100 suites).

The published runs land closer to 12 sensors at performance ≈ 0.69 (Engine 5, Fuel 2, EPS 2, ECS 3) — that is the canonical knee.3Suslu, Ali, Jennions (2026), Section 4. The 12-sensor knee is the headline result of the cross-subsystem case. The reference impl's knee is larger because the synthetic pool's cost-to-NDCI ratio is more permissive than the published pool's. Tightening that ratio shrinks the knee back. The shape of the front is faithful; the exact knee composition depends on the pool you seed.

07 What this isn't

This is not the production MOSOF code. The production version has a high-fidelity Simulink-based fault simulator behind the NDCI evaluator, validation against thesis Table 4-5 with repeated nested cross-validation, sensor-failure models inside the reliability term, and per-subsystem constraints that the reference impl does not enforce. The papers describe what those add. The point of the reference impl is to make the method legible — what the GA is doing, what the three objectives are, how the knee gets picked — without the reader having to read the simulator first.

If you want to extend it: the most useful next step is replacing the synthetic NDCI per sensor with the real 12×18 matrix from /data/ndci-matrix.json and re-running. The ndci-calculator sibling repo already loads that matrix; lifting its ndci_per_sensor function into MOSOF's fitness gives you a front that lives much closer to the published numbers.

From there: the prior over fault subsystems becomes a real knob (the stakeholder-aware axis), the cost model can pull in per-family installation overheads, and the constraints can encode per-subsystem coverage minima. None of those changes the 100-line core. They change what the 100 lines are scoring.

Endnotes

  1. Suslu, B., Ali, F., & Jennions, I. K. (2025). NDCI Integration to Multi-Objective Sensor Optimisation Framework — An ECS Case. Sensors, 25(9), 2661. DOI: 10.3390/s25092661. ⸻ Suslu, B., Ali, F., & Jennions, I. K. (2026). MOSOF with NDCI: A Cross-Subsystem Evaluation of an Aircraft for an Airline Case Scenario. Sensors, 26(1), 160. DOI: 10.3390/s26010160.
  2. The dataset is on this site at /data/ndci-matrix.json with provenance notes attached. Per the metadata: fault list and sensor list are from thesis Table 4-1 and Section 3.4; per-cell NDCI values are synthesised against the published qualitative rankings because the original CSV is not in the PDF text layer.
  3. Suslu, B., Ali, F., & Jennions, I. K. (2026). Section 4 and Table 4-5. The 12-sensor knee at NDCI 0.69 / $36k / 145 kh MTBF is the headline of the cross-subsystem case.