# BornAgain Python API: migration guide v23 → v24

This guide shall help Claude Code to automatically convert a given
user script that worked under BornAgain-23 to an up-to-date script
that works with current BornAgain (to be released as v24).

Read the top section of the CHANGELOG for an overview of
changes after v23.

This guide covers every change in the public Python API after
BornAgain 23.

Conventions used in tables:
- **Unchanged** – class/function exists with the same name and signature.
- **Renamed** – identical semantics, new name.
- **Replaced** – same concept, different interface.
- **Removed** – no direct replacement; usage notes give context.

---

## 1  Materials

| BA23 | BA24 | Status |
|------|------|--------|
| `ba.MaterialBySLD(name, sld_r, sld_i)` | `ba.SLDMaterial(name, color, sld_r, sld_i)` | Renamed, color required |
| `ba.MaterialBySLD("Vacuum", 0, 0)` | `ba.Vacuum()` | Replaced (prefer `Vacuum()`) |
| `ba.RefractiveMaterial(name, delta, beta)` | `ba.RefractiveMaterial(name, color, delta, beta)` | Changed, color required |

`color` is a mandatory display color tuple `(r, g, b)`, with components in
the range `[0, 1]`. It controls colors in the 3D sample viewer and does not
affect the scattering simulation. Omitting `color` for `SLDMaterial` or
`RefractiveMaterial` is an error.

For readability, define colors separately:
```python
si_color = (0.72, 0.74, 0.78)
si_material = ba.SLDMaterial("Si", si_color, 2.0704e-6, 0)
```

Magnetic overload: both `SLDMaterial` and `RefractiveMaterial` accept an
optional `magnetization` (`ba.R3`) argument after the physical parameters,
same as in BA23.

---

## 2  Particle constructor — alignment removed

BA23 required no alignment argument:
```python
particle = ba.Particle(material, formfactor)
```

BA24 alignment is inferred from the placement method:
```python
particle = ba.Particle(material, formfactor)
layer.deposit2D(...)   # bottom at interface (default)
layer.suspend2D(...)   # top at interface (default)
layer.fill3D(...)      # center at interface (default)
```

For non-default placement, use one of the three shift helpers
before depositing:
```python
particle.translate(0, 0, particle.height_below_center())
particle.translate(0, 0, particle.height())
particle.translate(0, 0, particle.height_above_center())
```

The removed alignment constants `ba.AlignAt_Bottom`,
`ba.AlignAt_Center`, `ba.AlignAt_Top` are no longer available.

---

## 3  Particle deposition — `ParticleLayout` removed

The `ParticleLayout` / `addLayout` API is gone. Every layer accumulates 2D
structures via `layer.deposit2D(aggregate)`, where the aggregate type also
encodes the positional correlations.

### 3.1  Disordered particles (no correlations)

BA23:
```python
layout = ba.ParticleLayout()
layout.addParticle(particle, 1.0)
layout.setTotalParticleSurfaceDensity(dens)
layer.addLayout(layout)
```

BA24:
```python
layer.deposit2D(ba.Dilute2D(dens, particle))
```

### 3.2  Hard-disk correlated particles

BA23:
```python
layout = ba.ParticleLayout()
layout.addParticle(particle, 1.0)
layout.setInterferenceFunction(ba.InterferenceHardDisk(...))
layer.addLayout(layout)
```

BA24:
```python
layer.deposit2D(ba.Dense2D(dens, particle))
```

`Dense2D` uses a Percus–Yevick hard-disk structure factor.

### 3.3  Multi-species (several particle types, incoherent)

BA23:
```python
layout = ba.ParticleLayout()
layout.addParticle(particle_1, abundance_1)
layout.addParticle(particle_2, abundance_2)
layout.setTotalParticleSurfaceDensity(dens)
layer.addLayout(layout)
```

BA24:
```python
mix = ba.Mixture()
mix.addParticle(particle_1, abundance_1)
mix.addParticle(particle_2, abundance_2)
layer.deposit2D(ba.Dilute2D(dens, mix))
```

`ba.Mixture` replaces `ba.ParticleComposition` for incoherent multi-species
layouts. `ba.Compound` replaces `ba.ParticleComposition` for coherent assemblies
(fixed relative positions, summed form factors).

---

## 4  Interference functions → aggregate classes

In BA23 the interference function was attached to the layout:
```python
layout = ba.ParticleLayout()
layout.addParticle(particle)
layout.setInterferenceFunction(iff)
layout.setTotalParticleSurfaceDensity(dens)
layer.addLayout(layout)
```

In BA24 the particle is passed directly to the aggregate constructor, and the
aggregate is deposited on the layer:
```python
layer.deposit2D(ba.Crystal2D(particle, lattice))
```

### 4.1  Mapping table

| BA23 | BA24 | Notes |
|------|------|-------|
| `Interference2DLattice(lattice)` | `Crystal2D(particle, lattice)` | Particle moved into constructor |
| `InterferenceFinite2DLattice(lattice, N1, N2)` | `FiniteCrystal2D(particle, lattice, N1, N2)` | |
| `Interference2DParacrystal(lattice, damping, d1, d2)` | `Paracrystal2D(particle, lattice, damping, d1, d2)` | |
| `InterferenceRadialParacrystal(peak, damping)` | `RadialParacrystal(particle, peak, damping)` | |
| `Interference1DLattice(length, xi, density)` | `Crystal1D(particle, length, xi, density)` | |
| `InterferenceHardDisk(peak, density)` | `Dense2D(density, particle)` | See §3.2 |
| `Interference2DSuperLattice` | — | unused in 23 rawEx, withdrawn in 24 |

In BA23, probability distributions (peak shape) were set on the interference
object. In BA24 they are set on the aggregate, e.g.:
```python
agg = ba.Paracrystal2D(particle, lattice, damping, d1, d2)
agg.setProbabilityDistributions(profile_1, profile_2)
layer.deposit2D(agg)
```

`Profile1DGauss`, `Profile1DCauchy`, `Profile2DGauss`, etc. are unchanged.

---

## 5  Shape / formfactor renames

| BA23 | BA24 | Notes |
|------|------|-------|
| `Bar(length_x, length_y, height)` | `Box(length, width, height)` | Renamed |
| `Gauss(radius)` | `GaussSphere(mean_radius)` | Renamed |
| `HemiEllipsoid(rx, ry, h)` | `EllipsoidalSegment(rx, ry, h, 0, h)` | New parametrisation (rm_top=0, rm_bottom=h) |
| `CosineRipple(l, w, h)` | `CosineRippleBox(l, w, h)` | Box profile; `CosineRippleGauss` and `CosineRippleLorentz` also available |
| `SawtoothRipple(l, w, h, asym)` | `SawtoothRippleBox(l, w, h, asym)` | Same; Gauss/Lorentz variants available |

All other shapes unchanged: `Sphere`, `SphericalSegment`, `Cylinder`,
`EllipsoidalCylinder`, `HorizontalCylinder`, `Cone`, `FuzzySphere`,
`LongBoxGauss`, `LongBoxLorentz`, `Pyramid2–6`, `Prism3`, `Prism6`,
`Bipyramid4`, `CantellatedCube`, `TruncatedCube`, `Dodecahedron`,
`Icosahedron`, `PlatonicOctahedron`, `PlatonicTetrahedron`.

New in BA24 (no BA23 equivalent): `BarGauss`, `BarLorentz`, `Ellipsoid`,
`EllipsoidalSegment`, `SpheroidalSegment`.

---

## 6  Rotations

| BA23 | BA24 | Notes |
|------|------|-------|
| `RotationX(angle)` | same | Unchanged |
| `RotationY(angle)` | same | Unchanged |
| `RotationZ(angle)` | same | Unchanged |
| `RotationEulerZXZ(a, b, g)` | `RotationEuler(a, b, g)` | Direct mapping (ZXZ convention) |
| `RotationEulerZYZ(a, b, g)` | compose rotations | No direct equivalent; use `RotationZ(a)` then `RotationY(b)` then `RotationZ(g)` |

---

## 7  Simulation classes

All unchanged:

| Class | Notes |
|-------|-------|
| `ScatteringSimulation(beam, sample, det)` | GISAS / SAXS |
| `SpecularSimulation(scan, sample)` | Specular reflectometry |
| `OffspecSimulation(scan, sample, det)` | Off-specular |
| `DepthprobeSimulation` | Depth probing |

`simulation.options().setNumberOfThreads(n)` and
`simulation.options().setUseAvgMaterials(flag)` unchanged.

`simulation.simulate()` still returns a `Datafield`.

---

## 8  Scan classes

All unchanged: `AlphaScan`, `QzScan`, `LambdaScan` — same names and
constructors.

---

## 9  Beam, detector, footprint, resolution

All unchanged:

| Item | Signature |
|------|-----------|
| `Beam` | `ba.Beam(intensity, wavelength, alpha)` |
| `beam.setFootprint` | `beam.setFootprint(foot)` |
| `FootprintSquare` | `ba.FootprintSquare(width_ratio)` |
| `FootprintGauss` | `ba.FootprintGauss(width_ratio)` |
| `SphericalDetector` | `ba.SphericalDetector(n_phi, phi_min, phi_max, n_alpha, alpha_min, alpha_max)` |
| `OffspecDetector` | `ba.OffspecDetector(n_alpha, alpha_min, alpha_max, phi_min, phi_max)` |
| `ResolutionFunction2DGaussian` | `ba.ResolutionFunction2DGaussian(sigma_x, sigma_y)` |

---

## 10  Roughness and layer interface

All unchanged:

```python
autocorr = ba.SelfAffineFractalModel(sigma, hurst, corr_length)
transient = ba.ErfTransient()          # or ba.TanhTransient()
crosscorr = ba.CommonDepthCrosscorrelation(depth)
roughness = ba.Roughness(autocorr, transient, crosscorr)

layer = ba.Layer(material, thickness, roughness)   # finite layer
substrate = ba.Layer(material, roughness)          # semi-infinite substrate
```

---

## 11  Datafield / result access

| BA23 | BA24 |
|------|------|
| `from bornagain.numpyutil import Arrayf64Converter as dac` | **remove** — module gone |
| `dac.npArray(result.dataArray())` | `result.intensities()` |
| `dac.asNpArray(result.dataArray())` | `result.intensities()` |
| `result.xAxis().binCenters()` (and similar) | `result.xCenters()` / `result.yCenters()` |
| `result.errors()` | same |

`result.intensities()`, `result.xCenters()`, `result.yCenters()`, and
`result.errors()` all return NumPy arrays directly.

Scripts in ba23/rawEx using `dataArray` / `Arrayf64Converter`:
`likelihood_sampling.py`, `OffspecResolved.py`, `PolarizedSpinAsymmetry.py`,
`RoughSurface.py`.

---

## 12  Plot functions (`ba_plot`)

| BA23 | BA24 |
|------|------|
| `bp.plot_simres(result, **kwargs)` | `bp.plot_datafield(result, **kwargs)` |
| `bp.plot_specular_curve(result, **kwargs)` | `bp.plot_datafield(result, **kwargs)` (auto-dispatches 1D/2D) |
| `bp.plot_datafield(result, **kwargs)` | same |
| `bp.plot_multicurve(results, **kwargs)` | same |
| `bp.plot_to_row(results, **kwargs)` | `bp.plot2d_to_row(results, **kwargs)` |
| `bp.plot_to_grid(results, ncol, **kwargs)` | `bp.plot2d_to_grid(results, ncol, **kwargs)` |

Scripts in ba23/rawEx using `plot_simres`:
`AxesInDifferentUnits.py`, `PositionVariance.py`.

---

## 13  Python helper modules

| BA23 | BA24 | Status |
|------|------|--------|
| `bornagain.numpyutil` (Arrayf64Converter) | **removed** | Not needed; see §11 |
| `from bornagain import sample_tools` | same | Unchanged |
| `sample_tools.materialProfile(sample, ...)` | same | Unchanged |
| `from bornagain import std_samples` | same | Unchanged |
| `from bornagain import std_simulations` | same | Unchanged (std_simulations.sas → std_simulations.particle) |

Script in ba23/rawEx using `sample_tools`: `MaterialProfileWithParticles.py`.

---

## 14  Fitting API

The C++ fitting/minimizer layer was removed. Use BornAgain's Python
`FitObjective` together with an external optimizer such as `lmfit` or
`scipy.optimize`.

Still present:

- `ba.FitObjective` — `addFitPair`, `evaluate`, `evaluate_residuals`,
  `simulationResult`, `experimentalData`, `absoluteDifference`,
  `relativeDifference`, `initPlot`, `initPrint`, `iterationInfo`
- `ba_fitmonitor.PlotterGISAS`, `ba_fitmonitor.PlotterSpecular`
- `ba_fitmonitor.Printer`

Removed:

- `ba.Parameter`, `ba.Parameters`, `ba.Minimizer`, `ba.MinimizerFactory`,
  `ba.MinimizerResult`
- `ba.AttLimits`

Typical replacement:

```python
import lmfit

fit_objective = ba.FitObjective()
fit_objective.addFitPair(get_simulation, data)

P = lmfit.Parameters()
P.add("radius", value=5*nm, min=4*nm, max=6*nm)

result = lmfit.minimize(fit_objective.evaluate_residuals, P)
print(lmfit.fit_report(result))
```

---

## 15  I/O functions

All unchanged:

```python
ba.writeDatafield(data, fname)
ba.dataMatchesFile(data, ref_file, tolerance)
```

---

## 16  Resample / material-profile utilities

All unchanged:

```python
ba.generateZValues(n_points, z_min, z_max)
ba.materialProfileSLD(sample, n_points, z_min, z_max)
ba.magnetizationProfile(sample, xyz, n_points, z_min, z_max)
ba.defaultMaterialProfileLimits(sample)
```

---

## 17  Withdrawn classes with no obvious replacement

| BA23 class | Used in ba23/rawEx | BA24 status |
|------------|--------------------|-------------|
| `Interference2DSuperLattice` | unused in rawEx | withdrawn in v24 |
| `OffSpecularSimulation` (alternate spelling) | unused in rawEx | withdrawn in v24 (use `OffspecSimulation`) |
| `DepthProbeSimulation` (alternate spelling) | unused in rawEx | withdrawn in v24 (use `DepthprobeSimulation`) |
| `FixedBinAxis` | unused in rawEx | withdrawn in v24; replacement to be studied |
| `VariableBinAxis` | unused in rawEx | withdrawn in v24; replacement to be studied |
| `RectangularDetector` | unused in rawEx | withdrawn in v24; in 23 used in (none in rawEx); replacement to be studied |
| `LayerRoughness` (simple sigma-only roughness) | unused in rawEx | withdrawn in v24; use `ba.Roughness(SelfAffineFractalModel(...), ErfTransient())` for equivalent |
