Skip to content

16 - Scene and Project: Saving, Reuse and Task Snapshots

The simulation core of LESS is less.Scene. As long as the scene, lights and sensors are already in Python If defined in the script, you can directly call scene.simulate(sensor) to complete all simulations.

less.Project is an optional engineering management layer. It combines Scene, named sensors, resources and engineering elements Data is organized for long-term preservation, sharing and remote tasks.

This chapter uses LESS Project Format v2 and covers:

  • When to use only Scene and when to use Project;
  • Save and load independent scenes;
  • Create complete projects and manage multiple sensors;
  • Build a Scene Session and repeatedly execute multiple simulations;
  • Modify lighting or physical properties without rebuilding geometry;
  • Save directory projects, export portable packages and generate remote task snapshots;
  • Migrate from legacy LESS project.

1. Choose first: Scene or Project

The two are not two simulation systems. Project still holds an ordinary Scene internally:

less.Project
├── scene → real simulation runtime and build status
├── sensors → Named sensors registered in the project
├── resources → File resource registry
└── metadata → title, author, project ID and revision number

Just choose according to the purpose of use:

Usage scenarios Recommended entrance
Notebook, single experiment, algorithm verification Scene
The sensor is temporarily created by the script Scene
Reuse or share a pure scene template scene.save()
Save scenes and multiple sensors Project
Manage local resources and project revisions Project
Submit remote computing task Project.export_snapshot()

If project management and packaging are not required, it is enough to use Scene directly:

import less

scene = less.Scene()
scene.terrain = less.Terrain(
    property=less.Lambertian(reflectance=0.2)
)
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), atmosphere=less.HosekWilkieAtmosphere(turbidity=2.0))

sensor = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[450, 550, 650, 850],
)

# Scene will be automatically built the first time simulate() is executed.
image = scene.simulate(sensor)

2. Save and load standalone Scene

scene.save() only saves the scene itself, including:

  • Terrain and DEM;
  • Objects and instances;
  • physical properties;
  • lighting;
  • Space and backend configuration of Scene.

It does not save project sensor lists, resource registries, project IDs, or task information.

2.1 Three saving modes

scene.save("forest-scene.less")

By default you get a self-contained ZIP file and the mesh is copied into the package.

scene.save("forest-scene.less", pack=False)

Get an uncompressed directory with the same name, suitable for local debugging and large grids.

scene.save("forest-scene.json")

Get the lightweight JSON reference file. The mesh maintains its original path and is not copied, so it is not necessarily portable.

2.2 Load Scene

scene = less.Scene.load("forest-scene.less")

sensor = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[650, 550, 450],
)
image = scene.simulate(sensor)

The loaded Scene has not yet been built; simulate() will be built automatically for the first time without additional preparation.

It is recommended to use easily identifiable file names for pure scene files:

forest-scene.less
maize-template.less
urban-block-scene.less

3. Create a complete Project

3.1 Start with an empty project

import numpy as np
import less

project = less.Project.create(
title="Northern Birch Canopy Experiment",
    author="LESS Lab",
description="Comparing ortho and multi-angle canopy reflectance.",
    backend="auto",
)

scene = project.scene
scene.size = (100.0, 100.0)
scene.terrain = less.Terrain(
    property=less.Lambertian(reflectance=0.18)
)
scene.illumination = less.Illumination(source=less.Sun(zenith=32, azimuth=145), atmosphere=less.HosekWilkieAtmosphere(turbidity=2.2))

tree = less.Object("birch", mesh="assets/birch.obj")
tree.set_property("leaves", less.Prospect(cab=42))

positions = np.array([
    [20, 20, 0],
    [40, 30, 0],
    [65, 25, 0],
    [30, 70, 0],
    [70, 65, 0],
])
scene.add(tree, positions=positions)

Project.create() just helped us create the project and initial Scene. All of Scene API Still Available via project.scene.

3.2 Create from existing Scene

Existing code does not need to be rewritten:

project = less.Project(
    scene=scene,
title="Northern Birch Canopy Experiment",
    author="LESS Lab",
)

This is also a compatible entry point for the old Project(scene=..., sensors=...) call.


4. Manage engineering resources

Project Format divides resources into two storage strategies: managed and external.

4.1 Hosted Resources

Documents that are small or must be delivered with the project should be included in the project:

mesh_resource = project.resources.import_file(
    "assets/birch.obj",
    resource_type="mesh",
)

mesh_path = project.resources.resolve(mesh_resource.id)
tree = less.Object("birch", mesh=mesh_path)

When you save a catalog project or portable package, the managed resources are copied to resources/ and the file size and SHA-256 hash.

Resource registration does not automatically create Object. It is only responsible for file identity, path and storage strategy, scene objects Still created by less.Object.

4.2 External resources

Very large DEMs, point clouds or shared data can retain only links:

dem_resource = project.resources.register_external(
    "F:/remote-sensing/dem/large-area.tif",
    resource_type="terrain",
)

External resources are not copied into the daily project, so the project is smaller but no longer fully portable. Open the project The machine must have access to the same path or shared mount point.

Before submitting a remote task, ensure that the external resource is visible on the server, or change it to a managed resource first. Currently export_snapshot() will retain the external path and will not implicitly copy very large files.

4.3 Check resources

for resource in project.resources:
    errors = project.resources.verify(
        resource.id,
        checksum=True,
    )
    print(resource.name, errors or "OK")

5. Add multiple named sensors

nadir = less.OpticalImager(
    less.Orthographic(
        image_size=512,
        view_zenith=0,
    ),
    bands=[450, 550, 650, 850],
)
project.add_sensor(nadir, name="Canopy Multispectral (Orthophoto)")

oblique = less.OpticalImager(
    less.Orthographic(
        image_size=512,
        view_zenith=30,
        view_azimuth=90,
    ),
    bands=[550, 650, 850],
)
project.add_sensor(oblique, name="Canopy multi-angle (30°)")

View and get sensors:

print([sensor.name for sensor in project.sensors])

sensor = project.get_sensor("Canopy Multispectral (Orthophoto)")

Remove the sensor:

project.remove_sensor("Canopy multi-angle (30°)")

Sensor configuration is not scene geometry. Adding, deleting or modifying sensors does not require rebuilding Scene.


6. Build once, run multiple sensors

It is recommended to build explicitly in batch simulations:

project.build()

image_nadir = project.simulate("Canopy Multispectral (Orthophoto)")
image_oblique = project.simulate("Canopy multi-angle (30°)")

Both sensors share the same project.scene, so the second simulation will no longer create the geometric acceleration structure.

When no name is specified, Project will execute all registration sensors in sequence:

results = project.simulate()

for sensor_name, product in results.items():
    product.save(f"outputs/{sensor_name}.tif")

The return value is:

{
"Canopy Multispectral (Orthophoto)": product1,
"Canopy multi-angle (30°)": product2,
}

Project.simulate() still ends up calling the same Scene:

project.simulate("Canopy Multispectral (Orthophoto)")

# Equivalent to:
sensor = project.get_sensor("Canopy Multispectral (Orthophoto)")
project.scene.simulate(sensor)

Project will not create another copy of GPU Scene, nor will it relay the local simulation over HTTP.


7. Parameter hot update and geometric reconstruction

7.1 Modify lighting: do not rebuild

project.set_illumination(
    less.Illumination(source=less.Sun(zenith=50, azimuth=210), atmosphere=less.HosekWilkieAtmosphere(turbidity=2.5))
)

print(project.session_status)  # parameters_dirty

image = project.simulate("Canopy Multispectral (Orthophoto)")

Only lighting parameters will be synchronized before the next simulation, and existing geometric acceleration structures will continue to be reused.

7.2 Modify physical attributes: do not rebuild

forest = project.scene.objects["birch"]
forest.set_property(
    "leaves",
    less.Prospect(cab=25),
)

image = project.simulate("Canopy Multispectral (Orthophoto)")

You can also use path API:

project.scene.update_property(
    "birch.leaves",
    less.SpectrumDB("birch_leaf_green"),
)

7.3 Modify geometry: need to rebuild

The following changes require the geometry to be rebuilt or updated:

  • Add and delete objects;
  • Change the grid;
  • Change terrain geometry;
  • Change a large number of instance positions, rotations or scales.
# ... modify an object, instance, or terrain ...
project.rebuild()

image = project.simulate("Canopy Multispectral (Orthophoto)")

Quickly determine the current status:

print(project.is_built)
print(project.session_status)

The meaning of session_status:

Status Meaning
unbuilt Not yet built
ready Can be reused directly
parameters_dirty Parameters to be synchronized, no need to rebuild
geometry_dirty Geometry changes, need to rebuild

8. Save Project

8.1 Editable directory project

When frequent editing is required, it is recommended to use a normal directory project:

project.save_directory("birch-experiment.less")

Directory contents:

birch-experiment.less/
├── project.json       # Format version, project ID, title, and revision number
├── metadata.json      # Compatible metadata
├── scene.json         # Scene configuration
├── sensors.json       # Name the sensor
├── resources.json     # Resource Registry and Hashes
└── resources/
    ├── mesh/
    ├── terrain/
    └── ...

After saving for the first time, you can save directly back to the current location:

project.save_directory()

Directory mode does not require compressing the entire large scene each time, and is suitable for frequent editing and version management.

8.2 Single file portable project

project.save("birch-experiment.less")

By default, a single file .less in ZIP format is generated and set as the current project path.

To export just another portable copy without changing the current editing project:

project.pack("birch-experiment-portable.less")

The difference between the two:

API Function
save_directory() Save and continue editing directory project
save(path) Save and use this path as the current project
pack(path) Export a portable copy without switching the current project

The old call still works:

project.save("birch-experiment.less", pack=False)

It is equivalent to saving an uncompressed directory.

8.3 JSON reference mode

project.save("birch-experiment.json")

JSON mode is mainly used for compatibility and lightweight configuration exchange. Resources remain path references and are not suitable as fully portable archive format.


9. Open, verify and close the project

Directory and ZIP use the same entry:

project = less.Project.open("birch-experiment.less")

Compatible portals can also continue to be used:

project = less.Project.load("birch-experiment.less")

Verify the project before running:

report = project.validate(verify_checksums=True)

for issue in report.issues:
    print(issue.severity, issue.path, issue.message)

report.raise_for_errors()

Validation checks:

  • Is Scene valid?
  • Whether the sensor name is missing or duplicated;
  • Whether the grid and resources exist;
  • File size consistent with optional SHA-256.

Projects opened from ZIP will be extracted to a temporary directory. You can clean up after finishing building and simulating:

project.close()

Do not call close() before simulation as the grid in Scene may still point to the unzipped directory.


10. Generate remote task snapshot

The remote task does not directly modify the current Project object, but generates an independent snapshot from a project revision:

snapshot = project.export_snapshot(
    "jobs/nadir-job.less",
sensors=["Canopy multispectral (orthophoto)"],
)

print(snapshot.project_id)
print(snapshot.revision)
print(snapshot.fingerprint)
print(snapshot.sensors)

snapshot.json is also generated in the snapshot:

project_id project identity
revision project revision at submission
fingerprint configuration content fingerprint
sensors The sensors selected for this task
created_at snapshot creation time

The server can open the snapshot using normal Project API:

job = less.Project.open("jobs/nadir-job.less")
job.validate().raise_for_errors()
job.build()
results = job.simulate()

Local simulation does not require snapshots and does not require HTTP:

result = project.simulate("Canopy multispectral (orthophoto)")

11. Project API Quick Check

# Create and open
less.Project.create(...)
less.Project(scene=..., sensors=[...])
less.Project.open(path)
less.Project.load(path)

# sensor
project.add_sensor(sensor, name="Nadir")
project.get_sensor("Nadir")
project.remove_sensor("Nadir")

# Sessions and Simulations
project.build()
project.rebuild()
project.simulate("Nadir")
project.simulate()                  # All sensors
project.set_illumination(...)
project.show()

# Validation and persistence
project.validate()
project.save_directory(path)
project.save(path)
project.pack(path)
project.export_snapshot(path, sensors=[...])
project.close()

Common status:

project.path
project.is_dirty
project.is_built
project.session_status
project.project_id
project.revision

12. Migrate from the old LESS project

The simulation directory of the original LESS can be converted to Scene first:

scene = less.Scene.from_less(
    "D:/LESS/simulations/my_project/"
)

from_less() imports scene geometry, terrain, attributes and lighting, but does not automatically import all observations in the old configuration Parameters become the new sensor. It is recommended to create the sensor explicitly and package it into a Project:

sensor = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[550, 650, 850],
)

project = less.Project(
    scene=scene,
    title="Migrated LESS experiment",
)
project.add_sensor(sensor, name="Nadir multispectral")
project.save_directory("migrated-experiment.less")

The new version can also read the v1 .less project generated by the old version less.Project.save(). first save The v2 structure will be used later.


Pure Python temporary experiment

scene = make_scene()
result = scene.simulate(make_sensor())

Reproducible local projects

project = less.Project.create(title="Experiment")
# Configure project.scene, resources and sensors
project.save_directory("experiment.less")
project.build()
results = project.simulate()

Local project + remote service

Open directory project
→ Edit Scene and sensor in Python
→ Build a Scene Session locally
→ Repeat simulation for multiple sensors
→ Hot update when lighting/attributes are modified
→ Export task snapshot when remote computing is required
→ lessd opens the snapshot and executes

There is only one key principle: **Scene is responsible for simulation, and Project is responsible for organization and reproduction. **

Next chapter: 17 - Reconstruction of 3D scene from LiDAR point cloud

  • less.Scene.save()less.Scene.load()
  • less.Project.create()less.Project.open()less.Project.save()
  • less.Project.add_sensor()less.Project.simulate()
  • less.Project.validate()less.Project.snapshot()