Skip to content

Describe an Instrument

An NBO Instrument is the hardware description of a microscope: the stand it is built on, the objectives mounted on it, the detectors that record, and the sources that illuminate.

Prerequisites

This example builds on the accessor patterns of Object Accessors and on the substitution mechanism of Explore the Object Hierarchy.

Overview

Instrument groups its content into lists, one per hardware family. The list states the family, the class you add states the hardware.

flowchart LR
    classDef root   fill:#00a8cc,stroke:#00a8cc,stroke-width:4px,color:#fff,font-size:13px,font-weight:bold
    classDef family fill:#fff,stroke:#00a8cc,stroke-width:1.5px,color:#222,font-size:12px

    I["Instrument"]:::root
    S["MicroscopeStandGroup<br/>upright · inverted"]:::family
    O["Objective"]:::family
    D["DetectorGroup<br/>CCD · CMOS · point detector"]:::family
    L["LightSourceGroup<br/>laser · LED · arc · filament"]:::family

    I -- "addToMicroscopeStandGroup_List" --> S
    I -- "addToObjective_List" --> O
    I -- "addToDetectorGroup_List" --> D
    I -- "addToLightSourceGroup_List" --> L

    linkStyle 0 stroke:#00a8cc,stroke-width:1.5px
    linkStyle 1 stroke:#00a8cc,stroke-width:1.5px
    linkStyle 2 stroke:#00a8cc,stroke-width:1.5px
    linkStyle 3 stroke:#00a8cc,stroke-width:1.5px
Accessor Family Concrete classes
addToMicroscopeStandGroup_List() Stand UprightMicroscopeStand, InvertedMicroscopeStand
addToObjective_List() Objective Objective
addToDetectorGroup_List() Detector CCD, CMOS, IntensifiedCamera, PointDetector_Type, GenericDetector
addToLightSourceGroup_List() Light source Laser, LightEmittingDiode, Arc, Filament, and other concrete sources
addToFilterGroup_List() Filter EmissionFilter, ExcitationFilter, DichroicMirror, and related optics
addToLensGroup_List() Lens TubeLens, RelayLens, and other lenses
addToSamplePositioningGroup_List() Sample positioning Stages, inserts, and holders

A list whose name ends in Group is backed by an abstract type, so it accepts any concrete class of that family: a CMOS is accepted by the detector group because CMOS derives from Camera_Type, which derives from Detector_Type. Call getChildren() on the abstract type to list what a family accepts.

The schema also asks for more than an identifier: twelve required attributes on Objective, seven on Detector, and Manufacturer, Model, and CatalogNumber on everything deriving from ManufacturerSpec. Export validates the document, so a missing attribute names itself instead of producing a partial description. This example sets each of them.

Note

This example describes an inverted Nikon Ti2 with two objectives, one CMOS camera, and one laser line. Replace the manufacturers, models, and values with those of your instrument. Attributes beyond the required ones are optional: describe what you know, and leave the rest unset.

Assemble a full instrument description

# Step 1: Create the instrument and identify it
instrument = inscoper_nbo.Instrument()

instrument_id = inscoper_nbo.InstrumentID_Type()
instrument_id.set("Instrument:1")
instrument.setID(instrument_id)

instrument_name = inscoper_nbo.Denomination_Type()
instrument_name.set("Nikon Ti2 - Room 214")
instrument.setName(instrument_name)

# Step 2: Add the microscope stand
# MicroscopeStand is abstract: the concrete class states the geometry of the
# stand, here an inverted one.
stand = inscoper_nbo.InvertedMicroscopeStand()

stand_id = inscoper_nbo.LSID_Type()
stand_id.set("MicroscopeStand:1")
stand.setID(stand_id)

stand.setManufacturer("Nikon")
stand.setModel("Eclipse Ti2-E")
stand.setCatalogNumber("Ti2-E")

stand_type = inscoper_nbo.MicroscopeType_Type()
stand_type.set("Compound")
stand.setType(stand_type)

instrument.addToMicroscopeStandGroup_List(stand)

# Step 3: Add the objectives
# Objective declares twelve required attributes, so a compliant description of a
# lens states its optical characteristics in full.
for index, (model, catalog_number, magnification, na, immersion, working_distance) in enumerate(
    [
        ("CFI Plan Apo Lambda 20x", "MRD00205", 20.0, 0.75, "Air", 1.0),
        ("CFI Plan Apo Lambda 60x", "MRD01605", 60.0, 1.4, "Mineral Oil", 0.13),
    ],
    start=1,
):
    objective = inscoper_nbo.Objective()

    objective_id = inscoper_nbo.ObjectiveID_Type()
    objective_id.set(f"Objective:{index}")
    objective.setID(objective_id)

    objective.setManufacturer("Nikon")
    objective.setModel(model)
    objective.setCatalogNumber(catalog_number)

    objective.setMagnification(magnification)
    objective.setLensNA(na)
    objective.setInfinityCorrected(True)
    objective.setDIC(False)
    objective.setCorrectionCollar(False)
    objective.setWorkingDistance(working_distance)
    objective.setObjectiveViewField(25.0)
    objective.setImageDistance(200.0)

    correction = inscoper_nbo.ObjectiveCorrection_Type()
    correction.set("Apochromat")
    objective.setCorrection(correction)

    immersion_type = inscoper_nbo.ImmersionTypeList_Type()
    immersion_type.set(immersion)
    objective.setImmersionType(immersion_type)

    contrast_modulation = inscoper_nbo.ContrastModulationPlate_Type()
    contrast_modulation.set("None")
    objective.setContrastModulation(contrast_modulation)

    # Lengths carry their unit as a separate attribute
    millimeter = inscoper_nbo.UnitsLength_Type()
    millimeter.set("mm")
    objective.setWorkingDistanceUnit(millimeter)

    instrument.addToObjective_List(objective)

# Step 4: Add the detector
# CMOS derives from Camera, which derives from the abstract Detector, so a CMOS
# instance is accepted by the detector group of the instrument.
camera = inscoper_nbo.CMOS()

camera_id = inscoper_nbo.DetectorID_Type()
camera_id.set("Detector:1")
camera.setID(camera_id)

camera.setManufacturer("Hamamatsu")
camera.setModel("ORCA-Fusion BT")
camera.setCatalogNumber("C15440-20UP")

camera.setPixelWidth(6.5)
camera.setPixelHeight(6.5)
camera.setArrayWidth(2304)
camera.setArrayHeight(2304)
camera.setPixelWellCapacity(15000)
camera.setMaximumFrameRate(89.1)
camera.setMaximumReadoutRate(100.0)
camera.setQuantumEfficiency(0.8)
camera.setElectronicConversionFactor(0.24)
camera.setReadOutNoise(0.7)
camera.setDarkCurrentRate(0.06)

noise_model = inscoper_nbo.DetectorNoiseModel_Type()
noise_model.set("Gaussian")
camera.setDetectorNoiseModel(noise_model)

illumination = inscoper_nbo.CameraIllumination_Type()
illumination.set("Back")
camera.setIllumination(illumination)

max_bit_depth = inscoper_nbo.DigitizerType_Type()
max_bit_depth.set("16bit")
camera.setMaxBitDepth(max_bit_depth)

# Detectors are characterized over one or more spectral bands
wavelength_range = inscoper_nbo.Detector_Type_InlineWavelengthRange()
cut_on = inscoper_nbo.PositiveFloat_Type()
cut_on.set(400.0)
cut_off = inscoper_nbo.PositiveFloat_Type()
cut_off.set(700.0)
wavelength_range.setCutOn(cut_on)
wavelength_range.setCutOff(cut_off)
wavelength_range.setWavelengthProfile("orca_fusion_qe.csv")
wavelength_range.setPeakWavelength(560.0)
camera.addToWavelengthRange_List(wavelength_range)

instrument.addToDetectorGroup_List(camera)

# Step 5: Add the light source
# LightSource is abstract as well; a Laser is one of its concrete forms.
laser = inscoper_nbo.Laser()

laser_id = inscoper_nbo.LightSourceID_Type()
laser_id.set("LightSource:1")
laser.setID(laser_id)

laser.setManufacturer("Coherent")
laser.setModel("OBIS 488")
laser.setCatalogNumber("1185053")

laser.setTuneable(False)
laser.setPulse(False)
laser.setIsPump(False)
laser.setIsPumped(False)

laser_type = inscoper_nbo.LaserType_Type()
laser_type.set("SemiconductorLaserDiode")
laser.setType(laser_type)

modulation = inscoper_nbo.LaserModulation_Type()
modulation.set("Direct")
laser.setModulationMechanism(modulation)

illumination_range = inscoper_nbo.IlluminationWavelengthRange()
laser_cut_on = inscoper_nbo.PositiveFloat_Type()
laser_cut_on.set(486.0)
laser_cut_off = inscoper_nbo.PositiveFloat_Type()
laser_cut_off.set(490.0)
illumination_range.setCutOn(laser_cut_on)
illumination_range.setCutOff(laser_cut_off)
illumination_range.setPeakWavelength(488.0)
illumination_range.setIlluminationPower(60.0)
laser.addToIlluminationWavelengthRange_List(illumination_range)

instrument.addToLightSourceGroup_List(laser)

# Step 6: Export the description
output_path = os.path.join(tempfile.gettempdir(), "instrument.xml")
instrument.toXmlFile(output_path)
print(f"Instrument description written to {output_path}")

# Step 7: Read the description back through the model
print(f"Objectives: {len(instrument.getObjective_List())}")
for objective in instrument.getObjective_List():
    print(f"  {objective.getModel()} - {objective.getMagnification()}x NA {objective.getLensNA()}")

for detector in instrument.getDetectorGroup_List():
    print(f"Detector: {detector.getManufacturer()} {detector.getModel()}")

for light_source in instrument.getLightSourceGroup_List():
    print(f"Light source: {light_source.getManufacturer()} {light_source.getModel()}")

int main() {
    // Step 1: Create the instrument and identify it
    NBO::Instrument instrument;

    NBO::InstrumentID_TypePtr instrumentId = std::make_shared<NBO::InstrumentID_Type>();
    instrumentId->set("Instrument:1");
    instrument.setID(instrumentId);

    NBO::Denomination_TypePtr instrumentName = std::make_shared<NBO::Denomination_Type>();
    instrumentName->set("Nikon Ti2 - Room 214");
    instrument.setName(instrumentName);

    // Step 2: Add the microscope stand
    // MicroscopeStand is abstract: the concrete class states the geometry of the
    // stand, here an inverted one.
    NBO::InvertedMicroscopeStandPtr stand = std::make_shared<NBO::InvertedMicroscopeStand>();

    NBO::LSID_TypePtr standId = std::make_shared<NBO::LSID_Type>();
    standId->set("MicroscopeStand:1");
    stand->setID(standId);

    stand->setManufacturer("Nikon");
    stand->setModel("Eclipse Ti2-E");
    stand->setCatalogNumber("Ti2-E");

    NBO::MicroscopeType_TypePtr standType = std::make_shared<NBO::MicroscopeType_Type>();
    standType->set("Compound");
    stand->setType(standType);

    instrument.addToMicroscopeStandGroup_List(stand);

    // Step 3: Add the objectives
    // Objective declares twelve required attributes, so a compliant description of
    // a lens states its optical characteristics in full.
    struct ObjectiveSpec {
        std::string model;
        std::string catalogNumber;
        float magnification;
        float lensNA;
        std::string immersion;
        float workingDistance;
    };

    const std::vector<ObjectiveSpec> objectiveSpecs = {
        {"CFI Plan Apo Lambda 20x", "MRD00205", 20.0f, 0.75f, "Air", 1.0f},
        {"CFI Plan Apo Lambda 60x", "MRD01605", 60.0f, 1.4f, "Mineral Oil", 0.13f},
    };

    int index = 1;
    for (const auto &spec : objectiveSpecs) {
        NBO::ObjectivePtr objective = std::make_shared<NBO::Objective>();

        NBO::ObjectiveID_TypePtr objectiveId = std::make_shared<NBO::ObjectiveID_Type>();
        objectiveId->set("Objective:" + std::to_string(index));
        objective->setID(objectiveId);

        objective->setManufacturer("Nikon");
        objective->setModel(spec.model);
        objective->setCatalogNumber(spec.catalogNumber);

        objective->setMagnification(spec.magnification);
        objective->setLensNA(spec.lensNA);
        objective->setInfinityCorrected(true);
        objective->setDIC(false);
        objective->setCorrectionCollar(false);
        objective->setWorkingDistance(spec.workingDistance);
        objective->setObjectiveViewField(25.0f);
        objective->setImageDistance(200.0f);

        NBO::ObjectiveCorrection_TypePtr correction =
            std::make_shared<NBO::ObjectiveCorrection_Type>();
        correction->set("Apochromat");
        objective->setCorrection(correction);

        NBO::ImmersionTypeList_TypePtr immersionType =
            std::make_shared<NBO::ImmersionTypeList_Type>();
        immersionType->set(spec.immersion);
        objective->setImmersionType(immersionType);

        NBO::ContrastModulationPlate_TypePtr contrastModulation =
            std::make_shared<NBO::ContrastModulationPlate_Type>();
        contrastModulation->set("None");
        objective->setContrastModulation(contrastModulation);

        // Lengths carry their unit as a separate attribute
        NBO::UnitsLength_TypePtr millimeter = std::make_shared<NBO::UnitsLength_Type>();
        millimeter->set("mm");
        objective->setWorkingDistanceUnit(millimeter);

        instrument.addToObjective_List(objective);
        ++index;
    }

    // Step 4: Add the detector
    // CMOS derives from Camera, which derives from the abstract Detector, so a CMOS
    // instance is accepted by the detector group of the instrument.
    NBO::CMOSPtr camera = std::make_shared<NBO::CMOS>();

    NBO::DetectorID_TypePtr cameraId = std::make_shared<NBO::DetectorID_Type>();
    cameraId->set("Detector:1");
    camera->setID(cameraId);

    camera->setManufacturer("Hamamatsu");
    camera->setModel("ORCA-Fusion BT");
    camera->setCatalogNumber("C15440-20UP");

    camera->setPixelWidth(6.5f);
    camera->setPixelHeight(6.5f);
    camera->setArrayWidth(2304);
    camera->setArrayHeight(2304);
    camera->setPixelWellCapacity(15000);
    camera->setMaximumFrameRate(89.1f);
    camera->setMaximumReadoutRate(100.0f);
    camera->setQuantumEfficiency(0.8f);
    camera->setElectronicConversionFactor(0.24f);
    camera->setReadOutNoise(0.7f);
    camera->setDarkCurrentRate(0.06f);

    NBO::DetectorNoiseModel_TypePtr noiseModel = std::make_shared<NBO::DetectorNoiseModel_Type>();
    noiseModel->set("Gaussian");
    camera->setDetectorNoiseModel(noiseModel);

    NBO::CameraIllumination_TypePtr illumination =
        std::make_shared<NBO::CameraIllumination_Type>();
    illumination->set("Back");
    camera->setIllumination(illumination);

    NBO::DigitizerType_TypePtr maxBitDepth = std::make_shared<NBO::DigitizerType_Type>();
    maxBitDepth->set("16bit");
    camera->setMaxBitDepth(maxBitDepth);

    // Detectors are characterized over one or more spectral bands. WavelengthRange is
    // declared inside Detector_Type, so in C++ it is the nested class
    // Detector_Type::InlineWavelengthRange.
    NBO::Detector_Type::InlineWavelengthRangePtr wavelengthRange =
        std::make_shared<NBO::Detector_Type::InlineWavelengthRange>();
    NBO::PositiveFloat_TypePtr cutOn = std::make_shared<NBO::PositiveFloat_Type>();
    cutOn->set(400.0f);
    NBO::PositiveFloat_TypePtr cutOff = std::make_shared<NBO::PositiveFloat_Type>();
    cutOff->set(700.0f);
    wavelengthRange->setCutOn(cutOn);
    wavelengthRange->setCutOff(cutOff);
    wavelengthRange->setPeakWavelength(560.0f);
    wavelengthRange->setWavelengthProfile("orca_fusion_qe.csv");
    camera->addToWavelengthRange_List(wavelengthRange);

    instrument.addToDetectorGroup_List(camera);

    // Step 5: Add the light source
    // LightSource is abstract as well; a Laser is one of its concrete forms.
    NBO::LaserPtr laser = std::make_shared<NBO::Laser>();

    NBO::LightSourceID_TypePtr laserId = std::make_shared<NBO::LightSourceID_Type>();
    laserId->set("LightSource:1");
    laser->setID(laserId);

    laser->setManufacturer("Coherent");
    laser->setModel("OBIS 488");
    laser->setCatalogNumber("1185053");

    laser->setTuneable(false);
    laser->setPulse(false);
    laser->setIsPump(false);
    laser->setIsPumped(false);

    NBO::LaserType_TypePtr laserType = std::make_shared<NBO::LaserType_Type>();
    laserType->set("SemiconductorLaserDiode");
    laser->setType(laserType);

    NBO::LaserModulation_TypePtr modulation = std::make_shared<NBO::LaserModulation_Type>();
    modulation->set("Direct");
    laser->setModulationMechanism(modulation);

    NBO::IlluminationWavelengthRangePtr illuminationRange =
        std::make_shared<NBO::IlluminationWavelengthRange>();
    NBO::PositiveFloat_TypePtr laserCutOn = std::make_shared<NBO::PositiveFloat_Type>();
    laserCutOn->set(486.0f);
    NBO::PositiveFloat_TypePtr laserCutOff = std::make_shared<NBO::PositiveFloat_Type>();
    laserCutOff->set(490.0f);
    illuminationRange->setCutOn(laserCutOn);
    illuminationRange->setCutOff(laserCutOff);
    illuminationRange->setPeakWavelength(488.0f);
    illuminationRange->setIlluminationPower(60.0f);
    laser->addToIlluminationWavelengthRange_List(illuminationRange);

    instrument.addToLightSourceGroup_List(laser);

    // Step 6: Export the description
    std::filesystem::path outputPath = std::filesystem::temp_directory_path() / "instrument.xml";
    instrument.toXmlFile(outputPath.string());
    std::cout << "Instrument description written to " << outputPath.string() << std::endl;

    // Step 7: Read the description back through the model
    std::cout << "Objectives: " << instrument.getObjective_List().size() << std::endl;
    for (const auto &objective : instrument.getObjective_List()) {
        std::cout << "  " << objective->getModel() << " - " << objective->getMagnification()
                  << "x NA " << objective->getLensNA() << std::endl;
    }

    for (const auto &detector : instrument.getDetectorGroup_List()) {
        std::cout << "Detector: " << detector->getManufacturer() << " " << detector->getModel()
                  << std::endl;
    }

    for (const auto &lightSource : instrument.getLightSourceGroup_List()) {
        std::cout << "Light source: " << lightSource->getManufacturer() << " "
                  << lightSource->getModel() << std::endl;
    }

    return 0;
}

    record ObjectiveSpec(String model, String catalogNumber, float magnification, float lensNA,
            String immersion, float workingDistance) {
    }

    public static void main(String[] args) {
        // Step 1: Create the instrument and identify it
        Instrument instrument = new Instrument();

        InstrumentID_Type instrumentId = new InstrumentID_Type();
        instrumentId.set("Instrument:1");
        instrument.setID(instrumentId);

        Denomination_Type instrumentName = new Denomination_Type();
        instrumentName.set("Nikon Ti2 - Room 214");
        instrument.setName(instrumentName);

        // Step 2: Add the microscope stand
        // MicroscopeStand is abstract: the concrete class states the geometry of the
        // stand, here an inverted one.
        InvertedMicroscopeStand stand = new InvertedMicroscopeStand();

        LSID_Type standId = new LSID_Type();
        standId.set("MicroscopeStand:1");
        stand.setID(standId);

        stand.setManufacturer("Nikon");
        stand.setModel("Eclipse Ti2-E");
        stand.setCatalogNumber("Ti2-E");

        MicroscopeType_Type standType = new MicroscopeType_Type();
        standType.set("Compound");
        stand.setType(standType);

        instrument.addToMicroscopeStandGroup_List(stand);

        // Step 3: Add the objectives
        // Objective declares twelve required attributes, so a compliant description of
        // a lens states its optical characteristics in full.
        ObjectiveSpec[] objectiveSpecs = {
            new ObjectiveSpec("CFI Plan Apo Lambda 20x", "MRD00205", 20.0f, 0.75f, "Air", 1.0f),
            new ObjectiveSpec("CFI Plan Apo Lambda 60x", "MRD01605", 60.0f, 1.4f, "Mineral Oil",
                    0.13f),
        };

        int index = 1;
        for (ObjectiveSpec spec : objectiveSpecs) {
            Objective objective = new Objective();

            ObjectiveID_Type objectiveId = new ObjectiveID_Type();
            objectiveId.set("Objective:" + index);
            objective.setID(objectiveId);

            objective.setManufacturer("Nikon");
            objective.setModel(spec.model());
            objective.setCatalogNumber(spec.catalogNumber());

            objective.setMagnification(spec.magnification());
            objective.setLensNA(spec.lensNA());
            objective.setInfinityCorrected(true);
            objective.setDIC(false);
            objective.setCorrectionCollar(false);
            objective.setWorkingDistance(spec.workingDistance());
            objective.setObjectiveViewField(25.0f);
            objective.setImageDistance(200.0f);

            ObjectiveCorrection_Type correction = new ObjectiveCorrection_Type();
            correction.set("Apochromat");
            objective.setCorrection(correction);

            ImmersionTypeList_Type immersionType = new ImmersionTypeList_Type();
            immersionType.set(spec.immersion());
            objective.setImmersionType(immersionType);

            ContrastModulationPlate_Type contrastModulation = new ContrastModulationPlate_Type();
            contrastModulation.set("None");
            objective.setContrastModulation(contrastModulation);

            // Lengths carry their unit as a separate attribute
            UnitsLength_Type millimeter = new UnitsLength_Type();
            millimeter.set("mm");
            objective.setWorkingDistanceUnit(millimeter);

            instrument.addToObjective_List(objective);
            index++;
        }

        // Step 4: Add the detector
        // CMOS derives from Camera, which derives from the abstract Detector, so a CMOS
        // instance is accepted by the detector group of the instrument.
        CMOS camera = new CMOS();

        DetectorID_Type cameraId = new DetectorID_Type();
        cameraId.set("Detector:1");
        camera.setID(cameraId);

        camera.setManufacturer("Hamamatsu");
        camera.setModel("ORCA-Fusion BT");
        camera.setCatalogNumber("C15440-20UP");

        camera.setPixelWidth(6.5f);
        camera.setPixelHeight(6.5f);
        camera.setArrayWidth(2304L);
        camera.setArrayHeight(2304L);
        camera.setPixelWellCapacity(15000L);
        camera.setMaximumFrameRate(89.1f);
        camera.setMaximumReadoutRate(100.0f);
        camera.setQuantumEfficiency(0.8f);
        camera.setElectronicConversionFactor(0.24f);
        camera.setReadOutNoise(0.7f);
        camera.setDarkCurrentRate(0.06f);

        DetectorNoiseModel_Type noiseModel = new DetectorNoiseModel_Type();
        noiseModel.set("Gaussian");
        camera.setDetectorNoiseModel(noiseModel);

        CameraIllumination_Type illumination = new CameraIllumination_Type();
        illumination.set("Back");
        camera.setIllumination(illumination);

        DigitizerType_Type maxBitDepth = new DigitizerType_Type();
        maxBitDepth.set("16bit");
        camera.setMaxBitDepth(maxBitDepth);

        // Detectors are characterized over one or more spectral bands
        Detector_Type_InlineWavelengthRange wavelengthRange =
                new Detector_Type_InlineWavelengthRange();
        PositiveFloat_Type cutOn = new PositiveFloat_Type();
        cutOn.set(400.0f);
        PositiveFloat_Type cutOff = new PositiveFloat_Type();
        cutOff.set(700.0f);
        wavelengthRange.setCutOn(cutOn);
        wavelengthRange.setCutOff(cutOff);
        wavelengthRange.setPeakWavelength(560.0f);
        wavelengthRange.setWavelengthProfile("orca_fusion_qe.csv");
        camera.addToWavelengthRange_List(wavelengthRange);

        instrument.addToDetectorGroup_List(camera);

        // Step 5: Add the light source
        // LightSource is abstract as well; a Laser is one of its concrete forms.
        Laser laser = new Laser();

        LightSourceID_Type laserId = new LightSourceID_Type();
        laserId.set("LightSource:1");
        laser.setID(laserId);

        laser.setManufacturer("Coherent");
        laser.setModel("OBIS 488");
        laser.setCatalogNumber("1185053");

        laser.setTuneable(false);
        laser.setPulse(false);
        laser.setIsPump(false);
        laser.setIsPumped(false);

        LaserType_Type laserType = new LaserType_Type();
        laserType.set("SemiconductorLaserDiode");
        laser.setType(laserType);

        LaserModulation_Type modulation = new LaserModulation_Type();
        modulation.set("Direct");
        laser.setModulationMechanism(modulation);

        IlluminationWavelengthRange illuminationRange = new IlluminationWavelengthRange();
        PositiveFloat_Type laserCutOn = new PositiveFloat_Type();
        laserCutOn.set(486.0f);
        PositiveFloat_Type laserCutOff = new PositiveFloat_Type();
        laserCutOff.set(490.0f);
        illuminationRange.setCutOn(laserCutOn);
        illuminationRange.setCutOff(laserCutOff);
        illuminationRange.setPeakWavelength(488.0f);
        illuminationRange.setIlluminationPower(60.0f);
        laser.addToIlluminationWavelengthRange_List(illuminationRange);

        instrument.addToLightSourceGroup_List(laser);

        // Step 6: Export the description
        Path outputPath = Paths.get(System.getProperty("java.io.tmpdir"), "instrument.xml");
        instrument.toXmlFile(outputPath.toString());
        System.out.println("Instrument description written to " + outputPath);

        // Step 7: Read the description back through the model
        System.out.println("Objectives: " + instrument.getObjective_List().size());
        for (Objective objective : instrument.getObjective_List()) {
            System.out.println("  " + objective.getModel() + " - " + objective.getMagnification()
                    + "x NA " + objective.getLensNA());
        }

        for (var detector : instrument.getDetectorGroup_List()) {
            System.out.println(
                    "Detector: " + detector.getManufacturer() + " " + detector.getModel());
        }

        for (var lightSource : instrument.getLightSourceGroup_List()) {
            System.out.println(
                    "Light source: " + lightSource.getManufacturer() + " " + lightSource.getModel());
        }
    }

Starting from MicroMetaApp

If the instrument is already described in MicroMetaApp, skip this code: Import a MicroMetaApp Configuration produces the same object in one call.