Skip to content

Commit

Permalink
[ng] Data model cleanup with tests and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
DraTeots committed Sep 19, 2024
1 parent dc51d9f commit 5b36c06
Show file tree
Hide file tree
Showing 13 changed files with 669 additions and 26 deletions.
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,117 @@ Hit has
- "t": time information [ns] (time, err_time),
- "ed": energy deposit with error [GeV] (edep, err_edep)

## TypeScript Event Model


The Firebird Data Exchange model provides a structured way to serialize and deserialize
event data. This model is implemented in TypeScript and designed to be extensible,
allowing users to add their own custom components without modifying the core parsing logic.

The implementation consists of several TypeScript classes and interfaces that mirror the data exchange
format and provide methods for serialization and deserialization.

- **DataExchange** class - Represents the entire data exchange object.
- **Entry** class - Represents an individual event entry.
- **EntryComponent** abstract class - A base class representing a generic entry component. Described below.

It Typescript Firebird Data Exchange often referenced as Dex (Data EXchange). E.g.
`toDexObject`, `fromDexObject`. `DexObject` is a JS object, that if serialized to JSON
would correspond a part of DataExchangeFormat. E.g. Dex HitBox will have `pos`, `dim` etc.

We can put it differently. In general JSON format is very close to object definition in JS => TS.
But despite that, Firebird JSON format is just a data exchange layer and when deserialized from
JSON is not designed to be a 100% a convenient to work with JS data structure.
More over, it lacks some methods and abstractions, that our domain data-model should have,
such as links between event model and three.js rendering tree objects.

Summarizing:

- Firebird Data Exchange - is JSON schema shaping data exchange between backend and frontend
- DexObject - JSON parsed to JS object
- TypeScript event model - Frontend set of classes mimicking DexObject structure but designed
to be convenient in terms of the frontend API


### EntryComponent machinery

Different collection of objects such as hits, tracks, vertexes, etc.
that firebird can work with are represented as various event or Entry Component-s.
Each type derive from EntryComponent and registered in a system.
Then corresponding Painters classes know how to render/paint them, there are rules how
to display them in tables, etc.

- **EntryComponent** An abstract class representing a generic entry component.
- Contains common properties:
- `name`: Unique identifier.
- `type`: Component type (used for determining the appropriate factory).
- `originType`: Optional string indicating the origin type.
- Declares an abstract method `toDexObject()` that must be implemented by subclasses.

- **EntryComponentFactory Interface**: Defines a factory for creating `EntryComponent`
instances from Dex objects - deserialized data.
- **Component Registry**: A mapping of component types to their corresponding factories.
- Functions:
- `registerComponentFactory(factory: EntryComponentFactory)`: Registers a new factory.
- `getComponentFactory(type: string)`: Retrieves a factory based on the component type.


## Extending the Model

### Adding a New Component Type

To add a new component type, follow these steps:

1. **Create a New Component Class**: Extend `EntryComponent` and implement the `toDexObject()` method.

```typescript
export class CustomComponent extends EntryComponent {
static type = 'CustomType';
// Define additional properties

constructor(name: string, /* additional parameters */, originType?: string) {
super(name, CustomComponent.type, originType);
// Initialize additional properties
}

toDexObject(): any {
return {
name: this.name,
type: this.type,
originType: this.originType,
// Serialize additional properties
};
}
}
```

2. **Create a Factory for the Component**: Implement `EntryComponentFactory` for your component.

```typescript
export class CustomComponentFactory implements EntryComponentFactory {
type: string = CustomComponent.type;

fromDexObject(obj: any): EntryComponent {
const name = obj["name"];
// Extract additional properties
const originType = obj["originType"];

// Validate required fields
if (!name /* || missing other required fields */) {
throw new Error("Missing required fields in CustomComponent");
}

return new CustomComponent(name, /* additional parameters */, originType);
}
}
```

3. **Register the Factory**: Use the `registerComponentFactory()` function to register your component factory.

```typescript
// Register the custom component factory
registerComponentFactory(new CustomComponentFactory());
```

4. **Update JSON Parsing Logic**: No need to modify existing parsing logic. The registry will dynamically resolve the new component type.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// box-tracker-hit.component.factory.spec.ts

import { BoxTrackerHitComponentFactory, BoxTrackerHitComponent } from './box-tracker-hit.component';
import { EntryComponent } from './entry-component';

describe('BoxTrackerHitComponentFactory', () => {
const factory = new BoxTrackerHitComponentFactory();

it('should have the correct type', () => {
expect(factory.type).toBe('BoxTrackerHit');
});

it('should create a BoxTrackerHitComponent from DexObject', () => {
const dexObject = {
name: 'TestComponent',
type: 'BoxTrackerHit',
originType: 'TestOriginType',
hits: [
{
pos: [1, 2, 3],
dim: [10, 10, 1],
t: [4, 1],
ed: [0.001, 0.0001],
},
{
pos: [4, 5, 6],
dim: [10, 10, 2],
t: [5, 1],
ed: [0.002, 0.0002],
},
],
};

const component = factory.fromDexObject(dexObject) as BoxTrackerHitComponent;

expect(component).toBeInstanceOf(BoxTrackerHitComponent);
expect(component.name).toBe('TestComponent');
expect(component.type).toBe('BoxTrackerHit');
expect(component.originType).toBe('TestOriginType');
expect(component.hits.length).toBe(2);

const hit1 = component.hits[0];
expect(hit1.position).toEqual([1, 2, 3]);
expect(hit1.dimensions).toEqual([10, 10, 1]);
expect(hit1.time).toEqual([4, 1]);
expect(hit1.energyDeposit).toEqual([0.001, 0.0001]);

const hit2 = component.hits[1];
expect(hit2.position).toEqual([4, 5, 6]);
expect(hit2.dimensions).toEqual([10, 10, 2]);
expect(hit2.time).toEqual([5, 1]);
expect(hit2.energyDeposit).toEqual([0.002, 0.0002]);
});
});
30 changes: 30 additions & 0 deletions firebird-ng/src/app/model/box-tracker-hit.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// box-tracker-hit.component.spec.ts

import { BoxTrackerHitComponent, BoxTrackerHit } from './box-tracker-hit.component';

describe('BoxTrackerHitComponent', () => {
it('should create an instance with given name', () => {
const component = new BoxTrackerHitComponent('TestComponent');

expect(component.name).toBe('TestComponent');
expect(component.type).toBe(BoxTrackerHitComponent.type);
expect(component.hits.length).toBe(0);
});

it('should serialize to DexObject correctly', () => {
const hit1 = new BoxTrackerHit([1, 2, 3], [10, 10, 1], [4, 1], [0.001, 0.0001]);
const hit2 = new BoxTrackerHit([4, 5, 6], [10, 10, 2], [5, 1], [0.002, 0.0002]);

const component = new BoxTrackerHitComponent('TestComponent', 'TestOriginType');
component.hits.push(hit1, hit2);

const dexObject = component.toDexObject();

expect(dexObject).toEqual({
name: 'TestComponent',
type: 'BoxTrackerHit',
originType: 'TestOriginType',
hits: [hit1.toDexObject(), hit2.toDexObject()],
});
});
});
141 changes: 141 additions & 0 deletions firebird-ng/src/app/model/box-tracker-hit.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// box-tracker-hit.component.ts

import { EntryComponent, EntryComponentFactory, registerComponentFactory } from './entry-component';

/**
* Represents an individual tracker hit with position, dimensions, time, and energy deposit.
*/
export class BoxTrackerHit {

/** The position of the hit [mm] as [x, y, z]. */
position: [number, number, number];

/** The dimensions of the hit box [mm] as [dx, dy, dz]. */
dimensions: [number, number, number];

/** The time information [ns] as [time, err_time]. */
time: [number, number];

/** The energy deposit with error [GeV] as [edep, err_edep]. */
energyDeposit: [number, number];

/**
* Constructs a new BoxTrackerHit instance.
*
* @param position - The position of the hit as [x, y, z].
* @param dimensions - The dimensions of the hit box as [dx, dy, dz].
* @param time - The time information as [time, err_time].
* @param energyDeposit - The energy deposit with error as [edep, err_edep].
*/
constructor(
position: [number, number, number],
dimensions: [number, number, number],
time: [number, number],
energyDeposit: [number, number]
) {
this.position = position;
this.dimensions = dimensions;
this.time = time;
this.energyDeposit = energyDeposit;
}

/**
* Serializes the BoxTrackerHit instance into a JSON-compatible object.
*
* @returns A JSON-compatible object representing the serialized BoxTrackerHit.
*/
toDexObject(): any {
return {
pos: this.position,
dim: this.dimensions,
t: this.time,
ed: this.energyDeposit,
};
}

/**
* Creates a BoxTrackerHit instance from a deserialized object.
*
* @param obj - The deserialized object representing a BoxTrackerHit.
* @returns A new instance of BoxTrackerHit populated with data from the object.
*/
static fromDexObject(obj: any): BoxTrackerHit {
return new BoxTrackerHit(
obj["pos"],
obj["dim"],
obj["t"],
obj["ed"]
);
}
}

/**
* Represents a component that contains multiple BoxTrackerHits.
*/
export class BoxTrackerHitComponent extends EntryComponent {
/** The static type identifier for the BoxTrackerHitComponent. */
static type = 'BoxTrackerHit';

/** An array of BoxTrackerHits contained in this component. */
hits: BoxTrackerHit[] = [];

/**
* Constructs a new BoxTrackerHitComponent instance.
*
* @param name - The name of the component.
* @param originType - Optional origin type of the component.
*/
constructor(name: string, originType?: string) {
super(name, BoxTrackerHitComponent.type, originType);
}

/**
* Serializes the BoxTrackerHitComponent instance into a JSON-compatible object.
*
* @returns A JSON-compatible object representing the serialized BoxTrackerHitComponent.
*/
toDexObject(): any {
const objHits = [];
for (const hit of this.hits) {
objHits.push(hit.toDexObject());
}

return {
name: this.name,
type: this.type,
originType: this.originType,
hits: objHits,
};
}
}

/**
* Factory for creating instances of BoxTrackerHitComponent from deserialized data.
*/
export class BoxTrackerHitComponentFactory implements EntryComponentFactory {
/** The type of the component that this factory creates. */
type: string = BoxTrackerHitComponent.type;

/**
* Creates an instance of BoxTrackerHitComponent from a deserialized object.
*
* @param obj - The deserialized object representing a BoxTrackerHitComponent.
* @returns An instance of BoxTrackerHitComponent.
*/
fromDexObject(obj: any): EntryComponent {
const result = new BoxTrackerHitComponent(obj["name"]);

if (obj["originType"]) {
result.originType = obj["originType"];
}

for (const objHit of obj["hits"]) {
result.hits.push(BoxTrackerHit.fromDexObject(objHit));
}

return result;
}
}

// Register the component factory
registerComponentFactory(new BoxTrackerHitComponentFactory());
22 changes: 22 additions & 0 deletions firebird-ng/src/app/model/component-registry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// component-registry.spec.ts

import { registerComponentFactory, getComponentFactory } from './entry-component';
import { BoxTrackerHitComponentFactory } from './box-tracker-hit.component';

describe('Component Registry', () => {
it('should register and retrieve BoxTrackerHitComponentFactory correctly', () => {
const factory = new BoxTrackerHitComponentFactory();
registerComponentFactory(factory);

const retrievedFactory = getComponentFactory('BoxTrackerHit');

expect(retrievedFactory).toBeDefined();
expect(retrievedFactory).toBe(factory);
});

it('should return undefined for unregistered component types', () => {
const retrievedFactory = getComponentFactory('UnknownType');

expect(retrievedFactory).toBeUndefined();
});
});
Loading

0 comments on commit 5b36c06

Please sign in to comment.