API

BeamletOpticsProjects.BeamletOpticsProjects — Module
BeamletOpticsProjects

Projects of BeamletOptics: a folder that holds an optical setup as code (system.jl), own code (src/<Name>.jl) and its project file (<folder name>.bmo, which records the versions it was saved with). The GUI saves to and opens from it; agents edit system.jl and src/ directly. A project is no Julia environment: it is opened from any environment that has the packages it needs.

See load_project, save_project, new_project and the macros @component, @source and @system.

source

Load and save

BeamletOpticsProjects.load_project — Function
load_project(path) -> Project

Loads the project in the folder path, or of the project file path (<name>.bmo, see project_marker): includes the own code src/<name>.jl (a module <name>, whose exported names system.jl can use) and runs system.jl, whose @component, @source, @group and @system fill the returned Project. Both run in a new module of their own, which using BeamletOptics, BeamletOpticsProjects. The groups are built when all lines are read, inner groups first, and then the systems.

Errors name the line of system.jl. A load that fails while another participant writes system.jl (it is half written at that moment, or cannot be opened while it is replaced, e.g. on Windows or a shared drive) is tried again, up to 3 times, if the file changed or could not be read a quarter of a second later; an error of a file that stays as it is is thrown after that one wait. A project whose format (format of its project file <name>.bmo, none: 1) is newer than the one this package reads is refused; the versions that the project file records are not checked (see check_versions). If the own code uses (using, import) a package that is not installed in the active environment, throws an ArgumentError that names it.

source
BeamletOpticsProjects.save_project — Function
save_project(project; packages = Module[], force = false, by = nothing) -> String
save_project(project, path; packages = Module[], force = false, by = nothing) -> String

Writes system.jl of project (in the folder path, which becomes the folder of the project) and returns its path. A folder that is not a project yet (it has no system.jl) gets the files of new_project and its first version (see save_version).

Every save raises the versions that the project file records (see project_marker): per package the higher of the recorded version and the one of this session, for BeamletOptics, BeamletOpticsProjects and packages ([versions]) and for the packages that the own code in src/ uses now ([packages], found from its using and import). The project file is written only if this changes it.

Throws ProjectChanged and writes nothing if the files changed since the project was loaded or saved (its stamp, see project_stamp), unless force is true. Afterwards the stamp of project is that of the written files. Saving to another folder (path) does not check. With the participant by (see claim!), throws an ArgumentError and writes nothing if the save changes a line claimed by another participant.

The check and the write happen under the lock .bmo/write.lock, so of two participants that save at the same moment, the second sees the first's write and throws ProjectChanged instead of overwriting it. A save waits for the lock of another writer for at most 2 s and then throws ProjectChanged as well: that writer is writing, so load (or merge) and save again. A lock older than 10 s is of a writer that died and is taken over. Its age is compared with the clock of this machine: the clocks of the machines are assumed to agree within a few seconds (one Windows domain).

source
BeamletOpticsProjects.new_project — Function
new_project(path; name, packages = Module[]) -> Project

Creates an empty project in the folder path:

  • the project file <folder name>.bmo, by which people open it (see project_marker), with the module name name (by default from the folder, see module_name), format and the versions ([versions]) of BeamletOptics, BeamletOpticsProjects and the modules packages (e.g. BeamletOpticsGUI); the folder is no Julia environment: a project is opened from any environment that has these packages;
  • src/<name>.jl with the module <name> for own code;
  • system.jl without entries;
  • AGENTS.md and CLAUDE.md with the instructions for agents that are specific to the project, .gitignore and .claude/settings.json, whose hook shows .bmo/session*.md (the selection and the recent actions in every open GUI window) to Claude Code with every prompt when Claude Code is started in the project folder;
  • the agent skills of the installed BeamletOptics and BeamletOpticsProjects in .claude/skills/ (see install_agent_skill of both), which .gitignore keeps out of the versions: they come from the installed packages.

Then saves the first version, "New project", see save_version: in the git repository that holds the folder, else in a new one in the folder.

Throws if path is a project already (it has a system.jl).

source
BeamletOpticsProjects.Project — Type
Project(path, name; entries = Entry[], systems = SystemEntry[], groups = GroupEntry[])

A project in the folder path with the module name name (of src/<name>.jl), its entries (@component and @source in file order), its systems, its groups, the module mod that system.jl ran in (nothing for a project built in memory) and the stamp of its files when it was loaded or saved (see project_stamp, nothing for a project built in memory), against which save_project checks that nobody else changed them meanwhile.

source
BeamletOpticsProjects.Entry — Type
Entry(kind, name, code, object; pos = nothing, rot = nothing, pose0, meta = Pair{Symbol, Any}[],
      label = nothing, uid = nothing)

One @component (kind = :component) or @source (kind = :source) of a project: its unique readable id name (e.g. L1, see next_id), the constructor expression code, the constructed and posed object, its pose pos (absolute position in m, nothing = as constructed) and rot (rotation since construction, as (x, y, z) angles in degrees, extrinsic Rz(z)·Ry(y)·Rx(x), or a 3×3 matrix, nothing = none), the pose pose0 = (position, frame) right after construction and further keywords meta, which the project keeps but does not interpret (e.g. :catalog of the GUI), the label that is shown instead of the id (nothing: the id) and the machine id uid, a UUID that never changes (nothing until the first save), by which the GUI recognizes the entry after a rename.

The frame of pose0 is the orientation, or for things with only a direction (beams) a right-handed frame with the local y-axis along the direction, see frame.

source
BeamletOpticsProjects.SystemEntry — Type
SystemEntry(name, members, sources = Symbol[]; static = false, object = nothing, label = nothing,
            uid = nothing)

One @system of a project: its readable id name, the ids of its members and sources, whether it is a StaticSystem, the built system object, its label and machine id uid, see Entry.

source
BeamletOpticsProjects.GroupEntry — Type
GroupEntry(name, members; pos, rot = nothing, object = nothing, label = nothing, uid = nothing)

One @group of a project, a BeamletOptics.ObjectGroup: its readable id name (e.g. G1), the ids of its members (entries of @component or other groups, in the order of the group's objects), its pivot pos (absolute position in m) and orientation rot (as for an Entry, nothing = the identity), the built group object, its label and machine id uid.

The members keep their own lines with their absolute poses; building the group moves none of them. An entry or group is a member of at most one group, and an entry inside a group is in systems only through its outermost group, which the systems list, see @group.

source
BeamletOpticsProjects.system_code — Function
system_code(project) -> String

The text of system.jl of project: a header, then one line per @component, @source, @group and @system, in this order and in the order of the entries (groups inner first). Numbers are written exactly and as short as possible; a rotation given as a matrix is written as angles in degrees if it can be, see rotation_matrix.

source
BeamletOpticsProjects.project_marker — Function
project_marker(dir) -> String

The path of the project file of the project in the folder dir: the file *.bmo in it, or <folder name>.bmo if there is none yet. People and the dialogs of the GUI open a project by it; the project is the folder. It is the only file of metadata of the project, a TOML file with

  • format: the version of the format of system.jl (see FORMAT), none: 1;
  • name: the module name of the own code src/<name>.jl (none: module_name of the folder);
  • [versions]: the newest versions of BeamletOptics, BeamletOpticsProjects and the packages of the program that saved it (e.g. BeamletOpticsGUI) that the project was saved with: every save raises them, none lowers them;
  • [packages]: the packages that the own code in src/ uses (using, import), with the newest version they were saved with ("" if it was not known).

See project_versions, check_versions.

source
BeamletOpticsProjects.project_stamp — Function
project_stamp(dir) -> String

A fingerprint of the files of the project in the folder dir that define the setup: system.jl and every .jl file in src/, with their paths. It changes with any change of their content; the GUI's own files (view.toml) do not count, nor do files and folders in src/ whose name starts with . (e.g. the ._X.jl that macOS leaves on network drives).

source

The form of system.jl

BeamletOpticsProjects.@component — Macro
@component name = expr [pos = (x, y, z)] [rot = (a, b, c)] [key = value ...]

Builds the component expr, binds it to name and, while a project is loaded, registers it under name with its recipe expr. pos is the absolute position in m, rot the rotation since construction as angles in degrees about the global x-, y- and z-axes (or a 3×3 matrix), see apply_pose!. Further keywords are kept with the entry (e.g. catalog of the GUI).

source
BeamletOpticsProjects.@group — Macro
@group name = [members...] pos = (x, y, z) [rot = (a, b, c)]

Builds a BeamletOptics.ObjectGroup of the components or groups members (in this order) and binds it to name. Building moves no member: the members keep the poses of their own lines, and the group gets the pivot pos (absolute position in m) and the orientation rot (angles in degrees as for @component, or a 3×3 matrix; none: the identity).

An entry or group is a member of at most one group, members are never sources or systems, and an entry inside a group is in systems only through its outermost group, which the @system lines list. While a project is loaded, the members may be defined below the line; in a plain include they must be bound above, as for @system.

source
BeamletOpticsProjects.@system — Macro
@system name = [members...] [sources = [names...]] [static = true]

Builds a System (a StaticSystem with static = true) of the components members and binds it to name; sources are the sources traced in it. A component may be a member of several systems, a source belongs to at most one.

source
BeamletOpticsProjects.id_prefix — Function
id_prefix(x) -> String

The prefix of the readable id of x (see next_id), chosen by its type: "L" lenses, "P" prisms, "M" mirrors and other reflective optics, "D" detectors, "BS" beamsplitters, "B" sources (beams and beam groups), "G" groups, "S" systems, "X" anything else. A package with own component types may add methods.

source
BeamletOpticsProjects.next_id — Function
next_id(project, prefix) -> Symbol

The next free readable id with the prefix in project: the prefix and one more than the largest number of an entry, group or system with that prefix, e.g. :L4 after L1, L3.

source
BeamletOpticsProjects.apply_pose! — Function
apply_pose!(obj, pos, rot) -> obj

Poses the just constructed obj: rotates it by rot (see rotation_matrix) about its position and moves it to pos. pos is reached exactly: the object is first moved to the origin, which is exact, and then by pos. nothing leaves the position or orientation as it is.

source
BeamletOpticsProjects.rotation_matrix — Function
rotation_matrix(rot) -> Matrix{Float64}

The rotation matrix of rot: (x, y, z) angles in degrees mean Rz(z)·Ry(y)·Rx(x) (first about the global x-axis, then y, then z), a matrix is taken as it is, nothing is the identity. Multiples of 90° give exact matrices.

source

Change lines

Each function is one checked write: it respects the claims of the others and writes nothing when the change is refused.

BeamletOpticsProjects.add! — Function
add!(dir, code::AbstractString; id = nothing, pos = (0, 0, 0), rot = (0, 0, 0), label = nothing,
     systems = Symbol[], by = nothing) -> Symbol

Adds a line for the object built by code (a constructor, e.g. "RoundPlanoMirror(0.0254, 0.006)") to the live project in the folder dir and returns its id. The line is a @source if the object is a beam or a beam group, else a @component.

id = nothing takes the next free id of the prefix of the object (see next_id and id_prefix), chosen in the same load and write, so no other participant can take it in between; a given id that is taken throws. pos (in m) and rot (angles in degrees or a 3×3 matrix, see rotation_matrix) are the pose of the line, label the name shown instead of the id. The new line becomes a member of the systems (ids of systems); a source goes into the sources of at most one system.

The changed project is checked before anything is written: the code builds in the module of the project (with its own code of src/), the pose is finite and the whole system.jl loads again. A check that fails throws an ArgumentError naming the id, and nothing is written. The write is one save_project with the participant by, which respects the claims of the others (see claim!). If the files changed between loading and writing (e.g. an open window added the machine ids of lines that were just written as text), all of it is done again from the files, up to 3 times; then it throws ProjectChanged.

Examples

add!(".", "RoundPlanoMirror(0.0254, 0.006)"; pos = (0.1, 0.2, 0.0), rot = (0, 0, 45),
     systems = [:S1], by = "claude")                        # -> :M3
add!(".", "Beam([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1.064e-6)"; systems = [:S1])   # a source
source
BeamletOpticsProjects.set! — Function
set!(dir, id::Symbol; pos = nothing, rot = nothing, code = nothing, label = nothing,
     by = nothing) -> Project

Changes the given fields of the line id of the live project in the folder dir and keeps the others, also its machine id; nothing keeps a field. pos (in m) and rot (angles in degrees or a 3×3 matrix) are the pose of the line, as written in system.jl; code replaces the constructor (the object is built again, a component stays a component and a source a source); label = "" removes the label. Returns the project loaded again from the written files.

Of a group only the label is set: its pos is the pivot of its members, which a group moves with it, see move!; of a system only the label.

The checks and the write are those of add!: a check that fails throws an ArgumentError naming the id, and nothing is written.

Examples

set!(".", :L1; pos = (0.0, 0.12, 0.0), by = "claude")
set!(".", :L1; code = "SphericalLens(0.05, -0.05, 0.006, 0.0254, ConstantRefractiveIndex(1.5))", label = "Collimator")
source
BeamletOpticsProjects.move! — Function
move!(dir, ids::Vector{Symbol}; offset = (0, 0, 0), rot = nothing, pivot = :center,
      by = nothing) -> Project

Moves the components, sources and groups ids of the live project in the folder dir by one rigid motion, as a selection of several in the window, and writes system.jl once. Returns the project loaded again from the written files.

The motion first rotates by rot about pivot with the axes of the world, then moves by offset (in m): a position x becomes pivot + R (x - pivot) + offset with R = rotation_matrix(rot) (angles in degrees as rot of a line, or a 3×3 matrix, nothing: no rotation, see rotation_matrix), and the orientation of each line is turned by R. pivot is :center (the mean of the positions of the ids: of the components and sources, and the pivot of each group), the id of a component, source or group (its position), or a point (x, y, z).

A group moves with all its members, also those of the groups inside it, and its pivot and orientation move with them. An id inside a group must be given with its group (or a group around it); the members of a group are never moved alone, which would break the group. Unknown ids throw an ArgumentError naming the id.

An id of a system stands for its members and sources (as listed by the @system line, groups as groups); an object named twice, e.g. by a system and by one of its members, moves once.

The write is one save_project with the participant by: it throws an ArgumentError if it would change a line claimed by another participant (see claim!); then nothing is written. If the files changed between loading and writing, the move is done again from the files, up to 3 times, as by add!; then it throws ProjectChanged.

Examples

move!(".", [:L1, :M1]; offset = (0.0, 0.01, 0.0), by = "claude")   # 10 mm along y
move!(".", [:G1]; rot = (0, 0, 90), by = "claude")                # G1 turned about its pivot
move!(".", [:L1, :L2]; rot = (0, 0, 5), pivot = :L1)              # about the position of L1
source
BeamletOpticsProjects.duplicate! — Function
duplicate!(dir, ids::Vector{Symbol}; offset = (0, 0, 0), rot = nothing, pivot = :center,
           systems = :same, by = nothing) -> Vector{Symbol}

Copies the components, sources and groups ids of the live project in the folder dir and moves the copies by one rigid motion, as move! moves the originals (offset, rot and pivot as there; the default leaves the copies on the originals). Returns the ids of the copies: in the order of ids, a group after the copies of its members (in their order, also of the groups inside it).

A copy has the code, pose and further keywords of its original, a new id (the next free one of its prefix, see next_id), no label and no machine id (it gets one when it is written). A copy of a group is a new group of the copies of its members. As in move!, an id inside a group must be given with its group (or a group around it).

systems = :same puts each copy into the systems of its original (a component into their members, a source into their sources); a vector of system ids puts each copy into those (a source into at most one), Symbol[] into none. The copies of the members of a group are in systems only through the copy of the group.

The checks and the write are those of add!: a check that fails throws an ArgumentError naming the id, and nothing is written.

Examples

duplicate!(".", [:M1]; offset = (0.0, 0.05, 0.0), by = "claude")   # -> [:M3]
duplicate!(".", [:G1]; rot = (0, 0, 180), pivot = (0.0, 0.0, 0.0))  # -> [:L3, :M4, :G2]
source
BeamletOpticsProjects.remove! — Function
remove!(dir, ids::Vector{Symbol}; by = nothing) -> Project

Removes the components, sources and groups ids of the live project in the folder dir, as the window does: their lines and their places in the systems (members and sources) and groups. A group is removed with its members, also those of the groups inside it. As in move!, an id inside a group must be given with its group (or a group around it); unknown ids and ids of systems throw an ArgumentError naming the id. Returns the project loaded again from the written files.

The checks and the write are those of add!: a check that fails throws an ArgumentError, and nothing is written.

Examples

remove!(".", [:M3, :G2]; by = "claude")
source

Read lines

BeamletOpticsProjects.members — Function
members(p::Project, system::Symbol) -> Vector{Symbol}

The ids of the members of the system system as its @system line lists them (a group is one member: its id, not those of the objects inside). Throws an ArgumentError for an unknown system.

source
BeamletOpticsProjects.sources — Function
sources(p::Project, system::Symbol) -> Vector{Symbol}

The ids of the sources of the system system (its sources = [...] list, beam groups as groups). Throws an ArgumentError for an unknown system.

source
BeamletOpticsProjects.pose — Function
pose(p::Project, id::Symbol) -> @NamedTuple{pos::NTuple{3, Float64}, rot::Matrix{Float64}}

The pose of the component, source or group id as its line has it: the position pos in m (a line without pos: the position of its object as constructed) and its rot as a rotation matrix (see rotation_matrix: the rotation since construction, the identity for a line without rot). A pose read before a change puts the line back with set!(dir, id; pos, rot). Throws an ArgumentError for an unknown id.

source
BeamletOpticsProjects.moved — Function
moved(p::Project, ids::Vector{Symbol}; offset = (0, 0, 0), rot = nothing, pivot = :center)
    -> Project

A copy of p in which the lines ids (components, sources, groups with their members, or systems with their members and sources) are moved as move! moves them, with the built objects moved too, so that the copy can be traced with trace. Nothing is written and p is unchanged. For scans of a position or an angle without a write; saving the copy is allowed but not its purpose.

source

Versions of the packages

BeamletOpticsProjects.project_versions — Function
project_versions(path) -> (; versions::Dict{String, String}, packages::Dict{String, String})

The versions that the project file of the project in the folder path (or the project file path) records, by package name (see project_marker): versions, the newest versions of BeamletOptics, BeamletOpticsProjects and the programs that saved it (e.g. BeamletOpticsGUI), and packages, the packages that the own code in src/ uses with their newest versions ("": not known). Both are empty for a project file without them (or without a project file).

source
BeamletOpticsProjects.check_versions — Function
check_versions(path; packages = Module[]) -> Vector{VersionCheck}

Compares the versions that the project file of the project in the folder path records (see project_versions) with those of this session, one VersionCheck per package:

  • BeamletOptics, BeamletOpticsProjects and the modules packages (e.g. the program that opens the project passes itself) with pkgversion against [versions]; the other entries of [versions] are not checked;
  • every entry of [packages] with what the active environment would load for using (:missing if it does not have the package or has not installed it).

Checks only: nothing is loaded or installed, and load_project does not call it.

source
BeamletOpticsProjects.VersionCheck — Type
VersionCheck(package, recorded, installed, status)

The result of check_versions for one package: its name, the version that the project records (nothing: none or not a version), the version of this session (nothing: not known or not installed) and status:

  • :same: the same version, or nothing to compare (no version recorded or known);
  • :newer: a newer version is installed that is compatible with the recorded one (same major version from 1.0 on, same minor version for 0.x);
  • :breaking: a newer version is installed that is not compatible with the recorded one;
  • :older: an older version is installed than the project was saved with;
  • :missing: the package is not installed in the active environment.
source

Trace and read

BeamletOpticsProjects.trace — Function
trace(project, system::Symbol; flen = 0.1) ->
    @NamedTuple{segments::Vector, ids::Vector{Union{Nothing, Symbol}},
                detectors::Dict{Symbol, @NamedTuple{hits::Int, power::Union{Nothing, Float64}}},
                warnings::Vector{String}}

Solves the system with the id system of project (e.g. :S1) with the sources declared in its @system line and returns what it measured:

  • segments: the path_segments of each source, concatenated in the order of the sources (BeamletOptics.PathSegment: start, stop [m], s_start, s_stop, object, ...). A final ray without an intersection is flen [m] long.
  • ids: one per segment, the id of the object hit at its end (segment.object): of its @component or @source line, for an object inside a group the id of the outermost group that holds it, and nothing for a final segment without a hit (or an object not in the project).
  • detectors: for every member of the system that is a Detector, by its id, the number of its hits (length of the detector) and its optical_power [W] (0.0 without hits). The hits of rays (Beam) carry no power: power is nothing for them, not 0.0.
  • warnings: the warnings (and errors) that BeamletOptics logged during the solve (e.g. a violation of the Lagrange invariant), in order, as "message (Module)". They are collected instead of printed: the global logger does not see them.

The objects keep nothing of an earlier solve: the system is initialized first (initialize!, which empties its detectors), then the sources are solved one after the other, so their hits superpose on the detectors. Only the sources of system are solved: a Detector that is also a member of another system holds only the hits of the sources of this one, while the window traces all systems that share a detector together and superposes their hits. Tracing again gives the same segments and hit counts. The power too, except for a beam group as source (e.g. CollimatedSource) traced with several threads: BeamletOptics adds the hits in the order in which its threads finish, so power can differ in the last bits. The objects of the project are changed by the solve (beams, detector hits) as with solve_system!.

Throws a KeyError if the project has no system system.

Examples

p = load_project("Bench")
r = trace(p, :S1)
r.detectors[:D1]        # (hits = 1, power = 0.001)
r.segments[end].object  # the object hit at the end of the last segment
source
BeamletOpticsProjects.look — Function
look(project; around = nothing, io = stdout) -> String

Prints the setup of project as text to io and returns the text, for agents that can not see the window. Each system is traced as with trace (this changes the beams and detector hits of the objects). The sections:

  • Environment: per package the version of this session and, in parentheses, the one that the project file records (see project_versions): BeamletOptics (with its path), BeamletOpticsProjects, the other packages it was saved with (e.g. BeamletOpticsGUI), and the packages that the own code uses (used by src/; not installed if the active environment does not have it), then the Julia version, e.g. BeamletOptics 0.14.0 (project: 0.14.1) at ….
  • Participants: the people and agents in .bmo/presence-*.toml (see read_presences): id, name, kind, the id they work on and what they do. A file without an integer beat is not listed. Whether a participant is still there is not shown: that needs the time at which a reader saw its beat change (alive).
  • Systems: per @system its members and sources, then one line per chain of ids along a branch of a beam, e.g. B1 → L1 (50.0 mm) → M1 (50.0 mm) → D1 (20.0 mm): the distance to each object is measured from where the beam left the previous one (the path inside an object is not shown), out is a ray that leaves the system, ? an object without an id. Branches with the same chain are one line (×n, with the distances of the first); at most 8 chains per source. Then the hits and the power of each detector (see trace; rays, no power for the hits of rays, which carry none). A source with a label is shown with it, e.g. sources [B1 "Pump"]. A detector in several systems shows in each only the hits of the sources of that system.
  • Free objects: the components and sources that belong to no system, with their positions.

around = :L3 restricts the output to the objects whose position is within 50 mm of the one of L3 (listed with their distances) and to the chains of beams through one of them. Throws a KeyError if there is no object around.

source

Versions and changes

BeamletOpticsProjects.save_version — Function
save_version(dir; message = nothing) -> Union{Nothing, String}

Saves a version of the project in the folder dir: commits its files (respecting .gitignore, also deleted files, never .bmo/ or .claude/skills/) to the git repository that holds the folder, or to a new one in the folder if there is none. Returns the id of the commit, or nothing if nothing changed since the last version. message = nothing uses version_summary, or "Changed <files>" if that is empty while other files of the folder changed.

Only the files of the project folder are committed. Throws an ArgumentError naming them and commits nothing if changes outside the folder are staged in the repository. Throws an ArgumentError if the repository that holds the folder ignores it (e.g. by its .gitignore).

Known limits:

  • If the folder is the root of an existing repository, all changes of that repository that are not ignored are committed, also files that do not belong to the project.
  • On a file system that ignores case (Windows), a folder in a repository that was renamed only in case (e.g. mysetup to MySetup) is not found in the earlier commits: its versions are lost to versions and version_summary.
source
BeamletOpticsProjects.versions — Function
versions(dir) -> Vector{@NamedTuple{id::String, time::DateTime, message::String}}

The versions of the project in the folder dir, newest first: the commits that change a file of the folder, with their id, time (of the author, UTC) and message. Empty if the folder is in no repository; throws an ArgumentError if the repository that holds the folder ignores it.

source
BeamletOpticsProjects.version_summary — Function
version_summary(dir) -> String

What changed in the project in the folder dir since its last version, e.g. "moved L1, L3; added M4; removed D2; own code changed", by the lines of system.jl (paired by their uid, else by their id) without loading the project. The kinds are added, removed, renamed (same uid, other id), moved (only pos or rot differ), changed (other code, label or further keywords) and own code changed (a file in src/ that git does not ignore); more than 6 ids of a kind are counted ("moved 9 objects"). "first version" if there is no version yet, "" if neither system.jl nor a file in src/ changed: other files of the project (e.g. view.toml, the project file *.bmo) are not looked at, though save_version commits them. Throws an ArgumentError if the repository that holds the folder ignores it.

source
BeamletOpticsProjects.diff_projects — Function
diff_projects(base, new; rebuild = Symbol[]) -> (; entries, systems, groups)

The changes from the project base to new, as entries::Vector{EntryChange}, systems::Vector{SystemChange} and groups::Vector{GroupChange}. Entries, systems and groups are paired by their uid; one without a uid in new is :added. The order of the lines does not matter, nor the order of the members of a system or group. The entries whose readable ids are in rebuild count as :rebuilt even with the same code, e.g. since the own code that they call changed.

source
BeamletOpticsProjects.EntryChange — Type
EntryChange(uid, kind, old, new)

A change of the entry with the machine id uid from old to new (an Entry or nothing), see diff_projects. kind is one of :added, :removed, :renamed (other readable id), :rebuilt (other code or own code), :moved (other pos or rot), :relabeled and :meta; one entry can have several changes, e.g. :renamed and :moved.

source
BeamletOpticsProjects.GroupChange — Type
GroupChange(uid, kind, old, new)

A change of the group with the machine id uid, like EntryChange: kind is one of :added, :removed, :renamed, :relabeled, :members (other members) and :moved (other pivot pos or orientation rot).

source
BeamletOpticsProjects.copy_code — Function
copy_code(from, to)

Copies the own code of the project in the folder from (every file in src/) to the folder to, without overwriting a file that exists there; nothing if from has no src/.

source

Participants and claims

BeamletOpticsProjects.announce — Function
announce(dir; name, doing = "", selected_id = "", ttl = 120, kind = "agent", wrote = false,
         versions = Dict{String, String}()) -> String

Takes part in the project in the folder dir, or tells the others the next step: writes the presence file (see presence_file) of the id "<name>@agent" (characters outside [A-Za-z0-9_.@-] replaced by _) with name, the host, kind (a label only), ttl, the readable id selected_id of the object it works on, doing (a few words) and versions: those of BeamletOptics and BeamletOpticsProjects in this session and the entries of the versions passed (package name => version, e.g. of a program of the caller; this session wins for BeamletOptics and BeamletOpticsProjects). beat grows by one with every call (see write_presence). Call it before changing the project, after each change and at every step; call leave when done. Returns the id. The claims of the participant (see claim!) live as long as its presence, i.e. ttl seconds after its last call; claim! writes the presence again.

name is the id of the participant and must be unique per participant (e.g. "claude-w1" for a worker of a team): participants with the same name share one presence file, so their calls overwrite each other and the first leave removes all of them.

With wrote = true, call right after writing the files: it publishes written, the stamp of the files now (see project_stamp), so that the windows name this participant as the writer of that change, as they do for a window after a save, also when the call comes after the windows merged the change. Otherwise the written of the presence file before is kept ("" if there is none), so a call that wrote nothing never claims the change of someone else.

source
BeamletOpticsProjects.leave — Function
leave(dir; name)

Ends taking part in the project in the folder dir: deletes the presence file that announce with the same name wrote, the one of every participant of that name. A missing file is fine.

source
BeamletOpticsProjects.claim! — Function
claim!(dir; by, ids, refresh = true) -> Bool

Claims the objects of the readable ids ids (e.g. [:L1, :M2], the lines that the participant by is going to change) in the project or team draft in the folder dir: true if all of them are claimed by by now (also those it held already), false if one is claimed by another participant who is still there; then none of ids is claimed by this call.

A claim is the file .bmo/claims/<key>.toml of the project (claims/ of a draft) with by, the readable id and the machine id of the line (key), so it keeps its object when the line is renamed. Of two participants that claim the same object at the same time exactly one gets it; the objects are claimed in sorted order, so this also holds for overlapping ids. A claim lives as long as its holder's presence (see announce): one of a participant who is gone is taken over. by is the participant id of the presence ("anna@pc2") or the name an agent announced with ("w1"); with refresh, a claim also writes the presence file of by again (a window, which writes its presence itself, passes false). Windows of people refuse to change an object claimed by someone else, and save_project with by refuses to write its line. See release!, claims.

source
BeamletOpticsProjects.release! — Function
release!(dir; by, ids = nothing)

Releases the claims of the participant by in the project or team draft in the folder dir: all of them, or those of the readable ids ids. See claim!.

source
BeamletOpticsProjects.claims — Function
claims(dir) -> Dict{String, @NamedTuple{by::String, id::Symbol}}

The claims of the project or team draft in the folder dir whose holders are still there, by the machine id of their line ("id:L1" for a line without one): the participant by and the readable id id at the time of the claim. A claim made while its line had no machine id counts for the line of its readable id once that line has one. See claim!.

source

Teams

BeamletOpticsProjects.fork_team — Function
fork_team(dir, name; by, goal = "") -> String

Forks a draft of the team name from the project in the folder dir and returns the folder of the draft, <dir>/.bmo/drafts/<name>/. It holds copies of system.jl, src/ and the project file *.bmo (it loads as a project, see load_project, but has no git versions), the same three in base/ (the state it was forked from, against which merge_draft! finds the changes of the team), team.toml (by, the participant who forked it, created, UTC, and goal), and the folders board/ (see post!) and claims/ (see claim!).

name may contain letters, digits and _.@-. Throws an ArgumentError if a team of that name exists: of two participants that fork the same name at the same time, exactly one gets the draft (the folder is created with mkdir). See teams.

source
BeamletOpticsProjects.teams — Function
teams(dir) -> Vector{String}

The names of the teams of the project in the folder dir (the drafts under .bmo/drafts/ that were forked completely, see fork_team), sorted.

source
BeamletOpticsProjects.post! — Function
post!(draft; by, kind, text = "", data = Dict()) -> Int

Posts an entry on the board of the team draft draft (a folder of fork_team) and returns its number. The entry is the file board/<n>-<by>-<kind>.toml with by (the participant), kind (e.g. :task, :data, :done), text, time (UTC) and the table data; values that TOML does not know (e.g. symbols) are written as strings. Entries are numbered 1, 2, … in the order they are posted, also by several processes: each number is taken with mkdir of board/.seq/<n>/, then the file is written atomically. See board, wait_for.

source
BeamletOpticsProjects.board — Function
board(draft) -> Vector{Dict{String, Any}}

The entries on the board of the team draft draft in the order they were posted, each with n (its number), by, kind (a string), text, time and data, see post!. A file that does not parse is left out.

source
BeamletOpticsProjects.wait_for — Function
wait_for(draft; kind, after = 0, timeout = 60) -> Union{Nothing, Dict{String, Any}}

The first entry of the kind (e.g. :done) on the board of the team draft draft whose number is larger than after, see board. Only the entries after + 1, after + 2, … up to the first number that is not on the board yet count: a number is taken before its entry is written (see post!), so a later entry may appear first, and waiting on with after the number of the returned entry never skips one. Looks every 0.5 s until there is one; nothing after timeout seconds.

source
BeamletOpticsProjects.merge_draft! — Function
merge_draft!(dir, name) -> (; taken::Vector{String}, conflicts::Vector{String})

Merges the draft of the team name (see fork_team) into the project in the folder dir: the changes of the draft since its base/ that the project did not change differently meanwhile (see merge_plan) are written into system.jl of the project (see save_project), and the files of the draft's own code (src/) that the project did not change since base/ are copied into it. The project then gets the version "Team <name>: <summary>" (see save_version), whose summary names only what the merge changed. Systems that both changed only in their members get the members of both; an entry or a system that the draft added or renamed with a readable id that the project has already gets the next free one, also in the draft. Returns the keys of the entries and systems that were taken (machine ids, "id:L1" for a line without one, "src/<file>" for a file of own code), and those of the conflicts, sorted; the conflicts are not written, the project keeps its state there.

Afterwards the draft's base/ is the state that the project and the draft agree on, so the draft can be merged again: a later merge takes what the team changed since, and a conflict stays one until both agree. Meant to be called by the participant who has the say over the project. The draft stays; delete its folder when the team is done.

source

Guards

BeamletOpticsProjects.evaluate — Function
evaluate(project; base) -> @NamedTuple{deviations, foreign_hits, clearance}

Measures what project changed with respect to base, the project it started from (e.g. loaded from .bmo/drafts/<name>/base). It reports numbers and does not judge: which deviation counts as unchanged depends on the task, so there is no tolerance or threshold. Systems and objects are matched by their machine id (uid), or by their id if they have none.

  • deviations: a Dict by the id of every system of base (traced with trace in both projects) with nothing if project has no such system, otherwise a named tuple:
    • position: the largest distance [m] between the start points or the stop points of the segments of the same index (0.0 without segments). The segments of a trace are deterministic, hence it is exactly 0.0 for a system that was not changed.
    • segments: (base = n, project = m), the numbers of segments; those of the same index are compared, a difference of the numbers is reported here.
    • objects: the number of segments of the same index that end on another object (compared by id, nothing for a segment that ends on nothing; an object inside a group has the id of the group).
    • detectors: for every detector of the system in either project, by its id, the change (project minus base) of hits and power [W]; a detector that one project lacks counts as having no hits and no power there. power is nothing if either side has the hits of rays, which carry none (see trace). For a beam group as source traced with several threads the change of power of an unchanged system can be nonzero in the last bits.
  • foreign_hits: the segments of the sources of the systems of project that are not in base that end on an object of a system of base, as a Vector of (system, source, object, at) with ids and the end point at [m] (in the order of the systems of project and their sources).
  • clearance: for each object of project that is not in base, the smallest distance [m] between its bounding sphere (BeamletOptics.bounding_sphere_of) and those of the objects that are in base, as a Vector of (object, nearest, distance) sorted ascending by distance. It is the distance of the surfaces of the spheres, negative if they overlap. Objects without a bounding sphere (NoBoundingSphere, e.g. sources) are left out on either side.

Tracing solves the systems and changes their objects (beams, detector hits), as trace does.

Examples

r = evaluate(load_project("."); base = load_project(".bmo/drafts/t1/base"))
r.deviations[:S1].position   # 0.0: the beam of S1 was not moved
first(r.clearance)           # the new object closest to an old one
source

Public, not exported

BeamletOpticsProjects.install_agent_skill — Function
install_agent_skill(dest = joinpath(pwd(), ".claude", "skills"))

Copies the agent skill that ships with the installed BeamletOpticsProjects version into joinpath(dest, "beamletopticsprojects") and returns that path. The skill teaches AI coding assistants (e.g. Claude Code) how to work on a project: the form of system.jl, loading, tracing and reading it (load_project, trace, look), the open windows of the people, taking part and versions.

The default dest is the project-level skill directory of Claude Code in the current working directory. Use e.g. joinpath(homedir(), ".claude", "skills") for a personal installation. new_project installs it, and the skill of BeamletOptics, into the new project.

An existing beamletopticsprojects skill in dest is replaced, so calling this function again after Pkg.update keeps the skill in sync with the installed package version. Local edits to the copy are lost.

File permissions

Pkg installs packages read-only. The files are written anew instead of copied with their permissions, so that the copy can be edited and replaced later.

source
BeamletOpticsProjects.person — Function
person() -> (; id, name, host)

The person at this machine: name from ENV["BMO_PERSON"], else the user name of the system (USERNAME, USER), else "someone"; host the name of the machine; id is "name@host" with every character outside [A-Za-z0-9_.@-] replaced by _, for the names of the files of the person in .bmo/ (see presence_file, session_file).

source
BeamletOpticsProjects.presence_file — Function
presence_file(dir, id) -> String

The presence file .bmo/presence-<id>.toml of the participant id (e.g. person().id or the id that announce returns) in the project folder dir. Every participant, a window or a coding agent, has one while it works on the project. It is a TOML file with the fields

  • name: how the others see the participant, e.g. "Claude (Opus 5.5)" (default: the id);
  • host: the name of the machine;
  • kind: e.g. "person" or "agent" (default "person"), a label for display only;
  • beat: an integer that grows by one with every write (required; a file without it is ignored);
  • ttl: the seconds that the last change of beat counts (default 60, clamped to 5…3600); windows write 60, agents 120 by default (announce), since they write only when they act;
  • selected: the machine id (uid) of the object it works on, "" if none;
  • selected_id: the readable id of that object (e.g. "L1"), used when selected is missing or unknown;
  • doing: a few words of what it does;
  • written: the project_stamp of the files after its last write, by which the others find the writer of a change;
  • versions: a table of the versions of the packages it works with by name (e.g. BeamletOptics = "0.14.1"), by which the others see that they work with other versions.

Whether a participant is there is judged on the clock of the reader only, see alive. Write the file with write_presence, read those of the others with read_presences.

source
BeamletOpticsProjects.session_file — Function
session_file(dir, id) -> String

The session file .bmo/session-<id>.md of the participant id in the project folder dir: a few lines of prose of what it sees and does, for the agents that work on the same project (the hook of the project shows every .bmo/session*.md to Claude Code).

source
BeamletOpticsProjects.write_presence — Function
write_presence(dir, id, data) -> Int

Writes the presence file of the participant id in the project folder dir (see presence_file) with the fields of data (keys converted to strings) and beat one higher than in the file before (1 for a new file; a beat in data is ignored), so that a participant that comes back continues its counter and the others see a change. Creates .bmo/. A temporary file is moved into place, so that a reader sees the whole file. Windows can not replace a file that another participant reads at that moment (on a shared drive its client may keep the file open for a while after it was read): then, after a few short tries, the file is overwritten in place, which a reader may see half written once (it skips such a file, see read_presences). Returns the new beat.

source
BeamletOpticsProjects.read_presences — Function
read_presences(dir; except = nothing, cache = nothing) -> Dict{String, Dict{String, Any}}

The presence files of the participants of the project folder dir (see presence_file) by id, without the one of the id except (e.g. the own person().id). A file that does not parse (e.g. half written on a network drive) or has no integer beat is skipped. The fields are normalized: beat an Int; ttl a Float64 clamped to 5…3600 s (60 if missing or invalid); name (default: the id), kind (default "person"), host, selected, selected_id, doing and written strings (default ""); versions a Dict{String, String} (default empty, entries that are not strings are left out); other fields are kept as they are. Whether a participant is there is up to the reader, which notes when it saw beat change, see alive.

A reader that looks often passes a cache, a Dict that it keeps between the calls (empty at first): a file whose time of change and size are those of the last call is not read again. On a shared drive this keeps the files of the others closed most of the time, which their writers need (see write_presence).

source
BeamletOpticsProjects.alive — Function
alive(o, now) -> Bool

Whether the participant o is there at the time now (s, e.g. time()): its beat changed within its ttl seconds, now - o.seen_at < o.ttl. o is anything with the properties seen_at, the time on the clock of the reader at which the reader last saw the beat of the participant change, and ttl (see read_presences). Only the clock of the reader is used, so the clocks of the machines are never compared.

source
BeamletOpticsProjects.merge_plan — Function
merge_plan(base, mine, theirs; stale = key -> false, rebuild = Symbol[], keep = String[])
    -> (; take, conflicts, merged, renames, mine, theirs)

What to take from the project theirs into mine, which both changed from the common state base (e.g. the files changed by an agent and the setup in an open window, or the draft of a team and the live project). Entries, systems and groups are compared by their keys: the machine id, or "id:L1" for a line without one (see diff_projects).

  • take: the keys that only theirs changed, or that both changed to the same state; the caller applies the entries and systems of theirs for them.
  • conflicts: the keys that both changed differently, those that only theirs changed but for which stale(key) is true (e.g. theirs sets back a state that mine wrote, from an older read), and the keys of keep (conflicts still open from before) unless mine and theirs agree on them now (or merge their members and sources, see merged), the systems that would hold the same source otherwise (e.g. both moved it into another system), since a source belongs to at most one system, and the systems and groups that would break the tree of groups otherwise (an entry or group in two groups, in a group and in a system, or a system or group that holds a group that the merged state does not have). Nothing of them is taken.
  • merged: the systems and groups that both changed only in their members (and sources), which is no conflict: their members and sources merged as sets of keys of entries and groups, key => (; members, sources), each (mine ∪ (theirs − base)) − (base − theirs), e.g. both added a lens to S1; the sources of a group are empty. The places of the conflicts in the systems and groups are part of them and stay as in mine: a system or group (also one of take, whose other changes are taken) whose members would hold one of them where mine has it not, or the other way round, is in merged too, with them as in mine. E.g. theirs removed L1 from S1 and mine moved L1: L1 stays a member of S1 until the conflict is decided.
  • renames: the entries, systems and groups of take that theirs added or renamed to a readable id that another one of mine has already, by key, with the next free id of the same prefix; the caller adds them under that id, and the systems refer to them by it.
  • mine, theirs: the changes of each since base, see diff_projects; rebuild names the entries of theirs that count as rebuilt (e.g. since the own code that they call changed).
source
BeamletOpticsProjects.compose — Function
compose(base, mine, theirs, keys) -> (; project, keys)

The state of the project mine with that of theirs at the keys keys (both changed from the common state base): the entries, systems and groups of theirs for those keys (also removed: what theirs does not have), those of mine elsewhere. mine stays as it is. Keys are those of merge_plan: the machine id, or "id:L1" for a line without one; the key of a line is the same in all three states. Systems and groups refer to their members by key, so a system of mine holds an entry of theirs and a group of theirs holds the entries of mine; members that are gone are left out. The places of the entries, sources and groups of keys in the systems and groups belong to them: each is a member exactly where theirs has it (nowhere if theirs has it not), the other members of those systems and groups stay as in mine.

The keys are widened where the result would break a rule otherwise: a system of mine that would hold a source that a system of theirs at keys holds (a source belongs to at most one system), and a system or group that would break the tree of groups (an entry or a group in two groups, or in a group and in a system, or a group that the result does not have), get the state of theirs too, until no rule is broken. An entry, system or group of theirs whose readable id one of mine that stays has gets the next free id of its prefix.

Returns the result and the widened keys, sorted. The result has the folder, the name and the stamp of mine: save_project of it throws ProjectChanged if the files changed since mine was loaded.

E.g. a window with an open conflict at L1 saves its state everywhere else, and the files keep theirs at L1:

r = compose(base, window, files, ["l1-uid"])
save_project(r.project)
source
BeamletOpticsProjects.normalize_code — Function
normalize_code(ex)

ex without line numbers and with the bodies begin x end of short functions such as x -> 2x unwrapped, so that it prints on one line and reads back the same.

source
BeamletOpticsProjects.same_entry — Function
same_entry(a, b) -> Bool

Whether the entries a and b (of two states of a project) describe the same thing: the same kind, readable id, code, pose, label and further keywords.

source
BeamletOpticsProjects.same_system — Function
same_system(a, pa, b, pb) -> Bool

Whether the systems a of the project pa and b of pb are the same: id, label, static, and members and sources (compared by their machine ids).

source
BeamletOpticsProjects.same_group — Function
same_group(a, pa, b, pb) -> Bool

Whether the groups a of the project pa and b of pb are the same: id, label, pivot, orientation and members (compared by their machine ids, in any order).

source
BeamletOpticsProjects.match_entries — Function
match_entries(olds, news) -> (; pairs, added, removed)

Pairs the entries (or systems, or groups) of two states like diff_projects: by uid, then the remaining ones without a uid in olds by their readable id. pairs are the (old, new) tuples, added the unpaired ones of news, removed those of olds.

source