---
name: blender_particle_systems_basics
description: "Build a Blender particle system on a mesh: add a system, choose EMITTER or HAIR settings, set count, emission frame range, lifetime, initial velocity, physics and effector weights such as gravity, choose a real render path (HALO, OBJECT, COLLECTION, PATH, LINE, or NONE) and a separate viewport display, add children only when needed, and verify the result through the evaluated depsgraph and a scripted audit. Use when a mesh needs emitted particles or hair strands and the settings need to be set, checked, or handed off with evidence."
metadata: { "includeInPrompt": true }
---

# Blender Particle Systems Basics

## Purpose

Put a working particle system on a mesh and prove that the settings are what the record says they are. The work runs in a fixed order: start from a clean mesh, add one system and name it, decide emitter or hair before touching any other setting, set emission and lifetime, set velocity and physics for emitters or length and steps for hair, choose the render path and the viewport display as separate decisions, and check the evaluated system at a stated frame before saving. Particle settings are data on the system. Guessing at them from a viewport view is how counts, lifetimes, and gravity weights drift away from the file.

Scope:
- blender-animation-basics animates a rigged mesh (no particles)
- blender-geometry-nodes-basics generates geometry with node groups (particles are a physics/simulation system, not a node tree)
- blender-sculpting-basics shapes mesh by hand
- headless-blender is render plumbing
- This skill is the stage that builds the particle system itself: one or more systems on a mesh object, each with its own ParticleSettings, a ParticleSystem entry on the object, and a PARTICLE_SYSTEM modifier that appears when the system is added. It sits before any render pass and before any timing pass that animates scene objects around the particles.
- Verification note, 2026-10-22: every operator and property named in this skill was probe-checked in real headless runs of Blender 5.1.1, the version `~/.local/bin/blender --version` returned on the drafting machine, started as `~/.local/bin/blender -b --factory-startup --python script.py`. Operators called and returned FINISHED: `bpy.ops.mesh.primitive_cube_add`, `bpy.ops.mesh.primitive_uv_sphere_add`, `bpy.ops.mesh.primitive_ico_sphere_add`, `bpy.ops.object.particle_system_add`, `bpy.ops.object.particle_system_remove`, `bpy.ops.particle.duplicate_particle_system`, `bpy.ops.particle.particle_edit_toggle`, `bpy.ops.object.mode_set` with PARTICLE_EDIT and OBJECT, `bpy.ops.wm.read_factory_settings`, `bpy.ops.wm.save_as_mainfile`, and `bpy.ops.wm.open_mainfile`. A cube at size 2.0 measured 8 vertices and 6 polygons, an icosphere at subdivisions 2 measured 42 vertices and 80 polygons, and a UV sphere at segments 16 and ring_count 8 measured 114 vertices and 128 polygons. Properties read or set with the values stated in the Workflow: on ParticleSettings, type, count, frame_start, frame_end, lifetime, lifetime_random, emit_from, distribution, physics_type, integrator, normal_factor, object_factor, factor_random, tangent_factor, tangent_phase, object_align_factor, mass, particle_size, size_random, damping, drag_factor, brownian_factor, timestep, subframes, use_rotations, rotation_mode, phase_factor, render_type, instance_object, instance_collection, use_whole_collection, display_method, display_size, display_percentage, line_length_head, line_length_tail, trail_count, path_start, path_end, hair_length, hair_step, display_step, render_step, use_hair_bspline, use_strand_primitive, child_type, child_percent, rendered_child_count, and child_size; on ParticleSystem, name, seed, settings, particles, child_particles, and use_hair_dynamics; and on the settings effector_weights collection, gravity, all, force, and wind, with the full weight list read as gravity, all, force, vortex, magnetic, wind, curve_guide, texture, harmonic, charge, lennardjones, boid, turbulence, drag, and smokeflow, each 1.0 in a factory scene. Enums read in full: type EMITTER and HAIR; emit_from VERT, FACE, and VOLUME; distribution JIT, RAND, and GRID; physics_type NO, NEWTON, KEYED, BOIDS, and FLUID; render_type NONE, HALO, LINE, PATH, OBJECT, and COLLECTION for an emitter. Evaluated checks: an emitter at count 200 held 200 particles in the evaluated depsgraph at frames 1 and 10 while the unevaluated system held 0; an emitter at count 10, frame_start 5.0, frame_end 15.0, and lifetime 30.0 gave its first three evaluated particles at frame 20 birth times 5.0, 6.0, and 7.0, die times 35.0, 36.0, and 37.0, and lifetime 30.0 each; a hair system at count 100 evaluated to 100 particles and its first particle carried 6 hair_keys at hair_step 5; and setting child_type to SIMPLE on a 100 count system gave 1000 evaluated child_particles for both an emitter and a hair system. The audit fixture run in the Operating Rules section returned objects_total: 3, mesh_objects: 3, vertices_total: 164, polygons_total: 214, particle_systems_total: 2, emitter_systems: 1, hair_systems: 1, and verdict: PASS, on two separate audit runs with identical output. Probes that failed, with the fix applied in this skill: setting render_type to HALO or LINE on a HAIR system failed, because the accepted values in that state are NONE, PATH, OBJECT, and COLLECTION, so hair is never assigned HALO or LINE here; setting display_method to DOT, CIRC, CROSS, or AXIS on a HAIR system failed, because the accepted values in that state are NONE, RENDER, and PATH, so hair display is left at RENDER here; `bpy.ops.object.particle_edit_toggle` could not be found, so particle edit mode is entered with `bpy.ops.particle.particle_edit_toggle` instead; `bpy.ops.object.particle_system_add` failed its poll check with no active object, so a mesh is made active first; and assigning type FUR failed, because EMITTER and HAIR are the only type values. Names that do not exist in 5.1.1 and are not used here: ParticleSettings.gravity (gravity is effector_weights.gravity on the settings), ParticleSettings.initial_velocity, ParticleSettings.velocity_factor, and ParticleSettings.render_as. What was NOT verified: no render was run, so no render path is claimed to look correct on screen, and Cycles was not used; viewport playback smoothness, particle motion feel, and collision behavior on screen were not tested; no hair grooming stroke was run, so combing, cutting, and styling results in particle edit mode are untested and only entering and leaving that mode was checked; BOIDS, FLUID, and KEYED physics were set as enum values only, with no behavior check; and GUI panel layout was not exercised headless, so panel locations in a live session may differ from the data names used here.

## Workflow

1. Start from a clean mesh and save a pre-particle copy.
   - Use a mesh with scale and rotation applied. Emission area, hair length, velocity, and particle size are all measured against the object as it sits in the scene, and a scaled emitter makes those numbers hard to repeat on a second object.
   - Reference sizes from the probe runs: `bpy.ops.mesh.primitive_cube_add(size=2.0)` gives 8 vertices and 6 polygons, `bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.25)` gives 42 vertices and 80 polygons, and `bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=8, radius=1.0)` gives 114 vertices and 128 polygons.
   - Pick the emitter surface with the final form in mind. Particles emit from the mesh as it exists when the system runs, so large shape changes after the system is tuned change where particles start and which way surface normals point.
   - Save the mesh with no system as its own file with `bpy.ops.wm.save_as_mainfile`. Adding a system is easy to undo in a session and harder to strip cleanly from a saved file that also holds later work.

2. Add one system, name it, and read the defaults back.
   - Make the mesh the active object, then add the system: in the interface from the particle properties area, in code with `bpy.ops.object.particle_system_add()`. The call failed its poll check with no active object in the probe run, so active object state comes first.
   - Adding a system does three things at once: it adds an entry to `particle_systems` on the object, it creates a ParticleSettings data block, and it adds a modifier of type PARTICLE_SYSTEM to the object. Read all three back before changing anything.
   - Name the system on the ParticleSystem entry, for example EmitterMain or HairMain, and set `seed` on the system when the layout needs to be repeatable. The audit prints system name and seed from the file.
   - Factory defaults read in the probe run for a new emitter: type EMITTER, count 1000, frame_start 1.0, frame_end 200.0, lifetime 50.0, emit_from FACE, distribution JIT, physics_type NEWTON, render_type HALO, display_method RENDER, particle_size 0.05, and display_size 0.1. Any record that claims a tuned system while still holding all of these values is a record of an untuned system.
   - Add a second system only when the mesh needs two behaviors that one set of settings cannot hold, for example short hair and long hair, or sparks and smoke. `bpy.ops.object.particle_system_add()` a second time gave systems named ParticleSystem and ParticleSystem.001 in the probe run. Remove a system with `bpy.ops.object.particle_system_remove()` and duplicate one with `bpy.ops.particle.duplicate_particle_system()`, both of which returned FINISHED in the probe runs.

3. Decide emitter or hair before setting anything else.
   - Set `type` on the ParticleSettings to EMITTER or HAIR. Those are the only two values in 5.1.1. An emitter makes free particles that are born, move, and die inside a frame range. A hair system makes strands rooted on the surface, and its count means roots rather than moving particles.
   - The type choice changes which later settings apply and which enum values are accepted, so it comes before emission, velocity, and render settings. Switching a fresh emitter to HAIR in the probe run changed its render_type from HALO to PATH on its own. Check render_type after any type switch.
   - A settings block can also be made directly with `bpy.data.particles.new(name=...)` and assigned to a system through its `settings` property, which the probe run did with a new block that arrived as type EMITTER at count 1000. Prefer the add operator for normal work, because it creates the system, the settings, and the modifier together.

4. Set emission for an emitter: count, frame range, and lifetime.
   - Set `count` first. Count is the total particle budget for the system. Start small while tuning, for example 200, and raise it only after the motion and render path read correctly, because every later check costs more at a high count.
   - Set the emission window with `frame_start` and `frame_end`. Particles are born across that window. Probe evidence: at count 10, frame_start 5.0, and frame_end 15.0, the first three evaluated particles were born at frames 5.0, 6.0, and 7.0.
   - Set `lifetime` in frames and `lifetime_random` as a 0.0 to 1.0 share of variation. Probe evidence: at lifetime 30.0 and lifetime_random 0.0, each checked particle had die_time equal to birth_time plus 30.0. A system whose frame_end plus lifetime runs past the scene end will still hold live particles when the scene stops, which may or may not be the intent. State the intent in the record.
   - Set `emit_from` to VERT, FACE, or VOLUME, and `distribution` to JIT, RAND, or GRID. FACE with JIT is the factory pairing and suits a surface emitter. VOLUME fills the inside of a closed mesh. GRID places particles on a grid pattern and pairs with VOLUME work.
   - Leave even spacing behavior at its factory state while learning a system. The settings carry flags such as use_even_distribution and use_emit_random, both read as on in a factory scene. Change them one at a time and note each change, because distribution changes move every particle at once.

5. Set initial velocity for an emitter.
   - There is no single velocity property on ParticleSettings in 5.1.1. Initial velocity is built from parts: `normal_factor` pushes particles along the surface normal, `tangent_factor` and `tangent_phase` push along the surface tangent, `object_factor` pushes along object motion, and `object_align_factor` is a vector that aligns velocity to chosen object axes.
   - Factory values read in the probe run: normal_factor 1.0, tangent_factor 0.0, tangent_phase 0.0, object_factor 0.0, and object_align_factor (0.0, 0.0, 0.0). A normal_factor of 2.0 with factor_random 0.3 is the worked emitter setting in the audit fixture.
   - Add spread with `factor_random`, a 0.0 to 1.0 share. Random spread at zero gives a clean, even stream that shows the base direction clearly. Raise it after the base direction is right, or direction errors and spread errors become hard to tell apart.
   - Set one velocity part at a time and check the evaluated system at a frame after birth, using the check in step 10. Velocity claims from settings alone, with no evaluated check, are how a system set to emit inward gets recorded as emitting outward.

6. Set physics and effector weights for an emitter.
   - Keep `physics_type` at NEWTON for standard moving particles. The full enum is NO, NEWTON, KEYED, BOIDS, and FLUID. NO gives particles with no physics step. KEYED, BOIDS, and FLUID were set as enum values in the probe runs only, and this skill makes no behavior claim for them.
   - Core NEWTON settings: `mass` (1.0 in a factory scene), `particle_size` and `size_random`, `damping`, `drag_factor`, and `brownian_factor` for random jitter. Each was read and set in the probe runs. Raise brownian_factor only when jitter is wanted, because it reads as noise in every other case.
   - Integration settings: `integrator` takes EULER, VERLET, MIDPOINT, and RK4, with MIDPOINT as the factory value read in the probe run, plus `timestep` (0.04 in a factory scene, read as 0.03999999910593033) and `subframes`. Leave these at factory values until a fast particle tunnels or a curve bends wrong, then change timestep or subframes and record the change.
   - Gravity lives on the effector weights, as `effector_weights.gravity` on the settings. There is no gravity property directly on ParticleSettings. Factory gravity weight is 1.0. The audit fixture uses 0.5 for a slower fall. Setting gravity weight to 0.0 removes gravity pull for that system while leaving other systems alone.
   - The same weights collection carries one weight per effector kind: all, force, vortex, magnetic, wind, curve_guide, texture, harmonic, charge, lennardjones, boid, turbulence, drag, and smokeflow, each 1.0 in a factory scene. The `all` weight scales the group. Use the single kind weight when one effect should weaken and the rest should stay, for example effector_weights.wind at 0.7 while gravity stays at 1.0.
   - Rotations are off by default. Turn them on with `use_rotations` when the render path shows an object whose facing matters, then set `rotation_mode` (VEL is one value, aligning rotation to velocity) and `phase_factor`. Rotation settings on a HALO system have no visible object to turn, so pair this step with the render choice in step 7.

7. Choose the render path and the viewport display as separate decisions.
   - `render_type` on the settings decides what a render draws for an emitter. The full emitter enum is NONE, HALO, LINE, PATH, OBJECT, and COLLECTION. NONE draws nothing in a render while the system still simulates, which suits a system kept as source data for another step.
   - HALO draws each particle as a soft point sized by particle_size. It is the factory path and the fastest way to check emission and motion. No instance object is needed.
   - OBJECT draws `instance_object` at each particle. Set render_type to OBJECT first, then assign a real mesh object to instance_object, as the audit fixture does with a small icosphere. An OBJECT system with no instance_object assigned has nothing to draw, so the audit prints instance_object for every system.
   - COLLECTION draws objects from `instance_collection`. The `use_whole_collection` flag, off in a factory scene, decides whether the whole collection appears at each particle or its objects are spread across particles. State which behavior the record intends.
   - LINE draws a short line per particle, using `line_length_head` and `line_length_tail` (0.0 and 0.5 in a factory scene) and `trail_count`. PATH draws a path per particle using `path_start` and `path_end` (0.0 and 1.0 in a factory scene). Both suit motion trails and streak effects.
   - `display_method` decides what the viewport draws, and it is a separate setting from render_type. For an emitter it takes NONE, RENDER, DOT, CIRC, CROSS, and AXIS. RENDER shows the render path in the viewport. DOT, CIRC, CROSS, and AXIS show cheap markers that keep a dense system readable while tuning. `display_size` (0.1 in a factory scene) sizes those markers, and `display_percentage` (100 in a factory scene) shares how many particles the viewport shows.
   - No render was run while drafting this skill, per the verification note. The claims in this step are about which settings exist, which values they accept, and what each value names, checked headless. How a chosen path looks in a finished frame is checked in the render stage, and that stage is headless-blender work.

8. Build a hair system: roots, length, and steps.
   - Set type to HAIR, then set `count` as the number of roots on the surface. The audit fixture uses 500 roots on a UV sphere. Root placement follows the same `emit_from` values as emitters: VERT, FACE, or VOLUME, with FACE as the usual surface choice.
   - Set `hair_length` for strand length (4.0 in a factory scene, 1.5 in the audit fixture) and `hair_step` for the number of steps along each strand. Probe evidence for what a step means in data: at hair_step 5, the first evaluated hair particle carried 6 hair_keys, one key at the root and one at each step end.
   - Hair render paths are narrower than emitter paths. In the HAIR state, render_type accepts NONE, PATH, OBJECT, and COLLECTION, and assigning HALO or LINE failed in the probe run. PATH is the strand path, and it is what a type switch selects on its own. OBJECT and COLLECTION instance geometry along or at the strands instead.
   - Hair display is narrower too. In the HAIR state, display_method accepts NONE, RENDER, and PATH, and assigning DOT, CIRC, CROSS, or AXIS failed in the probe run. Leave hair display at RENDER while tuning settings headless.
   - Strand rendering detail settings read in the probe run: `display_step` 2 and `render_step` 3 in a factory scene, plus flags `use_hair_bspline` and `use_strand_primitive`, both off in a factory scene. Raise render_step for smoother strand curves in the render stage and record the value, because display_step and render_step can differ and a viewport check at display_step does not prove the render state.
   - Hair dynamics is a switch on the system, `use_hair_dynamics` on the ParticleSystem, off by default. Turning it on in the probe run created the cloth data the system uses for dynamics, read back as a non-empty `cloth` property on the system. This skill sets the switch and records it. Dynamics behavior on screen was not verified.

9. Enter particle edit mode for hair only when hand work is required.
   - Particle edit mode is entered with `bpy.ops.particle.particle_edit_toggle()`, which returned FINISHED in the probe run and left the object in PARTICLE_EDIT mode, as did `bpy.ops.object.mode_set(mode='PARTICLE_EDIT')`. Leave with mode OBJECT the same way.
   - The object level name for this toggle does not exist in 5.1.1, per the verification note. Scripts that call `bpy.ops.object.particle_edit_toggle` fail, so use the particle level call in any scripted step.
   - This skill claims mode entry and exit only. No grooming stroke was run in the probe work, so comb direction, strand cutting, and length editing by hand are live session work, checked on screen, and the record must say so when a deliverable depends on them.
   - Check `is_edited` on the system in the record when a file has passed through particle edit mode. It is a property on the ParticleSystem, read as False on an unedited system in the probe runs, and it tells the next stage whether hand edits exist in the file at all.

10. Add children only when the base system reads correctly.
    - Children multiply a system without raising its root count. `child_type` takes NONE, SIMPLE, and INTERPOLATED, with NONE as the factory value. SIMPLE places children around each parent. INTERPOLATED spreads children across the surface between parents.
    - Probe evidence for scale: setting child_type to SIMPLE on a system at count 100 gave 1000 evaluated child_particles, for both an emitter and a hair system at factory child settings. Children are a tenfold viewport and render cost decision at those settings, so they come after the base system is right, never before.
    - Child settings read in the probe run: `child_percent` 10, `rendered_child_count` 100, `child_size` 1.0, and `child_size_random` 0.0 in a factory scene. The audit script in this skill does not print child settings, so a deliverable that uses children records these values in its settings record by hand, from the settings data.
    - Hair children carry the clump and roughness settings named in the settings data, including clump_factor and the roughness values. Those were read as properties during drafting and were not behavior tested. Keep them at factory values in a first hair deliverable and say so in the limits.

11. Check the evaluated system, save in versions, and hand off.
    - Never judge a system from the unevaluated data. `particles` on an unevaluated ParticleSystem held 0 entries in the probe run for a system that held 200 in the evaluated depsgraph. Build the evaluated view in code by getting the evaluated object from `bpy.context.evaluated_depsgraph_get()` and reading its particle system, and set the frame first with `bpy.context.scene.frame_set(frame)`.
    - In the evaluated system, each particle carries location, velocity, size, birth_time, die_time, lifetime, alive_state, is_exist, and is_visible, all read on the first evaluated particle in the probe run. Use birth_time, die_time, and alive_state to prove the emission window and lifetime from step 4, rather than restating the settings as proof.
    - For hair, read the evaluated particles for root count and read hair_keys on a strand to prove hair_step produced the keys step 8 promised. Root count in the evaluated system matched count in the hair probe run, 100 for 100.
    - Save numbered versions at each boundary: system added, emission tuned, render path chosen, children added, final. Use `bpy.ops.wm.save_as_mainfile` with a new file name each time. A particle file with the wrong gravity weight or seed is recovered from the previous version, not by retuning from memory.
    - Run the audit from the Operating Rules on the saved final file before calling the work done. The audit prints per object counts, one block per system with type, count, seed, render path, and instance object, the emitter timing and gravity lines, and the hair length and step line, all read from the file itself.
    - Hand off to headless-blender for any render check of the chosen render path, with the frame or frame range stated. Hand timing work that moves other scene objects to blender-animation-basics with the emission window and lifetime stated, because animated emitters and animated targets both depend on those numbers. State in the handoff whether children are on, whether hair dynamics is on, and whether any hair was hand edited.

## Output Contract

Deliver these items, in this order:

1. Final particle file: the saved .blend holding the finished systems, named as the final version, with the mesh objects that carry the systems, any instance object or collection the render path needs, and no test objects left in the scene.
2. System record: one short record per particle system, with its object, system name, type, seed, and count, plus for an emitter its frame_start, frame_end, lifetime, and lifetime_random, and for hair its hair_length and hair_step.
3. Checker output: the full output of particle_audit.py for the final file. On the worked fixture in the Operating Rules section the headline numbers are: objects_total: 3, mesh_objects: 3, vertices_total: 164, polygons_total: 214, particle_systems_total: 2, emitter_systems: 1, hair_systems: 1, verdict: PASS.
4. Physics and velocity record: for each emitter, the velocity parts set (normal_factor, tangent_factor and tangent_phase, object_factor, factor_random), physics_type, mass, particle_size, and every effector weight changed from 1.0, starting with effector_weights.gravity.
5. Render record: render_type per system, the instance_object name or instance_collection name when the path needs one, display_method and display_percentage used while tuning, and a plain statement that no render result is included unless a separate render stage produced one.
6. Evaluated check: the frame checked, the evaluated particle count read at that frame, and for one emitter particle its birth_time, die_time, and lifetime, or for hair the evaluated root count and the hair_keys count on the first strand.
7. Limits: anything the work did not cover, stated plainly (for example hair grooming done by hand and checked only on screen, a physics type set as an enum with no behavior check, children left at factory settings, or a render path chosen but not yet rendered).

## Operating Rules

1. Type first, always. EMITTER or HAIR is set before emission, velocity, physics, or render settings, because the type decides which of those settings exist and which enum values the render and display settings accept.
2. One system, one job. A second behavior gets a second system with its own settings and its own name, rather than a compromise set of settings on one system.
3. Every number in the deliverable comes from the settings data, the system data, the evaluated depsgraph, or the audit output. Counts, lifetimes, seeds, and weights are read, not estimated from how dense a viewport view looks.
4. Render and display stay separate in every record. A system tuned while displayed as DOT markers and rendered as OBJECT instances has two states, and the record names both.
5. Gravity is set on effector_weights, never as a direct settings property, because no such property exists in 5.1.1. Scripts that assign settings.gravity fail, and the failed list in the verification note is binding for scripted steps.
6. Children and hair dynamics are stage boundaries. Save before turning either on, record the change, and check the evaluated system after, because both multiply cost and change behavior at once.
7. The unevaluated system proves nothing. Any claim about how many particles exist, when they are born, or when they die comes from the evaluated system at a stated frame.
8. Use the checker below for the state checks. Save it as particle_audit.py, run it inside Blender as its header describes, and put its full output in the deliverable as item 3 of the Output Contract. Report numbers from its output, not from the viewport.

### Particle audit (particle_audit.py)

Run: `~/.local/bin/blender -b --factory-startup --python particle_audit.py -- --make-fixture particle_fixture.blend` builds the worked fixture blend file. Then run: `~/.local/bin/blender -b --factory-startup --python particle_audit.py -- particle_fixture.blend` to reproduce the output below. Run it on a real particle file with `~/.local/bin/blender -b --factory-startup --python particle_audit.py -- FILE.blend`. Blender prints its own banner and a read line before the script output. The block below is the script output only.

Expected output for the fixture run:

```text
fixture_file: particle_fixture.blend
objects_total: 3
mesh_objects: 3
OBJECT EmitterCube verts=8 polys=6 particle_systems=1
OBJECT HairHead verts=114 polys=128 particle_systems=1
OBJECT ParticleInstance verts=42 polys=80 particle_systems=0
PARTICLE object=EmitterCube system=EmitterMain type=EMITTER count=200 seed=7 render_type=OBJECT instance_object=ParticleInstance
EMITTER object=EmitterCube frame_start=1.0 frame_end=80.0 lifetime=40.0 lifetime_random=0.25 emit_from=FACE distribution=JIT physics_type=NEWTON normal_factor=2.0 factor_random=0.3 particle_size=0.08 gravity=0.5
PARTICLE object=HairHead system=HairMain type=HAIR count=500 seed=3 render_type=PATH instance_object=-
HAIR object=HairHead hair_length=1.5 hair_step=5 emit_from=FACE child_type=NONE
vertices_total: 164
polygons_total: 214
particle_systems_total: 2
emitter_systems: 1
hair_systems: 1
verdict: PASS
```

The fixture is built to show each audit result once: three mesh objects, one emitter system rendering a small icosphere through the OBJECT path at gravity weight 0.5, and one hair system at 500 roots with hair_length 1.5 and hair_step 5 rendering through PATH, which gives vertices_total: 164, polygons_total: 214, and a verdict of PASS.

particle_audit.py (runs inside Blender, uses bpy). Save this code as particle_audit.py:

```python
#!/usr/bin/env python3
"""Audit particle system state in a Blender scene.
Runs inside Blender, not in plain Python:
  blender -b --factory-startup --python particle_audit.py -- --make-fixture particle_fixture.blend
  blender -b --factory-startup --python particle_audit.py -- particle_fixture.blend
The --make-fixture form builds a fixture with three mesh objects:
ParticleInstance (icosphere, the emitter render instance),
EmitterCube (cube, one EMITTER system), and HairHead (UV sphere,
one HAIR system), and prints two fixture lines. The audit form
opens the named blend, prints one line per mesh object sorted by
name, one block per particle system, summary counts, and a
verdict. PASS means exactly the fixture shape: three mesh
objects, two systems, one emitter and one hair. Any other shape
prints REVIEW. Blender banner lines are not script output.
"""
import sys
import bpy

def build_fixture(path):
    bpy.ops.wm.read_factory_settings(use_empty=True)
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.25)
    inst = bpy.context.active_object
    inst.name = "ParticleInstance"
    bpy.ops.mesh.primitive_cube_add(size=2.0)
    emitter = bpy.context.active_object
    emitter.name = "EmitterCube"
    bpy.ops.object.particle_system_add()
    eps = emitter.particle_systems[0]
    eps.name = "EmitterMain"
    eps.seed = 7
    es = eps.settings
    es.type = 'EMITTER'
    es.count = 200
    es.frame_start = 1.0
    es.frame_end = 80.0
    es.lifetime = 40.0
    es.lifetime_random = 0.25
    es.emit_from = 'FACE'
    es.distribution = 'JIT'
    es.physics_type = 'NEWTON'
    es.normal_factor = 2.0
    es.factor_random = 0.3
    es.particle_size = 0.08
    es.render_type = 'OBJECT'
    es.instance_object = inst
    es.effector_weights.gravity = 0.5
    bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=8, radius=1.0)
    hair_obj = bpy.context.active_object
    hair_obj.name = "HairHead"
    bpy.ops.object.particle_system_add()
    hps = hair_obj.particle_systems[0]
    hps.name = "HairMain"
    hps.seed = 3
    hs = hps.settings
    hs.type = 'HAIR'
    hs.count = 500
    hs.hair_length = 1.5
    hs.hair_step = 5
    hs.emit_from = 'FACE'
    hs.render_type = 'PATH'
    bpy.ops.wm.save_as_mainfile(filepath=path)
    print(f"fixture_file: {path}")
    print("fixture_objects: 3")
    return 0

def audit(path):
    bpy.ops.wm.open_mainfile(filepath=path)
    meshes = sorted((ob for ob in bpy.data.objects if ob.type == 'MESH'), key=lambda ob: ob.name)
    print(f"fixture_file: {path}")
    print(f"objects_total: {len(bpy.data.objects)}")
    print(f"mesh_objects: {len(meshes)}")
    verts_total = 0
    polys_total = 0
    rows = []
    for ob in meshes:
        verts = len(ob.data.vertices)
        polys = len(ob.data.polygons)
        verts_total += verts
        polys_total += polys
        print(f"OBJECT {ob.name} verts={verts} polys={polys} particle_systems={len(ob.particle_systems)}")
        for ps in ob.particle_systems:
            rows.append((ob.name, ps))
    systems_total = 0
    emitter_systems = 0
    hair_systems = 0
    for ob_name, ps in sorted(rows, key=lambda row: (row[0], row[1].name)):
        s = ps.settings
        systems_total += 1
        if s.type == 'EMITTER':
            emitter_systems += 1
        if s.type == 'HAIR':
            hair_systems += 1
        inst_name = s.instance_object.name if s.instance_object else "-"
        print(f"PARTICLE object={ob_name} system={ps.name} type={s.type} count={s.count} seed={ps.seed} render_type={s.render_type} instance_object={inst_name}")
        if s.type == 'EMITTER':
            print(f"EMITTER object={ob_name} frame_start={s.frame_start:.1f} frame_end={s.frame_end:.1f} lifetime={s.lifetime:.1f} lifetime_random={s.lifetime_random:.2f} emit_from={s.emit_from} distribution={s.distribution} physics_type={s.physics_type} normal_factor={s.normal_factor:.1f} factor_random={s.factor_random:.1f} particle_size={s.particle_size:.2f} gravity={s.effector_weights.gravity:.1f}")
        if s.type == 'HAIR':
            print(f"HAIR object={ob_name} hair_length={s.hair_length:.1f} hair_step={s.hair_step} emit_from={s.emit_from} child_type={s.child_type}")
    print(f"vertices_total: {verts_total}")
    print(f"polygons_total: {polys_total}")
    print(f"particle_systems_total: {systems_total}")
    print(f"emitter_systems: {emitter_systems}")
    print(f"hair_systems: {hair_systems}")
    ok = len(meshes) == 3 and systems_total == 2 and emitter_systems == 1 and hair_systems == 1
    print(f"verdict: {'PASS' if ok else 'REVIEW'}")
    return 0

def main():
    args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
    if len(args) == 2 and args[0] == "--make-fixture":
        return build_fixture(args[1])
    if len(args) == 1:
        return audit(args[0])
    print("usage: particle_audit.py -- [--make-fixture FILE.blend | FILE.blend]")
    return 2

if __name__ == "__main__":
    raise SystemExit(main())
```
